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:
bors[bot] 2022-02-09 15:07:25 +00:00 committed by GitHub
commit 3d6b8bd983
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1715 additions and 0 deletions

View 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"] }

View 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
}
}

View 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(())
}
}
}
}

View 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"

View 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

View 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
View 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");
}
}

View 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);

View 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
View 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
View 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
View 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)
}
}

View 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();
}
}

View File

@ -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"

View File

@ -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()
}
}

View 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
View 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
View 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
View 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
View 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);

View 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();
}
}
}

View 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;
}
}