From ed2a87a262e0e8c091627c96ced981dd3a97a6a1 Mon Sep 17 00:00:00 2001 From: Ulf Lilleengen Date: Mon, 24 Jan 2022 12:54:09 +0100 Subject: [PATCH 1/2] Add embassy-boot Embassy-boot is a simple bootloader that works together with an application to provide firmware update capabilities with a minimal risk. The bootloader consists of a platform-independent part, which implements the swap algorithm, and a platform-dependent part (currently only for nRF) that provides addition functionality such as watchdog timers softdevice support. --- embassy-boot/boot/Cargo.toml | 23 ++ embassy-boot/boot/src/fmt.rs | 225 ++++++++++++ embassy-boot/boot/src/lib.rs | 550 ++++++++++++++++++++++++++++ embassy-boot/nrf/.cargo/config.toml | 18 + embassy-boot/nrf/Cargo.toml | 65 ++++ embassy-boot/nrf/README.md | 11 + embassy-boot/nrf/build.rs | 37 ++ embassy-boot/nrf/memory-bm.x | 18 + embassy-boot/nrf/memory-s140.x | 31 ++ embassy-boot/nrf/memory.x | 18 + embassy-boot/nrf/src/fmt.rs | 225 ++++++++++++ embassy-boot/nrf/src/lib.rs | 203 ++++++++++ embassy-boot/nrf/src/main.rs | 46 +++ embassy-traits/Cargo.toml | 2 + embassy-traits/src/adapter.rs | 53 +++ examples/boot/.cargo/config.toml | 7 + examples/boot/Cargo.toml | 19 + examples/boot/README.md | 31 ++ examples/boot/build.rs | 34 ++ examples/boot/memory.x | 14 + examples/boot/src/bin/a.rs | 49 +++ examples/boot/src/bin/b.rs | 26 ++ 22 files changed, 1705 insertions(+) create mode 100644 embassy-boot/boot/Cargo.toml create mode 100644 embassy-boot/boot/src/fmt.rs create mode 100644 embassy-boot/boot/src/lib.rs create mode 100644 embassy-boot/nrf/.cargo/config.toml create mode 100644 embassy-boot/nrf/Cargo.toml create mode 100644 embassy-boot/nrf/README.md create mode 100644 embassy-boot/nrf/build.rs create mode 100644 embassy-boot/nrf/memory-bm.x create mode 100644 embassy-boot/nrf/memory-s140.x create mode 100644 embassy-boot/nrf/memory.x create mode 100644 embassy-boot/nrf/src/fmt.rs create mode 100644 embassy-boot/nrf/src/lib.rs create mode 100644 embassy-boot/nrf/src/main.rs create mode 100644 examples/boot/.cargo/config.toml create mode 100644 examples/boot/Cargo.toml create mode 100644 examples/boot/README.md create mode 100644 examples/boot/build.rs create mode 100644 examples/boot/memory.x create mode 100644 examples/boot/src/bin/a.rs create mode 100644 examples/boot/src/bin/b.rs diff --git a/embassy-boot/boot/Cargo.toml b/embassy-boot/boot/Cargo.toml new file mode 100644 index 00000000..0a3006ff --- /dev/null +++ b/embassy-boot/boot/Cargo.toml @@ -0,0 +1,23 @@ +[package] +authors = [ + "Ulf Lilleengen ", +] +edition = "2018" +name = "embassy-boot" +version = "0.1.0" +description = "Bootloader using Embassy" + +[lib] + +[dependencies] +defmt = { version = "0.3", optional = true } +log = { version = "0.4", optional = true } +embassy = { path = "../../embassy", default-features = false } +embedded-storage = "0.3.0" +embedded-storage-async = "0.3.0" + +[dev-dependencies] +log = "0.4" +env_logger = "0.9" +rand = "0.8" +futures = { version = "0.3", features = ["executor"] } diff --git a/embassy-boot/boot/src/fmt.rs b/embassy-boot/boot/src/fmt.rs new file mode 100644 index 00000000..06697081 --- /dev/null +++ b/embassy-boot/boot/src/fmt.rs @@ -0,0 +1,225 @@ +#![macro_use] +#![allow(unused_macros)] + +#[cfg(all(feature = "defmt", feature = "log"))] +compile_error!("You may not enable both `defmt` and `log` features."); + +macro_rules! assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert!($($x)*); + } + }; +} + +macro_rules! assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_eq!($($x)*); + } + }; +} + +macro_rules! assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_ne!($($x)*); + } + }; +} + +macro_rules! debug_assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert!($($x)*); + } + }; +} + +macro_rules! debug_assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert_eq!($($x)*); + } + }; +} + +macro_rules! debug_assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert_ne!($($x)*); + } + }; +} + +macro_rules! todo { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::todo!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::todo!($($x)*); + } + }; +} + +macro_rules! unreachable { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::unreachable!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::unreachable!($($x)*); + } + }; +} + +macro_rules! panic { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::panic!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::panic!($($x)*); + } + }; +} + +macro_rules! trace { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::trace!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::trace!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! debug { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::debug!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::debug!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! info { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::info!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::info!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! warn { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::warn!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::warn!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! error { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::error!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::error!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[cfg(feature = "defmt")] +macro_rules! unwrap { + ($($x:tt)*) => { + ::defmt::unwrap!($($x)*) + }; +} + +#[cfg(not(feature = "defmt"))] +macro_rules! unwrap { + ($arg:expr) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {:?}", ::core::stringify!($arg), e); + } + } + }; + ($arg:expr, $($msg:expr),+ $(,)? ) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {}: {:?}", ::core::stringify!($arg), ::core::format_args!($($msg,)*), e); + } + } + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct NoneError; + +pub trait Try { + type Ok; + type Error; + fn into_result(self) -> Result; +} + +impl Try for Option { + type Ok = T; + type Error = NoneError; + + #[inline] + fn into_result(self) -> Result { + self.ok_or(NoneError) + } +} + +impl Try for Result { + type Ok = T; + type Error = E; + + #[inline] + fn into_result(self) -> Self { + self + } +} diff --git a/embassy-boot/boot/src/lib.rs b/embassy-boot/boot/src/lib.rs new file mode 100644 index 00000000..909397b7 --- /dev/null +++ b/embassy-boot/boot/src/lib.rs @@ -0,0 +1,550 @@ +#![feature(type_alias_impl_trait)] +#![feature(generic_associated_types)] +#![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. +///! +mod fmt; + +use embedded_storage::nor_flash::{NorFlash, ReadNorFlash}; +use embedded_storage_async::nor_flash::AsyncNorFlash; + +pub const BOOT_MAGIC: u32 = 0xD00DF00D; +pub const SWAP_MAGIC: u32 = 0xF00FDAAD; + +#[derive(Copy, Clone, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Partition { + pub from: usize, + pub to: usize, +} + +impl Partition { + pub const fn new(from: usize, to: usize) -> Self { + Self { from, to } + } + pub const fn len(&self) -> usize { + self.to - self.from + } +} + +#[derive(PartialEq, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum State { + Boot, + Swap, +} + +#[derive(PartialEq, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum BootError { + Flash(E), + BadMagic, +} + +impl From for BootError { + fn from(error: E) -> Self { + BootError::Flash(error) + } +} + +/// BootLoader works with any flash implementing embedded_storage and can also work with +/// different page sizes. +pub struct BootLoader { + // Page with current state of bootloader. The state partition has the following format: + // | Range | Description | + // | 0 - 4 | Magic indicating bootloader state. BOOT_MAGIC means boot, SWAP_MAGIC means swap. | + // | 4 - N | Progress index used while swapping or reverting | + state: Partition, + // Location of the partition which will be booted from + active: Partition, + // Location of the partition which will be swapped in when requested + dfu: Partition, +} + +impl BootLoader { + 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); + // Ensure we have enough progress pages to store copy progress + assert!(active.len() / PAGE_SIZE >= (state.len() - 4) / PAGE_SIZE); + Self { active, dfu, state } + } + + pub fn boot_address(&self) -> usize { + self.active.from + } + + /// 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. + /// + /// 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, + flash: &mut F, + ) -> Result> { + // Copy contents from partition N to active + let state = self.read_state(flash)?; + 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(flash)? { + trace!("Swapping"); + self.swap(flash)?; + } else { + trace!("Reverting"); + self.revert(flash)?; + + // Overwrite magic and reset progress + flash.write(self.state.from as u32, &[0, 0, 0, 0])?; + flash.erase(self.state.from as u32, self.state.to as u32)?; + flash.write(self.state.from as u32, &BOOT_MAGIC.to_le_bytes())?; + } + } + _ => {} + } + Ok(state) + } + + fn is_swapped(&mut self, flash: &mut F) -> Result { + let page_count = self.active.len() / PAGE_SIZE; + let progress = self.current_progress(flash)?; + + Ok(progress >= page_count * 2) + } + + fn current_progress(&mut self, flash: &mut F) -> Result { + let max_index = ((self.state.len() - 4) / 4) - 1; + for i in 0..max_index { + let mut buf: [u8; 4] = [0; 4]; + flash.read((self.state.from + 4 + i * 4) as u32, &mut buf)?; + if buf == [0xFF, 0xFF, 0xFF, 0xFF] { + return Ok(i); + } + } + Ok(max_index) + } + + fn update_progress(&mut self, idx: usize, flash: &mut F) -> Result<(), F::Error> { + let w = self.state.from + 4 + idx * 4; + flash.write(w as u32, &[0, 0, 0, 0])?; + Ok(()) + } + + fn active_addr(&self, n: usize) -> usize { + self.active.from + n * PAGE_SIZE + } + + fn dfu_addr(&self, n: usize) -> usize { + self.dfu.from + n * PAGE_SIZE + } + + fn copy_page_once( + &mut self, + idx: usize, + from: usize, + to: usize, + flash: &mut F, + ) -> Result<(), F::Error> { + let mut buf: [u8; PAGE_SIZE] = [0; PAGE_SIZE]; + if self.current_progress(flash)? <= idx { + flash.read(from as u32, &mut buf)?; + flash.erase(to as u32, (to + PAGE_SIZE) as u32)?; + flash.write(to as u32, &buf)?; + self.update_progress(idx, flash)?; + } + Ok(()) + } + + fn swap(&mut self, flash: &mut F) -> Result<(), F::Error> { + let page_count = self.active.len() / PAGE_SIZE; + // trace!("Page count: {}", page_count); + for page in 0..page_count { + // 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); + // info!("Copy active {} to dfu {}", active_page, dfu_page); + self.copy_page_once(page * 2, active_page, dfu_page, flash)?; + + // 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); + //info!("Copy dfy {} to active {}", dfu_page, active_page); + self.copy_page_once(page * 2 + 1, dfu_page, active_page, flash)?; + } + + Ok(()) + } + + fn revert(&mut self, flash: &mut F) -> Result<(), F::Error> { + let page_count = self.active.len() / PAGE_SIZE; + for page 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(page_count * 2 + page * 2, active_page, dfu_page, flash)?; + + // 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(page_count * 2 + page * 2 + 1, dfu_page, active_page, flash)?; + } + + Ok(()) + } + + fn read_state(&mut self, flash: &mut F) -> Result> { + let mut magic: [u8; 4] = [0; 4]; + flash.read(self.state.from as u32, &mut magic)?; + + match u32::from_le_bytes(magic) { + SWAP_MAGIC => Ok(State::Swap), + _ => Ok(State::Boot), + } + } +} + +/// FirmwareUpdater is an application API for interacting with the BootLoader without the ability to +/// 'mess up' the internal bootloader state +pub struct FirmwareUpdater { + state: Partition, + dfu: Partition, +} + +impl FirmwareUpdater { + pub const fn new(dfu: Partition, state: Partition) -> Self { + Self { dfu, state } + } + + /// Return the length of the DFU area + pub fn firmware_len(&self) -> usize { + self.dfu.len() + } + + /// Instruct bootloader that DFU should commence at next boot. + pub async fn mark_update(&mut self, flash: &mut F) -> Result<(), F::Error> { + flash.write(self.state.from as u32, &[0, 0, 0, 0]).await?; + flash + .erase(self.state.from as u32, self.state.to as u32) + .await?; + info!( + "Setting swap magic at {} to 0x{:x}, LE: 0x{:x}", + self.state.from, + &SWAP_MAGIC, + &SWAP_MAGIC.to_le_bytes() + ); + flash + .write(self.state.from as u32, &SWAP_MAGIC.to_le_bytes()) + .await?; + Ok(()) + } + + /// Mark firmware boot successfully + pub async fn mark_booted(&mut self, flash: &mut F) -> Result<(), F::Error> { + flash.write(self.state.from as u32, &[0, 0, 0, 0]).await?; + flash + .erase(self.state.from as u32, self.state.to as u32) + .await?; + flash + .write(self.state.from as u32, &BOOT_MAGIC.to_le_bytes()) + .await?; + Ok(()) + } + + // Write to a region of the DFU page + pub async fn write_firmware( + &mut self, + offset: usize, + data: &[u8], + flash: &mut F, + ) -> Result<(), F::Error> { + info!( + "Writing firmware at offset 0x{:x} len {}", + self.dfu.from + offset, + data.len() + ); + + flash + .erase( + (self.dfu.from + offset) as u32, + (self.dfu.from + offset + data.len()) as u32, + ) + .await?; + flash.write((self.dfu.from + offset) as u32, data).await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use core::convert::Infallible; + use core::future::Future; + use embedded_storage_async::nor_flash::AsyncReadNorFlash; + use futures::executor::block_on; + + const STATE: Partition = Partition::new(0, 4096); + const ACTIVE: Partition = Partition::new(4096, 61440); + const DFU: Partition = Partition::new(61440, 122880); + + #[test] + fn test_bad_magic() { + let mut flash = MemFlash([0xff; 131072]); + + let mut bootloader = BootLoader::<4096>::new(ACTIVE, DFU, STATE); + + assert_eq!( + bootloader.prepare_boot(&mut flash), + Err(BootError::BadMagic) + ); + } + + #[test] + fn test_boot_state() { + let mut flash = MemFlash([0xff; 131072]); + flash.0[0..4].copy_from_slice(&BOOT_MAGIC.to_le_bytes()); + + let mut bootloader = BootLoader::<4096>::new(ACTIVE, DFU, STATE); + + assert_eq!(State::Boot, bootloader.prepare_boot(&mut flash).unwrap()); + } + + #[test] + fn test_swap_state() { + env_logger::init(); + let mut flash = MemFlash([0xff; 131072]); + + let original: [u8; ACTIVE.len()] = [rand::random::(); ACTIVE.len()]; + let update: [u8; DFU.len()] = [rand::random::(); DFU.len()]; + + for i in ACTIVE.from..ACTIVE.to { + flash.0[i] = original[i - ACTIVE.from]; + } + + let mut bootloader = BootLoader::<4096>::new(ACTIVE, DFU, STATE); + let mut updater = FirmwareUpdater::new(DFU, STATE); + for i in (DFU.from..DFU.to).step_by(4) { + let base = i - DFU.from; + let data: [u8; 4] = [ + update[base], + update[base + 1], + update[base + 2], + update[base + 3], + ]; + block_on(updater.write_firmware(i - DFU.from, &data, &mut flash)).unwrap(); + } + block_on(updater.mark_update(&mut flash)).unwrap(); + + assert_eq!(State::Swap, bootloader.prepare_boot(&mut flash).unwrap()); + + for i in ACTIVE.from..ACTIVE.to { + assert_eq!(flash.0[i], update[i - ACTIVE.from], "Index {}", i); + } + + // First DFU page is untouched + for i in DFU.from + 4096..DFU.to { + assert_eq!(flash.0[i], original[i - DFU.from - 4096], "Index {}", i); + } + + // Running again should cause a revert + assert_eq!(State::Swap, bootloader.prepare_boot(&mut flash).unwrap()); + + for i in ACTIVE.from..ACTIVE.to { + assert_eq!(flash.0[i], original[i - ACTIVE.from], "Index {}", i); + } + + // Last page is untouched + for i in DFU.from..DFU.to - 4096 { + assert_eq!(flash.0[i], update[i - DFU.from], "Index {}", i); + } + + // Mark as booted + block_on(updater.mark_booted(&mut flash)).unwrap(); + assert_eq!(State::Boot, bootloader.prepare_boot(&mut flash).unwrap()); + } + + struct MemFlash([u8; 131072]); + + impl NorFlash for MemFlash { + const WRITE_SIZE: usize = 4; + const ERASE_SIZE: usize = 4096; + fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> { + let from = from as usize; + let to = to as usize; + for i in from..to { + self.0[i] = 0xFF; + self.0[i] = 0xFF; + self.0[i] = 0xFF; + self.0[i] = 0xFF; + } + Ok(()) + } + + fn write(&mut self, offset: u32, data: &[u8]) -> Result<(), Self::Error> { + assert!(data.len() % 4 == 0); + assert!(offset % 4 == 0); + assert!(offset as usize + data.len() < 131072); + + self.0[offset as usize..offset as usize + data.len()].copy_from_slice(data); + + Ok(()) + } + } + + impl ReadNorFlash for MemFlash { + const READ_SIZE: usize = 4; + type Error = Infallible; + + fn read(&mut self, offset: u32, buf: &mut [u8]) -> Result<(), Self::Error> { + let len = buf.len(); + buf[..].copy_from_slice(&self.0[offset as usize..offset as usize + len]); + Ok(()) + } + + fn capacity(&self) -> usize { + 131072 + } + } + + impl AsyncReadNorFlash for MemFlash { + const READ_SIZE: usize = 4; + type Error = Infallible; + + type ReadFuture<'a> = impl Future> + 'a; + fn read<'a>(&'a mut self, offset: usize, buf: &'a mut [u8]) -> Self::ReadFuture<'a> { + async move { + let len = buf.len(); + buf[..].copy_from_slice(&self.0[offset as usize..offset as usize + len]); + Ok(()) + } + } + + fn capacity(&self) -> usize { + 131072 + } + } + + impl AsyncNorFlash for MemFlash { + const WRITE_SIZE: usize = 4; + const ERASE_SIZE: usize = 4096; + + type EraseFuture<'a> = impl Future> + 'a; + fn erase<'a>(&'a mut self, from: u32, to: u32) -> Self::EraseFuture<'a> { + async move { + let from = from as usize; + let to = to as usize; + for i in from..to { + self.0[i] = 0xFF; + self.0[i] = 0xFF; + self.0[i] = 0xFF; + self.0[i] = 0xFF; + } + Ok(()) + } + } + + type WriteFuture<'a> = impl Future> + 'a; + fn write<'a>(&'a mut self, offset: u32, data: &'a [u8]) -> Self::WriteFuture<'a> { + async move { + assert!(data.len() % 4 == 0); + assert!(offset % 4 == 0); + assert!(offset as usize + data.len() < 131072); + + self.0[offset as usize..offset as usize + data.len()].copy_from_slice(data); + + Ok(()) + } + } + } +} diff --git a/embassy-boot/nrf/.cargo/config.toml b/embassy-boot/nrf/.cargo/config.toml new file mode 100644 index 00000000..c3957b86 --- /dev/null +++ b/embassy-boot/nrf/.cargo/config.toml @@ -0,0 +1,18 @@ +[unstable] +namespaced-features = true +build-std = ["core"] +build-std-features = ["panic_immediate_abort"] + +[target.'cfg(all(target_arch = "arm", target_os = "none"))'] +#runner = "./fruitrunner" +runner = "probe-run --chip nrf52840_xxAA" + +rustflags = [ + # Code-size optimizations. + "-Z", "trap-unreachable=no", + #"-C", "no-vectorize-loops", + "-C", "force-frame-pointers=yes", +] + +[build] +target = "thumbv7em-none-eabi" diff --git a/embassy-boot/nrf/Cargo.toml b/embassy-boot/nrf/Cargo.toml new file mode 100644 index 00000000..512e7d37 --- /dev/null +++ b/embassy-boot/nrf/Cargo.toml @@ -0,0 +1,65 @@ +[package] +authors = [ + "Ulf Lilleengen ", +] +edition = "2018" +name = "embassy-boot-nrf" +version = "0.1.0" +description = "Bootloader for nRF chips" + +[dependencies] +defmt = { version = "0.3", optional = true } +defmt-rtt = { version = "0.3", optional = true } + +embassy = { path = "../../embassy", default-features = false } +embassy-nrf = { path = "../../embassy-nrf", default-features = false } +embassy-boot = { path = "../boot", default-features = false } +cortex-m = { version = "0.7" } +cortex-m-rt = { version = "0.7" } +embedded-storage = "0.3.0" +embedded-storage-async = "0.3.0" +cfg-if = "1.0.0" + +nrf-softdevice-mbr = { version = "0.1.0", git = "https://github.com/embassy-rs/nrf-softdevice.git", branch = "master", optional = true } + +[features] +defmt = [ + "dep:defmt", + "embassy-boot/defmt", + "embassy-nrf/defmt", +] +softdevice = [ + "nrf-softdevice-mbr", +] +debug = ["defmt-rtt"] + +[profile.dev] +debug = 2 +debug-assertions = true +incremental = false +opt-level = 'z' +overflow-checks = true + +[profile.release] +codegen-units = 1 +debug = 2 +debug-assertions = false +incremental = false +lto = 'fat' +opt-level = 'z' +overflow-checks = false + +# do not optimize proc-macro crates = faster builds from scratch +[profile.dev.build-override] +codegen-units = 8 +debug = false +debug-assertions = false +opt-level = 0 +overflow-checks = false + +[profile.release.build-override] +codegen-units = 8 +debug = false +debug-assertions = false +opt-level = 0 +overflow-checks = false diff --git a/embassy-boot/nrf/README.md b/embassy-boot/nrf/README.md new file mode 100644 index 00000000..23497a03 --- /dev/null +++ b/embassy-boot/nrf/README.md @@ -0,0 +1,11 @@ +# Bootloader for nRF + +The bootloader uses `embassy-boot` to interact with the flash. + +# Usage + +Flash the bootloader + +``` +cargo flash --features embassy-nrf/nrf52832 --release --chip nRF52832_xxAA +``` diff --git a/embassy-boot/nrf/build.rs b/embassy-boot/nrf/build.rs new file mode 100644 index 00000000..e1da6932 --- /dev/null +++ b/embassy-boot/nrf/build.rs @@ -0,0 +1,37 @@ +//! This build script copies the `memory.x` file from the crate root into +//! a directory where the linker can always find it at build time. +//! For many projects this is optional, as the linker always searches the +//! project root directory -- wherever `Cargo.toml` is. However, if you +//! are using a workspace or have a more complicated build setup, this +//! build script becomes required. Additionally, by requesting that +//! Cargo re-run the build script whenever `memory.x` is changed, +//! updating `memory.x` ensures a rebuild of the application with the +//! new memory settings. + +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +fn main() { + // Put `memory.x` in our output directory and ensure it's + // on the linker search path. + let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); + File::create(out.join("memory.x")) + .unwrap() + .write_all(include_bytes!("memory.x")) + .unwrap(); + println!("cargo:rustc-link-search={}", out.display()); + + // By default, Cargo will re-run a build script whenever + // any file in the project changes. By specifying `memory.x` + // here, we ensure the build script is only re-run when + // `memory.x` is changed. + println!("cargo:rerun-if-changed=memory.x"); + + println!("cargo:rustc-link-arg-bins=--nmagic"); + println!("cargo:rustc-link-arg-bins=-Tlink.x"); + if env::var("CARGO_FEATURE_DEFMT").is_ok() { + println!("cargo:rustc-link-arg-bins=-Tdefmt.x"); + } +} diff --git a/embassy-boot/nrf/memory-bm.x b/embassy-boot/nrf/memory-bm.x new file mode 100644 index 00000000..257d6564 --- /dev/null +++ b/embassy-boot/nrf/memory-bm.x @@ -0,0 +1,18 @@ +MEMORY +{ + /* NOTE 1 K = 1 KiBi = 1024 bytes */ + FLASH : ORIGIN = 0x00000000, LENGTH = 24K + BOOTLOADER_STATE : ORIGIN = 0x00006000, LENGTH = 4K + ACTIVE : ORIGIN = 0x00007000, LENGTH = 64K + DFU : ORIGIN = 0x00017000, LENGTH = 68K + RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 32K +} + +__bootloader_state_start = ORIGIN(BOOTLOADER_STATE); +__bootloader_state_end = ORIGIN(BOOTLOADER_STATE) + LENGTH(BOOTLOADER_STATE); + +__bootloader_active_start = ORIGIN(ACTIVE); +__bootloader_active_end = ORIGIN(ACTIVE) + LENGTH(ACTIVE); + +__bootloader_dfu_start = ORIGIN(DFU); +__bootloader_dfu_end = ORIGIN(DFU) + LENGTH(DFU); diff --git a/embassy-boot/nrf/memory-s140.x b/embassy-boot/nrf/memory-s140.x new file mode 100644 index 00000000..105db997 --- /dev/null +++ b/embassy-boot/nrf/memory-s140.x @@ -0,0 +1,31 @@ +MEMORY +{ + /* NOTE 1 K = 1 KiBi = 1024 bytes */ + MBR : ORIGIN = 0x00000000, LENGTH = 4K + SOFTDEVICE : ORIGIN = 0x00001000, LENGTH = 155648 + ACTIVE : ORIGIN = 0x00027000, LENGTH = 425984 + DFU : ORIGIN = 0x0008F000, LENGTH = 430080 + FLASH : ORIGIN = 0x000f9000, LENGTH = 24K + BOOTLOADER_STATE : ORIGIN = 0x000ff000, LENGTH = 4K + RAM (rwx) : ORIGIN = 0x20000008, LENGTH = 0x2fff8 + uicr_bootloader_start_address (r) : ORIGIN = 0x10001014, LENGTH = 0x4 +} + +__bootloader_state_start = ORIGIN(BOOTLOADER_STATE); +__bootloader_state_end = ORIGIN(BOOTLOADER_STATE) + LENGTH(BOOTLOADER_STATE); + +__bootloader_active_start = ORIGIN(ACTIVE); +__bootloader_active_end = ORIGIN(ACTIVE) + LENGTH(ACTIVE); + +__bootloader_dfu_start = ORIGIN(DFU); +__bootloader_dfu_end = ORIGIN(DFU) + LENGTH(DFU); + +__bootloader_start = ORIGIN(FLASH); + +SECTIONS +{ + .uicr_bootloader_start_address : + { + LONG(__bootloader_start) + } > uicr_bootloader_start_address +} diff --git a/embassy-boot/nrf/memory.x b/embassy-boot/nrf/memory.x new file mode 100644 index 00000000..257d6564 --- /dev/null +++ b/embassy-boot/nrf/memory.x @@ -0,0 +1,18 @@ +MEMORY +{ + /* NOTE 1 K = 1 KiBi = 1024 bytes */ + FLASH : ORIGIN = 0x00000000, LENGTH = 24K + BOOTLOADER_STATE : ORIGIN = 0x00006000, LENGTH = 4K + ACTIVE : ORIGIN = 0x00007000, LENGTH = 64K + DFU : ORIGIN = 0x00017000, LENGTH = 68K + RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 32K +} + +__bootloader_state_start = ORIGIN(BOOTLOADER_STATE); +__bootloader_state_end = ORIGIN(BOOTLOADER_STATE) + LENGTH(BOOTLOADER_STATE); + +__bootloader_active_start = ORIGIN(ACTIVE); +__bootloader_active_end = ORIGIN(ACTIVE) + LENGTH(ACTIVE); + +__bootloader_dfu_start = ORIGIN(DFU); +__bootloader_dfu_end = ORIGIN(DFU) + LENGTH(DFU); diff --git a/embassy-boot/nrf/src/fmt.rs b/embassy-boot/nrf/src/fmt.rs new file mode 100644 index 00000000..06697081 --- /dev/null +++ b/embassy-boot/nrf/src/fmt.rs @@ -0,0 +1,225 @@ +#![macro_use] +#![allow(unused_macros)] + +#[cfg(all(feature = "defmt", feature = "log"))] +compile_error!("You may not enable both `defmt` and `log` features."); + +macro_rules! assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert!($($x)*); + } + }; +} + +macro_rules! assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_eq!($($x)*); + } + }; +} + +macro_rules! assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_ne!($($x)*); + } + }; +} + +macro_rules! debug_assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert!($($x)*); + } + }; +} + +macro_rules! debug_assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert_eq!($($x)*); + } + }; +} + +macro_rules! debug_assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert_ne!($($x)*); + } + }; +} + +macro_rules! todo { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::todo!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::todo!($($x)*); + } + }; +} + +macro_rules! unreachable { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::unreachable!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::unreachable!($($x)*); + } + }; +} + +macro_rules! panic { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::panic!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::panic!($($x)*); + } + }; +} + +macro_rules! trace { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::trace!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::trace!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! debug { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::debug!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::debug!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! info { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::info!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::info!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! warn { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::warn!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::warn!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! error { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::error!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::error!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[cfg(feature = "defmt")] +macro_rules! unwrap { + ($($x:tt)*) => { + ::defmt::unwrap!($($x)*) + }; +} + +#[cfg(not(feature = "defmt"))] +macro_rules! unwrap { + ($arg:expr) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {:?}", ::core::stringify!($arg), e); + } + } + }; + ($arg:expr, $($msg:expr),+ $(,)? ) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {}: {:?}", ::core::stringify!($arg), ::core::format_args!($($msg,)*), e); + } + } + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct NoneError; + +pub trait Try { + type Ok; + type Error; + fn into_result(self) -> Result; +} + +impl Try for Option { + type Ok = T; + type Error = NoneError; + + #[inline] + fn into_result(self) -> Result { + self.ok_or(NoneError) + } +} + +impl Try for Result { + type Ok = T; + type Error = E; + + #[inline] + fn into_result(self) -> Self { + self + } +} diff --git a/embassy-boot/nrf/src/lib.rs b/embassy-boot/nrf/src/lib.rs new file mode 100644 index 00000000..af7ec7da --- /dev/null +++ b/embassy-boot/nrf/src/lib.rs @@ -0,0 +1,203 @@ +#![no_std] +#![feature(generic_associated_types)] +#![feature(type_alias_impl_trait)] + +mod fmt; + +pub use embassy_boot::{FirmwareUpdater, Partition, State, BOOT_MAGIC}; +use embassy_nrf::{ + nvmc::{Nvmc, PAGE_SIZE}, + peripherals::WDT, + wdt, +}; +use embedded_storage::nor_flash::{ErrorType, NorFlash, ReadNorFlash}; + +pub struct BootLoader { + boot: embassy_boot::BootLoader, +} + +impl BootLoader { + /// Create a new bootloader instance using parameters from linker script + pub fn default() -> 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 { + Partition::new( + &__bootloader_active_start as *const u32 as usize, + &__bootloader_active_end as *const u32 as usize, + ) + }; + let dfu = unsafe { + Partition::new( + &__bootloader_dfu_start as *const u32 as usize, + &__bootloader_dfu_end as *const u32 as usize, + ) + }; + let state = unsafe { + Partition::new( + &__bootloader_state_start as *const u32 as usize, + &__bootloader_state_end as *const u32 as usize, + ) + }; + + trace!("ACTIVE: 0x{:x} - 0x{:x}", active.from, active.to); + trace!("DFU: 0x{:x} - 0x{:x}", dfu.from, dfu.to); + trace!("STATE: 0x{:x} - 0x{:x}", state.from, state.to); + + Self::new(active, dfu, state) + } + + /// Create a new bootloader instance using the supplied partitions for active, dfu and state. + pub fn new(active: Partition, dfu: Partition, state: Partition) -> Self { + Self { + boot: embassy_boot::BootLoader::new(active, dfu, state), + } + } + + /// Boots the application without softdevice mechanisms + pub fn prepare(&mut self, flash: &mut F) -> usize { + match self.boot.prepare_boot(flash) { + Ok(_) => self.boot.boot_address(), + Err(_) => panic!("boot prepare error!"), + } + } + + #[cfg(not(feature = "softdevice"))] + pub unsafe fn load(&mut self, start: usize) -> ! { + let mut p = cortex_m::Peripherals::steal(); + p.SCB.invalidate_icache(); + p.SCB.vtor.write(start as u32); + cortex_m::asm::bootload(start as *const u32) + } + + #[cfg(feature = "softdevice")] + pub unsafe fn load(&mut self, _app: usize) -> ! { + use nrf_softdevice_mbr as mbr; + const NRF_SUCCESS: u32 = 0; + + // Address of softdevice which we'll forward interrupts to + let addr = 0x1000; + let mut cmd = mbr::sd_mbr_command_t { + command: mbr::NRF_MBR_COMMANDS_SD_MBR_COMMAND_IRQ_FORWARD_ADDRESS_SET, + params: mbr::sd_mbr_command_t__bindgen_ty_1 { + irq_forward_address_set: mbr::sd_mbr_command_irq_forward_address_set_t { + address: addr, + }, + }, + }; + let ret = mbr::sd_mbr_command(&mut cmd); + assert_eq!(ret, NRF_SUCCESS); + + let msp = *(addr as *const u32); + let rv = *((addr + 4) as *const u32); + + trace!("msp = {=u32:x}, rv = {=u32:x}", msp, rv); + + core::arch::asm!( + "mrs {tmp}, CONTROL", + "bics {tmp}, {spsel}", + "msr CONTROL, {tmp}", + "isb", + "msr MSP, {msp}", + "mov lr, {new_lr}", + "bx {rv}", + // `out(reg) _` is not permitted in a `noreturn` asm! call, + // so instead use `in(reg) 0` and don't restore it afterwards. + tmp = in(reg) 0, + spsel = in(reg) 2, + new_lr = in(reg) 0xFFFFFFFFu32, + msp = in(reg) msp, + rv = in(reg) rv, + options(noreturn), + ); + } +} + +/// A flash implementation that wraps NVMC and will pet a watchdog when touching flash. +pub struct WatchdogFlash<'d> { + flash: Nvmc<'d>, + wdt: wdt::WatchdogHandle, +} + +impl<'d> WatchdogFlash<'d> { + /// Start a new watchdog with a given flash and WDT peripheral and a timeout + pub fn start(flash: Nvmc<'d>, wdt: WDT, timeout: u32) -> Self { + let mut config = wdt::Config::default(); + config.timeout_ticks = 32768 * timeout; // timeout seconds + config.run_during_sleep = true; + config.run_during_debug_halt = false; + let (_wdt, [wdt]) = match wdt::Watchdog::try_new(wdt, config) { + Ok(x) => x, + Err(_) => { + // In case the watchdog is already running, just spin and let it expire, since + // we can't configure it anyway. This usually happens when we first program + // the device and the watchdog was previously active + info!("Watchdog already active with wrong config, waiting for it to timeout..."); + loop {} + } + }; + Self { flash, wdt } + } +} + +impl<'d> ErrorType for WatchdogFlash<'d> { + type Error = as ErrorType>::Error; +} + +impl<'d> NorFlash for WatchdogFlash<'d> { + const WRITE_SIZE: usize = as NorFlash>::WRITE_SIZE; + const ERASE_SIZE: usize = as NorFlash>::ERASE_SIZE; + + fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> { + self.wdt.pet(); + self.flash.erase(from, to) + } + fn write(&mut self, offset: u32, data: &[u8]) -> Result<(), Self::Error> { + self.wdt.pet(); + self.flash.write(offset, data) + } +} + +impl<'d> ReadNorFlash for WatchdogFlash<'d> { + const READ_SIZE: usize = as ReadNorFlash>::READ_SIZE; + fn read(&mut self, offset: u32, data: &mut [u8]) -> Result<(), Self::Error> { + self.wdt.pet(); + self.flash.read(offset, data) + } + fn capacity(&self) -> usize { + self.flash.capacity() + } +} + +pub mod updater { + use super::*; + pub fn new() -> embassy_boot::FirmwareUpdater { + extern "C" { + static __bootloader_state_start: u32; + static __bootloader_state_end: u32; + static __bootloader_dfu_start: u32; + static __bootloader_dfu_end: u32; + } + + let dfu = unsafe { + Partition::new( + &__bootloader_dfu_start as *const u32 as usize, + &__bootloader_dfu_end as *const u32 as usize, + ) + }; + let state = unsafe { + Partition::new( + &__bootloader_state_start as *const u32 as usize, + &__bootloader_state_end as *const u32 as usize, + ) + }; + embassy_boot::FirmwareUpdater::new(dfu, state) + } +} diff --git a/embassy-boot/nrf/src/main.rs b/embassy-boot/nrf/src/main.rs new file mode 100644 index 00000000..08b854a7 --- /dev/null +++ b/embassy-boot/nrf/src/main.rs @@ -0,0 +1,46 @@ +#![no_std] +#![no_main] + +use cortex_m_rt::{entry, exception}; + +#[cfg(feature = "defmt")] +use defmt_rtt as _; + +use embassy_boot_nrf::*; +use embassy_nrf::nvmc::Nvmc; + +#[entry] +fn main() -> ! { + let p = embassy_nrf::init(Default::default()); + /* + for i in 0..10000000 { + cortex_m::asm::nop(); + } + */ + + let mut bl = BootLoader::default(); + let start = bl.prepare(&mut WatchdogFlash::start(Nvmc::new(p.NVMC), p.WDT, 5)); + unsafe { bl.load(start) } +} + +#[no_mangle] +#[cfg_attr(target_os = "none", link_section = ".HardFault.user")] +unsafe extern "C" fn HardFault() { + cortex_m::peripheral::SCB::sys_reset(); +} + +#[exception] +unsafe fn DefaultHandler(_: i16) -> ! { + const SCB_ICSR: *const u32 = 0xE000_ED04 as *const u32; + let irqn = core::ptr::read_volatile(SCB_ICSR) as u8 as i16 - 16; + + panic!("DefaultHandler #{:?}", irqn); +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + unsafe { + core::arch::asm!("udf #0"); + core::hint::unreachable_unchecked(); + } +} diff --git a/embassy-traits/Cargo.toml b/embassy-traits/Cargo.toml index 39875687..fa2082ef 100644 --- a/embassy-traits/Cargo.toml +++ b/embassy-traits/Cargo.toml @@ -11,4 +11,6 @@ std = [] embedded-hal-02 = { package = "embedded-hal", version = "0.2.6", features = ["unproven"] } embedded-hal-1 = { package = "embedded-hal", version = "1.0.0-alpha.6", git = "https://github.com/embassy-rs/embedded-hal", branch = "embassy" } embedded-hal-async = { version = "0.0.1", git = "https://github.com/embassy-rs/embedded-hal", branch = "embassy"} +embedded-storage = "0.3.0" +embedded-storage-async = "0.3.0" nb = "1.0.0" diff --git a/embassy-traits/src/adapter.rs b/embassy-traits/src/adapter.rs index 415b5e81..735f9aac 100644 --- a/embassy-traits/src/adapter.rs +++ b/embassy-traits/src/adapter.rs @@ -254,3 +254,56 @@ where async move { self.wrapped.bflush() } } } + +/// NOR flash wrapper +use embedded_storage::nor_flash::{ErrorType, NorFlash, ReadNorFlash}; +use embedded_storage_async::nor_flash::{AsyncNorFlash, AsyncReadNorFlash}; + +impl ErrorType for BlockingAsync +where + T: ErrorType, +{ + type Error = T::Error; +} + +impl AsyncNorFlash for BlockingAsync +where + T: NorFlash, +{ + const WRITE_SIZE: usize = ::WRITE_SIZE; + const ERASE_SIZE: usize = ::ERASE_SIZE; + + type WriteFuture<'a> + where + Self: 'a, + = impl Future> + 'a; + fn write<'a>(&'a mut self, offset: u32, data: &'a [u8]) -> Self::WriteFuture<'a> { + async move { self.wrapped.write(offset, data) } + } + + type EraseFuture<'a> + where + Self: 'a, + = impl Future> + 'a; + fn erase<'a>(&'a mut self, from: u32, to: u32) -> Self::EraseFuture<'a> { + async move { self.wrapped.erase(from, to) } + } +} + +impl AsyncReadNorFlash for BlockingAsync +where + T: ReadNorFlash, +{ + const READ_SIZE: usize = ::READ_SIZE; + type ReadFuture<'a> + where + Self: 'a, + = impl Future> + 'a; + fn read<'a>(&'a mut self, address: u32, data: &'a mut [u8]) -> Self::ReadFuture<'a> { + async move { self.wrapped.read(address, data) } + } + + fn capacity(&self) -> usize { + self.wrapped.capacity() + } +} diff --git a/examples/boot/.cargo/config.toml b/examples/boot/.cargo/config.toml new file mode 100644 index 00000000..d044e9b4 --- /dev/null +++ b/examples/boot/.cargo/config.toml @@ -0,0 +1,7 @@ +[unstable] +namespaced-features = true +build-std = ["core"] +build-std-features = ["panic_immediate_abort"] + +[build] +target = "thumbv7em-none-eabi" diff --git a/examples/boot/Cargo.toml b/examples/boot/Cargo.toml new file mode 100644 index 00000000..36e2e169 --- /dev/null +++ b/examples/boot/Cargo.toml @@ -0,0 +1,19 @@ +[package] +authors = ["Ulf Lilleengen "] +edition = "2018" +name = "embassy-boot-examples" +version = "0.1.0" + +[dependencies] +embassy = { version = "0.1.0", path = "../../embassy" } +embassy-nrf = { version = "0.1.0", path = "../../embassy-nrf", features = ["time-driver-rtc1", "gpiote"] } +embassy-boot-nrf = { version = "0.1.0", path = "../../embassy-boot/nrf" } +embassy-traits = { version = "0.1.0", path = "../../embassy-traits" } + +defmt = { version = "0.3", optional = true } +defmt-rtt = { version = "0.3", optional = true } +panic-reset = { version = "0.1.1" } +embedded-hal = { version = "0.2.6" } + +cortex-m = "0.7.3" +cortex-m-rt = "0.7.0" diff --git a/examples/boot/README.md b/examples/boot/README.md new file mode 100644 index 00000000..b97513a9 --- /dev/null +++ b/examples/boot/README.md @@ -0,0 +1,31 @@ +# Examples using bootloader + +Example for nRF52 demonstrating the bootloader. The example consists of application binaries, 'a' +which allows you to press a button to start the DFU process, and 'b' which is the updated +application. + + +## Prerequisites + +* `cargo-binutils` +* `cargo-flash` +* `embassy-boot-nrf` + +## Usage + + + +``` +# Flash bootloader +cargo flash --manifest-path ../../embassy-boot/nrf/Cargo.toml --release --features embassy-nrf/nrf52840 --chip nRF52840_xxAA +# Build 'b' +cargo build --release --features embassy-nrf/nrf52840 --bin b +# Generate binary for 'b' +cargo objcopy --release --features embassy-nrf/nrf52840 --bin b -- -O binary b.bin +``` + +# Flash `a` (which includes b.bin) + +``` +cargo flash --release --features embassy-nrf/nrf52840 --bin a --chip nRF52840_xxAA +``` diff --git a/examples/boot/build.rs b/examples/boot/build.rs new file mode 100644 index 00000000..cd1a264c --- /dev/null +++ b/examples/boot/build.rs @@ -0,0 +1,34 @@ +//! This build script copies the `memory.x` file from the crate root into +//! a directory where the linker can always find it at build time. +//! For many projects this is optional, as the linker always searches the +//! project root directory -- wherever `Cargo.toml` is. However, if you +//! are using a workspace or have a more complicated build setup, this +//! build script becomes required. Additionally, by requesting that +//! Cargo re-run the build script whenever `memory.x` is changed, +//! updating `memory.x` ensures a rebuild of the application with the +//! new memory settings. + +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +fn main() { + // Put `memory.x` in our output directory and ensure it's + // on the linker search path. + let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); + File::create(out.join("memory.x")) + .unwrap() + .write_all(include_bytes!("memory.x")) + .unwrap(); + println!("cargo:rustc-link-search={}", out.display()); + + // By default, Cargo will re-run a build script whenever + // any file in the project changes. By specifying `memory.x` + // here, we ensure the build script is only re-run when + // `memory.x` is changed. + println!("cargo:rerun-if-changed=memory.x"); + + println!("cargo:rustc-link-arg-bins=--nmagic"); + println!("cargo:rustc-link-arg-bins=-Tlink.x"); +} diff --git a/examples/boot/memory.x b/examples/boot/memory.x new file mode 100644 index 00000000..dfb72103 --- /dev/null +++ b/examples/boot/memory.x @@ -0,0 +1,14 @@ +MEMORY +{ + /* NOTE 1 K = 1 KiBi = 1024 bytes */ + BOOTLOADER_STATE : ORIGIN = 0x00006000, LENGTH = 4K + FLASH : ORIGIN = 0x00007000, LENGTH = 64K + DFU : ORIGIN = 0x00017000, LENGTH = 68K + RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 32K +} + +__bootloader_state_start = ORIGIN(BOOTLOADER_STATE); +__bootloader_state_end = ORIGIN(BOOTLOADER_STATE) + LENGTH(BOOTLOADER_STATE); + +__bootloader_dfu_start = ORIGIN(DFU); +__bootloader_dfu_end = ORIGIN(DFU) + LENGTH(DFU); diff --git a/examples/boot/src/bin/a.rs b/examples/boot/src/bin/a.rs new file mode 100644 index 00000000..88880e68 --- /dev/null +++ b/examples/boot/src/bin/a.rs @@ -0,0 +1,49 @@ +#![no_std] +#![no_main] +#![macro_use] +#![feature(generic_associated_types)] +#![feature(type_alias_impl_trait)] + +use embassy_boot_nrf::updater; +use embassy_nrf::{ + gpio::{Input, Pull}, + gpio::{Level, Output, OutputDrive}, + nvmc::Nvmc, + Peripherals, +}; +use embassy_traits::adapter::BlockingAsync; +use embedded_hal::digital::v2::InputPin; +use panic_reset as _; + +static APP_B: &[u8] = include_bytes!("../../b.bin"); + +#[embassy::main] +async fn main(_s: embassy::executor::Spawner, p: Peripherals) { + let mut button = Input::new(p.P0_11, Pull::Up); + let mut led = Output::new(p.P0_13, Level::Low, OutputDrive::Standard); + //let mut led = Output::new(p.P1_10, Level::Low, OutputDrive::Standard); + //let mut button = Input::new(p.P1_02, Pull::Up); + + let nvmc = Nvmc::new(p.NVMC); + let mut nvmc = BlockingAsync::new(nvmc); + + loop { + button.wait_for_any_edge().await; + if button.is_low().unwrap() { + let mut updater = updater::new(); + let mut offset = 0; + for chunk in APP_B.chunks(4096) { + let mut buf: [u8; 4096] = [0; 4096]; + buf[..chunk.len()].copy_from_slice(chunk); + updater + .write_firmware(offset, &buf, &mut nvmc) + .await + .unwrap(); + offset += chunk.len(); + } + updater.mark_update(&mut nvmc).await.unwrap(); + led.set_high(); + cortex_m::peripheral::SCB::sys_reset(); + } + } +} diff --git a/examples/boot/src/bin/b.rs b/examples/boot/src/bin/b.rs new file mode 100644 index 00000000..18bb6330 --- /dev/null +++ b/examples/boot/src/bin/b.rs @@ -0,0 +1,26 @@ +#![no_std] +#![no_main] +#![macro_use] +#![feature(generic_associated_types)] +#![feature(type_alias_impl_trait)] + +use embassy::time::{Duration, Timer}; +use embassy_nrf::{ + gpio::{Level, Output, OutputDrive}, + Peripherals, +}; + +use panic_reset as _; + +#[embassy::main] +async fn main(_s: embassy::executor::Spawner, p: Peripherals) { + let mut led = Output::new(p.P0_13, Level::Low, OutputDrive::Standard); + //let mut led = Output::new(p.P1_10, Level::Low, OutputDrive::Standard); + + loop { + led.set_high(); + Timer::after(Duration::from_millis(300)).await; + led.set_low(); + Timer::after(Duration::from_millis(300)).await; + } +} From e990021b9a9d3acc309c21bd4ddf3ff090bb7999 Mon Sep 17 00:00:00 2001 From: Ulf Lilleengen Date: Wed, 9 Feb 2022 12:46:46 +0100 Subject: [PATCH 2/2] Address review comments --- embassy-boot/nrf/src/lib.rs | 7 +++++++ embassy-boot/nrf/src/main.rs | 5 ++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/embassy-boot/nrf/src/lib.rs b/embassy-boot/nrf/src/lib.rs index af7ec7da..32250b2d 100644 --- a/embassy-boot/nrf/src/lib.rs +++ b/embassy-boot/nrf/src/lib.rs @@ -100,6 +100,13 @@ impl BootLoader { trace!("msp = {=u32:x}, rv = {=u32:x}", msp, rv); + // These instructions perform the following operations: + // + // * Modify control register to use MSP as stack pointer (clear spsel bit) + // * Synchronize instruction barrier + // * Initialize stack pointer (0x1000) + // * Set link register to not return (0xFF) + // * Jump to softdevice reset vector core::arch::asm!( "mrs {tmp}, CONTROL", "bics {tmp}, {spsel}", diff --git a/embassy-boot/nrf/src/main.rs b/embassy-boot/nrf/src/main.rs index 08b854a7..cd264d4c 100644 --- a/embassy-boot/nrf/src/main.rs +++ b/embassy-boot/nrf/src/main.rs @@ -12,6 +12,9 @@ use embassy_nrf::nvmc::Nvmc; #[entry] fn main() -> ! { let p = embassy_nrf::init(Default::default()); + + // Uncomment this if you are debugging the bootloader with debugger/RTT attached, + // as it prevents a hard fault when accessing flash 'too early' after boot. /* for i in 0..10000000 { cortex_m::asm::nop(); @@ -40,7 +43,7 @@ unsafe fn DefaultHandler(_: i16) -> ! { #[panic_handler] fn panic(_info: &core::panic::PanicInfo) -> ! { unsafe { - core::arch::asm!("udf #0"); + cortex_m::asm::udf(); core::hint::unreachable_unchecked(); } }