Skip to main content

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}