embassy_boot/boot_loader.rs
1use core::cell::RefCell;
2
3use embassy_embedded_hal::flash::partition::BlockingPartition;
4use embassy_sync::blocking_mutex::Mutex;
5use embassy_sync::blocking_mutex::raw::NoopRawMutex;
6use embedded_storage::nor_flash::{NorFlash, NorFlashError, NorFlashErrorKind};
7
8use crate::{DFU_DETACH_MAGIC, REVERT_MAGIC, STATE_ERASE_VALUE, SWAP_MAGIC, State};
9
10/// Errors returned by bootloader
11#[derive(PartialEq, Eq, Debug)]
12pub enum BootError {
13 /// Error from flash.
14 Flash(NorFlashErrorKind),
15 /// Invalid bootloader magic
16 BadMagic,
17}
18
19#[cfg(feature = "defmt")]
20impl defmt::Format for BootError {
21 fn format(&self, fmt: defmt::Formatter) {
22 match self {
23 BootError::Flash(_) => defmt::write!(fmt, "BootError::Flash(_)"),
24 BootError::BadMagic => defmt::write!(fmt, "BootError::BadMagic"),
25 }
26 }
27}
28
29impl<E> From<E> for BootError
30where
31 E: NorFlashError,
32{
33 fn from(error: E) -> Self {
34 BootError::Flash(error.kind())
35 }
36}
37
38/// Bootloader flash configuration holding the three flashes used by the bootloader
39///
40/// If only a single flash is actually used, then that flash should be partitioned into three partitions before use.
41/// The easiest way to do this is to use [`BootLoaderConfig::from_linkerfile_blocking`] which will partition
42/// the provided flash according to symbols defined in the linkerfile.
43pub struct BootLoaderConfig<ACTIVE, DFU, STATE> {
44 /// Flash type used for the active partition - the partition which will be booted from.
45 pub active: ACTIVE,
46 /// Flash type used for the dfu partition - the partition which will be swapped in when requested.
47 pub dfu: DFU,
48 /// Flash type used for the state partition.
49 pub state: STATE,
50}
51
52impl<'a, ACTIVE: NorFlash, DFU: NorFlash, STATE: NorFlash>
53 BootLoaderConfig<
54 BlockingPartition<'a, NoopRawMutex, ACTIVE>,
55 BlockingPartition<'a, NoopRawMutex, DFU>,
56 BlockingPartition<'a, NoopRawMutex, STATE>,
57 >
58{
59 /// Constructs a `BootLoaderConfig` instance from flash memory and address symbols defined in the linker file.
60 ///
61 /// This method initializes `BlockingPartition` instances for the active, DFU (Device Firmware Update),
62 /// and state partitions, leveraging start and end addresses specified by the linker. These partitions
63 /// are critical for managing firmware updates, application state, and boot operations within the bootloader.
64 ///
65 /// # Parameters
66 /// - `active_flash`: A reference to a mutex-protected `RefCell` for the active partition's flash interface.
67 /// - `dfu_flash`: A reference to a mutex-protected `RefCell` for the DFU partition's flash interface.
68 /// - `state_flash`: A reference to a mutex-protected `RefCell` for the state partition's flash interface.
69 ///
70 /// # Safety
71 /// The method contains `unsafe` blocks for dereferencing raw pointers that represent the start and end addresses
72 /// of the bootloader's partitions in flash memory. It is crucial that these addresses are accurately defined
73 /// in the memory.x file to prevent undefined behavior.
74 ///
75 /// The caller must ensure that the memory regions defined by these symbols are valid and that the flash memory
76 /// interfaces provided are compatible with these regions.
77 ///
78 /// # Returns
79 /// A `BootLoaderConfig` instance with `BlockingPartition` instances for the active, DFU, and state partitions.
80 ///
81 /// # Example
82 /// ```ignore
83 /// // Assume `active_flash`, `dfu_flash`, and `state_flash` all share the same flash memory interface.
84 /// let layout = Flash::new_blocking(p.FLASH).into_blocking_regions();
85 /// let flash = Mutex::new(RefCell::new(layout.bank1_region));
86 ///
87 /// let config = BootLoaderConfig::from_linkerfile_blocking(&flash, &flash, &flash);
88 /// // `config` can now be used to create a `BootLoader` instance for managing boot operations.
89 /// ```
90 /// Working examples can be found in the bootloader examples folder.
91 // #[cfg(target_os = "none")]
92 pub fn from_linkerfile_blocking(
93 active_flash: &'a Mutex<NoopRawMutex, RefCell<ACTIVE>>,
94 dfu_flash: &'a Mutex<NoopRawMutex, RefCell<DFU>>,
95 state_flash: &'a Mutex<NoopRawMutex, RefCell<STATE>>,
96 ) -> Self {
97 unsafe extern "C" {
98 static __bootloader_state_start: u32;
99 static __bootloader_state_end: u32;
100 static __bootloader_active_start: u32;
101 static __bootloader_active_end: u32;
102 static __bootloader_dfu_start: u32;
103 static __bootloader_dfu_end: u32;
104 }
105
106 let active = unsafe {
107 let start = &__bootloader_active_start as *const u32 as u32;
108 let end = &__bootloader_active_end as *const u32 as u32;
109 trace!("ACTIVE: 0x{:x} - 0x{:x}", start, end);
110
111 BlockingPartition::new(active_flash, start, end - start)
112 };
113 let dfu = unsafe {
114 let start = &__bootloader_dfu_start as *const u32 as u32;
115 let end = &__bootloader_dfu_end as *const u32 as u32;
116 trace!("DFU: 0x{:x} - 0x{:x}", start, end);
117
118 BlockingPartition::new(dfu_flash, start, end - start)
119 };
120 let state = unsafe {
121 let start = &__bootloader_state_start as *const u32 as u32;
122 let end = &__bootloader_state_end as *const u32 as u32;
123 trace!("STATE: 0x{:x} - 0x{:x}", start, end);
124
125 BlockingPartition::new(state_flash, start, end - start)
126 };
127
128 Self { active, dfu, state }
129 }
130}
131
132/// BootLoader works with any flash implementing embedded_storage.
133pub struct BootLoader<ACTIVE: NorFlash, DFU: NorFlash, STATE: NorFlash> {
134 active: ACTIVE,
135 dfu: DFU,
136 /// The state partition has the following format:
137 /// All ranges are in multiples of WRITE_SIZE bytes.
138 /// N = Active partition size divided by WRITE_SIZE.
139 /// | Range | Description |
140 /// | 0..1 | Magic indicating bootloader state. BOOT_MAGIC means boot, SWAP_MAGIC means swap. |
141 /// | 1..2 | Progress validity. ERASE_VALUE means valid, !ERASE_VALUE means invalid. |
142 /// | 2..(2 + 2N) | Progress index used while swapping |
143 /// | (2 + 2N)..(2 + 4N) | Progress index used while reverting
144 state: STATE,
145}
146
147impl<ACTIVE: NorFlash, DFU: NorFlash, STATE: NorFlash> BootLoader<ACTIVE, DFU, STATE> {
148 /// Get the page size which is the "unit of operation" within the bootloader.
149 const PAGE_SIZE: u32 = if ACTIVE::ERASE_SIZE > DFU::ERASE_SIZE {
150 ACTIVE::ERASE_SIZE as u32
151 } else {
152 DFU::ERASE_SIZE as u32
153 };
154
155 /// Create a new instance of a bootloader with the flash partitions.
156 ///
157 /// - All partitions must be aligned with the PAGE_SIZE const generic parameter.
158 /// - The dfu partition must be at least PAGE_SIZE bigger than the active partition.
159 pub fn new(config: BootLoaderConfig<ACTIVE, DFU, STATE>) -> Self {
160 Self {
161 active: config.active,
162 dfu: config.dfu,
163 state: config.state,
164 }
165 }
166
167 /// Perform necessary boot preparations like swapping images.
168 ///
169 /// The DFU partition is assumed to be 1 page bigger than the active partition for the swap
170 /// algorithm to work correctly.
171 ///
172 /// The provided aligned_buf argument must satisfy any alignment requirements
173 /// given by the partition flashes. All flash operations will use this buffer.
174 ///
175 /// ## SWAPPING
176 ///
177 /// Assume a flash size of 3 pages for the active partition, and 4 pages for the DFU partition.
178 /// The swap index contains the copy progress, as to allow continuation of the copy process on
179 /// power failure. The index counter is represented within 1 or more pages (depending on total
180 /// flash size), where a page X is considered swapped if index at location (`X + WRITE_SIZE`)
181 /// contains a zero value. This ensures that index updates can be performed atomically and
182 /// avoid a situation where the wrong index value is set (page write size is "atomic").
183 ///
184 ///
185 /// | Partition | Swap Index | Page 0 | Page 1 | Page 3 | Page 4 |
186 /// |-----------|------------|--------|--------|--------|--------|
187 /// | Active | 0 | 1 | 2 | 3 | - |
188 /// | DFU | 0 | 4 | 5 | 6 | X |
189 ///
190 /// The algorithm starts by copying 'backwards', and after the first step, the layout is
191 /// as follows:
192 ///
193 /// | Partition | Swap Index | Page 0 | Page 1 | Page 3 | Page 4 |
194 /// |-----------|------------|--------|--------|--------|--------|
195 /// | Active | 1 | 1 | 2 | 6 | - |
196 /// | DFU | 1 | 4 | 5 | 6 | 3 |
197 ///
198 /// The next iteration performs the same steps
199 ///
200 /// | Partition | Swap Index | Page 0 | Page 1 | Page 3 | Page 4 |
201 /// |-----------|------------|--------|--------|--------|--------|
202 /// | Active | 2 | 1 | 5 | 6 | - |
203 /// | DFU | 2 | 4 | 5 | 2 | 3 |
204 ///
205 /// And again until we're done
206 ///
207 /// | Partition | Swap Index | Page 0 | Page 1 | Page 3 | Page 4 |
208 /// |-----------|------------|--------|--------|--------|--------|
209 /// | Active | 3 | 4 | 5 | 6 | - |
210 /// | DFU | 3 | 4 | 1 | 2 | 3 |
211 ///
212 /// ## REVERTING
213 ///
214 /// The reverting algorithm uses the swap index to discover that images were swapped, but that
215 /// the application failed to mark the boot successful. In this case, the revert algorithm will
216 /// run.
217 ///
218 /// The revert index is located separately from the swap index, to ensure that revert can continue
219 /// on power failure.
220 ///
221 /// The revert algorithm works forwards, by starting copying into the 'unused' DFU page at the start.
222 ///
223 /// | Partition | Revert Index | Page 0 | Page 1 | Page 3 | Page 4 |
224 /// |-----------|--------------|--------|--------|--------|--------|
225 /// | Active | 3 | 1 | 5 | 6 | - |
226 /// | DFU | 3 | 4 | 1 | 2 | 3 |
227 ///
228 ///
229 /// | Partition | Revert Index | Page 0 | Page 1 | Page 3 | Page 4 |
230 /// |-----------|--------------|--------|--------|--------|--------|
231 /// | Active | 3 | 1 | 2 | 6 | - |
232 /// | DFU | 3 | 4 | 5 | 2 | 3 |
233 ///
234 /// | Partition | Revert Index | Page 0 | Page 1 | Page 3 | Page 4 |
235 /// |-----------|--------------|--------|--------|--------|--------|
236 /// | Active | 3 | 1 | 2 | 3 | - |
237 /// | DFU | 3 | 4 | 5 | 6 | 3 |
238 ///
239 pub fn prepare_boot(&mut self, aligned_buf: &mut [u8]) -> Result<State, BootError> {
240 const {
241 core::assert!(Self::PAGE_SIZE % ACTIVE::WRITE_SIZE as u32 == 0);
242 core::assert!(Self::PAGE_SIZE % ACTIVE::ERASE_SIZE as u32 == 0);
243 core::assert!(Self::PAGE_SIZE % DFU::WRITE_SIZE as u32 == 0);
244 core::assert!(Self::PAGE_SIZE % DFU::ERASE_SIZE as u32 == 0);
245 }
246
247 // Ensure we have enough progress pages to store copy progress
248 assert_eq!(0, Self::PAGE_SIZE % aligned_buf.len() as u32);
249 assert!(aligned_buf.len() >= STATE::WRITE_SIZE);
250 assert_eq!(0, aligned_buf.len() % ACTIVE::WRITE_SIZE);
251 assert_eq!(0, aligned_buf.len() % DFU::WRITE_SIZE);
252
253 // Ensure our partitions are able to handle boot operations
254 assert_partitions(&self.active, &self.dfu, &self.state, Self::PAGE_SIZE);
255
256 // Copy contents from partition N to active
257 let state = self.read_state(aligned_buf)?;
258 if state == State::Swap {
259 //
260 // Check if we already swapped. If we're in the swap state, this means we should revert
261 // since the app has failed to mark boot as successful
262 //
263 if !self.is_swapped(aligned_buf)? {
264 trace!("Swapping");
265 self.swap(aligned_buf)?;
266 trace!("Swapping done");
267 } else {
268 trace!("Reverting");
269 self.revert(aligned_buf)?;
270
271 let state_word = &mut aligned_buf[..STATE::WRITE_SIZE];
272
273 // Invalidate progress
274 state_word.fill(!STATE_ERASE_VALUE);
275 self.state.write(STATE::WRITE_SIZE as u32, state_word)?;
276
277 // Clear magic and progress
278 self.state.erase(0, self.state.capacity() as u32)?;
279
280 // Set magic
281 state_word.fill(REVERT_MAGIC);
282 self.state.write(0, state_word)?;
283 }
284 }
285 Ok(state)
286 }
287
288 /// Read the magic state from flash
289 pub fn read_state(&mut self, aligned_buf: &mut [u8]) -> Result<State, BootError> {
290 let state_word = &mut aligned_buf[..STATE::WRITE_SIZE];
291 self.state.read(0, state_word)?;
292
293 if !state_word.iter().any(|&b| b != SWAP_MAGIC) {
294 Ok(State::Swap)
295 } else if !state_word.iter().any(|&b| b != DFU_DETACH_MAGIC) {
296 Ok(State::DfuDetach)
297 } else if !state_word.iter().any(|&b| b != REVERT_MAGIC) {
298 Ok(State::Revert)
299 } else {
300 Ok(State::Boot)
301 }
302 }
303
304 fn is_swapped(&mut self, aligned_buf: &mut [u8]) -> Result<bool, BootError> {
305 let page_count = self.active.capacity() / Self::PAGE_SIZE as usize;
306 let progress = self.current_progress(aligned_buf)?;
307
308 Ok(progress >= page_count * 2)
309 }
310
311 fn current_progress(&mut self, aligned_buf: &mut [u8]) -> Result<usize, BootError> {
312 let write_size = STATE::WRITE_SIZE as u32;
313 let max_index = ((self.state.capacity() - STATE::WRITE_SIZE) / STATE::WRITE_SIZE) - 2;
314 let state_word = &mut aligned_buf[..write_size as usize];
315
316 self.state.read(write_size, state_word)?;
317 if state_word.iter().any(|&b| b != STATE_ERASE_VALUE) {
318 // Progress is invalid
319 return Ok(max_index);
320 }
321
322 for index in 0..max_index {
323 self.state.read((2 + index) as u32 * write_size, state_word)?;
324
325 if state_word.iter().any(|&b| b == STATE_ERASE_VALUE) {
326 return Ok(index);
327 }
328 }
329 Ok(max_index)
330 }
331
332 fn update_progress(&mut self, progress_index: usize, aligned_buf: &mut [u8]) -> Result<(), BootError> {
333 let state_word = &mut aligned_buf[..STATE::WRITE_SIZE];
334 state_word.fill(!STATE_ERASE_VALUE);
335 self.state
336 .write((2 + progress_index) as u32 * STATE::WRITE_SIZE as u32, state_word)?;
337 Ok(())
338 }
339
340 fn copy_page_once_to_active(
341 &mut self,
342 progress_index: usize,
343 from_offset: u32,
344 to_offset: u32,
345 aligned_buf: &mut [u8],
346 ) -> Result<(), BootError> {
347 if self.current_progress(aligned_buf)? <= progress_index {
348 let page_size = Self::PAGE_SIZE as u32;
349
350 self.active.erase(to_offset, to_offset + page_size)?;
351
352 for offset_in_page in (0..page_size).step_by(aligned_buf.len()) {
353 self.dfu.read(from_offset + offset_in_page as u32, aligned_buf)?;
354 self.active.write(to_offset + offset_in_page as u32, aligned_buf)?;
355 }
356
357 self.update_progress(progress_index, aligned_buf)?;
358 }
359 Ok(())
360 }
361
362 fn copy_page_once_to_dfu(
363 &mut self,
364 progress_index: usize,
365 from_offset: u32,
366 to_offset: u32,
367 aligned_buf: &mut [u8],
368 ) -> Result<(), BootError> {
369 if self.current_progress(aligned_buf)? <= progress_index {
370 let page_size = Self::PAGE_SIZE as u32;
371
372 self.dfu.erase(to_offset as u32, to_offset + page_size)?;
373
374 for offset_in_page in (0..page_size).step_by(aligned_buf.len()) {
375 self.active.read(from_offset + offset_in_page as u32, aligned_buf)?;
376 self.dfu.write(to_offset + offset_in_page as u32, aligned_buf)?;
377 }
378
379 self.update_progress(progress_index, aligned_buf)?;
380 }
381 Ok(())
382 }
383
384 fn swap(&mut self, aligned_buf: &mut [u8]) -> Result<(), BootError> {
385 let page_count = self.active.capacity() as u32 / Self::PAGE_SIZE;
386 for page_num in 0..page_count {
387 let progress_index = (page_num * 2) as usize;
388
389 // Copy active page to the 'next' DFU page.
390 let active_from_offset = (page_count - 1 - page_num) * Self::PAGE_SIZE;
391 let dfu_to_offset = (page_count - page_num) * Self::PAGE_SIZE;
392 //trace!("Copy active {} to dfu {}", active_from_offset, dfu_to_offset);
393 self.copy_page_once_to_dfu(progress_index, active_from_offset, dfu_to_offset, aligned_buf)?;
394
395 // Copy DFU page to the active page
396 let active_to_offset = (page_count - 1 - page_num) * Self::PAGE_SIZE;
397 let dfu_from_offset = (page_count - 1 - page_num) * Self::PAGE_SIZE;
398 //trace!("Copy dfy {} to active {}", dfu_from_offset, active_to_offset);
399 self.copy_page_once_to_active(progress_index + 1, dfu_from_offset, active_to_offset, aligned_buf)?;
400 }
401
402 Ok(())
403 }
404
405 fn revert(&mut self, aligned_buf: &mut [u8]) -> Result<(), BootError> {
406 let page_count = self.active.capacity() as u32 / Self::PAGE_SIZE;
407 for page_num in 0..page_count {
408 let progress_index = (page_count * 2 + page_num * 2) as usize;
409
410 // Copy the bad active page to the DFU page
411 let active_from_offset = page_num * Self::PAGE_SIZE;
412 let dfu_to_offset = page_num * Self::PAGE_SIZE;
413 self.copy_page_once_to_dfu(progress_index, active_from_offset, dfu_to_offset, aligned_buf)?;
414
415 // Copy the DFU page back to the active page
416 let active_to_offset = page_num * Self::PAGE_SIZE;
417 let dfu_from_offset = (page_num + 1) * Self::PAGE_SIZE;
418 self.copy_page_once_to_active(progress_index + 1, dfu_from_offset, active_to_offset, aligned_buf)?;
419 }
420
421 Ok(())
422 }
423}
424
425fn assert_partitions<ACTIVE: NorFlash, DFU: NorFlash, STATE: NorFlash>(
426 active: &ACTIVE,
427 dfu: &DFU,
428 state: &STATE,
429 page_size: u32,
430) {
431 assert_eq!(active.capacity() as u32 % page_size, 0);
432 assert_eq!(dfu.capacity() as u32 % page_size, 0);
433 // DFU partition has to be bigger than ACTIVE partition to handle swap algorithm
434 assert!(dfu.capacity() as u32 - active.capacity() as u32 >= page_size);
435 assert!(2 + 4 * (active.capacity() as u32 / page_size) <= state.capacity() as u32 / STATE::WRITE_SIZE as u32);
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441 use crate::mem_flash::MemFlash;
442
443 #[test]
444 #[should_panic]
445 fn test_range_asserts() {
446 const ACTIVE_SIZE: usize = 4194304 - 4096;
447 const DFU_SIZE: usize = 4194304;
448 const STATE_SIZE: usize = 4096;
449 static ACTIVE: MemFlash<ACTIVE_SIZE, 4, 4> = MemFlash::new(0xFF);
450 static DFU: MemFlash<DFU_SIZE, 4, 4> = MemFlash::new(0xFF);
451 static STATE: MemFlash<STATE_SIZE, 4, 4> = MemFlash::new(0xFF);
452 assert_partitions(&ACTIVE, &DFU, &STATE, 4096);
453 }
454}