Skip to main content

ops/
flash.rs

1//! Reading and writing the card's flash: configuration, dumps, and firmware.
2
3use crate::model::{bank_bytes, flash_map};
4use crate::util::{await_reply, contains_lattice_header, has_lattice_header, hex, open, warn};
5use crate::{check, protocol, rcvbp, Ctx, Progress};
6use anyhow::{Context, Result};
7use rawlink::Link;
8use receivers::CardModel;
9use std::time::Duration;
10
11/// Record type of the driver-chip register table.
12const CHIP_REGS: u16 = 0x0a84;
13
14/// True when the file carries a driver-chip register table with content.
15fn has_chip_regs(f: &rcvbp::Rcvbp) -> bool {
16    f.find(CHIP_REGS).is_some_and(|r| !r.is_empty_table())
17}
18
19/// Byte offset of a 256-byte page within a firmware-bank image.
20const fn page_offset(block: u8, page: u16) -> usize {
21    (block as usize * 256 + page as usize) * protocol::FLASH_PAGE_BYTES
22}
23
24/// The `i`th 256-byte page of a block image.
25fn page(image: &[u8], i: usize) -> &[u8] {
26    &image[i * protocol::FLASH_PAGE_BYTES..(i + 1) * protocol::FLASH_PAGE_BYTES]
27}
28
29/// Indices of the pages of `after` that differ from `pages`.
30fn mismatched_pages(after: &[u8], pages: &[&[u8]]) -> Vec<usize> {
31    after
32        .chunks(protocol::FLASH_PAGE_BYTES)
33        .zip(pages)
34        .enumerate()
35        .filter(|(_, (a, b))| a != *b)
36        .map(|(i, _)| i)
37        .collect()
38}
39
40/// Read the card's stored configuration out of flash as `.rcvbp` file bytes.
41///
42/// `page` defaults to the model's parameter page. Only ever sends read-opcode
43/// flash frames, which carry no data of their own and so cannot modify the card.
44pub fn read_config(
45    ctx: &Ctx,
46    index: u16,
47    page: Option<u16>,
48    max_chunks: u16,
49    wait: u64,
50) -> Result<Vec<u8>> {
51    let page = match page {
52        Some(p) => p,
53        None => ctx.model()?.memory.config_page(),
54    };
55    let mut dev = open(ctx)?;
56    let mut flash: Vec<u8> = Vec::new();
57    let mut expected: Option<usize> = None;
58
59    for chunk in 0..max_chunks {
60        let page = page + chunk * protocol::FLASH_PAGES_PER_CHUNK;
61        flash.extend_from_slice(&read_chunk(&mut dev, index, page, wait)?);
62
63        // The blob opens with its own total length, so we know when to stop.
64        if expected.is_none() && flash.len() >= 4 {
65            expected = Some(u32::from_le_bytes(flash[..4].try_into()?) as usize);
66        }
67        if expected.is_some_and(|n| flash.len() >= n) {
68            break;
69        }
70    }
71
72    let total = expected.context("card returned no length prefix")?;
73    if flash.len() < total + 4 {
74        anyhow::bail!(
75            "only read {} of {total} bytes; raise --max-chunks",
76            flash.len()
77        );
78    }
79    // The prefix counts the file including its 4-byte CRC trailer.
80    let file = flash
81        .get(4..4 + total)
82        .context("card reported more configuration than it returned")?;
83    Ok(file.to_vec())
84}
85
86/// Save what [`read_config`] returned as `out`, print the path, and warn
87/// when the file will not drive PWM chips or does not parse.
88pub fn save_config(file: &[u8], out: &str, p: &mut dyn Progress) -> Result<()> {
89    std::fs::write(out, file).with_context(|| format!("write {out}"))?;
90    p.out(out);
91
92    match rcvbp::Rcvbp::load(out) {
93        Ok(f) if !has_chip_regs(&f) => warn(
94            p,
95            format!(
96            "{out} has no driver-chip register table; panels with PWM driver ICs will stay dark"
97        ),
98        ),
99        Ok(_) => {}
100        Err(e) => warn(p, format!("{out} does not parse as .rcvbp: {e:#}")),
101    }
102    Ok(())
103}
104
105/// Request one 1024-byte chunk of flash and return it.
106pub(crate) fn read_chunk(dev: &mut Link, index: u16, page: u16, wait: u64) -> Result<Vec<u8>> {
107    dev.send(&protocol::read_flash(index, page))?;
108    await_reply(dev, Duration::from_secs(wait), |f| {
109        protocol::flash_reply_data(f).map(<[u8]>::to_vec)
110    })?
111    .with_context(|| format!("no reply for page 0x{page:04x} within {wait}s"))
112}
113
114/// Read the firmware region back and count bytes that differ from `img`.
115fn verify_firmware(m: &CardModel, dev: &mut Link, index: u16, img: &[u8], wait: u64) -> Result<usize> {
116    let mut bad = 0usize;
117    for block in m.memory.primary_blocks() {
118        for lo in (0u16..0x100).step_by(protocol::FLASH_PAGES_PER_CHUNK as usize) {
119            let page = (u16::from(block) << 8) | lo;
120            let got = read_chunk(dev, index, page, wait)?;
121            let off = page_offset(block, lo);
122            let want = &img[off..off + got.len()];
123            bad += got.iter().zip(want).filter(|(g, w)| g != w).count();
124        }
125    }
126    Ok(bad)
127}
128
129/// Print the human-readable fields Lattice puts in a bitstream's header.
130fn describe_image(img: &[u8], p: &mut dyn Progress) {
131    let header: String = img[..200.min(img.len())]
132        .iter()
133        .map(|&b| {
134            if b.is_ascii_graphic() || b == b' ' {
135                b as char
136            } else {
137                ' '
138            }
139        })
140        .collect();
141    for field in ["Design name", "Part", "Date"] {
142        if let Some(i) = header.find(field) {
143            p.err(&format!(
144                "firmware: {}",
145                header[i..].split("  ").next().unwrap_or("").trim()
146            ));
147        }
148    }
149}
150
151/// Install an FPGA bitstream into the primary firmware bank.
152///
153/// Only the primary is written; the golden backup is left alone
154/// so the card retains an in-hardware fallback. A local dump of the current
155/// primary is required as well, so the previous image can be put back.
156///
157/// `blocks` limits the write to part of the bank, so a partially-programmed
158/// image can be repaired without disturbing what is already correct.
159/// `image` is a manifest name or a path (`crate::firmware`).
160#[allow(clippy::too_many_arguments)]
161pub fn flash_firmware(
162    ctx: &Ctx,
163    image: &str,
164    backup: &str,
165    commit: bool,
166    blocks: std::ops::Range<u8>,
167    index: u16,
168    wait: u64,
169    p: &mut dyn Progress,
170) -> Result<()> {
171    let m = ctx.model()?;
172    let map = flash_map(m);
173    anyhow::ensure!(
174        blocks.start < blocks.end
175            && map.firmware_blocks.contains(&blocks.start)
176            && blocks.end <= map.firmware_blocks.end,
177        "blocks 0x{:02x}..0x{:02x} fall outside the primary bank",
178        blocks.start,
179        blocks.end
180    );
181
182    let loaded = crate::firmware::load(image, p)?;
183    let checked = crate::firmware::checked(&loaded);
184    let (image, img) = (loaded.path.as_str(), loaded.bytes.as_slice());
185    anyhow::ensure!(
186        has_lattice_header(img),
187        "{image} does not look like a Lattice bitstream"
188    );
189    let span = bank_bytes(m);
190    anyhow::ensure!(
191        img.len() >= span,
192        "{image} is only {} bytes; the primary bank is {span}",
193        img.len()
194    );
195    // Images carry padding past the end marker and CRC, which sit just inside
196    // the bank; write one bank's worth and drop the tail.
197    let img = &img[..span];
198
199    let old = std::fs::read(backup).with_context(|| format!("read backup {backup}"))?;
200    anyhow::ensure!(
201        old.len() >= span && has_lattice_header(&old),
202        "{backup} is not a usable dump of the current primary bank"
203    );
204
205    p.err(&format!(
206        "firmware: {image} ({checked}) -> blocks 0x{:02x}..0x{:02x}, recovery {backup}",
207        blocks.start,
208        blocks.end - 1
209    ));
210    describe_image(img, p);
211
212    if !commit {
213        p.out("dry run: nothing written (add --commit)");
214        return Ok(());
215    }
216
217    let mut dev = open(ctx)?;
218
219    // The program region is write-protected; without this every erase and
220    // write is silently ignored.
221    dev.send(&protocol::set_program_writable(index, true))?;
222    std::thread::sleep(Duration::from_millis(200));
223
224    for block in blocks.clone() {
225        p.err(&format!("firmware: erase 0x{block:02x}"));
226        dev.send(&map.erase_firmware_block(index, block)?)?;
227        std::thread::sleep(Duration::from_secs(3));
228    }
229
230    for block in blocks {
231        p.err(&format!("firmware: write 0x{block:02x}"));
232        for page in 0..=0xffu8 {
233            let off = page_offset(block, u16::from(page));
234            let data = &img[off..off + protocol::FLASH_PAGE_BYTES];
235            dev.send(&map.write_firmware_page(index, block, page, data)?)?;
236            std::thread::sleep(Duration::from_millis(6));
237        }
238    }
239
240    // Relock before verifying, so the region is protected even if we stop here.
241    dev.send(&protocol::set_program_writable(index, false))?;
242
243    p.err("firmware: verify");
244    let bad = verify_firmware(m, &mut dev, index, img, wait)?;
245    if bad == 0 {
246        p.err("firmware: bank verified");
247    } else {
248        // Not fatal: provision writes one block at a time and verifies the
249        // whole bank itself once every path has run.
250        warn(
251            p,
252            format!(
253                "{bad} bytes differ after writing; golden bank at 0x{:02x} untouched; \
254             recover with: rxp firmware write {backup} --backup {backup} --commit",
255                map.golden_block
256            ),
257        );
258    }
259    Ok(())
260}
261
262/// Read page 0 of each block and report what it looks like. Read-only.
263pub fn scan_flash(
264    ctx: &Ctx,
265    first: u8,
266    last: u8,
267    index: u16,
268    wait: u64,
269    p: &mut dyn Progress,
270) -> Result<()> {
271    let mut dev = open(ctx)?;
272    for blk in first..=last {
273        let page = u16::from(blk) << 8;
274        let Ok(d) = read_chunk(&mut dev, index, page, wait) else {
275            continue;
276        };
277        if d.iter().all(|&b| b == 0xff) || d.iter().all(|&b| b == 0) {
278            continue;
279        }
280        let kind = if contains_lattice_header(&d) {
281            "lattice bitstream header"
282        } else if d.starts_with(&[0x20, 0x20, 0x19, 0xbe]) {
283            "rcvbp config"
284        } else {
285            "data"
286        };
287        p.out(&format!(
288            "0x{blk:02x}  0x{:06x}  {kind:<24} {}",
289            u32::from(blk) << 16,
290            hex(&d[..d.len().min(12)], " ")
291        ));
292    }
293    Ok(())
294}
295
296/// Dump an arbitrary flash range using linear addressing. Read-only.
297pub fn dump_range(
298    ctx: &Ctx,
299    start: &str,
300    len: &str,
301    index: u16,
302    wait: u64,
303    out: &str,
304    p: &mut dyn Progress,
305) -> Result<()> {
306    let start = u32::from_str_radix(start.trim_start_matches("0x"), 16).context("bad --start")?;
307    let len = u32::from_str_radix(len.trim_start_matches("0x"), 16).context("bad --len")?;
308    let mut dev = open(ctx)?;
309    let mut image = Vec::with_capacity(len as usize);
310
311    // The card answers linear reads one 256-byte page at a time; asking for
312    // more returns nothing useful.
313    let step = protocol::FLASH_PAGE_BYTES as u32;
314    let mut addr = start;
315    let mut misses = 0u32;
316    while addr < start + len {
317        dev.send(&protocol::read_flash_linear(index, addr, step))?;
318        // Linear reads answer with a different type than page reads; take any
319        // reply long enough to hold a page.
320        let reply = await_reply(&mut dev, Duration::from_secs(wait), |f| {
321            (f.len() >= 15 + step as usize).then(|| f[15..15 + step as usize].to_vec())
322        })?;
323        if let Some(data) = reply {
324            image.extend_from_slice(&data);
325        } else {
326            misses += 1;
327            image.extend(std::iter::repeat_n(0xffu8, step as usize));
328            if misses > 8 {
329                warn(
330                    p,
331                    format!("giving up after {misses} unanswered reads at 0x{addr:08x}"),
332                );
333                break;
334            }
335        }
336        if (addr - start).is_multiple_of(0x10000) {
337            p.err(&format!("read 0x{addr:08x}"));
338        }
339        addr += step;
340    }
341    std::fs::write(out, &image).with_context(|| format!("write {out}"))?;
342    if misses > 0 {
343        warn(p, format!("{misses} unanswered reads filled with 0xff"));
344    }
345    p.out(out);
346    Ok(())
347}
348
349/// Dump an entire 64KB flash block. Read-only.
350pub fn dump_flash(
351    ctx: &Ctx,
352    block: u8,
353    blocks: u16,
354    index: u16,
355    wait: u64,
356    out: &str,
357    p: &mut dyn Progress,
358) -> Result<()> {
359    let mut dev = open(ctx)?;
360    let image = read_blocks(&mut dev, index, block, blocks, wait, p)?;
361    std::fs::write(out, &image).with_context(|| format!("write {out}"))?;
362    p.out(out);
363    Ok(())
364}
365
366/// Read the whole primary firmware bank into memory.
367pub fn read_primary_bank(
368    m: &CardModel,
369    dev: &mut Link,
370    index: u16,
371    wait: u64,
372    p: &mut dyn Progress,
373) -> Result<Vec<u8>> {
374    let blocks = m.memory.primary_blocks();
375    read_blocks(dev, index, blocks.start, u16::from(m.memory.bank_blocks()), wait, p)
376}
377
378/// Read the whole parameter block into memory.
379fn read_block(m: &CardModel, dev: &mut Link, index: u16, wait: u64, p: &mut dyn Progress) -> Result<Vec<u8>> {
380    read_blocks(dev, index, m.memory.parameter_block, 1, wait, p)
381}
382
383/// Read `count` consecutive 64KB blocks starting at `first`; stops between
384/// blocks when cancelled.
385///
386/// # Errors
387/// Fails if the card stops answering partway through.
388pub fn read_blocks(
389    dev: &mut Link,
390    index: u16,
391    first: u8,
392    count: u16,
393    wait: u64,
394    p: &mut dyn Progress,
395) -> Result<Vec<u8>> {
396    let mut image = Vec::with_capacity(64 * 1024 * count as usize);
397    for b in 0..count {
398        check(p)?;
399        let block = first.wrapping_add(b as u8);
400        for lo in (0u16..0x100).step_by(protocol::FLASH_PAGES_PER_CHUNK as usize) {
401            let page = (u16::from(block) << 8) | lo;
402            image.extend_from_slice(&read_chunk(dev, index, page, wait)?);
403        }
404        p.err(&format!("read 0x{block:02x}"));
405    }
406    Ok(image)
407}
408
409/// Erase the parameter block, write `image` over it, then verify and repair.
410///
411/// Pages that did not take are rewritten. A page can only be rewritten while
412/// still erased, so a mismatched page holding other data re-erases the block.
413pub fn rewrite_block(
414    m: &CardModel,
415    dev: &mut Link,
416    index: u16,
417    image: &[u8],
418    wait: u64,
419    must_verify: std::ops::Range<usize>,
420    p: &mut dyn Progress,
421) -> Result<()> {
422    anyhow::ensure!(image.len() == 64 * 1024, "image must be exactly 64KB");
423    let map = flash_map(m);
424    let pages: Vec<&[u8]> = image.chunks(protocol::FLASH_PAGE_BYTES).collect();
425
426    for attempt in 1..=4 {
427        let repair: Vec<usize> = if attempt == 1 {
428            erase_and_settle(&map, dev, index, p)?;
429            (0..pages.len()).collect()
430        } else {
431            let after = read_block(m, dev, index, wait, p)?;
432            let bad = mismatched_pages(&after, &pages);
433            if bad.is_empty() {
434                p.err("flash: block verified");
435                return Ok(());
436            }
437            let dirty = bad
438                .iter()
439                .any(|&i| page(&after, i).iter().any(|&b| b != 0xff));
440            p.err(&format!(
441                "flash: attempt {attempt}: {} pages to rewrite{}",
442                bad.len(),
443                if dirty { " (re-erasing first)" } else { "" }
444            ));
445            if dirty {
446                erase_and_settle(&map, dev, index, p)?;
447                (0..pages.len()).collect()
448            } else {
449                bad
450            }
451        };
452
453        for (n, &i) in repair.iter().enumerate() {
454            dev.send(&map.write_page(index, map.param_block, i as u8, pages[i])?)?;
455            std::thread::sleep(Duration::from_millis(8));
456            if repair.len() > 32 && n.is_multiple_of(64) {
457                p.err(&format!("flash: page {n}/{}", repair.len()));
458            }
459        }
460    }
461    // Some pages sit outside the window the card lets us write. Those are not
462    // part of the configuration blob, so report them rather than failing.
463    let after = read_block(m, dev, index, wait, p)?;
464    let bad = mismatched_pages(&after, &pages);
465    let in_config = bad.iter().any(|i| must_verify.contains(i));
466    anyhow::ensure!(
467        !in_config,
468        "verify failed: {} pages differ, including configuration pages",
469        bad.len()
470    );
471    p.err(&format!(
472        "flash: configuration pages verified; {} page(s) outside them would not take writes: {}",
473        bad.len(),
474        bad.iter()
475            .map(|i| format!("0x{i:02x}"))
476            .collect::<Vec<_>>()
477            .join(", ")
478    ));
479    Ok(())
480}
481
482/// Erase the parameter block and wait for the chip to finish.
483fn erase_and_settle(map: &protocol::FlashMap, dev: &mut Link, index: u16, p: &mut dyn Progress) -> Result<()> {
484    p.err(&format!("flash: erase 0x{:02x}", map.param_block));
485    dev.send(&map.erase_block(index, map.param_block)?)?;
486    // Pages written while the erase is still running are silently dropped.
487    std::thread::sleep(Duration::from_secs(3));
488    Ok(())
489}
490
491/// Install a .rcvbp into the card's parameter flash.
492///
493/// The erase covers the whole 64KB block, so the block is read first, only the
494/// parameter region is replaced, and everything else is written back byte for
495/// byte. A backup of the original block is always saved before any write.
496#[allow(clippy::too_many_arguments)]
497pub fn write_config(
498    ctx: &Ctx,
499    config: &str,
500    commit: bool,
501    backup: &str,
502    base_image: Option<&str>,
503    index: u16,
504    wait: u64,
505    p: &mut dyn Progress,
506) -> Result<()> {
507    let parsed = rcvbp::Rcvbp::load(config)?;
508    let file = std::fs::read(config).with_context(|| format!("read {config}"))?;
509    let m = ctx.model()?;
510    let at = m.memory.boot_image.rcvbp;
511    let max = m.memory.boot_image.rcvbp_max;
512    anyhow::ensure!(
513        file.len() <= max,
514        "{config} is {} bytes, over the {max}-byte limit the card accepts",
515        file.len()
516    );
517    if !has_chip_regs(&parsed) {
518        warn(p, format!("{config} has no driver-chip register table"));
519    }
520
521    let mut dev = open(ctx)?;
522    let original = match base_image {
523        Some(path) => {
524            let img = std::fs::read(path).with_context(|| format!("read {path}"))?;
525            anyhow::ensure!(img.len() == 64 * 1024, "{path} must be exactly 65536 bytes");
526            img
527        }
528        None => {
529            let img = read_block(m, &mut dev, index, wait, p)?;
530            std::fs::write(backup, &img).with_context(|| format!("write {backup}"))?;
531            p.err(&format!("flash: backup {backup}"));
532            img
533        }
534    };
535
536    let mut image = original.clone();
537    let old_len = u32::from_le_bytes(image[at..at + 4].try_into()?) as usize;
538    // A base image cut from a firmware dump holds bitstream bytes here, not a
539    // length; clamp to the area the card uses.
540    let old_len = old_len.min(max);
541    let region = at + 4 + old_len.max(file.len());
542    anyhow::ensure!(region <= image.len(), "parameter region overruns the block");
543    // Clear the old blob so no tail of it survives behind the new one.
544    image[at..region].fill(0);
545    image[at..at + 4].copy_from_slice(&(file.len() as u32).to_le_bytes());
546    image[at + 4..at + 4 + file.len()].copy_from_slice(&file);
547
548    let changed = original.iter().zip(&image).filter(|(a, b)| a != b).count();
549    p.err(&format!(
550        "flash: parameter blob {old_len} -> {} bytes, {changed} bytes of block 0x{:02x} change",
551        file.len(),
552        m.memory.parameter_block
553    ));
554
555    if !commit {
556        p.out("dry run: nothing written (add --commit)");
557        return Ok(());
558    }
559
560    // Only the pages holding the configuration blob itself have to verify.
561    let first = at / protocol::FLASH_PAGE_BYTES;
562    let last = (at + 4 + file.len()).div_ceil(protocol::FLASH_PAGE_BYTES);
563    rewrite_block(m, &mut dev, index, &image, wait, first..last, p).with_context(|| {
564        format!(
565            "original block saved at {backup}; restore with: rxp flash restore-block {backup} --commit"
566        )
567    })?;
568
569    // The erase also clears the screen-size record, which only the linear
570    // path can rewrite. A firmware image holds bitstream bytes at that
571    // offset, not a record, so only a block read off the card is put back.
572    let map = flash_map(m);
573    let off = map.screen_record_addr as usize % image.len();
574    let record = &original[off..off + protocol::SCREEN_RECORD_LEN];
575    if base_image.is_none() && record.iter().any(|&b| b != 0xff) {
576        dev.send(&map.write_screen_record(index, map.screen_record_addr, record)?)?;
577        std::thread::sleep(Duration::from_millis(100));
578        p.err(&format!(
579            "flash: screen-size record restored ({}x{})",
580            u16::from_be_bytes([record[6], record[7]]),
581            u16::from_be_bytes([record[8], record[9]])
582        ));
583    }
584
585    p.err("power-cycle the card to apply");
586    Ok(())
587}
588
589/// Write a previously dumped block image back to the card, for recovery.
590pub fn restore_flash(
591    ctx: &Ctx,
592    image_path: &str,
593    commit: bool,
594    index: u16,
595    p: &mut dyn Progress,
596) -> Result<()> {
597    let image = std::fs::read(image_path).with_context(|| format!("read {image_path}"))?;
598    anyhow::ensure!(
599        image.len() == 64 * 1024,
600        "{image_path} is {} bytes; a block image must be exactly 65536",
601        image.len()
602    );
603    let m = ctx.model()?;
604    if !commit {
605        p.out(&format!(
606            "dry run: {image_path} -> block 0x{:02x} (add --commit)",
607            m.memory.parameter_block
608        ));
609        return Ok(());
610    }
611    let mut dev = open(ctx)?;
612    rewrite_block(m, &mut dev, index, &image, 2, 0..256, p)?;
613    Ok(())
614}