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}