422 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			422 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| use core::cell::RefCell;
 | |
| 
 | |
| use embassy_embedded_hal::flash::partition::BlockingPartition;
 | |
| use embassy_sync::blocking_mutex::raw::NoopRawMutex;
 | |
| use embassy_sync::blocking_mutex::Mutex;
 | |
| use embedded_storage::nor_flash::{NorFlash, NorFlashError, NorFlashErrorKind};
 | |
| 
 | |
| use crate::{State, BOOT_MAGIC, STATE_ERASE_VALUE, SWAP_MAGIC};
 | |
| 
 | |
| /// Errors returned by bootloader
 | |
| #[derive(PartialEq, Eq, Debug)]
 | |
| pub enum BootError {
 | |
|     /// Error from flash.
 | |
|     Flash(NorFlashErrorKind),
 | |
|     /// Invalid bootloader magic
 | |
|     BadMagic,
 | |
| }
 | |
| 
 | |
| #[cfg(feature = "defmt")]
 | |
| impl defmt::Format for BootError {
 | |
|     fn format(&self, fmt: defmt::Formatter) {
 | |
|         match self {
 | |
|             BootError::Flash(_) => defmt::write!(fmt, "BootError::Flash(_)"),
 | |
|             BootError::BadMagic => defmt::write!(fmt, "BootError::BadMagic"),
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl<E> From<E> for BootError
 | |
| where
 | |
|     E: NorFlashError,
 | |
| {
 | |
|     fn from(error: E) -> Self {
 | |
|         BootError::Flash(error.kind())
 | |
|     }
 | |
| }
 | |
| 
 | |
| /// Bootloader flash configuration holding the three flashes used by the bootloader
 | |
| ///
 | |
| /// If only a single flash is actually used, then that flash should be partitioned into three partitions before use.
 | |
| /// The easiest way to do this is to use [`BootLoaderConfig::from_linkerfile_blocking`] which will partition
 | |
| /// the provided flash according to symbols defined in the linkerfile.
 | |
| pub struct BootLoaderConfig<ACTIVE, DFU, STATE> {
 | |
|     /// Flash type used for the active partition - the partition which will be booted from.
 | |
|     pub active: ACTIVE,
 | |
|     /// Flash type used for the dfu partition - the partition which will be swapped in when requested.
 | |
|     pub dfu: DFU,
 | |
|     /// Flash type used for the state partition.
 | |
|     pub state: STATE,
 | |
| }
 | |
| 
 | |
| impl<'a, FLASH: NorFlash>
 | |
|     BootLoaderConfig<
 | |
|         BlockingPartition<'a, NoopRawMutex, FLASH>,
 | |
|         BlockingPartition<'a, NoopRawMutex, FLASH>,
 | |
|         BlockingPartition<'a, NoopRawMutex, FLASH>,
 | |
|     >
 | |
| {
 | |
|     /// Create a bootloader config from the flash and address symbols defined in the linkerfile
 | |
|     // #[cfg(target_os = "none")]
 | |
|     pub fn from_linkerfile_blocking(flash: &'a Mutex<NoopRawMutex, RefCell<FLASH>>) -> Self {
 | |
|         extern "C" {
 | |
|             static __bootloader_state_start: u32;
 | |
|             static __bootloader_state_end: u32;
 | |
|             static __bootloader_active_start: u32;
 | |
|             static __bootloader_active_end: u32;
 | |
|             static __bootloader_dfu_start: u32;
 | |
|             static __bootloader_dfu_end: u32;
 | |
|         }
 | |
| 
 | |
|         let active = unsafe {
 | |
|             let start = &__bootloader_active_start as *const u32 as u32;
 | |
|             let end = &__bootloader_active_end as *const u32 as u32;
 | |
|             trace!("ACTIVE: 0x{:x} - 0x{:x}", start, end);
 | |
| 
 | |
|             BlockingPartition::new(flash, start, end - start)
 | |
|         };
 | |
|         let dfu = unsafe {
 | |
|             let start = &__bootloader_dfu_start as *const u32 as u32;
 | |
|             let end = &__bootloader_dfu_end as *const u32 as u32;
 | |
|             trace!("DFU: 0x{:x} - 0x{:x}", start, end);
 | |
| 
 | |
|             BlockingPartition::new(flash, start, end - start)
 | |
|         };
 | |
|         let state = unsafe {
 | |
|             let start = &__bootloader_state_start as *const u32 as u32;
 | |
|             let end = &__bootloader_state_end as *const u32 as u32;
 | |
|             trace!("STATE: 0x{:x} - 0x{:x}", start, end);
 | |
| 
 | |
|             BlockingPartition::new(flash, start, end - start)
 | |
|         };
 | |
| 
 | |
|         Self { active, dfu, state }
 | |
|     }
 | |
| }
 | |
| 
 | |
| /// BootLoader works with any flash implementing embedded_storage.
 | |
| pub struct BootLoader<ACTIVE: NorFlash, DFU: NorFlash, STATE: NorFlash> {
 | |
|     active: ACTIVE,
 | |
|     dfu: DFU,
 | |
|     /// The state partition has the following format:
 | |
|     /// All ranges are in multiples of WRITE_SIZE bytes.
 | |
|     /// | Range    | Description                                                                      |
 | |
|     /// | 0..1     | Magic indicating bootloader state. BOOT_MAGIC means boot, SWAP_MAGIC means swap. |
 | |
|     /// | 1..2     | Progress validity. ERASE_VALUE means valid, !ERASE_VALUE means invalid.          |
 | |
|     /// | 2..2 + N | Progress index used while swapping or reverting      
 | |
|     state: STATE,
 | |
| }
 | |
| 
 | |
| impl<ACTIVE: NorFlash, DFU: NorFlash, STATE: NorFlash> BootLoader<ACTIVE, DFU, STATE> {
 | |
|     /// Get the page size which is the "unit of operation" within the bootloader.
 | |
|     const PAGE_SIZE: u32 = if ACTIVE::ERASE_SIZE > DFU::ERASE_SIZE {
 | |
|         ACTIVE::ERASE_SIZE as u32
 | |
|     } else {
 | |
|         DFU::ERASE_SIZE as u32
 | |
|     };
 | |
| 
 | |
|     /// Create a new instance of a bootloader with the flash partitions.
 | |
|     ///
 | |
|     /// - All partitions must be aligned with the PAGE_SIZE const generic parameter.
 | |
|     /// - The dfu partition must be at least PAGE_SIZE bigger than the active partition.
 | |
|     pub fn new(config: BootLoaderConfig<ACTIVE, DFU, STATE>) -> Self {
 | |
|         Self {
 | |
|             active: config.active,
 | |
|             dfu: config.dfu,
 | |
|             state: config.state,
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /// Perform necessary boot preparations like swapping images.
 | |
|     ///
 | |
|     /// The DFU partition is assumed to be 1 page bigger than the active partition for the swap
 | |
|     /// algorithm to work correctly.
 | |
|     ///
 | |
|     /// The provided aligned_buf argument must satisfy any alignment requirements
 | |
|     /// given by the partition flashes. All flash operations will use this buffer.
 | |
|     ///
 | |
|     /// SWAPPING
 | |
|     ///
 | |
|     /// Assume a flash size of 3 pages for the active partition, and 4 pages for the DFU partition.
 | |
|     /// The swap index contains the copy progress, as to allow continuation of the copy process on
 | |
|     /// power failure. The index counter is represented within 1 or more pages (depending on total
 | |
|     /// flash size), where a page X is considered swapped if index at location (X + WRITE_SIZE)
 | |
|     /// contains a zero value. This ensures that index updates can be performed atomically and
 | |
|     /// avoid a situation where the wrong index value is set (page write size is "atomic").
 | |
|     ///
 | |
|     /// +-----------+------------+--------+--------+--------+--------+
 | |
|     /// | Partition | Swap Index | Page 0 | Page 1 | Page 3 | Page 4 |
 | |
|     /// +-----------+------------+--------+--------+--------+--------+
 | |
|     /// |    Active |          0 |      1 |      2 |      3 |      - |
 | |
|     /// |       DFU |          0 |      3 |      2 |      1 |      X |
 | |
|     /// +-----------+------------+--------+--------+--------+--------+
 | |
|     ///
 | |
|     /// The algorithm starts by copying 'backwards', and after the first step, the layout is
 | |
|     /// as follows:
 | |
|     ///
 | |
|     /// +-----------+------------+--------+--------+--------+--------+
 | |
|     /// | Partition | Swap Index | Page 0 | Page 1 | Page 3 | Page 4 |
 | |
|     /// +-----------+------------+--------+--------+--------+--------+
 | |
|     /// |    Active |          1 |      1 |      2 |      1 |      - |
 | |
|     /// |       DFU |          1 |      3 |      2 |      1 |      3 |
 | |
|     /// +-----------+------------+--------+--------+--------+--------+
 | |
|     ///
 | |
|     /// The next iteration performs the same steps
 | |
|     ///
 | |
|     /// +-----------+------------+--------+--------+--------+--------+
 | |
|     /// | Partition | Swap Index | Page 0 | Page 1 | Page 3 | Page 4 |
 | |
|     /// +-----------+------------+--------+--------+--------+--------+
 | |
|     /// |    Active |          2 |      1 |      2 |      1 |      - |
 | |
|     /// |       DFU |          2 |      3 |      2 |      2 |      3 |
 | |
|     /// +-----------+------------+--------+--------+--------+--------+
 | |
|     ///
 | |
|     /// And again until we're done
 | |
|     ///
 | |
|     /// +-----------+------------+--------+--------+--------+--------+
 | |
|     /// | Partition | Swap Index | Page 0 | Page 1 | Page 3 | Page 4 |
 | |
|     /// +-----------+------------+--------+--------+--------+--------+
 | |
|     /// |    Active |          3 |      3 |      2 |      1 |      - |
 | |
|     /// |       DFU |          3 |      3 |      1 |      2 |      3 |
 | |
|     /// +-----------+------------+--------+--------+--------+--------+
 | |
|     ///
 | |
|     /// REVERTING
 | |
|     ///
 | |
|     /// The reverting algorithm uses the swap index to discover that images were swapped, but that
 | |
|     /// the application failed to mark the boot successful. In this case, the revert algorithm will
 | |
|     /// run.
 | |
|     ///
 | |
|     /// The revert index is located separately from the swap index, to ensure that revert can continue
 | |
|     /// on power failure.
 | |
|     ///
 | |
|     /// The revert algorithm works forwards, by starting copying into the 'unused' DFU page at the start.
 | |
|     ///
 | |
|     /// +-----------+--------------+--------+--------+--------+--------+
 | |
|     /// | Partition | Revert Index | Page 0 | Page 1 | Page 3 | Page 4 |
 | |
|     //*/
 | |
|     /// +-----------+--------------+--------+--------+--------+--------+
 | |
|     /// |    Active |            3 |      1 |      2 |      1 |      - |
 | |
|     /// |       DFU |            3 |      3 |      1 |      2 |      3 |
 | |
|     /// +-----------+--------------+--------+--------+--------+--------+
 | |
|     ///
 | |
|     ///
 | |
|     /// +-----------+--------------+--------+--------+--------+--------+
 | |
|     /// | Partition | Revert Index | Page 0 | Page 1 | Page 3 | Page 4 |
 | |
|     /// +-----------+--------------+--------+--------+--------+--------+
 | |
|     /// |    Active |            3 |      1 |      2 |      1 |      - |
 | |
|     /// |       DFU |            3 |      3 |      2 |      2 |      3 |
 | |
|     /// +-----------+--------------+--------+--------+--------+--------+
 | |
|     ///
 | |
|     /// +-----------+--------------+--------+--------+--------+--------+
 | |
|     /// | Partition | Revert Index | Page 0 | Page 1 | Page 3 | Page 4 |
 | |
|     /// +-----------+--------------+--------+--------+--------+--------+
 | |
|     /// |    Active |            3 |      1 |      2 |      3 |      - |
 | |
|     /// |       DFU |            3 |      3 |      2 |      1 |      3 |
 | |
|     /// +-----------+--------------+--------+--------+--------+--------+
 | |
|     ///
 | |
|     pub fn prepare_boot(&mut self, aligned_buf: &mut [u8]) -> Result<State, BootError> {
 | |
|         // Ensure we have enough progress pages to store copy progress
 | |
|         assert_eq!(0, Self::PAGE_SIZE % aligned_buf.len() as u32);
 | |
|         assert_eq!(0, Self::PAGE_SIZE % ACTIVE::WRITE_SIZE as u32);
 | |
|         assert_eq!(0, Self::PAGE_SIZE % ACTIVE::ERASE_SIZE as u32);
 | |
|         assert_eq!(0, Self::PAGE_SIZE % DFU::WRITE_SIZE as u32);
 | |
|         assert_eq!(0, Self::PAGE_SIZE % DFU::ERASE_SIZE as u32);
 | |
|         assert!(aligned_buf.len() >= STATE::WRITE_SIZE);
 | |
|         assert_eq!(0, aligned_buf.len() % ACTIVE::WRITE_SIZE);
 | |
|         assert_eq!(0, aligned_buf.len() % DFU::WRITE_SIZE);
 | |
| 
 | |
|         assert_partitions(&self.active, &self.dfu, &self.state, Self::PAGE_SIZE);
 | |
| 
 | |
|         // Copy contents from partition N to active
 | |
|         let state = self.read_state(aligned_buf)?;
 | |
|         if state == State::Swap {
 | |
|             //
 | |
|             // Check if we already swapped. If we're in the swap state, this means we should revert
 | |
|             // since the app has failed to mark boot as successful
 | |
|             //
 | |
|             if !self.is_swapped(aligned_buf)? {
 | |
|                 trace!("Swapping");
 | |
|                 self.swap(aligned_buf)?;
 | |
|                 trace!("Swapping done");
 | |
|             } else {
 | |
|                 trace!("Reverting");
 | |
|                 self.revert(aligned_buf)?;
 | |
| 
 | |
|                 let state_word = &mut aligned_buf[..STATE::WRITE_SIZE];
 | |
| 
 | |
|                 // Invalidate progress
 | |
|                 state_word.fill(!STATE_ERASE_VALUE);
 | |
|                 self.state.write(STATE::WRITE_SIZE as u32, state_word)?;
 | |
| 
 | |
|                 // Clear magic and progress
 | |
|                 self.state.erase(0, self.state.capacity() as u32)?;
 | |
| 
 | |
|                 // Set magic
 | |
|                 state_word.fill(BOOT_MAGIC);
 | |
|                 self.state.write(0, state_word)?;
 | |
|             }
 | |
|         }
 | |
|         Ok(state)
 | |
|     }
 | |
| 
 | |
|     fn is_swapped(&mut self, aligned_buf: &mut [u8]) -> Result<bool, BootError> {
 | |
|         let page_count = self.active.capacity() / Self::PAGE_SIZE as usize;
 | |
|         let progress = self.current_progress(aligned_buf)?;
 | |
| 
 | |
|         Ok(progress >= page_count * 2)
 | |
|     }
 | |
| 
 | |
|     fn current_progress(&mut self, aligned_buf: &mut [u8]) -> Result<usize, BootError> {
 | |
|         let write_size = STATE::WRITE_SIZE as u32;
 | |
|         let max_index = ((self.state.capacity() - STATE::WRITE_SIZE) / STATE::WRITE_SIZE) - 2;
 | |
|         let state_word = &mut aligned_buf[..write_size as usize];
 | |
| 
 | |
|         self.state.read(write_size, state_word)?;
 | |
|         if state_word.iter().any(|&b| b != STATE_ERASE_VALUE) {
 | |
|             // Progress is invalid
 | |
|             return Ok(max_index);
 | |
|         }
 | |
| 
 | |
|         for index in 0..max_index {
 | |
|             self.state.read((2 + index) as u32 * write_size, state_word)?;
 | |
| 
 | |
|             if state_word.iter().any(|&b| b == STATE_ERASE_VALUE) {
 | |
|                 return Ok(index);
 | |
|             }
 | |
|         }
 | |
|         Ok(max_index)
 | |
|     }
 | |
| 
 | |
|     fn update_progress(&mut self, progress_index: usize, aligned_buf: &mut [u8]) -> Result<(), BootError> {
 | |
|         let state_word = &mut aligned_buf[..STATE::WRITE_SIZE];
 | |
|         state_word.fill(!STATE_ERASE_VALUE);
 | |
|         self.state
 | |
|             .write((2 + progress_index) as u32 * STATE::WRITE_SIZE as u32, state_word)?;
 | |
|         Ok(())
 | |
|     }
 | |
| 
 | |
|     fn copy_page_once_to_active(
 | |
|         &mut self,
 | |
|         progress_index: usize,
 | |
|         from_offset: u32,
 | |
|         to_offset: u32,
 | |
|         aligned_buf: &mut [u8],
 | |
|     ) -> Result<(), BootError> {
 | |
|         if self.current_progress(aligned_buf)? <= progress_index {
 | |
|             let page_size = Self::PAGE_SIZE as u32;
 | |
| 
 | |
|             self.active.erase(to_offset, to_offset + page_size)?;
 | |
| 
 | |
|             for offset_in_page in (0..page_size).step_by(aligned_buf.len()) {
 | |
|                 self.dfu.read(from_offset + offset_in_page as u32, aligned_buf)?;
 | |
|                 self.active.write(to_offset + offset_in_page as u32, aligned_buf)?;
 | |
|             }
 | |
| 
 | |
|             self.update_progress(progress_index, aligned_buf)?;
 | |
|         }
 | |
|         Ok(())
 | |
|     }
 | |
| 
 | |
|     fn copy_page_once_to_dfu(
 | |
|         &mut self,
 | |
|         progress_index: usize,
 | |
|         from_offset: u32,
 | |
|         to_offset: u32,
 | |
|         aligned_buf: &mut [u8],
 | |
|     ) -> Result<(), BootError> {
 | |
|         if self.current_progress(aligned_buf)? <= progress_index {
 | |
|             let page_size = Self::PAGE_SIZE as u32;
 | |
| 
 | |
|             self.dfu.erase(to_offset as u32, to_offset + page_size)?;
 | |
| 
 | |
|             for offset_in_page in (0..page_size).step_by(aligned_buf.len()) {
 | |
|                 self.active.read(from_offset + offset_in_page as u32, aligned_buf)?;
 | |
|                 self.dfu.write(to_offset + offset_in_page as u32, aligned_buf)?;
 | |
|             }
 | |
| 
 | |
|             self.update_progress(progress_index, aligned_buf)?;
 | |
|         }
 | |
|         Ok(())
 | |
|     }
 | |
| 
 | |
|     fn swap(&mut self, aligned_buf: &mut [u8]) -> Result<(), BootError> {
 | |
|         let page_count = self.active.capacity() as u32 / Self::PAGE_SIZE;
 | |
|         for page_num in 0..page_count {
 | |
|             let progress_index = (page_num * 2) as usize;
 | |
| 
 | |
|             // Copy active page to the 'next' DFU page.
 | |
|             let active_from_offset = (page_count - 1 - page_num) * Self::PAGE_SIZE;
 | |
|             let dfu_to_offset = (page_count - page_num) * Self::PAGE_SIZE;
 | |
|             //trace!("Copy active {} to dfu {}", active_from_offset, dfu_to_offset);
 | |
|             self.copy_page_once_to_dfu(progress_index, active_from_offset, dfu_to_offset, aligned_buf)?;
 | |
| 
 | |
|             // Copy DFU page to the active page
 | |
|             let active_to_offset = (page_count - 1 - page_num) * Self::PAGE_SIZE;
 | |
|             let dfu_from_offset = (page_count - 1 - page_num) * Self::PAGE_SIZE;
 | |
|             //trace!("Copy dfy {} to active {}", dfu_from_offset, active_to_offset);
 | |
|             self.copy_page_once_to_active(progress_index + 1, dfu_from_offset, active_to_offset, aligned_buf)?;
 | |
|         }
 | |
| 
 | |
|         Ok(())
 | |
|     }
 | |
| 
 | |
|     fn revert(&mut self, aligned_buf: &mut [u8]) -> Result<(), BootError> {
 | |
|         let page_count = self.active.capacity() as u32 / Self::PAGE_SIZE;
 | |
|         for page_num in 0..page_count {
 | |
|             let progress_index = (page_count * 2 + page_num * 2) as usize;
 | |
| 
 | |
|             // Copy the bad active page to the DFU page
 | |
|             let active_from_offset = page_num * Self::PAGE_SIZE;
 | |
|             let dfu_to_offset = page_num * Self::PAGE_SIZE;
 | |
|             self.copy_page_once_to_dfu(progress_index, active_from_offset, dfu_to_offset, aligned_buf)?;
 | |
| 
 | |
|             // Copy the DFU page back to the active page
 | |
|             let active_to_offset = page_num * Self::PAGE_SIZE;
 | |
|             let dfu_from_offset = (page_num + 1) * Self::PAGE_SIZE;
 | |
|             self.copy_page_once_to_active(progress_index + 1, dfu_from_offset, active_to_offset, aligned_buf)?;
 | |
|         }
 | |
| 
 | |
|         Ok(())
 | |
|     }
 | |
| 
 | |
|     fn read_state(&mut self, aligned_buf: &mut [u8]) -> Result<State, BootError> {
 | |
|         let state_word = &mut aligned_buf[..STATE::WRITE_SIZE];
 | |
|         self.state.read(0, state_word)?;
 | |
| 
 | |
|         if !state_word.iter().any(|&b| b != SWAP_MAGIC) {
 | |
|             Ok(State::Swap)
 | |
|         } else {
 | |
|             Ok(State::Boot)
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| fn assert_partitions<ACTIVE: NorFlash, DFU: NorFlash, STATE: NorFlash>(
 | |
|     active: &ACTIVE,
 | |
|     dfu: &DFU,
 | |
|     state: &STATE,
 | |
|     page_size: u32,
 | |
| ) {
 | |
|     assert_eq!(active.capacity() as u32 % page_size, 0);
 | |
|     assert_eq!(dfu.capacity() as u32 % page_size, 0);
 | |
|     assert!(dfu.capacity() as u32 - active.capacity() as u32 >= page_size);
 | |
|     assert!(2 + 2 * (active.capacity() as u32 / page_size) <= state.capacity() as u32 / STATE::WRITE_SIZE as u32);
 | |
| }
 | |
| 
 | |
| #[cfg(test)]
 | |
| mod tests {
 | |
|     use super::*;
 | |
|     use crate::mem_flash::MemFlash;
 | |
| 
 | |
|     #[test]
 | |
|     #[should_panic]
 | |
|     fn test_range_asserts() {
 | |
|         const ACTIVE_SIZE: usize = 4194304 - 4096;
 | |
|         const DFU_SIZE: usize = 4194304;
 | |
|         const STATE_SIZE: usize = 4096;
 | |
|         static ACTIVE: MemFlash<ACTIVE_SIZE, 4, 4> = MemFlash::new(0xFF);
 | |
|         static DFU: MemFlash<DFU_SIZE, 4, 4> = MemFlash::new(0xFF);
 | |
|         static STATE: MemFlash<STATE_SIZE, 4, 4> = MemFlash::new(0xFF);
 | |
|         assert_partitions(&ACTIVE, &DFU, &STATE, 4096);
 | |
|     }
 | |
| }
 |