Skip to main content

autd3_cpu_wire/
fpga_update.rs

1use crate::params::{
2    FLASH_END, FLASH_IMAGE_BASE, FLASH_USR_ACCESS_GOLDEN, FLASH_USR_ACCESS_UPDATE,
3    FLASH_WRITABLE_BASE, FUNC_FLASH_OTA_BIT,
4};
5
6pub const FPGA_FLASH_BYTES: u32 = FLASH_END;
7pub const FPGA_SECTOR_BYTES: u32 = 0x1_0000;
8pub const FPGA_GOLDEN_REGION_END: u32 = FLASH_WRITABLE_BASE;
9pub const FPGA_IMAGE_BASE: u32 = FLASH_IMAGE_BASE;
10pub const FPGA_IMAGE_CAPACITY: u32 = FLASH_END - FLASH_IMAGE_BASE;
11pub const FPGA_BARRIER_BASE: u32 = FPGA_GOLDEN_REGION_END - 0x100;
12pub const FPGA_BARRIER_TIMER: u32 = 0x4000_4000;
13pub const FPGA_USR_ACCESS_GOLDEN: u32 = FLASH_USR_ACCESS_GOLDEN;
14pub const FPGA_USR_ACCESS_UPDATE: u32 = FLASH_USR_ACCESS_UPDATE;
15pub const FPGA_FUNC_FLASH_OTA: u8 = 1 << FUNC_FLASH_OTA_BIT;
16pub const FPGA_REBOOT_DELAY_MS: u16 = 100;
17pub const FPGA_RECONFIG_SETTLE_MS: u16 = 3000;
18pub const FPGA_REBOOT_ATTEMPTS: u8 = 3;
19pub const FPGA_RECONFIG_WORST_MS: u32 = FPGA_REBOOT_DELAY_MS as u32
20    + FPGA_REBOOT_ATTEMPTS as u32 * FPGA_RECONFIG_SETTLE_MS as u32
21    + (FPGA_REBOOT_ATTEMPTS as u32 - 1);
22
23const _: () = assert!(FPGA_GOLDEN_REGION_END.is_multiple_of(FPGA_SECTOR_BYTES));
24const _: () = assert!(FPGA_IMAGE_BASE >= FPGA_GOLDEN_REGION_END);
25const _: () = assert!(FPGA_FLASH_BYTES.is_multiple_of(FPGA_SECTOR_BYTES));
26const _: () =
27    assert!(FPGA_BARRIER_BASE as usize + FPGA_BARRIER_BYTES <= FPGA_GOLDEN_REGION_END as usize);
28
29#[must_use]
30pub const fn is_plausible_fpga_length(length: u32) -> bool {
31    length > 0 && length <= FPGA_IMAGE_CAPACITY
32}
33
34crate::wire_enum! {
35    pub enum FpgaBootImage {
36        Unknown = 0x00,
37        Golden = 0x01,
38        Update = 0x02,
39    }
40}
41
42impl FpgaBootImage {
43    #[must_use]
44    pub const fn from_usr_access(value: u32) -> Self {
45        match value {
46            FPGA_USR_ACCESS_GOLDEN => Self::Golden,
47            FPGA_USR_ACCESS_UPDATE => Self::Update,
48            _ => Self::Unknown,
49        }
50    }
51}
52
53pub const SYNC_WORD: u32 = 0xAA99_5566;
54
55const BARRIER_WORDS: [u32; 14] = [
56    0xFFFF_FFFF,
57    0xFFFF_FFFF,
58    0x0000_00BB,
59    0x1122_0044,
60    0xFFFF_FFFF,
61    0xFFFF_FFFF,
62    SYNC_WORD,
63    0x2000_0000,
64    0x2000_0000,
65    0x3002_2001,
66    FPGA_BARRIER_TIMER,
67    0x2000_0000,
68    0x2000_0000,
69    0x2000_0000,
70];
71pub const FPGA_BARRIER_BYTES: usize = BARRIER_WORDS.len() * 4;
72
73#[must_use]
74pub const fn fpga_barrier_image() -> [u8; FPGA_BARRIER_BYTES] {
75    let mut out = [0u8; FPGA_BARRIER_BYTES];
76    let mut i = 0;
77    while i < BARRIER_WORDS.len() {
78        let b = BARRIER_WORDS[i].to_be_bytes();
79        out[4 * i] = b[0];
80        out[4 * i + 1] = b[1];
81        out[4 * i + 2] = b[2];
82        out[4 * i + 3] = b[3];
83        i += 1;
84    }
85    out
86}
87
88const REG_CMD: u32 = 0x04;
89const REG_AXSS: u32 = 0x0D;
90const REG_WBSTAR: u32 = 0x10;
91const REG_TIMER: u32 = 0x11;
92const TIMER_CFG_MON: u32 = 1 << 30;
93const CMD_IPROG: u32 = 0x0F;
94const CMD_DESYNC: u32 = 0x0D;
95
96#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
97pub struct BitstreamSummary {
98    pub synced: bool,
99    pub usr_access: Option<u32>,
100    pub wbstar: Option<u32>,
101    pub timer: Option<u32>,
102    pub iprog: bool,
103}
104
105impl BitstreamSummary {
106    #[must_use]
107    pub fn boot_image(&self) -> FpgaBootImage {
108        self.usr_access
109            .map_or(FpgaBootImage::Unknown, FpgaBootImage::from_usr_access)
110    }
111
112    #[must_use]
113    pub fn monitors_configuration(&self) -> bool {
114        self.timer
115            .is_some_and(|t| t & TIMER_CFG_MON != 0 && t & !(TIMER_CFG_MON | (1 << 31)) != 0)
116    }
117}
118
119fn word_at(bytes: &[u8], offset: usize) -> Option<u32> {
120    let b = bytes.get(offset..offset + 4)?;
121    Some(u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
122}
123
124fn find_sync(bytes: &[u8], from: usize) -> Option<usize> {
125    let pattern = SYNC_WORD.to_be_bytes();
126    bytes
127        .get(from..)?
128        .windows(4)
129        .position(|w| w == pattern)
130        .map(|p| from + p)
131}
132
133#[must_use]
134pub fn summarize_bitstream(bytes: &[u8]) -> BitstreamSummary {
135    let mut summary = BitstreamSummary::default();
136    let mut cursor = 0;
137    while let Some(sync) = find_sync(bytes, cursor) {
138        summary.synced = true;
139        let mut offset = sync + 4;
140        let mut last_reg = None;
141        loop {
142            let Some(header) = word_at(bytes, offset) else {
143                return summary;
144            };
145            offset += 4;
146            let (reg, count) = match header >> 29 {
147                0b001 => {
148                    let reg = (header >> 13) & 0x3FFF;
149                    let write = (header >> 27) & 0b11 == 0b10;
150                    last_reg = Some((reg, write));
151                    (Some((reg, write)), (header & 0x7FF) as usize)
152                }
153                0b010 => (last_reg, (header & 0x07FF_FFFF) as usize),
154                _ => (None, 0),
155            };
156            let count = match reg {
157                Some((_, true)) => count,
158                _ => 0,
159            };
160            if let Some((reg, true)) = reg
161                && count == 1
162                && let Some(value) = word_at(bytes, offset)
163            {
164                match reg {
165                    REG_AXSS => summary.usr_access = Some(value),
166                    REG_WBSTAR => summary.wbstar = Some(value),
167                    REG_TIMER => summary.timer = Some(value),
168                    REG_CMD if value == CMD_IPROG => summary.iprog = true,
169                    REG_CMD if value == CMD_DESYNC => {
170                        cursor = offset + 4;
171                        break;
172                    }
173                    _ => {}
174                }
175            }
176            offset = offset.saturating_add(count.saturating_mul(4));
177        }
178    }
179    summary
180}
181
182#[derive(Clone, Copy, PartialEq, Eq, Debug)]
183pub enum FpgaImageError {
184    Empty,
185    TooLarge { length: usize },
186    NoSyncWord,
187    NotAnUpdateImage { boot_image: FpgaBootImage },
188    Reboots,
189}
190
191pub fn validate_update_image(bytes: &[u8]) -> Result<BitstreamSummary, FpgaImageError> {
192    if bytes.is_empty() {
193        return Err(FpgaImageError::Empty);
194    }
195    if u32::try_from(bytes.len()).map_or(true, |len| !is_plausible_fpga_length(len)) {
196        return Err(FpgaImageError::TooLarge {
197            length: bytes.len(),
198        });
199    }
200    let summary = summarize_bitstream(bytes);
201    if !summary.synced {
202        return Err(FpgaImageError::NoSyncWord);
203    }
204    if summary.iprog || summary.wbstar.is_some_and(|addr| addr != 0) {
205        return Err(FpgaImageError::Reboots);
206    }
207    match summary.boot_image() {
208        FpgaBootImage::Update => Ok(summary),
209        boot_image => Err(FpgaImageError::NotAnUpdateImage { boot_image }),
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    extern crate std;
218    use std::vec::Vec;
219
220    fn stream(words: &[u32]) -> Vec<u8> {
221        let mut out = std::vec![0xFF; 16];
222        out.extend_from_slice(&[0x00, 0x00, 0x00, 0xBB, 0x11, 0x22, 0x00, 0x44]);
223        for w in words {
224            out.extend_from_slice(&w.to_be_bytes());
225        }
226        out
227    }
228
229    const NOOP: u32 = 0x2000_0000;
230    const WRITE_AXSS: u32 = 0x3001_A001;
231    const WRITE_WBSTAR: u32 = 0x3002_0001;
232    const WRITE_CMD: u32 = 0x3000_8001;
233    const WRITE_TIMER: u32 = 0x3002_2001;
234    const WRITE_FDRI: u32 = 0x3000_4000;
235
236    #[test]
237    fn the_layout_keeps_golden_below_the_slot() {
238        assert_eq!(FPGA_GOLDEN_REGION_END, 0x80_0000);
239        assert_eq!(FPGA_IMAGE_BASE, 0x80_0100);
240        assert_eq!(FPGA_IMAGE_CAPACITY, 0x7F_FF00);
241        assert_eq!(FPGA_FUNC_FLASH_OTA, 0x04);
242        assert!(!is_plausible_fpga_length(0));
243        assert!(is_plausible_fpga_length(FPGA_IMAGE_CAPACITY));
244        assert!(!is_plausible_fpga_length(FPGA_IMAGE_CAPACITY + 1));
245    }
246
247    #[test]
248    fn usr_access_names_the_image() {
249        assert_eq!(FPGA_USR_ACCESS_GOLDEN.to_be_bytes(), *b"GOLD");
250        assert_eq!(FPGA_USR_ACCESS_UPDATE.to_be_bytes(), *b"UPDT");
251        assert_eq!(
252            FpgaBootImage::from_usr_access(FPGA_USR_ACCESS_GOLDEN),
253            FpgaBootImage::Golden
254        );
255        assert_eq!(
256            FpgaBootImage::from_usr_access(FPGA_USR_ACCESS_UPDATE),
257            FpgaBootImage::Update
258        );
259        assert_eq!(
260            FpgaBootImage::from_usr_access(0xFFFF_FFFF),
261            FpgaBootImage::Unknown
262        );
263    }
264
265    #[test]
266    fn an_update_image_passes() {
267        let bytes = stream(&[
268            SYNC_WORD,
269            NOOP,
270            WRITE_WBSTAR,
271            0,
272            WRITE_CMD,
273            0,
274            WRITE_AXSS,
275            FPGA_USR_ACCESS_UPDATE,
276            WRITE_FDRI,
277            0x5000_0003,
278            WRITE_WBSTAR,
279            WRITE_CMD,
280            CMD_IPROG,
281            NOOP,
282            WRITE_CMD,
283            CMD_DESYNC,
284            0xFFFF_FFFF,
285        ]);
286        let summary = validate_update_image(&bytes).unwrap();
287        assert_eq!(summary.usr_access, Some(FPGA_USR_ACCESS_UPDATE));
288        assert_eq!(summary.wbstar, Some(0));
289        assert!(!summary.iprog);
290    }
291
292    #[test]
293    fn the_timer_write_arms_the_watchdog_only_with_the_monitor_bit() {
294        let armed = summarize_bitstream(&stream(&[SYNC_WORD, WRITE_TIMER, 0x401D_0000]));
295        assert_eq!(armed.timer, Some(0x401D_0000));
296        assert!(armed.monitors_configuration());
297        assert!(
298            !summarize_bitstream(&stream(&[SYNC_WORD, WRITE_TIMER, 0])).monitors_configuration()
299        );
300        assert!(
301            !summarize_bitstream(&stream(&[SYNC_WORD, WRITE_TIMER, 0x001D_0000]))
302                .monitors_configuration()
303        );
304        assert!(
305            !summarize_bitstream(&stream(&[SYNC_WORD, WRITE_TIMER, TIMER_CFG_MON]))
306                .monitors_configuration()
307        );
308        assert!(!summarize_bitstream(&stream(&[SYNC_WORD, NOOP])).monitors_configuration());
309    }
310
311    #[test]
312    fn a_golden_image_is_rejected() {
313        let bytes = stream(&[
314            SYNC_WORD,
315            NOOP,
316            WRITE_WBSTAR,
317            FPGA_IMAGE_BASE,
318            WRITE_CMD,
319            CMD_IPROG,
320            NOOP,
321            WRITE_AXSS,
322            FPGA_USR_ACCESS_GOLDEN,
323        ]);
324        let summary = summarize_bitstream(&bytes);
325        assert_eq!(summary.wbstar, Some(FPGA_IMAGE_BASE));
326        assert!(summary.iprog);
327        assert_eq!(summary.boot_image(), FpgaBootImage::Golden);
328        assert_eq!(validate_update_image(&bytes), Err(FpgaImageError::Reboots));
329    }
330
331    #[test]
332    fn a_nonzero_wbstar_alone_is_rejected() {
333        assert_eq!(
334            validate_update_image(&stream(&[
335                SYNC_WORD,
336                WRITE_WBSTAR,
337                FPGA_IMAGE_BASE,
338                WRITE_AXSS,
339                FPGA_USR_ACCESS_UPDATE
340            ])),
341            Err(FpgaImageError::Reboots)
342        );
343    }
344
345    #[test]
346    fn a_missing_or_foreign_usr_access_is_rejected() {
347        assert_eq!(
348            validate_update_image(&stream(&[SYNC_WORD, NOOP])),
349            Err(FpgaImageError::NotAnUpdateImage {
350                boot_image: FpgaBootImage::Unknown
351            })
352        );
353        assert_eq!(
354            validate_update_image(&stream(&[SYNC_WORD, WRITE_AXSS, FPGA_USR_ACCESS_GOLDEN])),
355            Err(FpgaImageError::NotAnUpdateImage {
356                boot_image: FpgaBootImage::Golden
357            })
358        );
359    }
360
361    #[test]
362    fn the_barrier_syncs_and_arms_a_short_watchdog() {
363        let barrier = summarize_bitstream(&fpga_barrier_image());
364        assert!(barrier.synced);
365        assert_eq!(barrier.timer, Some(FPGA_BARRIER_TIMER));
366        assert!(barrier.monitors_configuration());
367        assert!(!barrier.iprog);
368        assert_eq!(barrier.wbstar, None);
369        assert_eq!(barrier.boot_image(), FpgaBootImage::Unknown);
370        assert_eq!(FPGA_BARRIER_BASE, 0x7F_FF00);
371    }
372
373    #[test]
374    fn a_stream_without_sync_is_rejected() {
375        assert_eq!(validate_update_image(&[]), Err(FpgaImageError::Empty));
376        assert_eq!(
377            validate_update_image(&[0xFF; 64]),
378            Err(FpgaImageError::NoSyncWord)
379        );
380    }
381
382    #[test]
383    fn packets_after_a_desync_are_found_after_the_next_sync() {
384        let bytes = stream(&[
385            SYNC_WORD,
386            WRITE_CMD,
387            CMD_DESYNC,
388            0xFFFF_FFFF,
389            SYNC_WORD,
390            WRITE_AXSS,
391            FPGA_USR_ACCESS_UPDATE,
392        ]);
393        assert_eq!(
394            summarize_bitstream(&bytes).boot_image(),
395            FpgaBootImage::Update
396        );
397    }
398
399    #[test]
400    fn data_words_are_not_parsed_as_packets() {
401        let bytes = stream(&[
402            SYNC_WORD,
403            WRITE_FDRI,
404            0x5000_0002,
405            WRITE_CMD,
406            CMD_IPROG,
407            WRITE_AXSS,
408            FPGA_USR_ACCESS_UPDATE,
409        ]);
410        let summary = summarize_bitstream(&bytes);
411        assert!(!summary.iprog);
412        assert_eq!(summary.boot_image(), FpgaBootImage::Update);
413    }
414}