Skip to main content

ops/
probe.rs

1//! `rxp card probe`: read the card and check every claim in its model file
2//! that a read can check. Nothing is written; guarded blocks stay
3//! `not checked` because checking them means writing.
4
5use crate::capture::discover_one;
6use crate::flash::{read_blocks, read_chunk};
7use crate::model::for_card;
8use crate::screen::looks_erased;
9use crate::util::{contains_lattice_header, hex, open};
10use crate::{protocol, rcvbp, Ctx, Progress};
11use anyhow::{bail, Context, Result};
12use protocol::{eeprom, DiscoveryInfo, SCREEN_RECORD_LEN};
13use rcvbp::record01::View;
14use receivers::{CardModel, Version};
15use std::fmt;
16
17/// What one check found.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum State {
20    /// As the model says; the text adds what was seen.
21    Ok(String),
22    Mismatch { expected: String, seen: String },
23    /// Reads cannot decide it; the text says why.
24    NotChecked(String),
25}
26
27/// One claim of the model file and its state.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct Check {
30    pub what: String,
31    pub state: State,
32}
33
34impl Check {
35    fn ok(what: impl Into<String>, seen: impl Into<String>) -> Self {
36        Self { what: what.into(), state: State::Ok(seen.into()) }
37    }
38
39    fn mismatch(what: impl Into<String>, expected: impl Into<String>, seen: impl Into<String>) -> Self {
40        Self {
41            what: what.into(),
42            state: State::Mismatch { expected: expected.into(), seen: seen.into() },
43        }
44    }
45
46    fn unchecked(what: impl Into<String>, why: impl Into<String>) -> Self {
47        Self { what: what.into(), state: State::NotChecked(why.into()) }
48    }
49
50    fn of(what: impl Into<String>, ok: bool, expected: impl Into<String>, seen: impl Into<String>) -> Self {
51        if ok {
52            Self::ok(what, seen)
53        } else {
54            Self::mismatch(what, expected, seen)
55        }
56    }
57}
58
59impl fmt::Display for Check {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        match &self.state {
62            State::Ok(seen) if seen.is_empty() => write!(f, "{:<12} {}", "ok", self.what),
63            State::Ok(seen) => write!(f, "{:<12} {}: {seen}", "ok", self.what),
64            State::Mismatch { expected, seen } => {
65                write!(f, "{:<12} {}: expected {expected}, seen {seen}", "mismatch", self.what)
66            }
67            State::NotChecked(why) => write!(f, "{:<12} {}: {why}", "not checked", self.what),
68        }
69    }
70}
71
72/// The checklist.
73#[derive(Debug, Default)]
74pub struct Report {
75    pub checks: Vec<Check>,
76}
77
78impl Report {
79    #[must_use]
80    pub fn mismatches(&self) -> usize {
81        self.checks
82            .iter()
83            .filter(|c| matches!(c.state, State::Mismatch { .. }))
84            .count()
85    }
86
87    /// `N ok, N mismatch, N not checked`.
88    #[must_use]
89    pub fn summary(&self) -> String {
90        let n = |f: fn(&State) -> bool| self.checks.iter().filter(|c| f(&c.state)).count();
91        format!(
92            "{} ok, {} mismatch, {} not checked",
93            n(|s| matches!(s, State::Ok(_))),
94            n(|s| matches!(s, State::Mismatch { .. })),
95            n(|s| matches!(s, State::NotChecked(_)))
96        )
97    }
98}
99
100/// The claims a discovery reply can check: the id byte and the size limits.
101#[must_use]
102pub fn check_discovery(m: &CardModel, info: &DiscoveryInfo) -> Vec<Check> {
103    let l = &m.limits;
104    vec![
105        // A model file with no sourced id byte has nothing to compare.
106        m.id.map_or_else(
107            || {
108                Check::unchecked(
109                    "discovery id",
110                    format!(
111                        "{} states no id byte; the card answers 0x{:02x}",
112                        m.name, info.card_id
113                    ),
114                )
115            },
116            |id| {
117                Check::of(
118                    "discovery id",
119                    info.card_id == id,
120                    format!("0x{id:02x}"),
121                    format!("0x{:02x}", info.card_id),
122                )
123            },
124        ),
125        Check::of(
126            "reported size within limits",
127            info.cols <= l.max_width && info.rows <= l.max_height,
128            format!("at most {}x{}", l.max_width, l.max_height),
129            format!("{}x{}", info.cols, info.rows),
130        ),
131    ]
132}
133
134/// The claims the first chunk of each firmware bank can check: a bitstream
135/// header at the bank's start.
136#[must_use]
137pub fn check_banks(m: &CardModel, primary_head: &[u8], golden_head: &[u8]) -> Vec<Check> {
138    let bank = |what: &str, addr: u32, head: &[u8]| {
139        Check::of(
140            format!("{what} bank at 0x{addr:06x}"),
141            contains_lattice_header(head),
142            "a bitstream header",
143            if contains_lattice_header(head) { "bitstream header".to_owned() } else { format!("none in the first {} bytes", head.len()) },
144        )
145    };
146    vec![
147        bank("primary", m.memory.primary_bank, primary_head),
148        bank("golden", m.memory.golden_bank, golden_head),
149    ]
150}
151
152/// Where the EEPROM mirror sits inside the parameter block, when it does.
153#[must_use]
154pub fn mirror_in_block(m: &CardModel) -> Option<usize> {
155    let mem = &m.memory;
156    (mem.eeprom_mirror / mem.block_bytes == u32::from(mem.parameter_block))
157        .then(|| (mem.eeprom_mirror % mem.block_bytes) as usize)
158}
159
160fn erased(bytes: &[u8]) -> bool {
161    bytes.iter().all(|&b| b == 0xFF)
162}
163
164/// The claims the parameter block and the EEPROM mirror can check: a boot
165/// image at the model's region offsets, consistent with the `.rcvbp` it
166/// embeds, and a programmed mirror.
167#[must_use]
168pub fn check_block(m: &CardModel, block: &[u8], mirror: &[u8]) -> Vec<Check> {
169    let mem = &m.memory;
170    let bi = &mem.boot_image;
171    let mut out = Vec::new();
172    if block.len() != mem.block_bytes as usize {
173        out.push(Check::mismatch(
174            format!("parameter block 0x{:02x}", mem.parameter_block),
175            format!("{} bytes", mem.block_bytes),
176            format!("{} bytes", block.len()),
177        ));
178        return out;
179    }
180
181    let pack = &block[bi.basic_pack..bi.basic_pack + 0x100];
182    let pack_ok = rcvbp::spec::verify_basic_pack(pack);
183    out.push(Check::of(
184        format!("basic pack at +0x{:04x}", bi.basic_pack),
185        pack_ok,
186        "marker 0xa8 and its CRC at +0xfc",
187        if pack_ok {
188            "marker and CRC".to_owned()
189        } else if pack[0] == 0xA8 {
190            "marker, CRC differs".to_owned()
191        } else {
192            format!("marker 0x{:02x}", pack[0])
193        },
194    ));
195
196    let at = bi.rcvbp;
197    let len = u32::from_le_bytes([block[at], block[at + 1], block[at + 2], block[at + 3]]) as usize;
198    let file = (len <= bi.rcvbp_max && at + 4 + len <= block.len()).then(|| &block[at + 4..at + 4 + len]);
199    let cfg = file.and_then(|f| rcvbp::Rcvbp::from_bytes(f).ok());
200    let what = format!("embedded .rcvbp at +0x{at:04x}");
201    match (&file, &cfg) {
202        (None, _) => out.push(Check::mismatch(
203            what,
204            format!("a length up to {}", bi.rcvbp_max),
205            if len == 0xFFFF_FFFF { "erased".to_owned() } else { format!("length {len}") },
206        )),
207        (Some(_), None) => out.push(Check::mismatch(what, "a parsable file", format!("{len} bytes that do not parse"))),
208        (Some(_), Some(c)) => out.push(Check::ok(what, format!("{len} bytes, {} records", c.records.len()))),
209    }
210
211    for (what, at, n) in [
212        ("mapping", bi.mapping, bi.mapping_len()),
213        ("scan table", bi.scan_table, 0x100),
214    ] {
215        out.push(Check::of(
216            format!("{what} at +0x{at:04x}"),
217            !erased(&block[at..at + n]),
218            "written",
219            if erased(&block[at..at + n]) { "erased" } else { "written" },
220        ));
221    }
222
223    let page = &block[bi.chip_page..bi.chip_page + 0x100];
224    let what = format!("chip page at +0x{:04x}", bi.chip_page);
225    let reg84 = cfg.as_ref().and_then(|c| c.find_by_id(0x84)).map(|r| r.payload.as_slice());
226    if erased(page) {
227        out.push(Check::ok(what, "erased, drivers not armed at boot"));
228    } else {
229        match reg84 {
230            Some(r) if r == page => out.push(Check::ok(what, "record 0x84")),
231            Some(r) => out.push(Check::mismatch(
232                what,
233                "record 0x84",
234                format!("{} bytes differ", r.iter().zip(page).filter(|(a, b)| a != b).count()),
235            )),
236            None => out.push(Check::unchecked(what, "no record 0x84 to compare")),
237        }
238    }
239
240    let what = "basic pack against record 0x01";
241    let rec = cfg.as_ref().and_then(|c| c.record_01()).and_then(|r| View::new(&r.payload).ok());
242    match rec {
243        Some(v) if pack_ok => {
244            let seen = (pack[0x07], pack[0x08], u16::from_be_bytes([pack[0x88], pack[0x89]]), u16::from_be_bytes([pack[0x8A], pack[0x8B]]));
245            let want = (v.scan(), v.gray(), v.max_width(), v.max_height());
246            let show = |(s, g, w, h): (u8, u8, u16, u16)| format!("scan 1/{s}, {g} bits, screen {w}x{h}");
247            out.push(Check::of(what, seen == want, show(want), show(seen)));
248        }
249        _ => out.push(Check::unchecked(what, "needs a verified pack and record 0x01")),
250    }
251
252    let what = format!("eeprom mirror at 0x{:06x}", mem.eeprom_mirror);
253    if mirror.len() < SCREEN_RECORD_LEN {
254        out.push(Check::mismatch(what, format!("{SCREEN_RECORD_LEN} bytes"), format!("{} bytes", mirror.len())));
255    } else if looks_erased(mirror) {
256        out.push(Check::mismatch(what, "a programmed record", "erased"));
257    } else {
258        let area = eeprom::parse_control_area(&mirror[2..])
259            .map_or_else(String::new, |(x0, y0, x1, y1)| format!("control area {x0},{y0}-{x1},{y1}"));
260        out.push(Check::ok(what, area));
261    }
262    out
263}
264
265/// The claims reads cannot decide.
266#[must_use]
267pub fn not_checked(m: &CardModel, running: Version) -> Vec<Check> {
268    let guarded = m.memory.guarded_blocks(running);
269    let mut out = vec![Check::unchecked(
270        if guarded.is_empty() {
271            format!("no guarded blocks on {running}")
272        } else {
273            format!("guarded blocks {} on {running}", hex(guarded, ","))
274        },
275        "checking means writing",
276    )];
277    out.push(Check::unchecked(format!("{} hub ports", m.limits.hub_ports), "not readable"));
278    if let Some(chain) = m.limits.chain {
279        out.push(Check::unchecked(format!("chain of {chain}"), "not readable"));
280    }
281    out.push(Check::unchecked(
282        format!("firmware {}", if m.firmware.sdram_staging { "via SDRAM staging" } else { "via host page writes" }),
283        "checking means installing firmware",
284    ));
285    out
286}
287
288/// What `rxp card probe` takes.
289#[derive(Clone, Debug)]
290pub struct Args<'a> {
291    /// Directory for the bytes read, when wanted; nothing is written otherwise.
292    pub out: Option<&'a str>,
293    pub index: u16,
294    /// Seconds to wait for each reply.
295    pub wait: u64,
296}
297
298/// Discover the card, read its banks' heads, its parameter block and the
299/// EEPROM mirror, and check the model. Read-only: every frame sent is a
300/// discovery or a flash read.
301///
302/// # Errors
303/// Fails when no card answers, its id has no model and none was named, or
304/// a read goes unanswered.
305pub fn probe(ctx: &Ctx, a: &Args, p: &mut dyn Progress) -> Result<Report> {
306    let Some(info) = discover_one(ctx, a.wait)? else {
307        bail!("no response on {} within {}s", ctx.iface, a.wait);
308    };
309    let m = match ctx.model {
310        Some(m) => m,
311        None => for_card(&info)?,
312    };
313    let running = Version(info.ver_major, info.ver_minor);
314    p.err(&format!(
315        "card: {} (id 0x{:02x}), firmware {running}, reports {}x{}",
316        m.name, info.card_id, info.cols, info.rows
317    ));
318    let mut report = Report::default();
319    report.checks.extend(check_discovery(m, &info));
320
321    let mut dev = open(ctx)?;
322    let head = |dev: &mut rawlink::Link, block: u8| read_chunk(dev, a.index, u16::from(block) << 8, a.wait);
323    let primary = head(&mut dev, m.memory.primary_blocks().start)?;
324    let golden = head(&mut dev, m.memory.golden_block())?;
325    report.checks.extend(check_banks(m, &primary, &golden));
326
327    let block = read_blocks(&mut dev, a.index, m.memory.parameter_block, 1, a.wait, p)?;
328    let mirror = match mirror_in_block(m) {
329        Some(off) => block[off..off + SCREEN_RECORD_LEN].to_vec(),
330        None => {
331            let page = (m.memory.eeprom_mirror / protocol::FLASH_PAGE_BYTES as u32) as u16;
332            read_chunk(&mut dev, a.index, page, a.wait)?[..SCREEN_RECORD_LEN].to_vec()
333        }
334    };
335    report.checks.extend(check_block(m, &block, &mirror));
336    report.checks.extend(not_checked(m, running));
337
338    for c in &report.checks {
339        p.out(&c.to_string());
340    }
341    p.err(&report.summary());
342
343    if let Some(dir) = a.out {
344        std::fs::create_dir_all(dir).with_context(|| format!("create {dir}"))?;
345        for (name, bytes) in [("parameter-block.bin", &block), ("eeprom-mirror.bin", &mirror)] {
346            let path = format!("{dir}/{name}");
347            std::fs::write(&path, bytes).with_context(|| format!("write {path}"))?;
348            p.out(&path);
349        }
350    }
351    Ok(report)
352}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357    use panelspec::{embedded, PanelSpec};
358    use receivers::by_name;
359
360    fn e120() -> &'static CardModel {
361        by_name("E120").unwrap()
362    }
363
364    /// The bench spec compiled for the E120, with a programmed mirror page.
365    fn synthetic_block() -> Vec<u8> {
366        let spec = PanelSpec::parse(embedded::PANELS[0].1).unwrap();
367        let chip = spec
368            .chip_library(&|p| embedded::chip(p).map(str::to_owned).ok_or_else(|| anyhow::anyhow!("{p}")))
369            .unwrap();
370        let g = rcvbp::spec::generate(&spec, &chip).unwrap();
371        let mut block = rcvbp::image::compile(&e120().memory.boot_image, &spec, &g).unwrap().image;
372        let off = mirror_in_block(e120()).unwrap();
373        block[off..off + SCREEN_RECORD_LEN].fill(0);
374        block[off + 2..off + 44].copy_from_slice(&eeprom::control_area(0, 0, 128, 64));
375        block
376    }
377
378    fn mirror(block: &[u8]) -> Vec<u8> {
379        let off = mirror_in_block(e120()).unwrap();
380        block[off..off + SCREEN_RECORD_LEN].to_vec()
381    }
382
383    fn states(checks: &[Check]) -> Vec<(&str, &State)> {
384        checks.iter().map(|c| (c.what.as_str(), &c.state)).collect()
385    }
386
387    #[test]
388    fn the_generated_image_passes_every_block_check() {
389        let block = synthetic_block();
390        let checks = check_block(e120(), &block, &mirror(&block));
391        let mismatches: Vec<_> = checks.iter().filter(|c| !matches!(c.state, State::Ok(_))).collect();
392        assert!(mismatches.is_empty(), "{mismatches:?}");
393        let lines: Vec<String> = checks.iter().map(ToString::to_string).collect();
394        assert_eq!(lines[0], "ok           basic pack at +0x0000: marker and CRC");
395        assert!(lines[1].starts_with("ok           embedded .rcvbp at +0x8000: "), "{}", lines[1]);
396        assert!(lines[1].ends_with(" bytes, 17 records"), "{}", lines[1]);
397        assert_eq!(lines[4], "ok           chip page at +0x0900: record 0x84");
398        assert_eq!(lines[5], "ok           basic pack against record 0x01: scan 1/16, 12 bits, screen 128x64");
399        assert_eq!(lines[6], "ok           eeprom mirror at 0x07f000: control area 0,0-128,64");
400    }
401
402    #[test]
403    fn each_damaged_region_is_reported_against_the_model() {
404        let m = e120();
405        let good = synthetic_block();
406
407        let mut block = good.clone();
408        block[0x07] ^= 1;
409        let c = check_block(m, &block, &mirror(&block));
410        assert_eq!(
411            c[0].state,
412            State::Mismatch { expected: "marker 0xa8 and its CRC at +0xfc".into(), seen: "marker, CRC differs".into() }
413        );
414        assert!(matches!(c[5].state, State::NotChecked(_)), "{:?}", c[5]);
415
416        let mut block = good.clone();
417        block[0x8000..].fill(0xFF);
418        let c = check_block(m, &block, &mirror(&block));
419        assert_eq!(c[1].state, State::Mismatch { expected: "a length up to 28668".into(), seen: "erased".into() });
420        assert_eq!(c[4].state, State::NotChecked("no record 0x84 to compare".into()));
421
422        let mut block = good.clone();
423        block[0x0900..0x0A00].fill(0xFF);
424        let c = check_block(m, &block, &mirror(&block));
425        assert_eq!(c[4].state, State::Ok("erased, drivers not armed at boot".into()));
426        block[0x0900] = 0x55;
427        let c = check_block(m, &block, &mirror(&block));
428        assert!(matches!(&c[4].state, State::Mismatch { seen, .. } if seen.ends_with("bytes differ")), "{:?}", c[4]);
429
430        let mut block = good.clone();
431        block[0x3000..0x6000].fill(0xFF);
432        let c = check_block(m, &block, &mirror(&block));
433        assert_eq!(c[2].state, State::Mismatch { expected: "written".into(), seen: "erased".into() });
434
435        let c = check_block(m, &good, &[0xFF; SCREEN_RECORD_LEN]);
436        assert_eq!(c[6].state, State::Mismatch { expected: "a programmed record".into(), seen: "erased".into() });
437
438        let c = check_block(m, &good[..0x8000], &mirror(&good));
439        assert_eq!(states(&c), [("parameter block 0x07", &State::Mismatch { expected: "65536 bytes".into(), seen: "32768 bytes".into() })]);
440    }
441
442    #[test]
443    fn discovery_and_banks_are_checked_against_the_model() {
444        let m = e120();
445        let info = |id, cols, rows| DiscoveryInfo { card_id: id, ver_major: 16, ver_minor: 53, cols, rows, controller: 0, raw: Vec::new() };
446        let c = check_discovery(m, &info(0x64, 128, 64));
447        assert!(c.iter().all(|c| matches!(c.state, State::Ok(_))), "{c:?}");
448        let c = check_discovery(m, &info(0x65, 2048, 64));
449        assert_eq!(c[0].state, State::Mismatch { expected: "0x64".into(), seen: "0x65".into() });
450        assert_eq!(c[1].state, State::Mismatch { expected: "at most 1024x192".into(), seen: "2048x64".into() });
451
452        let mut head = vec![0u8; 1024];
453        head[100..121].copy_from_slice(b"Lattice Semiconductor");
454        let c = check_banks(m, &head, &[0xFF; 1024]);
455        assert_eq!(c[0].to_string(), "ok           primary bank at 0x000000: bitstream header");
456        assert_eq!(c[1].to_string(), "mismatch     golden bank at 0x200000: expected a bitstream header, seen none in the first 1024 bytes");
457
458        let n = not_checked(m, Version(16, 53));
459        assert_eq!(n[0].to_string(), "not checked  guarded blocks 00,01,02,08 on 16.53: checking means writing");
460        assert_eq!(n[1].to_string(), "not checked  12 hub ports: not readable");
461        let r = Report { checks: [c, n].concat() };
462        assert_eq!(r.mismatches(), 1);
463        assert_eq!(r.summary(), "1 ok, 1 mismatch, 3 not checked");
464    }
465}