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