Skip to main content

colorlight/
upgrade.rs

1//! SDRAM-staged firmware upgrade: descriptor query, 1 KiB chunk uploads
2//! (unacknowledged; the caller paces them), erase, program, completion poll.
3//!
4//! The card programs only 0x000000-0x02FFFF and 0x080000-0x0AFFFF from the
5//! staged image; measured after the 16.53 install, the 320 KB between reads
6//! back unchanged (`docs/provisioning.md`, `docs/fpga/flash-layout.md`).
7
8use super::{frame_with, indexed};
9
10const SDRAM_TYPE: [u8; 2] = [0x1a, 0x00];
11
12const OP_DATA: u8 = 0x01;
13const OP_PROGRAM: u8 = 0x03;
14const OP_ERASE: u8 = 0x05;
15
16/// Bytes per upload chunk.
17pub const CHUNK: usize = 1024;
18
19/// Which stored image an erase or program operation targets.
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum Partition {
22    /// The image the card normally runs.
23    Primary,
24    /// The golden backup; only on cards whose descriptor reports one.
25    Golden,
26}
27
28impl Partition {
29    const fn selector(self) -> u8 {
30        match self {
31            Self::Primary => 0x04,
32            Self::Golden => 0x0d,
33        }
34    }
35}
36
37/// Any SDRAM operation frame: [1..3] receiver selector BE, [3] opcode,
38/// [4] partition/flag, [5..8] 24-bit BE offset, [8..12] u32 BE length, data.
39fn sdram_frame(sel: u16, op: u8, flag: u8, offset: u32, len: u32, data: &[u8]) -> Vec<u8> {
40    // The vendor always allocates room for a full chunk, even when sending none.
41    let body = data.len().max(CHUNK);
42    frame_with(SDRAM_TYPE, 12 + body, |p| {
43        indexed(p, sel, op);
44        p[4] = flag;
45        p[5..8].copy_from_slice(&offset.to_be_bytes()[1..]);
46        p[8..12].copy_from_slice(&len.to_be_bytes());
47        p[12..12 + data.len()].copy_from_slice(data);
48    })
49}
50
51/// Upload one chunk into SDRAM; `offset` is its byte position in the image.
52#[must_use]
53pub fn sdram_chunk(sel: u16, offset: u32, data: &[u8]) -> Vec<u8> {
54    sdram_frame(sel, OP_DATA, 0x00, offset, data.len() as u32, data)
55}
56
57/// Erase `len` bytes of the partition.
58#[must_use]
59pub fn sdram_erase(sel: u16, partition: Partition, len: u32) -> Vec<u8> {
60    sdram_frame(sel, OP_ERASE, partition.selector(), 0, len, &[])
61}
62
63/// Program flash from the image staged in SDRAM.
64#[must_use]
65pub fn sdram_program(sel: u16, partition: Partition, len: u32) -> Vec<u8> {
66    sdram_frame(sel, OP_PROGRAM, partition.selector(), 0, len, &[])
67}
68
69/// The card's reply to `upgrade_info`: image geometry and upgrade capabilities.
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub struct Descriptor {
72    /// Flash address the image starts at.
73    pub start: u32,
74    /// Bytes programmed into flash.
75    pub image_len: u32,
76    /// Required source-file size, including trailing padding.
77    pub file_len: u32,
78    /// Type byte for direct flash operations.
79    pub flash_op_type: u8,
80    capabilities: u8,
81}
82
83impl Descriptor {
84    /// The card can stage an image in SDRAM and program itself.
85    #[must_use]
86    pub const fn supports_sdram(self) -> bool {
87        self.capabilities & 0b0001 != 0
88    }
89
90    /// The card keeps a golden backup image.
91    #[must_use]
92    pub const fn has_golden(self) -> bool {
93        self.capabilities & 0b0010 != 0
94    }
95
96    /// The card accepts a partition selection.
97    #[must_use]
98    pub const fn supports_select_part(self) -> bool {
99        self.capabilities & 0b0100 != 0
100    }
101
102    /// The card accepts upgrades aimed at the golden bank.
103    #[must_use]
104    pub const fn supports_golden_upgrade(self) -> bool {
105        self.capabilities & 0b1000 != 0
106    }
107
108    /// Chunks needed to stage an image of this size.
109    #[must_use]
110    pub const fn chunks(self) -> usize {
111        (self.image_len as usize).div_ceil(CHUNK)
112    }
113
114    /// Vendor delay before the first completion poll: 150 ms per 64 KiB
115    /// block, at least 1000 ms (`timings_match_the_vendor_formulas`).
116    #[must_use]
117    pub const fn first_poll_ms(self) -> u64 {
118        let blocks = (self.image_len as u64).div_ceil(0x10000);
119        let by_size = 150 * blocks;
120        if by_size > 1000 {
121            by_size
122        } else {
123            1000
124        }
125    }
126
127    /// Vendor estimate of programming time: 500 ms per 64 KiB block plus
128    /// 3 ms per 256-byte page.
129    #[must_use]
130    pub const fn estimated_ms(self) -> u64 {
131        let blocks = (self.image_len as u64).div_ceil(0x10000);
132        let pages = (self.image_len as u64).div_ceil(0x100);
133        500 * blocks + 3 * pages
134    }
135}
136
137/// Decode a `Descriptor` from the 0x08xx reply; offsets below are relative to
138/// frame offset 12.
139#[must_use]
140pub fn parse_descriptor(eth_frame: &[u8]) -> Option<Descriptor> {
141    let p = eth_frame.get(12..)?;
142    // Bit 1 of the type's second byte marks a valid descriptor.
143    if *p.first()? != 0x08 || p.get(1)? & 0b10 == 0 {
144        return None;
145    }
146    let byte = |i: usize| p.get(i).copied();
147    let image_len = u32::from(byte(0x18)?) << 16 | u32::from(byte(0x19)?) << 8;
148    // File length low byte = [0x1b] + 4 * [0x1a], as the vendor computes it.
149    let low = u32::from(byte(0x1b)?.wrapping_add(byte(0x1a)?.wrapping_mul(4)));
150    Some(Descriptor {
151        start: u32::from(byte(0x16)?) << 16 | u32::from(byte(0x17)?) << 8,
152        image_len,
153        file_len: image_len | (low & 0xff),
154        flash_op_type: byte(0x12)?,
155        capabilities: byte(0x13)?,
156    })
157}
158
159/// Completion is polled with `upgrade_info`; bit 1 at payload offset 0xc0 of
160/// the reply is set once programming has finished.
161#[must_use]
162pub fn programming_finished(eth_frame: &[u8]) -> bool {
163    eth_frame
164        .get(12 + 0xc0)
165        .is_some_and(|status| status & 0b10 != 0)
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use crate::BROADCAST;
172
173    /// The `upgrade_info` reply captured from the bench card (fw 16.53).
174    fn real_reply() -> Vec<u8> {
175        let mut f = vec![0u8; 12];
176        f.extend_from_slice(&[0x08, 0x02]);
177        f.extend_from_slice(&[
178            0x00, 0x1a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
179            0xff, 0x00, 0x36, 0x05, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x20, 0x00,
180        ]);
181        f.resize(1070, 0);
182        f
183    }
184
185    #[test]
186    fn the_real_reply_decodes_as_the_card_reported() {
187        let d = parse_descriptor(&real_reply()).expect("should decode");
188        assert_eq!(d.start, 0x0000_0000, "image starts at zero");
189        assert_eq!(d.image_len, 0x000b_0000, "720896 bytes programmed");
190        assert_eq!(d.file_len, 0x000b_0080, "721024 bytes of source file");
191        assert_eq!(d.flash_op_type, 0x36);
192        assert!(d.supports_sdram(), "this card stages via SDRAM");
193        assert!(!d.has_golden(), "and reports no golden bank");
194        assert!(d.supports_select_part());
195        assert!(!d.supports_golden_upgrade());
196    }
197
198    #[test]
199    fn the_image_needs_704_chunks() {
200        let d = parse_descriptor(&real_reply()).unwrap();
201        assert_eq!(d.chunks(), 704);
202    }
203
204    #[test]
205    fn timings_match_the_vendor_formulas() {
206        let d = parse_descriptor(&real_reply()).unwrap();
207        // 11 blocks, 2816 pages
208        assert_eq!(d.first_poll_ms(), 1650);
209        assert_eq!(d.estimated_ms(), 500 * 11 + 3 * 2816);
210    }
211
212    #[test]
213    fn a_reply_of_the_wrong_type_is_rejected() {
214        let mut f = real_reply();
215        f[12] = 0x09;
216        assert!(parse_descriptor(&f).is_none());
217    }
218
219    #[test]
220    fn a_chunk_frame_matches_the_documented_layout() {
221        let data: Vec<u8> = (0..CHUNK).map(|i| i as u8).collect();
222        let f = sdram_chunk(BROADCAST, 0x000c_0400, &data);
223        assert_eq!(f.len(), 1050, "12 MAC + 2 type + 12 header + 1024 data");
224        assert_eq!(&f[12..14], &SDRAM_TYPE);
225        assert_eq!(&f[15..17], &BROADCAST.to_be_bytes());
226        assert_eq!(f[17], OP_DATA);
227        assert_eq!(f[18], 0x00);
228        assert_eq!(&f[19..22], &[0x0c, 0x04, 0x00], "24-bit big-endian offset");
229        assert_eq!(&f[22..26], &1024u32.to_be_bytes());
230        assert_eq!(&f[26..], &data[..]);
231    }
232
233    #[test]
234    fn erase_and_program_carry_the_partition_and_length() {
235        let len = 0x000b_0000;
236        let e = sdram_erase(BROADCAST, Partition::Primary, len);
237        assert_eq!(e[17], OP_ERASE);
238        assert_eq!(e[18], 0x04, "primary partition selector");
239        assert_eq!(&e[19..22], &[0, 0, 0], "no address on erase");
240        assert_eq!(&e[22..26], &len.to_be_bytes());
241
242        let p = sdram_program(BROADCAST, Partition::Primary, len);
243        assert_eq!(p[17], OP_PROGRAM);
244        // Program differs from erase only in the opcode.
245        assert_eq!(&p[18..], &e[18..]);
246    }
247
248    #[test]
249    fn the_golden_partition_uses_its_own_selector() {
250        let g = sdram_erase(BROADCAST, Partition::Golden, 0x1000);
251        assert_eq!(g[18], 0x0d);
252    }
253
254    #[test]
255    fn completion_is_read_from_the_status_bit() {
256        let mut f = vec![0u8; 12 + 0xc1];
257        assert!(!programming_finished(&f));
258        f[12 + 0xc0] = 0b10;
259        assert!(programming_finished(&f));
260    }
261}