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.
This commit is contained in:
parent
d91bd0b9a6
commit
ed2a87a262
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
|
||||||
|
}
|
||||||
|
}
|
203
embassy-boot/nrf/src/lib.rs
Normal file
203
embassy-boot/nrf/src/lib.rs
Normal file
@ -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<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);
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
46
embassy-boot/nrf/src/main.rs
Normal file
46
embassy-boot/nrf/src/main.rs
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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