embassy_boot/boot_loader.rs
1use core::cell::RefCell;
2
3use embassy_embedded_hal::flash::partition::BlockingPartition;
4use embassy_sync::blocking_mutex::raw::NoopRawMutex;
5use embassy_sync::blocking_mutex::Mutex;
6use embedded_storage::nor_flash::{NorFlash, NorFlashError, NorFlashErrorKind};
7
8use crate::{State, DFU_DETACH_MAGIC, REVERT_MAGIC, STATE_ERASE_VALUE, SWAP_MAGIC};
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 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 /// | Range | Description |
139 /// | 0..1 | Magic indicating bootloader state. BOOT_MAGIC means boot, SWAP_MAGIC means swap. |
140 /// | 1..2 | Progress validity. ERASE_VALUE means valid, !ERASE_VALUE means invalid. |
141 /// | 2..2 + N | Progress index used while swapping or reverting
142 state: STATE,
143}
144
145impl<ACTIVE: NorFlash, DFU: NorFlash, STATE: NorFlash> BootLoader<ACTIVE, DFU, STATE> {
146 /// Get the page size which is the "unit of operation" within the bootloader.
147 const PAGE_SIZE: u32 = if ACTIVE::ERASE_SIZE > DFU::ERASE_SIZE {
148 ACTIVE::ERASE_SIZE as u32
149 } else {
150 DFU::ERASE_SIZE as u32
151 };
152
153 /// Create a new instance of a bootloader with the flash partitions.
154 ///
155 /// - All partitions must be aligned with the PAGE_SIZE const generic parameter.
156 /// - The dfu partition must be at least PAGE_SIZE bigger than the active partition.
157 pub fn new(config: BootLoaderConfig<ACTIVE, DFU, STATE>) -> Self {
158 Self {
159 active: config.active,
160 dfu: config.dfu,
161 state: config.state,
162 }
163 }
164
165 /// Perform necessary boot preparations like swapping images.
166 ///
167 /// The DFU partition is assumed to be 1 page bigger than the active partition for the swap
168 /// algorithm to work correctly.
169 ///
170 /// The provided aligned_buf argument must satisfy any alignment requirements
171 /// given by the partition flashes. All flash operations will use this buffer.
172 ///
173 /// ## SWAPPING
174 ///
175 /// Assume a flash size of 3 pages for the active partition, and 4 pages for the DFU partition.
176 /// The swap index contains the copy progress, as to allow continuation of the copy process on
177 /// power failure. The index counter is represented within 1 or more pages (depending on total
178 /// flash size), where a page X is considered swapped if index at location (`X + WRITE_SIZE`)
179 /// contains a zero value. This ensures that index updates can be performed atomically and
180 /// avoid a situation where the wrong index value is set (page write size is "atomic").
181 ///
182 ///
183 /// | Partition | Swap Index | Page 0 | Page 1 | Page 3 | Page 4 |
184 /// |-----------|------------|--------|--------|--------|--------|
185 /// | Active | 0 | 1 | 2 | 3 | - |
186 /// | DFU | 0 | 4 | 5 | 6 | X |
187 ///
188 /// The algorithm starts by copying 'backwards', and after the first step, the layout is
189 /// as follows:
190 ///
191 /// | Partition | Swap Index | Page 0 | Page 1 | Page 3 | Page 4 |
192 /// |-----------|------------|--------|--------|--------|--------|
193 /// | Active | 1 | 1 | 2 | 6 | - |
194 /// | DFU | 1 | 4 | 5 | 6 | 3 |
195 ///
196 /// The next iteration performs the same steps
197 ///
198 /// | Partition | Swap Index | Page 0 | Page 1 | Page 3 | Page 4 |
199 /// |-----------|------------|--------|--------|--------|--------|
200 /// | Active | 2 | 1 | 5 | 6 | - |
201 /// | DFU | 2 | 4 | 5 | 2 | 3 |
202 ///
203 /// And again until we're done
204 ///
205 /// | Partition | Swap Index | Page 0 | Page 1 | Page 3 | Page 4 |
206 /// |-----------|------------|--------|--------|--------|--------|
207 /// | Active | 3 | 4 | 5 | 6 | - |
208 /// | DFU | 3 | 4 | 1 | 2 | 3 |
209 ///
210 /// ## REVERTING
211 ///
212 /// The reverting algorithm uses the swap index to discover that images were swapped, but that
213 /// the application failed to mark the boot successful. In this case, the revert algorithm will
214 /// run.
215 ///
216 /// The revert index is located separately from the swap index, to ensure that revert can continue
217 /// on power failure.
218 ///
219 /// The revert algorithm works forwards, by starting copying into the 'unused' DFU page at the start.
220 ///
221 /// | Partition | Revert Index | Page 0 | Page 1 | Page 3 | Page 4 |
222 /// |-----------|--------------|--------|--------|--------|--------|
223 /// | Active | 3 | 1 | 5 | 6 | - |
224 /// | DFU | 3 | 4 | 1 | 2 | 3 |
225 ///
226 ///
227 /// | Partition | Revert Index | Page 0 | Page 1 | Page 3 | Page 4 |
228 /// |-----------|--------------|--------|--------|--------|--------|
229 /// | Active | 3 | 1 | 2 | 6 | - |
230 /// | DFU | 3 | 4 | 5 | 2 | 3 |
231 ///
232 /// | Partition | Revert Index | Page 0 | Page 1 | Page 3 | Page 4 |
233 /// |-----------|--------------|--------|--------|--------|--------|
234 /// | Active | 3 | 1 | 2 | 3 | - |
235 /// | DFU | 3 | 4 | 5 | 6 | 3 |
236 ///
237 pub fn prepare_boot(&mut self, aligned_buf: &mut [u8]) -> Result<State, BootError> {
238 const {
239 core::assert!(Self::PAGE_SIZE % ACTIVE::WRITE_SIZE as u32 == 0);
240 core::assert!(Self::PAGE_SIZE % ACTIVE::ERASE_SIZE as u32 == 0);
241 core::assert!(Self::PAGE_SIZE % DFU::WRITE_SIZE as u32 == 0);
242 core::assert!(Self::PAGE_SIZE % DFU::ERASE_SIZE as u32 == 0);
243 }
244
245 // Ensure we have enough progress pages to store copy progress
246 assert_eq!(0, Self::PAGE_SIZE % aligned_buf.len() as u32);
247 assert!(aligned_buf.len() >= STATE::WRITE_SIZE);
248 assert_eq!(0, aligned_buf.len() % ACTIVE::WRITE_SIZE);
249 assert_eq!(0, aligned_buf.len() % DFU::WRITE_SIZE);
250
251 // Ensure our partitions are able to handle boot operations
252 assert_partitions(&self.active, &self.dfu, &self.state, Self::PAGE_SIZE);
253
254 // Copy contents from partition N to active
255 let state = self.read_state(aligned_buf)?;
256 if state == State::Swap {
257 //
258 // Check if we already swapped. If we're in the swap state, this means we should revert
259 // since the app has failed to mark boot as successful
260 //
261 if !self.is_swapped(aligned_buf)? {
262 trace!("Swapping");
263 self.swap(aligned_buf)?;
264 trace!("Swapping done");
265 } else {
266 trace!("Reverting");
267 self.revert(aligned_buf)?;
268
269 let state_word = &mut aligned_buf[..STATE::WRITE_SIZE];
270
271 // Invalidate progress
272 state_word.fill(!STATE_ERASE_VALUE);
273 self.state.write(STATE::WRITE_SIZE as u32, state_word)?;
274
275 // Clear magic and progress
276 self.state.erase(0, self.state.capacity() as u32)?;
277
278 // Set magic
279 state_word.fill(REVERT_MAGIC);
280 self.state.write(0, state_word)?;
281 }
282 }
283 Ok(state)
284 }
285
286 fn is_swapped(&mut self, aligned_buf: &mut [u8]) -> Result<bool, BootError> {
287 let page_count = self.active.capacity() / Self::PAGE_SIZE as usize;
288 let progress = self.current_progress(aligned_buf)?;
289
290 Ok(progress >= page_count * 2)
291 }
292
293 fn current_progress(&mut self, aligned_buf: &mut [u8]) -> Result<usize, BootError> {
294 let write_size = STATE::WRITE_SIZE as u32;
295 let max_index = ((self.state.capacity() - STATE::WRITE_SIZE) / STATE::WRITE_SIZE) - 2;
296 let state_word = &mut aligned_buf[..write_size as usize];
297
298 self.state.read(write_size, state_word)?;
299 if state_word.iter().any(|&b| b != STATE_ERASE_VALUE) {
300 // Progress is invalid
301 return Ok(max_index);
302 }
303
304 for index in 0..max_index {
305 self.state.read((2 + index) as u32 * write_size, state_word)?;
306
307 if state_word.iter().any(|&b| b == STATE_ERASE_VALUE) {
308 return Ok(index);
309 }
310 }
311 Ok(max_index)
312 }
313
314 fn update_progress(&mut self, progress_index: usize, aligned_buf: &mut [u8]) -> Result<(), BootError> {
315 let state_word = &mut aligned_buf[..STATE::WRITE_SIZE];
316 state_word.fill(!STATE_ERASE_VALUE);
317 self.state
318 .write((2 + progress_index) as u32 * STATE::WRITE_SIZE as u32, state_word)?;
319 Ok(())
320 }
321
322 fn copy_page_once_to_active(
323 &mut self,
324 progress_index: usize,
325 from_offset: u32,
326 to_offset: u32,
327 aligned_buf: &mut [u8],
328 ) -> Result<(), BootError> {
329 if self.current_progress(aligned_buf)? <= progress_index {
330 let page_size = Self::PAGE_SIZE as u32;
331
332 self.active.erase(to_offset, to_offset + page_size)?;
333
334 for offset_in_page in (0..page_size).step_by(aligned_buf.len()) {
335 self.dfu.read(from_offset + offset_in_page as u32, aligned_buf)?;
336 self.active.write(to_offset + offset_in_page as u32, aligned_buf)?;
337 }
338
339 self.update_progress(progress_index, aligned_buf)?;
340 }
341 Ok(())
342 }
343
344 fn copy_page_once_to_dfu(
345 &mut self,
346 progress_index: usize,
347 from_offset: u32,
348 to_offset: u32,
349 aligned_buf: &mut [u8],
350 ) -> Result<(), BootError> {
351 if self.current_progress(aligned_buf)? <= progress_index {
352 let page_size = Self::PAGE_SIZE as u32;
353
354 self.dfu.erase(to_offset as u32, to_offset + page_size)?;
355
356 for offset_in_page in (0..page_size).step_by(aligned_buf.len()) {
357 self.active.read(from_offset + offset_in_page as u32, aligned_buf)?;
358 self.dfu.write(to_offset + offset_in_page as u32, aligned_buf)?;
359 }
360
361 self.update_progress(progress_index, aligned_buf)?;
362 }
363 Ok(())
364 }
365
366 fn swap(&mut self, aligned_buf: &mut [u8]) -> Result<(), BootError> {
367 let page_count = self.active.capacity() as u32 / Self::PAGE_SIZE;
368 for page_num in 0..page_count {
369 let progress_index = (page_num * 2) as usize;
370
371 // Copy active page to the 'next' DFU page.
372 let active_from_offset = (page_count - 1 - page_num) * Self::PAGE_SIZE;
373 let dfu_to_offset = (page_count - page_num) * Self::PAGE_SIZE;
374 //trace!("Copy active {} to dfu {}", active_from_offset, dfu_to_offset);
375 self.copy_page_once_to_dfu(progress_index, active_from_offset, dfu_to_offset, aligned_buf)?;
376
377 // Copy DFU page to the active page
378 let active_to_offset = (page_count - 1 - page_num) * Self::PAGE_SIZE;
379 let dfu_from_offset = (page_count - 1 - page_num) * Self::PAGE_SIZE;
380 //trace!("Copy dfy {} to active {}", dfu_from_offset, active_to_offset);
381 self.copy_page_once_to_active(progress_index + 1, dfu_from_offset, active_to_offset, aligned_buf)?;
382 }
383
384 Ok(())
385 }
386
387 fn revert(&mut self, aligned_buf: &mut [u8]) -> Result<(), BootError> {
388 let page_count = self.active.capacity() as u32 / Self::PAGE_SIZE;
389 for page_num in 0..page_count {
390 let progress_index = (page_count * 2 + page_num * 2) as usize;
391
392 // Copy the bad active page to the DFU page
393 let active_from_offset = page_num * Self::PAGE_SIZE;
394 let dfu_to_offset = page_num * Self::PAGE_SIZE;
395 self.copy_page_once_to_dfu(progress_index, active_from_offset, dfu_to_offset, aligned_buf)?;
396
397 // Copy the DFU page back to the active page
398 let active_to_offset = page_num * Self::PAGE_SIZE;
399 let dfu_from_offset = (page_num + 1) * Self::PAGE_SIZE;
400 self.copy_page_once_to_active(progress_index + 1, dfu_from_offset, active_to_offset, aligned_buf)?;
401 }
402
403 Ok(())
404 }
405
406 fn read_state(&mut self, aligned_buf: &mut [u8]) -> Result<State, BootError> {
407 let state_word = &mut aligned_buf[..STATE::WRITE_SIZE];
408 self.state.read(0, state_word)?;
409
410 if !state_word.iter().any(|&b| b != SWAP_MAGIC) {
411 Ok(State::Swap)
412 } else if !state_word.iter().any(|&b| b != DFU_DETACH_MAGIC) {
413 Ok(State::DfuDetach)
414 } else if !state_word.iter().any(|&b| b != REVERT_MAGIC) {
415 Ok(State::Revert)
416 } else {
417 Ok(State::Boot)
418 }
419 }
420}
421
422fn assert_partitions<ACTIVE: NorFlash, DFU: NorFlash, STATE: NorFlash>(
423 active: &ACTIVE,
424 dfu: &DFU,
425 state: &STATE,
426 page_size: u32,
427) {
428 assert_eq!(active.capacity() as u32 % page_size, 0);
429 assert_eq!(dfu.capacity() as u32 % page_size, 0);
430 // DFU partition has to be bigger than ACTIVE partition to handle swap algorithm
431 assert!(dfu.capacity() as u32 - active.capacity() as u32 >= page_size);
432 assert!(2 + 2 * (active.capacity() as u32 / page_size) <= state.capacity() as u32 / STATE::WRITE_SIZE as u32);
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438 use crate::mem_flash::MemFlash;
439
440 #[test]
441 #[should_panic]
442 fn test_range_asserts() {
443 const ACTIVE_SIZE: usize = 4194304 - 4096;
444 const DFU_SIZE: usize = 4194304;
445 const STATE_SIZE: usize = 4096;
446 static ACTIVE: MemFlash<ACTIVE_SIZE, 4, 4> = MemFlash::new(0xFF);
447 static DFU: MemFlash<DFU_SIZE, 4, 4> = MemFlash::new(0xFF);
448 static STATE: MemFlash<STATE_SIZE, 4, 4> = MemFlash::new(0xFF);
449 assert_partitions(&ACTIVE, &DFU, &STATE, 4096);
450 }
451}