Merge #604
604: Add embassy-boot r=lulf a=lulf Continuation of https://github.com/embassy-rs/embassy/pull/588 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. The bootloader is intended to be configurable for different flash sizes and architectures, and only requires that embedded-storage flash traits are implemented. The nRF version can be configured programatically as a library, or using linker scripts to set the partition locations for DFU, ACTIVE and STATE * DFU: Where the next firmware version should be written. This is used by the FirmwareUpdater * ACTIVE: Where the current firmware version resides. Written by bootloader when swap magic is set * STATE: Contains the bootloader magic and the copy progress. Can be 1-N pages long (depending on how much flash you have which will determine the copy progress index size Co-authored-by: Ulf Lilleengen <lulf@redhat.com> Co-authored-by: Ulf Lilleengen <ulf.lilleengen@gmail.com>
This commit is contained in:
commit
3d6b8bd983
23
embassy-boot/boot/Cargo.toml
Normal file
23
embassy-boot/boot/Cargo.toml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
[package]
|
||||||
|
authors = [
|
||||||
|
"Ulf Lilleengen <lulf@redhat.com>",
|
||||||
|
]
|
||||||
|
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"] }
|
225
embassy-boot/boot/src/fmt.rs
Normal file
225
embassy-boot/boot/src/fmt.rs
Normal file
@ -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<Self::Ok, Self::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Try for Option<T> {
|
||||||
|
type Ok = T;
|
||||||
|
type Error = NoneError;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn into_result(self) -> Result<T, NoneError> {
|
||||||
|
self.ok_or(NoneError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, E> Try for Result<T, E> {
|
||||||
|
type Ok = T;
|
||||||
|
type Error = E;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn into_result(self) -> Self {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
550
embassy-boot/boot/src/lib.rs
Normal file
550
embassy-boot/boot/src/lib.rs
Normal file
@ -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<E> {
|
||||||
|
Flash(E),
|
||||||
|
BadMagic,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> From<E> for BootError<E> {
|
||||||
|
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<const PAGE_SIZE: usize> {
|
||||||
|
// 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<const PAGE_SIZE: usize> BootLoader<PAGE_SIZE> {
|
||||||
|
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<F: NorFlash + ReadNorFlash>(
|
||||||
|
&mut self,
|
||||||
|
flash: &mut F,
|
||||||
|
) -> Result<State, BootError<F::Error>> {
|
||||||
|
// 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<F: ReadNorFlash>(&mut self, flash: &mut F) -> Result<bool, F::Error> {
|
||||||
|
let page_count = self.active.len() / PAGE_SIZE;
|
||||||
|
let progress = self.current_progress(flash)?;
|
||||||
|
|
||||||
|
Ok(progress >= page_count * 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_progress<F: ReadNorFlash>(&mut self, flash: &mut F) -> Result<usize, F::Error> {
|
||||||
|
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<F: NorFlash>(&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<F: NorFlash + ReadNorFlash>(
|
||||||
|
&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<F: NorFlash + ReadNorFlash>(&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<F: NorFlash + ReadNorFlash>(&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<F: ReadNorFlash>(&mut self, flash: &mut F) -> Result<State, BootError<F::Error>> {
|
||||||
|
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<F: AsyncNorFlash>(&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<F: AsyncNorFlash>(&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<F: AsyncNorFlash>(
|
||||||
|
&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::<u8>(); ACTIVE.len()];
|
||||||
|
let update: [u8; DFU.len()] = [rand::random::<u8>(); 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<Output = Result<(), Self::Error>> + '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<Output = Result<(), Self::Error>> + '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<Output = Result<(), Self::Error>> + '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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
embassy-boot/nrf/.cargo/config.toml
Normal file
18
embassy-boot/nrf/.cargo/config.toml
Normal file
@ -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"
|
65
embassy-boot/nrf/Cargo.toml
Normal file
65
embassy-boot/nrf/Cargo.toml
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
[package]
|
||||||
|
authors = [
|
||||||
|
"Ulf Lilleengen <lulf@redhat.com>",
|
||||||
|
]
|
||||||
|
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
|
11
embassy-boot/nrf/README.md
Normal file
11
embassy-boot/nrf/README.md
Normal file
@ -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
|
||||||
|
```
|
37
embassy-boot/nrf/build.rs
Normal file
37
embassy-boot/nrf/build.rs
Normal file
@ -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");
|
||||||
|
}
|
||||||
|
}
|
18
embassy-boot/nrf/memory-bm.x
Normal file
18
embassy-boot/nrf/memory-bm.x
Normal file
@ -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);
|
31
embassy-boot/nrf/memory-s140.x
Normal file
31
embassy-boot/nrf/memory-s140.x
Normal file
@ -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
|
||||||
|
}
|
18
embassy-boot/nrf/memory.x
Normal file
18
embassy-boot/nrf/memory.x
Normal file
@ -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);
|
225
embassy-boot/nrf/src/fmt.rs
Normal file
225
embassy-boot/nrf/src/fmt.rs
Normal file
@ -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<Self::Ok, Self::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Try for Option<T> {
|
||||||
|
type Ok = T;
|
||||||
|
type Error = NoneError;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn into_result(self) -> Result<T, NoneError> {
|
||||||
|
self.ok_or(NoneError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, E> Try for Result<T, E> {
|
||||||
|
type Ok = T;
|
||||||
|
type Error = E;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn into_result(self) -> Self {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
210
embassy-boot/nrf/src/lib.rs
Normal file
210
embassy-boot/nrf/src/lib.rs
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
#![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<PAGE_SIZE>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<F: NorFlash + ReadNorFlash>(&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);
|
||||||
|
|
||||||
|
// 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}",
|
||||||
|
"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 = <Nvmc<'d> as ErrorType>::Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'d> NorFlash for WatchdogFlash<'d> {
|
||||||
|
const WRITE_SIZE: usize = <Nvmc<'d> as NorFlash>::WRITE_SIZE;
|
||||||
|
const ERASE_SIZE: usize = <Nvmc<'d> 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 = <Nvmc<'d> 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)
|
||||||
|
}
|
||||||
|
}
|
49
embassy-boot/nrf/src/main.rs
Normal file
49
embassy-boot/nrf/src/main.rs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
#![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());
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 {
|
||||||
|
cortex_m::asm::udf();
|
||||||
|
core::hint::unreachable_unchecked();
|
||||||
|
}
|
||||||
|
}
|
@ -11,4 +11,6 @@ std = []
|
|||||||
embedded-hal-02 = { package = "embedded-hal", version = "0.2.6", features = ["unproven"] }
|
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-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-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"
|
nb = "1.0.0"
|
||||||
|
@ -254,3 +254,56 @@ where
|
|||||||
async move { self.wrapped.bflush() }
|
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<T> ErrorType for BlockingAsync<T>
|
||||||
|
where
|
||||||
|
T: ErrorType,
|
||||||
|
{
|
||||||
|
type Error = T::Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> AsyncNorFlash for BlockingAsync<T>
|
||||||
|
where
|
||||||
|
T: NorFlash,
|
||||||
|
{
|
||||||
|
const WRITE_SIZE: usize = <T as NorFlash>::WRITE_SIZE;
|
||||||
|
const ERASE_SIZE: usize = <T as NorFlash>::ERASE_SIZE;
|
||||||
|
|
||||||
|
type WriteFuture<'a>
|
||||||
|
where
|
||||||
|
Self: 'a,
|
||||||
|
= impl Future<Output = Result<(), Self::Error>> + '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<Output = Result<(), Self::Error>> + 'a;
|
||||||
|
fn erase<'a>(&'a mut self, from: u32, to: u32) -> Self::EraseFuture<'a> {
|
||||||
|
async move { self.wrapped.erase(from, to) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> AsyncReadNorFlash for BlockingAsync<T>
|
||||||
|
where
|
||||||
|
T: ReadNorFlash,
|
||||||
|
{
|
||||||
|
const READ_SIZE: usize = <T as ReadNorFlash>::READ_SIZE;
|
||||||
|
type ReadFuture<'a>
|
||||||
|
where
|
||||||
|
Self: 'a,
|
||||||
|
= impl Future<Output = Result<(), Self::Error>> + '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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
7
examples/boot/.cargo/config.toml
Normal file
7
examples/boot/.cargo/config.toml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[unstable]
|
||||||
|
namespaced-features = true
|
||||||
|
build-std = ["core"]
|
||||||
|
build-std-features = ["panic_immediate_abort"]
|
||||||
|
|
||||||
|
[build]
|
||||||
|
target = "thumbv7em-none-eabi"
|
19
examples/boot/Cargo.toml
Normal file
19
examples/boot/Cargo.toml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
authors = ["Ulf Lilleengen <lulf@redhat.com>"]
|
||||||
|
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"
|
31
examples/boot/README.md
Normal file
31
examples/boot/README.md
Normal file
@ -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
|
||||||
|
```
|
34
examples/boot/build.rs
Normal file
34
examples/boot/build.rs
Normal file
@ -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");
|
||||||
|
}
|
14
examples/boot/memory.x
Normal file
14
examples/boot/memory.x
Normal file
@ -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);
|
49
examples/boot/src/bin/a.rs
Normal file
49
examples/boot/src/bin/a.rs
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
examples/boot/src/bin/b.rs
Normal file
26
examples/boot/src/bin/b.rs
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user