1use 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#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum State {
20 Ok(String),
22 Mismatch { expected: String, seen: String },
23 NotChecked(String),
25}
26
27#[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#[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 #[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#[must_use]
102pub fn check_discovery(m: &CardModel, info: &DiscoveryInfo) -> Vec<Check> {
103 let l = &m.limits;
104 vec![
105 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#[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#[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#[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#[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#[derive(Clone, Debug)]
290pub struct Args<'a> {
291 pub out: Option<&'a str>,
293 pub index: u16,
294 pub wait: u64,
296}
297
298pub 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 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}