Merge #935
935: Remove generic const expressions from embassy-boot r=lulf a=lulf * Remove the need for generic const expressions and use buffers provided in the flash config. * Extend embedded-storage traits to simplify generics. * Document all public APIs * Add toplevel README * Expose AlignedBuffer type for convenience. * Update examples Co-authored-by: Ulf Lilleengen <lulf@redhat.com>
This commit is contained in:
		
							
								
								
									
										30
									
								
								embassy-boot/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								embassy-boot/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| # embassy-boot | ||||
|  | ||||
| An [Embassy](https://embassy.dev) project. | ||||
|  | ||||
| A lightweight bootloader supporting firmware updates in a power-fail-safe way, with trial boots and rollbacks. | ||||
|  | ||||
| The bootloader can be used either as a library or be flashed directly with the default configuration derived from linker scripts. | ||||
|  | ||||
| By design, the bootloader does not provide any network capabilities. Networking capabilities for fetching new firmware can be provided by the user application, using the bootloader as a library for updating the firmware, or by using the bootloader as a library and adding this capability yourself. | ||||
|  | ||||
| ## Hardware support | ||||
|  | ||||
| The bootloader supports different hardware in separate crates: | ||||
|  | ||||
| * `embassy-boot-nrf` - for the nRF microcontrollers. | ||||
| * `embassy-boot-stm32` - for the STM32 microcontrollers. | ||||
|  | ||||
| ## Minimum supported Rust version (MSRV) | ||||
|  | ||||
| `embassy-boot` requires Rust nightly to compile as it relies on async traits for interacting with the flash peripherals. | ||||
|  | ||||
| ## License | ||||
|  | ||||
| This work is licensed under either of | ||||
|  | ||||
| - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or | ||||
|   <http://www.apache.org/licenses/LICENSE-2.0>) | ||||
| - MIT license ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>) | ||||
|  | ||||
| at your option. | ||||
| @@ -1,53 +1,54 @@ | ||||
| #![feature(type_alias_impl_trait)] | ||||
| #![feature(generic_associated_types)] | ||||
| #![feature(generic_const_exprs)] | ||||
| #![allow(incomplete_features)] | ||||
| #![no_std] | ||||
| ///! embassy-boot is a bootloader and firmware updater for embedded devices with flash | ||||
| ///! storage implemented using embedded-storage | ||||
| ///! | ||||
| ///! The bootloader works in conjunction with the firmware application, and only has the | ||||
| ///! ability to manage two flash banks with an active and a updatable part. It implements | ||||
| ///! a swap algorithm that is power-failure safe, and allows reverting to the previous | ||||
| ///! version of the firmware, should the application crash and fail to mark itself as booted. | ||||
| ///! | ||||
| ///! This library is intended to be used by platform-specific bootloaders, such as embassy-boot-nrf, | ||||
| ///! which defines the limits and flash type for that particular platform. | ||||
| ///! | ||||
| #![warn(missing_docs)] | ||||
| #![doc = include_str!("../../README.md")] | ||||
| mod fmt; | ||||
|  | ||||
| use embedded_storage::nor_flash::{NorFlash, NorFlashError, NorFlashErrorKind, ReadNorFlash}; | ||||
| use embedded_storage::nor_flash::{ErrorType, NorFlash, NorFlashError, NorFlashErrorKind, ReadNorFlash}; | ||||
| use embedded_storage_async::nor_flash::AsyncNorFlash; | ||||
|  | ||||
| const BOOT_MAGIC: u8 = 0xD0; | ||||
| const SWAP_MAGIC: u8 = 0xF0; | ||||
|  | ||||
| /// A region in flash used by the bootloader. | ||||
| #[derive(Copy, Clone, Debug)] | ||||
| #[cfg_attr(feature = "defmt", derive(defmt::Format))] | ||||
| pub struct Partition { | ||||
|     /// Start of the flash region. | ||||
|     pub from: usize, | ||||
|     /// End of the flash region. | ||||
|     pub to: usize, | ||||
| } | ||||
|  | ||||
| impl Partition { | ||||
|     /// Create a new partition with the provided range | ||||
|     pub const fn new(from: usize, to: usize) -> Self { | ||||
|         Self { from, to } | ||||
|     } | ||||
|  | ||||
|     /// Return the length of the partition | ||||
|     pub const fn len(&self) -> usize { | ||||
|         self.to - self.from | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(PartialEq, Debug)] | ||||
| /// The state of the bootloader after running prepare. | ||||
| #[derive(PartialEq, Eq, Debug)] | ||||
| #[cfg_attr(feature = "defmt", derive(defmt::Format))] | ||||
| pub enum State { | ||||
|     /// Bootloader is ready to boot the active partition. | ||||
|     Boot, | ||||
|     /// Bootloader has swapped the active partition with the dfu partition and will attempt boot. | ||||
|     Swap, | ||||
| } | ||||
|  | ||||
| #[derive(PartialEq, Debug)] | ||||
| /// Errors returned by bootloader | ||||
| #[derive(PartialEq, Eq, Debug)] | ||||
| pub enum BootError { | ||||
|     /// Error from flash. | ||||
|     Flash(NorFlashErrorKind), | ||||
|     /// Invalid bootloader magic | ||||
|     BadMagic, | ||||
| } | ||||
|  | ||||
| @@ -60,19 +61,39 @@ where | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub trait FlashConfig { | ||||
|     const BLOCK_SIZE: usize; | ||||
|     const ERASE_VALUE: u8; | ||||
|     type FLASH: NorFlash + ReadNorFlash; | ||||
| /// Buffer aligned to 32 byte boundary, largest known alignment requirement for embassy-boot. | ||||
| #[repr(align(32))] | ||||
| pub struct AlignedBuffer<const N: usize>(pub [u8; N]); | ||||
|  | ||||
|     fn flash(&mut self) -> &mut Self::FLASH; | ||||
| impl<const N: usize> AsRef<[u8]> for AlignedBuffer<N> { | ||||
|     fn as_ref(&self) -> &[u8] { | ||||
|         &self.0 | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl<const N: usize> AsMut<[u8]> for AlignedBuffer<N> { | ||||
|     fn as_mut(&mut self) -> &mut [u8] { | ||||
|         &mut self.0 | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Extension of the embedded-storage flash type information with block size and erase value. | ||||
| pub trait Flash: NorFlash + ReadNorFlash { | ||||
|     /// The block size that should be used when writing to flash. For most builtin flashes, this is the same as the erase | ||||
|     /// size of the flash, but for external QSPI flash modules, this can be lower. | ||||
|     const BLOCK_SIZE: usize; | ||||
|     /// The erase value of the flash. Typically the default of 0xFF is used, but some flashes use a different value. | ||||
|     const ERASE_VALUE: u8 = 0xFF; | ||||
| } | ||||
|  | ||||
| /// Trait defining the flash handles used for active and DFU partition | ||||
| pub trait FlashProvider { | ||||
|     type STATE: FlashConfig; | ||||
|     type ACTIVE: FlashConfig; | ||||
|     type DFU: FlashConfig; | ||||
| pub trait FlashConfig { | ||||
|     /// Flash type used for the state partition. | ||||
|     type STATE: Flash; | ||||
|     /// Flash type used for the active partition. | ||||
|     type ACTIVE: Flash; | ||||
|     /// Flash type used for the dfu partition. | ||||
|     type DFU: Flash; | ||||
|  | ||||
|     /// Return flash instance used to write/read to/from active partition. | ||||
|     fn active(&mut self) -> &mut Self::ACTIVE; | ||||
| @@ -84,9 +105,7 @@ pub trait FlashProvider { | ||||
|  | ||||
| /// BootLoader works with any flash implementing embedded_storage and can also work with | ||||
| /// different page sizes and flash write sizes. | ||||
| /// | ||||
| /// The PAGE_SIZE const parameter must be a multiple of the ACTIVE and DFU page sizes. | ||||
| pub struct BootLoader<const PAGE_SIZE: usize> { | ||||
| pub struct BootLoader { | ||||
|     // Page with current state of bootloader. The state partition has the following format: | ||||
|     // | Range          | Description                                                                      | | ||||
|     // | 0 - WRITE_SIZE | Magic indicating bootloader state. BOOT_MAGIC means boot, SWAP_MAGIC means swap. | | ||||
| @@ -98,15 +117,16 @@ pub struct BootLoader<const PAGE_SIZE: usize> { | ||||
|     dfu: Partition, | ||||
| } | ||||
|  | ||||
| impl<const PAGE_SIZE: usize> BootLoader<PAGE_SIZE> { | ||||
| impl BootLoader { | ||||
|     /// Create a new instance of a bootloader with the given 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(active: Partition, dfu: Partition, state: Partition) -> Self { | ||||
|         assert_eq!(active.len() % PAGE_SIZE, 0); | ||||
|         assert_eq!(dfu.len() % PAGE_SIZE, 0); | ||||
|         // DFU partition must have an extra page | ||||
|         assert!(dfu.len() - active.len() >= PAGE_SIZE); | ||||
|         Self { active, dfu, state } | ||||
|     } | ||||
|  | ||||
|     /// Return the boot address for the active partition. | ||||
|     pub fn boot_address(&self) -> usize { | ||||
|         self.active.from | ||||
|     } | ||||
| @@ -194,44 +214,43 @@ impl<const PAGE_SIZE: usize> BootLoader<PAGE_SIZE> { | ||||
|     /// |       DFU |            3 |      3 |      2 |      1 |      3 | | ||||
|     /// +-----------+--------------+--------+--------+--------+--------+ | ||||
|     /// | ||||
|     pub fn prepare_boot<P: FlashProvider>(&mut self, p: &mut P) -> Result<State, BootError> | ||||
|     where | ||||
|         [(); <<P as FlashProvider>::STATE as FlashConfig>::FLASH::WRITE_SIZE]:, | ||||
|         [(); <<P as FlashProvider>::ACTIVE as FlashConfig>::FLASH::ERASE_SIZE]:, | ||||
|     { | ||||
|     pub fn prepare_boot<P: FlashConfig>( | ||||
|         &mut self, | ||||
|         p: &mut P, | ||||
|         magic: &mut [u8], | ||||
|         page: &mut [u8], | ||||
|     ) -> Result<State, BootError> { | ||||
|         // Ensure we have enough progress pages to store copy progress | ||||
|         assert!( | ||||
|             self.active.len() / PAGE_SIZE | ||||
|                 <= (self.state.len() - <<P as FlashProvider>::STATE as FlashConfig>::FLASH::WRITE_SIZE) | ||||
|                     / <<P as FlashProvider>::STATE as FlashConfig>::FLASH::WRITE_SIZE | ||||
|         ); | ||||
|         assert_eq!(self.active.len() % page.len(), 0); | ||||
|         assert_eq!(self.dfu.len() % page.len(), 0); | ||||
|         assert!(self.dfu.len() - self.active.len() >= page.len()); | ||||
|         assert!(self.active.len() / page.len() <= (self.state.len() - P::STATE::WRITE_SIZE) / P::STATE::WRITE_SIZE); | ||||
|         assert_eq!(magic.len(), P::STATE::WRITE_SIZE); | ||||
|  | ||||
|         // Copy contents from partition N to active | ||||
|         let state = self.read_state(p.state())?; | ||||
|         let state = self.read_state(p, magic)?; | ||||
|         match 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(p.state())? { | ||||
|                 if !self.is_swapped(p, magic, page)? { | ||||
|                     trace!("Swapping"); | ||||
|                     self.swap(p)?; | ||||
|                     self.swap(p, magic, page)?; | ||||
|                     trace!("Swapping done"); | ||||
|                 } else { | ||||
|                     trace!("Reverting"); | ||||
|                     self.revert(p)?; | ||||
|                     self.revert(p, magic, page)?; | ||||
|  | ||||
|                     // Overwrite magic and reset progress | ||||
|                     let fstate = p.state().flash(); | ||||
|                     let aligned = Aligned( | ||||
|                         [!P::STATE::ERASE_VALUE; <<P as FlashProvider>::STATE as FlashConfig>::FLASH::WRITE_SIZE], | ||||
|                     ); | ||||
|                     fstate.write(self.state.from as u32, &aligned.0)?; | ||||
|                     let fstate = p.state(); | ||||
|                     magic.fill(!P::STATE::ERASE_VALUE); | ||||
|                     fstate.write(self.state.from as u32, magic)?; | ||||
|                     fstate.erase(self.state.from as u32, self.state.to as u32)?; | ||||
|                     let aligned = | ||||
|                         Aligned([BOOT_MAGIC; <<P as FlashProvider>::STATE as FlashConfig>::FLASH::WRITE_SIZE]); | ||||
|                     fstate.write(self.state.from as u32, &aligned.0)?; | ||||
|  | ||||
|                     magic.fill(BOOT_MAGIC); | ||||
|                     fstate.write(self.state.from as u32, magic)?; | ||||
|                 } | ||||
|             } | ||||
|             _ => {} | ||||
| @@ -239,166 +258,152 @@ impl<const PAGE_SIZE: usize> BootLoader<PAGE_SIZE> { | ||||
|         Ok(state) | ||||
|     } | ||||
|  | ||||
|     fn is_swapped<P: FlashConfig>(&mut self, p: &mut P) -> Result<bool, BootError> | ||||
|     where | ||||
|         [(); P::FLASH::WRITE_SIZE]:, | ||||
|     { | ||||
|         let page_count = self.active.len() / P::FLASH::ERASE_SIZE; | ||||
|         let progress = self.current_progress(p)?; | ||||
|     fn is_swapped<P: FlashConfig>(&mut self, p: &mut P, magic: &mut [u8], page: &mut [u8]) -> Result<bool, BootError> { | ||||
|         let page_size = page.len(); | ||||
|         let page_count = self.active.len() / page_size; | ||||
|         let progress = self.current_progress(p, magic)?; | ||||
|  | ||||
|         Ok(progress >= page_count * 2) | ||||
|     } | ||||
|  | ||||
|     fn current_progress<P: FlashConfig>(&mut self, p: &mut P) -> Result<usize, BootError> | ||||
|     where | ||||
|         [(); P::FLASH::WRITE_SIZE]:, | ||||
|     { | ||||
|         let write_size = P::FLASH::WRITE_SIZE; | ||||
|     fn current_progress<P: FlashConfig>(&mut self, config: &mut P, aligned: &mut [u8]) -> Result<usize, BootError> { | ||||
|         let write_size = aligned.len(); | ||||
|         let max_index = ((self.state.len() - write_size) / write_size) - 1; | ||||
|         let flash = p.flash(); | ||||
|         let mut aligned = Aligned([!P::ERASE_VALUE; P::FLASH::WRITE_SIZE]); | ||||
|         aligned.fill(!P::STATE::ERASE_VALUE); | ||||
|  | ||||
|         let flash = config.state(); | ||||
|         for i in 0..max_index { | ||||
|             flash.read((self.state.from + write_size + i * write_size) as u32, &mut aligned.0)?; | ||||
|             if aligned.0 == [P::ERASE_VALUE; P::FLASH::WRITE_SIZE] { | ||||
|             flash.read((self.state.from + write_size + i * write_size) as u32, aligned)?; | ||||
|  | ||||
|             if aligned.iter().any(|&b| b == P::STATE::ERASE_VALUE) { | ||||
|                 return Ok(i); | ||||
|             } | ||||
|         } | ||||
|         Ok(max_index) | ||||
|     } | ||||
|  | ||||
|     fn update_progress<P: FlashConfig>(&mut self, idx: usize, p: &mut P) -> Result<(), BootError> | ||||
|     where | ||||
|         [(); P::FLASH::WRITE_SIZE]:, | ||||
|     { | ||||
|         let flash = p.flash(); | ||||
|         let write_size = P::FLASH::WRITE_SIZE; | ||||
|     fn update_progress<P: FlashConfig>(&mut self, idx: usize, p: &mut P, magic: &mut [u8]) -> Result<(), BootError> { | ||||
|         let flash = p.state(); | ||||
|         let write_size = magic.len(); | ||||
|         let w = self.state.from + write_size + idx * write_size; | ||||
|         let aligned = Aligned([!P::ERASE_VALUE; P::FLASH::WRITE_SIZE]); | ||||
|         flash.write(w as u32, &aligned.0)?; | ||||
|  | ||||
|         let aligned = magic; | ||||
|         aligned.fill(!P::STATE::ERASE_VALUE); | ||||
|         flash.write(w as u32, aligned)?; | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     fn active_addr(&self, n: usize) -> usize { | ||||
|         self.active.from + n * PAGE_SIZE | ||||
|     fn active_addr(&self, n: usize, page_size: usize) -> usize { | ||||
|         self.active.from + n * page_size | ||||
|     } | ||||
|  | ||||
|     fn dfu_addr(&self, n: usize) -> usize { | ||||
|         self.dfu.from + n * PAGE_SIZE | ||||
|     fn dfu_addr(&self, n: usize, page_size: usize) -> usize { | ||||
|         self.dfu.from + n * page_size | ||||
|     } | ||||
|  | ||||
|     fn copy_page_once_to_active<P: FlashProvider>( | ||||
|     fn copy_page_once_to_active<P: FlashConfig>( | ||||
|         &mut self, | ||||
|         idx: usize, | ||||
|         from_page: usize, | ||||
|         to_page: usize, | ||||
|         p: &mut P, | ||||
|     ) -> Result<(), BootError> | ||||
|     where | ||||
|         [(); <<P as FlashProvider>::STATE as FlashConfig>::FLASH::WRITE_SIZE]:, | ||||
|     { | ||||
|         let mut buf: [u8; PAGE_SIZE] = [0; PAGE_SIZE]; | ||||
|         if self.current_progress(p.state())? <= idx { | ||||
|         magic: &mut [u8], | ||||
|         page: &mut [u8], | ||||
|     ) -> Result<(), BootError> { | ||||
|         let buf = page; | ||||
|         if self.current_progress(p, magic)? <= idx { | ||||
|             let mut offset = from_page; | ||||
|             for chunk in buf.chunks_mut(P::DFU::BLOCK_SIZE) { | ||||
|                 p.dfu().flash().read(offset as u32, chunk)?; | ||||
|                 p.dfu().read(offset as u32, chunk)?; | ||||
|                 offset += chunk.len(); | ||||
|             } | ||||
|  | ||||
|             p.active().flash().erase(to_page as u32, (to_page + PAGE_SIZE) as u32)?; | ||||
|             p.active().erase(to_page as u32, (to_page + buf.len()) as u32)?; | ||||
|  | ||||
|             let mut offset = to_page; | ||||
|             for chunk in buf.chunks(P::ACTIVE::BLOCK_SIZE) { | ||||
|                 p.active().flash().write(offset as u32, &chunk)?; | ||||
|                 p.active().write(offset as u32, chunk)?; | ||||
|                 offset += chunk.len(); | ||||
|             } | ||||
|             self.update_progress(idx, p.state())?; | ||||
|             self.update_progress(idx, p, magic)?; | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     fn copy_page_once_to_dfu<P: FlashProvider>( | ||||
|     fn copy_page_once_to_dfu<P: FlashConfig>( | ||||
|         &mut self, | ||||
|         idx: usize, | ||||
|         from_page: usize, | ||||
|         to_page: usize, | ||||
|         p: &mut P, | ||||
|     ) -> Result<(), BootError> | ||||
|     where | ||||
|         [(); <<P as FlashProvider>::STATE as FlashConfig>::FLASH::WRITE_SIZE]:, | ||||
|     { | ||||
|         let mut buf: [u8; PAGE_SIZE] = [0; PAGE_SIZE]; | ||||
|         if self.current_progress(p.state())? <= idx { | ||||
|         magic: &mut [u8], | ||||
|         page: &mut [u8], | ||||
|     ) -> Result<(), BootError> { | ||||
|         let buf = page; | ||||
|         if self.current_progress(p, magic)? <= idx { | ||||
|             let mut offset = from_page; | ||||
|             for chunk in buf.chunks_mut(P::ACTIVE::BLOCK_SIZE) { | ||||
|                 p.active().flash().read(offset as u32, chunk)?; | ||||
|                 p.active().read(offset as u32, chunk)?; | ||||
|                 offset += chunk.len(); | ||||
|             } | ||||
|  | ||||
|             p.dfu().flash().erase(to_page as u32, (to_page + PAGE_SIZE) as u32)?; | ||||
|             p.dfu().erase(to_page as u32, (to_page + buf.len()) as u32)?; | ||||
|  | ||||
|             let mut offset = to_page; | ||||
|             for chunk in buf.chunks(P::DFU::BLOCK_SIZE) { | ||||
|                 p.dfu().flash().write(offset as u32, chunk)?; | ||||
|                 p.dfu().write(offset as u32, chunk)?; | ||||
|                 offset += chunk.len(); | ||||
|             } | ||||
|             self.update_progress(idx, p.state())?; | ||||
|             self.update_progress(idx, p, magic)?; | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     fn swap<P: FlashProvider>(&mut self, p: &mut P) -> Result<(), BootError> | ||||
|     where | ||||
|         [(); <<P as FlashProvider>::STATE as FlashConfig>::FLASH::WRITE_SIZE]:, | ||||
|     { | ||||
|         let page_count = self.active.len() / PAGE_SIZE; | ||||
|     fn swap<P: FlashConfig>(&mut self, p: &mut P, magic: &mut [u8], page: &mut [u8]) -> Result<(), BootError> { | ||||
|         let page_size = page.len(); | ||||
|         let page_count = self.active.len() / page_size; | ||||
|         trace!("Page count: {}", page_count); | ||||
|         for page in 0..page_count { | ||||
|             trace!("COPY PAGE {}", page); | ||||
|         for page_num in 0..page_count { | ||||
|             trace!("COPY PAGE {}", page_num); | ||||
|             // Copy active page to the 'next' DFU page. | ||||
|             let active_page = self.active_addr(page_count - 1 - page); | ||||
|             let dfu_page = self.dfu_addr(page_count - page); | ||||
|             let active_page = self.active_addr(page_count - 1 - page_num, page_size); | ||||
|             let dfu_page = self.dfu_addr(page_count - page_num, page_size); | ||||
|             //trace!("Copy active {} to dfu {}", active_page, dfu_page); | ||||
|             self.copy_page_once_to_dfu(page * 2, active_page, dfu_page, p)?; | ||||
|             self.copy_page_once_to_dfu(page_num * 2, active_page, dfu_page, p, magic, page)?; | ||||
|  | ||||
|             // Copy DFU page to the active page | ||||
|             let active_page = self.active_addr(page_count - 1 - page); | ||||
|             let dfu_page = self.dfu_addr(page_count - 1 - page); | ||||
|             let active_page = self.active_addr(page_count - 1 - page_num, page_size); | ||||
|             let dfu_page = self.dfu_addr(page_count - 1 - page_num, page_size); | ||||
|             //trace!("Copy dfy {} to active {}", dfu_page, active_page); | ||||
|             self.copy_page_once_to_active(page * 2 + 1, dfu_page, active_page, p)?; | ||||
|             self.copy_page_once_to_active(page_num * 2 + 1, dfu_page, active_page, p, magic, page)?; | ||||
|         } | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     fn revert<P: FlashProvider>(&mut self, p: &mut P) -> Result<(), BootError> | ||||
|     where | ||||
|         [(); <<P as FlashProvider>::STATE as FlashConfig>::FLASH::WRITE_SIZE]:, | ||||
|     { | ||||
|         let page_count = self.active.len() / PAGE_SIZE; | ||||
|         for page in 0..page_count { | ||||
|     fn revert<P: FlashConfig>(&mut self, p: &mut P, magic: &mut [u8], page: &mut [u8]) -> Result<(), BootError> { | ||||
|         let page_size = page.len(); | ||||
|         let page_count = self.active.len() / page_size; | ||||
|         for page_num in 0..page_count { | ||||
|             // Copy the bad active page to the DFU page | ||||
|             let active_page = self.active_addr(page); | ||||
|             let dfu_page = self.dfu_addr(page); | ||||
|             self.copy_page_once_to_dfu(page_count * 2 + page * 2, active_page, dfu_page, p)?; | ||||
|             let active_page = self.active_addr(page_num, page_size); | ||||
|             let dfu_page = self.dfu_addr(page_num, page_size); | ||||
|             self.copy_page_once_to_dfu(page_count * 2 + page_num * 2, active_page, dfu_page, p, magic, page)?; | ||||
|  | ||||
|             // Copy the DFU page back to the active page | ||||
|             let active_page = self.active_addr(page); | ||||
|             let dfu_page = self.dfu_addr(page + 1); | ||||
|             self.copy_page_once_to_active(page_count * 2 + page * 2 + 1, dfu_page, active_page, p)?; | ||||
|             let active_page = self.active_addr(page_num, page_size); | ||||
|             let dfu_page = self.dfu_addr(page_num + 1, page_size); | ||||
|             self.copy_page_once_to_active(page_count * 2 + page_num * 2 + 1, dfu_page, active_page, p, magic, page)?; | ||||
|         } | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     fn read_state<P: FlashConfig>(&mut self, p: &mut P) -> Result<State, BootError> | ||||
|     where | ||||
|         [(); P::FLASH::WRITE_SIZE]:, | ||||
|     { | ||||
|         let mut magic: [u8; P::FLASH::WRITE_SIZE] = [0; P::FLASH::WRITE_SIZE]; | ||||
|         let flash = p.flash(); | ||||
|         flash.read(self.state.from as u32, &mut magic)?; | ||||
|     fn read_state<P: FlashConfig>(&mut self, config: &mut P, magic: &mut [u8]) -> Result<State, BootError> { | ||||
|         let flash = config.state(); | ||||
|         flash.read(self.state.from as u32, magic)?; | ||||
|  | ||||
|         if magic == [SWAP_MAGIC; P::FLASH::WRITE_SIZE] { | ||||
|         if !magic.iter().any(|&b| b != SWAP_MAGIC) { | ||||
|             Ok(State::Swap) | ||||
|         } else { | ||||
|             Ok(State::Boot) | ||||
| @@ -406,108 +411,149 @@ impl<const PAGE_SIZE: usize> BootLoader<PAGE_SIZE> { | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Convenience provider that uses a single flash for everything | ||||
| pub struct SingleFlashProvider<'a, F, const ERASE_VALUE: u8 = 0xFF> | ||||
| /// Convenience provider that uses a single flash for all partitions. | ||||
| pub struct SingleFlashConfig<'a, F> | ||||
| where | ||||
|     F: NorFlash + ReadNorFlash, | ||||
|     F: Flash, | ||||
| { | ||||
|     config: SingleFlashConfig<'a, F, ERASE_VALUE>, | ||||
|     flash: &'a mut F, | ||||
| } | ||||
|  | ||||
| impl<'a, F, const ERASE_VALUE: u8> SingleFlashProvider<'a, F, ERASE_VALUE> | ||||
| impl<'a, F> SingleFlashConfig<'a, F> | ||||
| where | ||||
|     F: NorFlash + ReadNorFlash, | ||||
|     F: Flash, | ||||
| { | ||||
|     /// Create a provider for a single flash. | ||||
|     pub fn new(flash: &'a mut F) -> Self { | ||||
|         Self { | ||||
|             config: SingleFlashConfig { flash }, | ||||
|         } | ||||
|         Self { flash } | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub struct SingleFlashConfig<'a, F, const ERASE_VALUE: u8 = 0xFF> | ||||
| impl<'a, F> FlashConfig for SingleFlashConfig<'a, F> | ||||
| where | ||||
|     F: Flash, | ||||
| { | ||||
|     type STATE = F; | ||||
|     type ACTIVE = F; | ||||
|     type DFU = F; | ||||
|  | ||||
|     fn active(&mut self) -> &mut Self::STATE { | ||||
|         self.flash | ||||
|     } | ||||
|     fn dfu(&mut self) -> &mut Self::ACTIVE { | ||||
|         self.flash | ||||
|     } | ||||
|     fn state(&mut self) -> &mut Self::DFU { | ||||
|         self.flash | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// A flash wrapper implementing the Flash and embedded_storage traits. | ||||
| pub struct BootFlash<'a, F, const BLOCK_SIZE: usize, const ERASE_VALUE: u8 = 0xFF> | ||||
| where | ||||
|     F: NorFlash + ReadNorFlash, | ||||
| { | ||||
|     flash: &'a mut F, | ||||
| } | ||||
|  | ||||
| impl<'a, F> FlashProvider for SingleFlashProvider<'a, F> | ||||
| impl<'a, F, const BLOCK_SIZE: usize, const ERASE_VALUE: u8> BootFlash<'a, F, BLOCK_SIZE, ERASE_VALUE> | ||||
| where | ||||
|     F: NorFlash + ReadNorFlash, | ||||
| { | ||||
|     type STATE = SingleFlashConfig<'a, F>; | ||||
|     type ACTIVE = SingleFlashConfig<'a, F>; | ||||
|     type DFU = SingleFlashConfig<'a, F>; | ||||
|  | ||||
|     fn active(&mut self) -> &mut Self::STATE { | ||||
|         &mut self.config | ||||
|     } | ||||
|     fn dfu(&mut self) -> &mut Self::ACTIVE { | ||||
|         &mut self.config | ||||
|     } | ||||
|     fn state(&mut self) -> &mut Self::DFU { | ||||
|         &mut self.config | ||||
|     /// Create a new instance of a bootable flash | ||||
|     pub fn new(flash: &'a mut F) -> Self { | ||||
|         Self { flash } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl<'a, F, const ERASE_VALUE: u8> FlashConfig for SingleFlashConfig<'a, F, ERASE_VALUE> | ||||
| impl<'a, F, const BLOCK_SIZE: usize, const ERASE_VALUE: u8> Flash for BootFlash<'a, F, BLOCK_SIZE, ERASE_VALUE> | ||||
| where | ||||
|     F: NorFlash + ReadNorFlash, | ||||
| { | ||||
|     const BLOCK_SIZE: usize = F::ERASE_SIZE; | ||||
|     const BLOCK_SIZE: usize = BLOCK_SIZE; | ||||
|     const ERASE_VALUE: u8 = ERASE_VALUE; | ||||
|     type FLASH = F; | ||||
|     fn flash(&mut self) -> &mut F { | ||||
|         self.flash | ||||
| } | ||||
|  | ||||
| impl<'a, F, const BLOCK_SIZE: usize, const ERASE_VALUE: u8> ErrorType for BootFlash<'a, F, BLOCK_SIZE, ERASE_VALUE> | ||||
| where | ||||
|     F: ReadNorFlash + NorFlash, | ||||
| { | ||||
|     type Error = F::Error; | ||||
| } | ||||
|  | ||||
| impl<'a, F, const BLOCK_SIZE: usize, const ERASE_VALUE: u8> NorFlash for BootFlash<'a, F, BLOCK_SIZE, ERASE_VALUE> | ||||
| where | ||||
|     F: ReadNorFlash + NorFlash, | ||||
| { | ||||
|     const WRITE_SIZE: usize = F::WRITE_SIZE; | ||||
|     const ERASE_SIZE: usize = F::ERASE_SIZE; | ||||
|  | ||||
|     fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> { | ||||
|         F::erase(self.flash, from, to) | ||||
|     } | ||||
|  | ||||
|     fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> { | ||||
|         F::write(self.flash, offset, bytes) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Convenience provider that uses a single flash for everything | ||||
| pub struct MultiFlashProvider<'a, ACTIVE, STATE, DFU> | ||||
| impl<'a, F, const BLOCK_SIZE: usize, const ERASE_VALUE: u8> ReadNorFlash for BootFlash<'a, F, BLOCK_SIZE, ERASE_VALUE> | ||||
| where | ||||
|     ACTIVE: NorFlash + ReadNorFlash, | ||||
|     STATE: NorFlash + ReadNorFlash, | ||||
|     DFU: NorFlash + ReadNorFlash, | ||||
|     F: ReadNorFlash + NorFlash, | ||||
| { | ||||
|     active: SingleFlashConfig<'a, ACTIVE>, | ||||
|     state: SingleFlashConfig<'a, STATE>, | ||||
|     dfu: SingleFlashConfig<'a, DFU>, | ||||
|     const READ_SIZE: usize = F::READ_SIZE; | ||||
|  | ||||
|     fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> { | ||||
|         F::read(self.flash, offset, bytes) | ||||
|     } | ||||
|  | ||||
|     fn capacity(&self) -> usize { | ||||
|         F::capacity(self.flash) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl<'a, ACTIVE, STATE, DFU> MultiFlashProvider<'a, ACTIVE, STATE, DFU> | ||||
| /// Convenience flash provider that uses separate flash instances for each partition. | ||||
| pub struct MultiFlashConfig<'a, ACTIVE, STATE, DFU> | ||||
| where | ||||
|     ACTIVE: NorFlash + ReadNorFlash, | ||||
|     STATE: NorFlash + ReadNorFlash, | ||||
|     DFU: NorFlash + ReadNorFlash, | ||||
|     ACTIVE: Flash, | ||||
|     STATE: Flash, | ||||
|     DFU: Flash, | ||||
| { | ||||
|     active: &'a mut ACTIVE, | ||||
|     state: &'a mut STATE, | ||||
|     dfu: &'a mut DFU, | ||||
| } | ||||
|  | ||||
| impl<'a, ACTIVE, STATE, DFU> MultiFlashConfig<'a, ACTIVE, STATE, DFU> | ||||
| where | ||||
|     ACTIVE: Flash, | ||||
|     STATE: Flash, | ||||
|     DFU: Flash, | ||||
| { | ||||
|     /// Create a new flash provider with separate configuration for all three partitions. | ||||
|     pub fn new(active: &'a mut ACTIVE, state: &'a mut STATE, dfu: &'a mut DFU) -> Self { | ||||
|         Self { | ||||
|             active: SingleFlashConfig { flash: active }, | ||||
|             state: SingleFlashConfig { flash: state }, | ||||
|             dfu: SingleFlashConfig { flash: dfu }, | ||||
|         } | ||||
|         Self { active, state, dfu } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl<'a, ACTIVE, STATE, DFU> FlashProvider for MultiFlashProvider<'a, ACTIVE, STATE, DFU> | ||||
| impl<'a, ACTIVE, STATE, DFU> FlashConfig for MultiFlashConfig<'a, ACTIVE, STATE, DFU> | ||||
| where | ||||
|     ACTIVE: NorFlash + ReadNorFlash, | ||||
|     STATE: NorFlash + ReadNorFlash, | ||||
|     DFU: NorFlash + ReadNorFlash, | ||||
|     ACTIVE: Flash, | ||||
|     STATE: Flash, | ||||
|     DFU: Flash, | ||||
| { | ||||
|     type STATE = SingleFlashConfig<'a, STATE>; | ||||
|     type ACTIVE = SingleFlashConfig<'a, ACTIVE>; | ||||
|     type DFU = SingleFlashConfig<'a, DFU>; | ||||
|     type STATE = STATE; | ||||
|     type ACTIVE = ACTIVE; | ||||
|     type DFU = DFU; | ||||
|  | ||||
|     fn active(&mut self) -> &mut Self::ACTIVE { | ||||
|         &mut self.active | ||||
|         self.active | ||||
|     } | ||||
|     fn dfu(&mut self) -> &mut Self::DFU { | ||||
|         &mut self.dfu | ||||
|         self.dfu | ||||
|     } | ||||
|     fn state(&mut self) -> &mut Self::STATE { | ||||
|         &mut self.state | ||||
|         self.state | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -518,10 +564,6 @@ pub struct FirmwareUpdater { | ||||
|     dfu: Partition, | ||||
| } | ||||
|  | ||||
| // NOTE: Aligned to the largest write size supported by flash | ||||
| #[repr(align(32))] | ||||
| pub struct Aligned<const N: usize>([u8; N]); | ||||
|  | ||||
| impl Default for FirmwareUpdater { | ||||
|     fn default() -> Self { | ||||
|         extern "C" { | ||||
| @@ -551,6 +593,7 @@ impl Default for FirmwareUpdater { | ||||
| } | ||||
|  | ||||
| impl FirmwareUpdater { | ||||
|     /// Create a firmware updater instance with partition ranges for the update and state partitions. | ||||
|     pub const fn new(dfu: Partition, state: Partition) -> Self { | ||||
|         Self { dfu, state } | ||||
|     } | ||||
| @@ -560,23 +603,24 @@ impl FirmwareUpdater { | ||||
|         self.dfu.len() | ||||
|     } | ||||
|  | ||||
|     /// Instruct bootloader that DFU should commence at next boot. | ||||
|     /// Must be provided with an aligned buffer to use for reading and writing magic; | ||||
|     pub async fn update<F: AsyncNorFlash>(&mut self, flash: &mut F) -> Result<(), F::Error> | ||||
|     where | ||||
|         [(); F::WRITE_SIZE]:, | ||||
|     { | ||||
|         let mut aligned = Aligned([0; { F::WRITE_SIZE }]); | ||||
|         self.set_magic(&mut aligned.0, SWAP_MAGIC, flash).await | ||||
|     /// Mark to trigger firmware swap on next boot. | ||||
|     /// | ||||
|     /// # Safety | ||||
|     /// | ||||
|     /// The `aligned` buffer must have a size of F::WRITE_SIZE, and follow the alignment rules for the flash being written to. | ||||
|     pub async fn mark_updated<F: AsyncNorFlash>(&mut self, flash: &mut F, aligned: &mut [u8]) -> Result<(), F::Error> { | ||||
|         assert_eq!(aligned.len(), F::WRITE_SIZE); | ||||
|         self.set_magic(aligned, SWAP_MAGIC, flash).await | ||||
|     } | ||||
|  | ||||
|     /// Mark firmware boot successfully | ||||
|     pub async fn mark_booted<F: AsyncNorFlash>(&mut self, flash: &mut F) -> Result<(), F::Error> | ||||
|     where | ||||
|         [(); F::WRITE_SIZE]:, | ||||
|     { | ||||
|         let mut aligned = Aligned([0; { F::WRITE_SIZE }]); | ||||
|         self.set_magic(&mut aligned.0, BOOT_MAGIC, flash).await | ||||
|     /// Mark firmware boot successful and stop rollback on reset. | ||||
|     /// | ||||
|     /// # Safety | ||||
|     /// | ||||
|     /// The `aligned` buffer must have a size of F::WRITE_SIZE, and follow the alignment rules for the flash being written to. | ||||
|     pub async fn mark_booted<F: AsyncNorFlash>(&mut self, flash: &mut F, aligned: &mut [u8]) -> Result<(), F::Error> { | ||||
|         assert_eq!(aligned.len(), F::WRITE_SIZE); | ||||
|         self.set_magic(aligned, BOOT_MAGIC, flash).await | ||||
|     } | ||||
|  | ||||
|     async fn set_magic<F: AsyncNorFlash>( | ||||
| @@ -587,7 +631,7 @@ impl FirmwareUpdater { | ||||
|     ) -> Result<(), F::Error> { | ||||
|         flash.read(self.state.from as u32, aligned).await?; | ||||
|  | ||||
|         if aligned.iter().find(|&&b| b != magic).is_some() { | ||||
|         if aligned.iter().any(|&b| b != magic) { | ||||
|             aligned.fill(0); | ||||
|  | ||||
|             flash.write(self.state.from as u32, aligned).await?; | ||||
| @@ -599,7 +643,13 @@ impl FirmwareUpdater { | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     // Write to a region of the DFU page | ||||
|     /// Write data to a flash page. | ||||
|     /// | ||||
|     /// The buffer must follow alignment requirements of the target flash and a multiple of page size big. | ||||
|     /// | ||||
|     /// # Safety | ||||
|     /// | ||||
|     /// Failing to meet alignment and size requirements may result in a panic. | ||||
|     pub async fn write_firmware<F: AsyncNorFlash>( | ||||
|         &mut self, | ||||
|         offset: usize, | ||||
| @@ -668,7 +718,7 @@ mod tests { | ||||
|     #[test] | ||||
|     fn test_bad_magic() { | ||||
|         let mut flash = MemFlash([0xff; 131072]); | ||||
|         let mut flash = SingleFlashProvider::new(&mut flash); | ||||
|         let mut flash = SingleFlashConfig::new(&mut flash); | ||||
|  | ||||
|         let mut bootloader = BootLoader::<4096>::new(ACTIVE, DFU, STATE); | ||||
|  | ||||
| @@ -687,11 +737,16 @@ mod tests { | ||||
|  | ||||
|         let mut flash = MemFlash::<131072, 4096, 4>([0xff; 131072]); | ||||
|         flash.0[0..4].copy_from_slice(&[BOOT_MAGIC; 4]); | ||||
|         let mut flash = SingleFlashProvider::new(&mut flash); | ||||
|         let mut flash = SingleFlashConfig::new(&mut flash); | ||||
|  | ||||
|         let mut bootloader: BootLoader<4096> = BootLoader::new(ACTIVE, DFU, STATE); | ||||
|         let mut bootloader: BootLoader = BootLoader::new(ACTIVE, DFU, STATE); | ||||
|  | ||||
|         assert_eq!(State::Boot, bootloader.prepare_boot(&mut flash).unwrap()); | ||||
|         let mut magic = [0; 4]; | ||||
|         let mut page = [0; 4096]; | ||||
|         assert_eq!( | ||||
|             State::Boot, | ||||
|             bootloader.prepare_boot(&mut flash, &mut magic, &mut page).unwrap() | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
| @@ -703,24 +758,27 @@ mod tests { | ||||
|  | ||||
|         let original: [u8; ACTIVE.len()] = [rand::random::<u8>(); ACTIVE.len()]; | ||||
|         let update: [u8; DFU.len()] = [rand::random::<u8>(); DFU.len()]; | ||||
|         let mut aligned = [0; 4]; | ||||
|  | ||||
|         for i in ACTIVE.from..ACTIVE.to { | ||||
|             flash.0[i] = original[i - ACTIVE.from]; | ||||
|         } | ||||
|  | ||||
|         let mut bootloader: BootLoader<4096> = BootLoader::new(ACTIVE, DFU, STATE); | ||||
|         let mut bootloader: BootLoader = BootLoader::new(ACTIVE, DFU, STATE); | ||||
|         let mut updater = FirmwareUpdater::new(DFU, STATE); | ||||
|         let mut offset = 0; | ||||
|         for chunk in update.chunks(4096) { | ||||
|             block_on(updater.write_firmware(offset, &chunk, &mut flash, 4096)).unwrap(); | ||||
|             block_on(updater.write_firmware(offset, chunk, &mut flash, 4096)).unwrap(); | ||||
|             offset += chunk.len(); | ||||
|         } | ||||
|         block_on(updater.update(&mut flash)).unwrap(); | ||||
|         block_on(updater.mark_updated(&mut flash, &mut aligned)).unwrap(); | ||||
|  | ||||
|         let mut magic = [0; 4]; | ||||
|         let mut page = [0; 4096]; | ||||
|         assert_eq!( | ||||
|             State::Swap, | ||||
|             bootloader | ||||
|                 .prepare_boot(&mut SingleFlashProvider::new(&mut flash)) | ||||
|                 .prepare_boot(&mut SingleFlashConfig::new(&mut flash), &mut magic, &mut page) | ||||
|                 .unwrap() | ||||
|         ); | ||||
|  | ||||
| @@ -737,7 +795,7 @@ mod tests { | ||||
|         assert_eq!( | ||||
|             State::Swap, | ||||
|             bootloader | ||||
|                 .prepare_boot(&mut SingleFlashProvider::new(&mut flash)) | ||||
|                 .prepare_boot(&mut SingleFlashConfig::new(&mut flash), &mut magic, &mut page) | ||||
|                 .unwrap() | ||||
|         ); | ||||
|  | ||||
| @@ -751,11 +809,11 @@ mod tests { | ||||
|         } | ||||
|  | ||||
|         // Mark as booted | ||||
|         block_on(updater.mark_booted(&mut flash)).unwrap(); | ||||
|         block_on(updater.mark_booted(&mut flash, &mut aligned)).unwrap(); | ||||
|         assert_eq!( | ||||
|             State::Boot, | ||||
|             bootloader | ||||
|                 .prepare_boot(&mut SingleFlashProvider::new(&mut flash)) | ||||
|                 .prepare_boot(&mut SingleFlashConfig::new(&mut flash), &mut magic, &mut page) | ||||
|                 .unwrap() | ||||
|         ); | ||||
|     } | ||||
| @@ -769,6 +827,7 @@ mod tests { | ||||
|         let mut active = MemFlash::<16384, 4096, 8>([0xff; 16384]); | ||||
|         let mut dfu = MemFlash::<16384, 2048, 8>([0xff; 16384]); | ||||
|         let mut state = MemFlash::<4096, 128, 4>([0xff; 4096]); | ||||
|         let mut aligned = [0; 4]; | ||||
|  | ||||
|         let original: [u8; ACTIVE.len()] = [rand::random::<u8>(); ACTIVE.len()]; | ||||
|         let update: [u8; DFU.len()] = [rand::random::<u8>(); DFU.len()]; | ||||
| @@ -781,16 +840,23 @@ mod tests { | ||||
|  | ||||
|         let mut offset = 0; | ||||
|         for chunk in update.chunks(2048) { | ||||
|             block_on(updater.write_firmware(offset, &chunk, &mut dfu, chunk.len())).unwrap(); | ||||
|             block_on(updater.write_firmware(offset, chunk, &mut dfu, chunk.len())).unwrap(); | ||||
|             offset += chunk.len(); | ||||
|         } | ||||
|         block_on(updater.update(&mut state)).unwrap(); | ||||
|         block_on(updater.mark_updated(&mut state, &mut aligned)).unwrap(); | ||||
|  | ||||
|         let mut bootloader: BootLoader = BootLoader::new(ACTIVE, DFU, STATE); | ||||
|         let mut magic = [0; 4]; | ||||
|         let mut page = [0; 4096]; | ||||
|  | ||||
|         let mut bootloader: BootLoader<4096> = BootLoader::new(ACTIVE, DFU, STATE); | ||||
|         assert_eq!( | ||||
|             State::Swap, | ||||
|             bootloader | ||||
|                 .prepare_boot(&mut MultiFlashProvider::new(&mut active, &mut state, &mut dfu,)) | ||||
|                 .prepare_boot( | ||||
|                     &mut MultiFlashConfig::new(&mut active, &mut state, &mut dfu), | ||||
|                     &mut magic, | ||||
|                     &mut page | ||||
|                 ) | ||||
|                 .unwrap() | ||||
|         ); | ||||
|  | ||||
| @@ -810,6 +876,7 @@ mod tests { | ||||
|         const ACTIVE: Partition = Partition::new(4096, 16384); | ||||
|         const DFU: Partition = Partition::new(0, 16384); | ||||
|  | ||||
|         let mut aligned = [0; 4]; | ||||
|         let mut active = MemFlash::<16384, 2048, 4>([0xff; 16384]); | ||||
|         let mut dfu = MemFlash::<16384, 4096, 8>([0xff; 16384]); | ||||
|         let mut state = MemFlash::<4096, 128, 4>([0xff; 4096]); | ||||
| @@ -825,16 +892,22 @@ mod tests { | ||||
|  | ||||
|         let mut offset = 0; | ||||
|         for chunk in update.chunks(4096) { | ||||
|             block_on(updater.write_firmware(offset, &chunk, &mut dfu, chunk.len())).unwrap(); | ||||
|             block_on(updater.write_firmware(offset, chunk, &mut dfu, chunk.len())).unwrap(); | ||||
|             offset += chunk.len(); | ||||
|         } | ||||
|         block_on(updater.update(&mut state)).unwrap(); | ||||
|         block_on(updater.mark_updated(&mut state, &mut aligned)).unwrap(); | ||||
|  | ||||
|         let mut bootloader: BootLoader<4096> = BootLoader::new(ACTIVE, DFU, STATE); | ||||
|         let mut bootloader: BootLoader = BootLoader::new(ACTIVE, DFU, STATE); | ||||
|         let mut magic = [0; 4]; | ||||
|         let mut page = [0; 4096]; | ||||
|         assert_eq!( | ||||
|             State::Swap, | ||||
|             bootloader | ||||
|                 .prepare_boot(&mut MultiFlashProvider::new(&mut active, &mut state, &mut dfu,)) | ||||
|                 .prepare_boot( | ||||
|                     &mut MultiFlashConfig::new(&mut active, &mut state, &mut dfu,), | ||||
|                     &mut magic, | ||||
|                     &mut page | ||||
|                 ) | ||||
|                 .unwrap() | ||||
|         ); | ||||
|  | ||||
| @@ -899,6 +972,13 @@ mod tests { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl<const SIZE: usize, const ERASE_SIZE: usize, const WRITE_SIZE: usize> super::Flash | ||||
|         for MemFlash<SIZE, ERASE_SIZE, WRITE_SIZE> | ||||
|     { | ||||
|         const BLOCK_SIZE: usize = ERASE_SIZE; | ||||
|         const ERASE_VALUE: u8 = 0xFF; | ||||
|     } | ||||
|  | ||||
|     impl<const SIZE: usize, const ERASE_SIZE: usize, const WRITE_SIZE: usize> AsyncReadNorFlash | ||||
|         for MemFlash<SIZE, ERASE_SIZE, WRITE_SIZE> | ||||
|     { | ||||
|   | ||||
| @@ -1,19 +1,21 @@ | ||||
| #![no_std] | ||||
| #![feature(generic_associated_types)] | ||||
| #![feature(type_alias_impl_trait)] | ||||
| #![allow(incomplete_features)] | ||||
| #![feature(generic_const_exprs)] | ||||
|  | ||||
| #![warn(missing_docs)] | ||||
| #![doc = include_str!("../../README.md")] | ||||
| mod fmt; | ||||
|  | ||||
| pub use embassy_boot::{FirmwareUpdater, FlashConfig, FlashProvider, Partition, SingleFlashProvider}; | ||||
| pub use embassy_boot::{AlignedBuffer, BootFlash, FirmwareUpdater, FlashConfig, Partition, SingleFlashConfig}; | ||||
| use embassy_nrf::nvmc::{Nvmc, PAGE_SIZE}; | ||||
| use embassy_nrf::peripherals::WDT; | ||||
| use embassy_nrf::wdt; | ||||
| use embedded_storage::nor_flash::{ErrorType, NorFlash, ReadNorFlash}; | ||||
|  | ||||
| /// A bootloader for nRF devices. | ||||
| pub struct BootLoader { | ||||
|     boot: embassy_boot::BootLoader<PAGE_SIZE>, | ||||
|     boot: embassy_boot::BootLoader, | ||||
|     magic: AlignedBuffer<4>, | ||||
|     page: AlignedBuffer<PAGE_SIZE>, | ||||
| } | ||||
|  | ||||
| impl BootLoader { | ||||
| @@ -58,21 +60,25 @@ impl BootLoader { | ||||
|     pub fn new(active: Partition, dfu: Partition, state: Partition) -> Self { | ||||
|         Self { | ||||
|             boot: embassy_boot::BootLoader::new(active, dfu, state), | ||||
|             magic: AlignedBuffer([0; 4]), | ||||
|             page: AlignedBuffer([0; PAGE_SIZE]), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Boots the application without softdevice mechanisms | ||||
|     pub fn prepare<F: FlashProvider>(&mut self, flash: &mut F) -> usize | ||||
|     where | ||||
|         [(); <<F as FlashProvider>::STATE as FlashConfig>::FLASH::WRITE_SIZE]:, | ||||
|         [(); <<F as FlashProvider>::ACTIVE as FlashConfig>::FLASH::ERASE_SIZE]:, | ||||
|     { | ||||
|         match self.boot.prepare_boot(flash) { | ||||
|     /// Inspect the bootloader state and perform actions required before booting, such as swapping | ||||
|     /// firmware. | ||||
|     pub fn prepare<F: FlashConfig>(&mut self, flash: &mut F) -> usize { | ||||
|         match self.boot.prepare_boot(flash, &mut self.magic.0, &mut self.page.0) { | ||||
|             Ok(_) => self.boot.boot_address(), | ||||
|             Err(_) => panic!("boot prepare error!"), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Boots the application without softdevice mechanisms. | ||||
|     /// | ||||
|     /// # Safety | ||||
|     /// | ||||
|     /// This modifies the stack pointer and reset vector and will run code placed in the active partition. | ||||
|     #[cfg(not(feature = "softdevice"))] | ||||
|     pub unsafe fn load(&mut self, start: usize) -> ! { | ||||
|         let mut p = cortex_m::Peripherals::steal(); | ||||
| @@ -81,6 +87,11 @@ impl BootLoader { | ||||
|         cortex_m::asm::bootload(start as *const u32) | ||||
|     } | ||||
|  | ||||
|     /// Boots the application assuming softdevice is present. | ||||
|     /// | ||||
|     /// # Safety | ||||
|     /// | ||||
|     /// This modifies the stack pointer and reset vector and will run code placed in the active partition. | ||||
|     #[cfg(feature = "softdevice")] | ||||
|     pub unsafe fn load(&mut self, _app: usize) -> ! { | ||||
|         use nrf_softdevice_mbr as mbr; | ||||
|   | ||||
| @@ -1,19 +1,20 @@ | ||||
| #![no_std] | ||||
| #![feature(generic_associated_types)] | ||||
| #![feature(type_alias_impl_trait)] | ||||
| #![allow(incomplete_features)] | ||||
| #![feature(generic_const_exprs)] | ||||
|  | ||||
| #![warn(missing_docs)] | ||||
| #![doc = include_str!("../../README.md")] | ||||
| mod fmt; | ||||
|  | ||||
| pub use embassy_boot::{FirmwareUpdater, FlashConfig, FlashProvider, Partition, SingleFlashProvider, State}; | ||||
| use embedded_storage::nor_flash::NorFlash; | ||||
| pub use embassy_boot::{AlignedBuffer, BootFlash, FirmwareUpdater, FlashConfig, Partition, SingleFlashConfig, State}; | ||||
|  | ||||
| pub struct BootLoader<const PAGE_SIZE: usize> { | ||||
|     boot: embassy_boot::BootLoader<PAGE_SIZE>, | ||||
| /// A bootloader for STM32 devices. | ||||
| pub struct BootLoader<const PAGE_SIZE: usize, const WRITE_SIZE: usize> { | ||||
|     boot: embassy_boot::BootLoader, | ||||
|     magic: AlignedBuffer<WRITE_SIZE>, | ||||
|     page: AlignedBuffer<PAGE_SIZE>, | ||||
| } | ||||
|  | ||||
| impl<const PAGE_SIZE: usize> BootLoader<PAGE_SIZE> { | ||||
| impl<const PAGE_SIZE: usize, const WRITE_SIZE: usize> BootLoader<PAGE_SIZE, WRITE_SIZE> { | ||||
|     /// Create a new bootloader instance using parameters from linker script | ||||
|     pub fn default() -> Self { | ||||
|         extern "C" { | ||||
| @@ -55,21 +56,25 @@ impl<const PAGE_SIZE: usize> BootLoader<PAGE_SIZE> { | ||||
|     pub fn new(active: Partition, dfu: Partition, state: Partition) -> Self { | ||||
|         Self { | ||||
|             boot: embassy_boot::BootLoader::new(active, dfu, state), | ||||
|             magic: AlignedBuffer([0; WRITE_SIZE]), | ||||
|             page: AlignedBuffer([0; PAGE_SIZE]), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Boots the application | ||||
|     pub fn prepare<F: FlashProvider>(&mut self, flash: &mut F) -> usize | ||||
|     where | ||||
|         [(); <<F as FlashProvider>::STATE as FlashConfig>::FLASH::WRITE_SIZE]:, | ||||
|         [(); <<F as FlashProvider>::ACTIVE as FlashConfig>::FLASH::ERASE_SIZE]:, | ||||
|     { | ||||
|         match self.boot.prepare_boot(flash) { | ||||
|     /// Inspect the bootloader state and perform actions required before booting, such as swapping | ||||
|     /// firmware. | ||||
|     pub fn prepare<F: FlashConfig>(&mut self, flash: &mut F) -> usize { | ||||
|         match self.boot.prepare_boot(flash, self.magic.as_mut(), self.page.as_mut()) { | ||||
|             Ok(_) => embassy_stm32::flash::FLASH_BASE + self.boot.boot_address(), | ||||
|             Err(_) => panic!("boot prepare error!"), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Boots the application. | ||||
|     /// | ||||
|     /// # Safety | ||||
|     /// | ||||
|     /// This modifies the stack pointer and reset vector and will run code placed in the active partition. | ||||
|     pub unsafe fn load(&mut self, start: usize) -> ! { | ||||
|         trace!("Loading app at 0x{:x}", start); | ||||
|         #[allow(unused_mut)] | ||||
|   | ||||
| @@ -36,7 +36,8 @@ async fn main(_spawner: Spawner) { | ||||
|                 updater.write_firmware(offset, &buf, &mut nvmc, 4096).await.unwrap(); | ||||
|                 offset += chunk.len(); | ||||
|             } | ||||
|             updater.update(&mut nvmc).await.unwrap(); | ||||
|             let mut magic = [0; 4]; | ||||
|             updater.mark_updated(&mut nvmc, &mut magic).await.unwrap(); | ||||
|             led.set_high(); | ||||
|             cortex_m::peripheral::SCB::sys_reset(); | ||||
|         } | ||||
|   | ||||
| @@ -4,11 +4,11 @@ | ||||
|  | ||||
| #[cfg(feature = "defmt-rtt")] | ||||
| use defmt_rtt::*; | ||||
| use embassy_boot_stm32::FirmwareUpdater; | ||||
| use embassy_boot_stm32::{AlignedBuffer, FirmwareUpdater}; | ||||
| use embassy_embedded_hal::adapter::BlockingAsync; | ||||
| use embassy_executor::Spawner; | ||||
| use embassy_stm32::exti::ExtiInput; | ||||
| use embassy_stm32::flash::Flash; | ||||
| use embassy_stm32::flash::{Flash, WRITE_SIZE}; | ||||
| use embassy_stm32::gpio::{Input, Level, Output, Pull, Speed}; | ||||
| use panic_reset as _; | ||||
|  | ||||
| @@ -35,7 +35,8 @@ async fn main(_spawner: Spawner) { | ||||
|         updater.write_firmware(offset, &buf, &mut flash, 2048).await.unwrap(); | ||||
|         offset += chunk.len(); | ||||
|     } | ||||
|     updater.update(&mut flash).await.unwrap(); | ||||
|     let mut magic = AlignedBuffer([0; WRITE_SIZE]); | ||||
|     updater.mark_updated(&mut flash, magic.as_mut()).await.unwrap(); | ||||
|     led.set_low(); | ||||
|     cortex_m::peripheral::SCB::sys_reset(); | ||||
| } | ||||
|   | ||||
| @@ -4,11 +4,11 @@ | ||||
|  | ||||
| #[cfg(feature = "defmt-rtt")] | ||||
| use defmt_rtt::*; | ||||
| use embassy_boot_stm32::FirmwareUpdater; | ||||
| use embassy_boot_stm32::{AlignedBuffer, FirmwareUpdater}; | ||||
| use embassy_embedded_hal::adapter::BlockingAsync; | ||||
| use embassy_executor::Spawner; | ||||
| use embassy_stm32::exti::ExtiInput; | ||||
| use embassy_stm32::flash::Flash; | ||||
| use embassy_stm32::flash::{Flash, WRITE_SIZE}; | ||||
| use embassy_stm32::gpio::{Input, Level, Output, Pull, Speed}; | ||||
| use panic_reset as _; | ||||
|  | ||||
| @@ -35,7 +35,8 @@ async fn main(_spawner: Spawner) { | ||||
|         updater.write_firmware(offset, &buf, &mut flash, 2048).await.unwrap(); | ||||
|         offset += chunk.len(); | ||||
|     } | ||||
|     updater.update(&mut flash).await.unwrap(); | ||||
|     let mut magic = AlignedBuffer([0; WRITE_SIZE]); | ||||
|     updater.mark_updated(&mut flash, magic.as_mut()).await.unwrap(); | ||||
|     led.set_low(); | ||||
|     cortex_m::peripheral::SCB::sys_reset(); | ||||
| } | ||||
|   | ||||
| @@ -4,7 +4,7 @@ name = "embassy-boot-stm32h7-examples" | ||||
| version = "0.1.0" | ||||
|  | ||||
| [dependencies] | ||||
| embassy-sync = { version = "0.1.0", path = "../../../../embassy-sync", features = ["defmt"] } | ||||
| embassy-sync = { version = "0.1.0", path = "../../../../embassy-sync" } | ||||
| embassy-executor = { version = "0.1.0", path = "../../../../embassy-executor", features = ["nightly", "integrated-timers"] } | ||||
| embassy-time = { version = "0.1.0", path = "../../../../embassy-time", features = ["nightly", "tick-32768hz"] } | ||||
| embassy-stm32 = { version = "0.1.0", path = "../../../../embassy-stm32", features = ["unstable-traits", "nightly", "stm32h743zi", "time-driver-any", "exti"]  } | ||||
|   | ||||
| @@ -1,8 +1,9 @@ | ||||
| #!/bin/bash | ||||
| probe-rs-cli erase --chip STM32H743ZITx | ||||
| mv ../../bootloader/stm32/memory.x ../../bootloader/stm32/memory-old.x | ||||
| cp memory-bl.x ../../bootloader/stm32/memory.x | ||||
|  | ||||
| cargo flash --manifest-path ../../bootloader/stm32/Cargo.toml --release --features embassy-stm32/stm32f767zi --chip STM32F767ZITx --target thumbv7em-none-eabihf | ||||
| cargo flash --manifest-path ../../bootloader/stm32/Cargo.toml --release --features embassy-stm32/stm32h743zi --chip STM32H743ZITx --target thumbv7em-none-eabihf | ||||
|  | ||||
| rm ../../bootloader/stm32/memory.x | ||||
| mv ../../bootloader/stm32/memory-old.x ../../bootloader/stm32/memory.x | ||||
|   | ||||
| @@ -4,11 +4,11 @@ | ||||
|  | ||||
| #[cfg(feature = "defmt-rtt")] | ||||
| use defmt_rtt::*; | ||||
| use embassy_boot_stm32::FirmwareUpdater; | ||||
| use embassy_boot_stm32::{AlignedBuffer, FirmwareUpdater}; | ||||
| use embassy_embedded_hal::adapter::BlockingAsync; | ||||
| use embassy_executor::Spawner; | ||||
| use embassy_stm32::exti::ExtiInput; | ||||
| use embassy_stm32::flash::Flash; | ||||
| use embassy_stm32::flash::{Flash, WRITE_SIZE}; | ||||
| use embassy_stm32::gpio::{Input, Level, Output, Pull, Speed}; | ||||
| use panic_reset as _; | ||||
|  | ||||
| @@ -29,13 +29,17 @@ async fn main(_spawner: Spawner) { | ||||
|     let mut updater = FirmwareUpdater::default(); | ||||
|     button.wait_for_rising_edge().await; | ||||
|     let mut offset = 0; | ||||
|     let mut buf: [u8; 128 * 1024] = [0; 128 * 1024]; | ||||
|     let mut buf = AlignedBuffer([0; 128 * 1024]); | ||||
|     for chunk in APP_B.chunks(128 * 1024) { | ||||
|         buf[..chunk.len()].copy_from_slice(chunk); | ||||
|         updater.write_firmware(offset, &buf, &mut flash, 2048).await.unwrap(); | ||||
|         buf.as_mut()[..chunk.len()].copy_from_slice(chunk); | ||||
|         updater | ||||
|             .write_firmware(offset, buf.as_ref(), &mut flash, 2048) | ||||
|             .await | ||||
|             .unwrap(); | ||||
|         offset += chunk.len(); | ||||
|     } | ||||
|     updater.update(&mut flash).await.unwrap(); | ||||
|     let mut magic = AlignedBuffer([0; WRITE_SIZE]); | ||||
|     updater.mark_updated(&mut flash, magic.as_mut()).await.unwrap(); | ||||
|     led.set_low(); | ||||
|     cortex_m::peripheral::SCB::sys_reset(); | ||||
| } | ||||
|   | ||||
| @@ -4,11 +4,11 @@ | ||||
|  | ||||
| #[cfg(feature = "defmt-rtt")] | ||||
| use defmt_rtt::*; | ||||
| use embassy_boot_stm32::FirmwareUpdater; | ||||
| use embassy_boot_stm32::{AlignedBuffer, FirmwareUpdater}; | ||||
| use embassy_embedded_hal::adapter::BlockingAsync; | ||||
| use embassy_executor::Spawner; | ||||
| use embassy_stm32::exti::ExtiInput; | ||||
| use embassy_stm32::flash::Flash; | ||||
| use embassy_stm32::flash::{Flash, WRITE_SIZE}; | ||||
| use embassy_stm32::gpio::{Input, Level, Output, Pull, Speed}; | ||||
| use embassy_time::{Duration, Timer}; | ||||
| use panic_reset as _; | ||||
| @@ -38,7 +38,8 @@ async fn main(_spawner: Spawner) { | ||||
|         offset += chunk.len(); | ||||
|     } | ||||
|  | ||||
|     updater.update(&mut flash).await.unwrap(); | ||||
|     let mut magic = AlignedBuffer([0; WRITE_SIZE]); | ||||
|     updater.mark_updated(&mut flash, magic.as_mut()).await.unwrap(); | ||||
|     led.set_low(); | ||||
|     Timer::after(Duration::from_secs(1)).await; | ||||
|     cortex_m::peripheral::SCB::sys_reset(); | ||||
|   | ||||
| @@ -4,11 +4,11 @@ | ||||
|  | ||||
| #[cfg(feature = "defmt-rtt")] | ||||
| use defmt_rtt::*; | ||||
| use embassy_boot_stm32::FirmwareUpdater; | ||||
| use embassy_boot_stm32::{AlignedBuffer, FirmwareUpdater}; | ||||
| use embassy_embedded_hal::adapter::BlockingAsync; | ||||
| use embassy_executor::Spawner; | ||||
| use embassy_stm32::exti::ExtiInput; | ||||
| use embassy_stm32::flash::Flash; | ||||
| use embassy_stm32::flash::{Flash, WRITE_SIZE}; | ||||
| use embassy_stm32::gpio::{Input, Level, Output, Pull, Speed}; | ||||
| use embassy_time::{Duration, Timer}; | ||||
| use panic_reset as _; | ||||
| @@ -38,7 +38,8 @@ async fn main(_spawner: Spawner) { | ||||
|         offset += chunk.len(); | ||||
|     } | ||||
|  | ||||
|     updater.update(&mut flash).await.unwrap(); | ||||
|     let mut magic = AlignedBuffer([0; WRITE_SIZE]); | ||||
|     updater.mark_updated(&mut flash, magic.as_mut()).await.unwrap(); | ||||
|     led.set_low(); | ||||
|     Timer::after(Duration::from_secs(1)).await; | ||||
|     cortex_m::peripheral::SCB::sys_reset(); | ||||
|   | ||||
| @@ -4,11 +4,11 @@ | ||||
|  | ||||
| #[cfg(feature = "defmt-rtt")] | ||||
| use defmt_rtt::*; | ||||
| use embassy_boot_stm32::FirmwareUpdater; | ||||
| use embassy_boot_stm32::{AlignedBuffer, FirmwareUpdater}; | ||||
| use embassy_embedded_hal::adapter::BlockingAsync; | ||||
| use embassy_executor::Spawner; | ||||
| use embassy_stm32::exti::ExtiInput; | ||||
| use embassy_stm32::flash::Flash; | ||||
| use embassy_stm32::flash::{Flash, WRITE_SIZE}; | ||||
| use embassy_stm32::gpio::{Input, Level, Output, Pull, Speed}; | ||||
| use panic_reset as _; | ||||
|  | ||||
| @@ -35,7 +35,8 @@ async fn main(_spawner: Spawner) { | ||||
|         updater.write_firmware(offset, &buf, &mut flash, 2048).await.unwrap(); | ||||
|         offset += chunk.len(); | ||||
|     } | ||||
|     updater.update(&mut flash).await.unwrap(); | ||||
|     let mut magic = AlignedBuffer([0; WRITE_SIZE]); | ||||
|     updater.mark_updated(&mut flash, magic.as_mut()).await.unwrap(); | ||||
|     led.set_low(); | ||||
|     cortex_m::peripheral::SCB::sys_reset(); | ||||
| } | ||||
|   | ||||
| @@ -4,11 +4,11 @@ | ||||
|  | ||||
| #[cfg(feature = "defmt-rtt")] | ||||
| use defmt_rtt::*; | ||||
| use embassy_boot_stm32::FirmwareUpdater; | ||||
| use embassy_boot_stm32::{AlignedBuffer, FirmwareUpdater}; | ||||
| use embassy_embedded_hal::adapter::BlockingAsync; | ||||
| use embassy_executor::Spawner; | ||||
| use embassy_stm32::exti::ExtiInput; | ||||
| use embassy_stm32::flash::Flash; | ||||
| use embassy_stm32::flash::{Flash, WRITE_SIZE}; | ||||
| use embassy_stm32::gpio::{Input, Level, Output, Pull, Speed}; | ||||
| use panic_reset as _; | ||||
|  | ||||
| @@ -37,7 +37,8 @@ async fn main(_spawner: Spawner) { | ||||
|         updater.write_firmware(offset, &buf, &mut flash, 2048).await.unwrap(); | ||||
|         offset += chunk.len(); | ||||
|     } | ||||
|     updater.update(&mut flash).await.unwrap(); | ||||
|     let mut magic = AlignedBuffer([0; WRITE_SIZE]); | ||||
|     updater.mark_updated(&mut flash, magic.as_mut()).await.unwrap(); | ||||
|     //defmt::info!("Marked as updated"); | ||||
|     led.set_low(); | ||||
|     cortex_m::peripheral::SCB::sys_reset(); | ||||
|   | ||||
| @@ -20,10 +20,8 @@ fn main() -> ! { | ||||
|     */ | ||||
|  | ||||
|     let mut bl = BootLoader::default(); | ||||
|     let start = bl.prepare(&mut SingleFlashProvider::new(&mut WatchdogFlash::start( | ||||
|         Nvmc::new(p.NVMC), | ||||
|         p.WDT, | ||||
|         5, | ||||
|     let start = bl.prepare(&mut SingleFlashConfig::new(&mut BootFlash::<_, 4096>::new( | ||||
|         &mut WatchdogFlash::start(Nvmc::new(p.NVMC), p.WDT, 5), | ||||
|     ))); | ||||
|     unsafe { bl.load(start) } | ||||
| } | ||||
|   | ||||
| @@ -5,7 +5,7 @@ use cortex_m_rt::{entry, exception}; | ||||
| #[cfg(feature = "defmt")] | ||||
| use defmt_rtt as _; | ||||
| use embassy_boot_stm32::*; | ||||
| use embassy_stm32::flash::{Flash, ERASE_SIZE}; | ||||
| use embassy_stm32::flash::{Flash, ERASE_SIZE, ERASE_VALUE, WRITE_SIZE}; | ||||
|  | ||||
| #[entry] | ||||
| fn main() -> ! { | ||||
| @@ -19,9 +19,11 @@ fn main() -> ! { | ||||
|         } | ||||
|     */ | ||||
|  | ||||
|     let mut bl: BootLoader<ERASE_SIZE> = BootLoader::default(); | ||||
|     let mut bl: BootLoader<ERASE_SIZE, WRITE_SIZE> = BootLoader::default(); | ||||
|     let mut flash = Flash::unlock(p.FLASH); | ||||
|     let start = bl.prepare(&mut SingleFlashProvider::new(&mut flash)); | ||||
|     let start = bl.prepare(&mut SingleFlashConfig::new( | ||||
|         &mut BootFlash::<_, ERASE_SIZE, ERASE_VALUE>::new(&mut flash), | ||||
|     )); | ||||
|     core::mem::drop(flash); | ||||
|     unsafe { bl.load(start) } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user