1use zerocopy::little_endian::U32;
2use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
3
4pub const FLASH_BYTES: u32 = 0x40_0000;
5pub const FLASH_SECTOR_BYTES: u32 = 0x1000;
6pub const FLASH_PAGE_BYTES: u32 = 0x100;
7pub const LOADER_REGION_END: u32 = 0x1_0000;
8pub const SLOT_BYTES: u32 = 0x8_0000;
9pub const SLOT_A_BASE: u32 = LOADER_REGION_END;
10pub const SLOT_B_BASE: u32 = SLOT_A_BASE + SLOT_BYTES;
11pub const SLOT_HEADER_BYTES: u32 = FLASH_PAGE_BYTES;
12pub const SLOT_IMAGE_CAPACITY: u32 = SLOT_BYTES - SLOT_HEADER_BYTES;
13pub const IMAGE_MAGIC: u32 = u32::from_le_bytes(*b"AUT2");
14pub const IMAGE_VECTOR_BYTES: u32 = 0x40;
15pub const IMAGE_VECTOR_LOAD_ADDR: u32 = 0x0000_0000;
16pub const IMAGE_RESET_VECTOR_OFFSET: u32 = 0x20;
17pub const IMAGE_APP_LOAD_ADDR: u32 = 0x0004_0000;
18pub const ATCM_APP_REGION_END: u32 = 0x0007_0000;
19pub const IMAGE_APP_CAPACITY: u32 = ATCM_APP_REGION_END - IMAGE_APP_LOAD_ADDR;
20
21#[must_use]
22pub const fn is_plausible_length(length: u32) -> bool {
23 length > IMAGE_VECTOR_BYTES
24 && length <= SLOT_IMAGE_CAPACITY
25 && length - IMAGE_VECTOR_BYTES <= IMAGE_APP_CAPACITY
26}
27
28const _: () = assert!(IMAGE_VECTOR_BYTES + IMAGE_APP_CAPACITY <= SLOT_IMAGE_CAPACITY);
29const _: () = assert!(IMAGE_RESET_VECTOR_OFFSET < IMAGE_VECTOR_BYTES);
30const _: () = assert!(SLOT_A_BASE.is_multiple_of(FLASH_SECTOR_BYTES));
31const _: () = assert!(SLOT_B_BASE.is_multiple_of(FLASH_SECTOR_BYTES));
32const _: () = assert!(SLOT_B_BASE + SLOT_BYTES <= FLASH_BYTES);
33
34#[derive(Clone, Copy, PartialEq, Eq, Debug)]
35pub enum Slot {
36 A,
37 B,
38}
39
40impl Slot {
41 #[must_use]
42 pub const fn base(self) -> u32 {
43 match self {
44 Self::A => SLOT_A_BASE,
45 Self::B => SLOT_B_BASE,
46 }
47 }
48
49 #[must_use]
50 pub const fn image_base(self) -> u32 {
51 self.base() + SLOT_HEADER_BYTES
52 }
53
54 #[must_use]
55 pub const fn other(self) -> Self {
56 match self {
57 Self::A => Self::B,
58 Self::B => Self::A,
59 }
60 }
61}
62
63pub const IMAGE_STATUS_NORMAL: u32 = 0xFFFF_FFFF;
64pub const IMAGE_STATUS_TRIAL: u32 = 0x5A5A_5A5A;
65pub const IMAGE_STATUS_CONFIRMED: u32 = 0x0000_0000;
66pub const IMAGE_ATTEMPTS_UNTRIED: u32 = 0xFFFF_FFFF;
67pub const IMAGE_MAX_ATTEMPTS: u32 = 1;
68
69pub const IMAGE_HEADER_STATUS_OFFSET: u32 = 16;
70pub const IMAGE_HEADER_ATTEMPTS_OFFSET: u32 = 20;
71
72const _: () = assert!(IMAGE_STATUS_CONFIRMED & !IMAGE_STATUS_TRIAL == 0);
73const _: () = assert!(IMAGE_MAX_ATTEMPTS >= 1 && IMAGE_MAX_ATTEMPTS <= u32::BITS);
74
75#[must_use]
76pub const fn next_attempts(attempts: u32) -> u32 {
77 attempts << 1
78}
79
80#[derive(
81 FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned, Clone, Copy, PartialEq, Eq, Debug,
82)]
83#[repr(C)]
84pub struct ImageHeader {
85 pub magic: U32,
86 pub generation: U32,
87 pub length: U32,
88 pub crc32: U32,
89 pub status: U32,
90 pub attempts: U32,
91}
92
93const _: () = assert!(core::mem::size_of::<ImageHeader>() == 24);
94const _: () = assert!(core::mem::size_of::<ImageHeader>() <= SLOT_HEADER_BYTES as usize);
95const _: () =
96 assert!(core::mem::offset_of!(ImageHeader, status) == IMAGE_HEADER_STATUS_OFFSET as usize);
97const _: () =
98 assert!(core::mem::offset_of!(ImageHeader, attempts) == IMAGE_HEADER_ATTEMPTS_OFFSET as usize);
99
100impl ImageHeader {
101 #[must_use]
102 pub fn new(generation: u32, length: u32, crc32: u32) -> Self {
103 Self::with_status(generation, length, crc32, IMAGE_STATUS_NORMAL)
104 }
105
106 #[must_use]
107 pub fn new_trial(generation: u32, length: u32, crc32: u32) -> Self {
108 Self::with_status(generation, length, crc32, IMAGE_STATUS_TRIAL)
109 }
110
111 fn with_status(generation: u32, length: u32, crc32: u32, status: u32) -> Self {
112 Self {
113 magic: U32::new(IMAGE_MAGIC),
114 generation: U32::new(generation),
115 length: U32::new(length),
116 crc32: U32::new(crc32),
117 status: U32::new(status),
118 attempts: U32::new(IMAGE_ATTEMPTS_UNTRIED),
119 }
120 }
121
122 #[must_use]
123 pub fn is_plausible(&self) -> bool {
124 self.magic.get() == IMAGE_MAGIC && is_plausible_length(self.length.get())
125 }
126
127 #[must_use]
128 pub fn is_trial(&self) -> bool {
129 self.status.get() == IMAGE_STATUS_TRIAL
130 }
131
132 #[must_use]
133 pub fn needs_confirmation(&self) -> bool {
134 !matches!(
135 self.status.get(),
136 IMAGE_STATUS_NORMAL | IMAGE_STATUS_CONFIRMED
137 )
138 }
139
140 #[must_use]
141 pub fn attempts_used(&self) -> u32 {
142 self.attempts.get().count_zeros()
143 }
144
145 #[must_use]
146 pub fn is_boot_eligible(&self) -> bool {
147 match self.status.get() {
148 IMAGE_STATUS_NORMAL | IMAGE_STATUS_CONFIRMED => true,
149 IMAGE_STATUS_TRIAL => self.attempts_used() < IMAGE_MAX_ATTEMPTS,
150 _ => false,
151 }
152 }
153}
154
155#[derive(Clone, Copy, PartialEq, Eq, Debug)]
156pub struct SlotCandidate {
157 pub generation: u32,
158 pub eligible: bool,
159}
160
161#[must_use]
162pub fn select_slot(a: Option<u32>, b: Option<u32>) -> Option<(Slot, u32)> {
163 match (a, b) {
164 (Some(ga), Some(gb)) if gb > ga => Some((Slot::B, gb)),
165 (Some(ga), _) => Some((Slot::A, ga)),
166 (None, Some(gb)) => Some((Slot::B, gb)),
167 (None, None) => None,
168 }
169}
170
171#[must_use]
172pub fn select_boot_slot(a: Option<SlotCandidate>, b: Option<SlotCandidate>) -> Option<(Slot, u32)> {
173 let eligible = |c: Option<SlotCandidate>| c.filter(|c| c.eligible).map(|c| c.generation);
174 let any = |c: Option<SlotCandidate>| c.map(|c| c.generation);
175 select_slot(eligible(a), eligible(b)).or_else(|| select_slot(any(a), any(b)))
176}
177
178pub const CRC32_INIT: u32 = 0xFFFF_FFFF;
179
180#[must_use]
181pub const fn crc32_update(mut crc: u32, bytes: &[u8]) -> u32 {
182 let mut i = 0;
183 while i < bytes.len() {
184 crc ^= bytes[i] as u32;
185 let mut bit = 0;
186 while bit < 8 {
187 let mask = 0u32.wrapping_sub(crc & 1);
188 crc = (crc >> 1) ^ (0xEDB8_8320 & mask);
189 bit += 1;
190 }
191 i += 1;
192 }
193 crc
194}
195
196#[must_use]
197pub const fn crc32_finish(crc: u32) -> u32 {
198 !crc
199}
200
201#[must_use]
202pub const fn crc32(bytes: &[u8]) -> u32 {
203 crc32_finish(crc32_update(CRC32_INIT, bytes))
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn crc32_matches_the_reference_vector() {
212 assert_eq!(crc32(b"123456789"), 0xCBF4_3926);
213 assert_eq!(crc32(b""), 0);
214 }
215
216 #[test]
217 fn crc32_streams_in_pieces() {
218 let whole = crc32(b"hello world");
219 let mut crc = crc32_update(CRC32_INIT, b"hello ");
220 crc = crc32_update(crc, b"world");
221 assert_eq!(crc32_finish(crc), whole);
222 }
223
224 #[test]
225 fn slot_selection_prefers_the_newest_generation_and_a_on_ties() {
226 assert_eq!(select_slot(None, None), None);
227 assert_eq!(select_slot(Some(3), None), Some((Slot::A, 3)));
228 assert_eq!(select_slot(None, Some(7)), Some((Slot::B, 7)));
229 assert_eq!(select_slot(Some(3), Some(7)), Some((Slot::B, 7)));
230 assert_eq!(select_slot(Some(9), Some(7)), Some((Slot::A, 9)));
231 assert_eq!(select_slot(Some(5), Some(5)), Some((Slot::A, 5)));
232 }
233
234 #[test]
235 fn header_plausibility() {
236 let max = IMAGE_VECTOR_BYTES + IMAGE_APP_CAPACITY;
237 assert!(ImageHeader::new(0, IMAGE_VECTOR_BYTES + 1, 0).is_plausible());
238 assert!(ImageHeader::new(0, max, 0).is_plausible());
239 assert!(!ImageHeader::new(0, 0, 0).is_plausible());
240 assert!(!ImageHeader::new(0, IMAGE_VECTOR_BYTES, 0).is_plausible());
241 assert!(!ImageHeader::new(0, max + 1, 0).is_plausible());
242 assert!(!ImageHeader::new(0, SLOT_IMAGE_CAPACITY, 0).is_plausible());
243 assert!(!ImageHeader::new(0, SLOT_IMAGE_CAPACITY + 1, 0).is_plausible());
244 let mut blank = ImageHeader::new(0, IMAGE_VECTOR_BYTES + 1, 0);
245 blank.magic = U32::new(0xFFFF_FFFF);
246 assert!(!blank.is_plausible());
247 }
248
249 #[test]
250 fn the_image_magic_spells_the_format() {
251 assert_eq!(IMAGE_MAGIC.to_le_bytes(), *b"AUT2");
252 }
253
254 #[test]
255 fn trial_state_machine_only_clears_bits() {
256 let normal = ImageHeader::new(1, 100, 0);
257 assert!(!normal.needs_confirmation());
258 assert!(normal.is_boot_eligible());
259
260 let mut trial = ImageHeader::new_trial(2, 100, 0);
261 assert!(trial.is_trial());
262 assert!(trial.needs_confirmation());
263 assert_eq!(trial.attempts_used(), 0);
264 assert!(trial.is_boot_eligible());
265 assert_eq!(IMAGE_STATUS_TRIAL & normal.status.get(), IMAGE_STATUS_TRIAL);
266
267 let before = trial.attempts.get();
268 trial.attempts = U32::new(next_attempts(before));
269 assert_eq!(trial.attempts.get() & !before, 0);
270 assert_eq!(trial.attempts_used(), 1);
271 assert!(!trial.is_boot_eligible());
272
273 trial.status = U32::new(IMAGE_STATUS_CONFIRMED);
274 assert!(!trial.needs_confirmation());
275 assert!(trial.is_boot_eligible());
276 }
277
278 #[test]
279 fn an_interrupted_status_write_is_not_eligible() {
280 let mut h = ImageHeader::new_trial(2, 100, 0);
281 h.status = U32::new(IMAGE_STATUS_TRIAL & 0x00FF_FFFF);
282 assert!(h.needs_confirmation());
283 assert!(!h.is_trial());
284 assert!(!h.is_boot_eligible());
285 }
286
287 #[test]
288 fn untried_trials_boot_and_tried_ones_fall_back() {
289 let ok = |generation| {
290 Some(SlotCandidate {
291 generation,
292 eligible: true,
293 })
294 };
295 let spent = |generation| {
296 Some(SlotCandidate {
297 generation,
298 eligible: false,
299 })
300 };
301 assert_eq!(select_boot_slot(ok(1), ok(2)), Some((Slot::B, 2)));
302 assert_eq!(select_boot_slot(ok(1), spent(2)), Some((Slot::A, 1)));
303 assert_eq!(select_boot_slot(spent(3), ok(2)), Some((Slot::B, 2)));
304 assert_eq!(select_boot_slot(spent(1), spent(2)), Some((Slot::B, 2)));
305 assert_eq!(select_boot_slot(None, spent(2)), Some((Slot::B, 2)));
306 assert_eq!(select_boot_slot(None, None), None);
307 }
308
309 #[test]
310 fn slots_do_not_overlap_the_loader() {
311 assert!(Slot::A.base() >= LOADER_REGION_END);
312 assert_eq!(Slot::A.other(), Slot::B);
313 assert_eq!(Slot::B.image_base(), SLOT_B_BASE + SLOT_HEADER_BYTES);
314 }
315}