Merge pull request #2284 from Redrield/feature/embassy-usb-dfu

Add embassy-usb-dfu crate, with related modifications to embassy-boot
This commit is contained in:
Ulf Lilleengen 2023-12-14 19:56:04 +00:00 committed by GitHub
commit 5ec2fbe3a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1219 additions and 7 deletions

2
ci.sh
View File

@ -173,10 +173,12 @@ cargo batch \
--- build --release --manifest-path examples/boot/application/stm32l1/Cargo.toml --target thumbv7m-none-eabi --features skip-include --out-dir out/examples/boot/stm32l1 \
--- build --release --manifest-path examples/boot/application/stm32l4/Cargo.toml --target thumbv7em-none-eabi --features skip-include --out-dir out/examples/boot/stm32l4 \
--- build --release --manifest-path examples/boot/application/stm32wl/Cargo.toml --target thumbv7em-none-eabihf --features skip-include --out-dir out/examples/boot/stm32wl \
--- build --release --manifest-path examples/boot/application/stm32wb-dfu/Cargo.toml --target thumbv7em-none-eabihf --out-dir out/examples/boot/stm32wb-dfu \
--- build --release --manifest-path examples/boot/bootloader/nrf/Cargo.toml --target thumbv7em-none-eabi --features embassy-nrf/nrf52840 \
--- build --release --manifest-path examples/boot/bootloader/nrf/Cargo.toml --target thumbv8m.main-none-eabihf --features embassy-nrf/nrf9160-ns \
--- build --release --manifest-path examples/boot/bootloader/rp/Cargo.toml --target thumbv6m-none-eabi \
--- build --release --manifest-path examples/boot/bootloader/stm32/Cargo.toml --target thumbv7em-none-eabi --features embassy-stm32/stm32wl55jc-cm4 \
--- build --release --manifest-path examples/boot/bootloader/stm32wb-dfu/Cargo.toml --target thumbv7em-none-eabihf \
--- build --release --manifest-path examples/wasm/Cargo.toml --target wasm32-unknown-unknown --out-dir out/examples/wasm \
--- build --release --manifest-path tests/stm32/Cargo.toml --target thumbv7m-none-eabi --features stm32f103c8 --out-dir out/tests/stm32f103c8 \
--- build --release --manifest-path tests/stm32/Cargo.toml --target thumbv7em-none-eabi --features stm32f429zi --out-dir out/tests/stm32f429zi \

View File

@ -5,7 +5,7 @@ use embassy_sync::blocking_mutex::raw::NoopRawMutex;
use embassy_sync::blocking_mutex::Mutex;
use embedded_storage::nor_flash::{NorFlash, NorFlashError, NorFlashErrorKind};
use crate::{State, BOOT_MAGIC, STATE_ERASE_VALUE, SWAP_MAGIC};
use crate::{State, BOOT_MAGIC, DFU_DETACH_MAGIC, STATE_ERASE_VALUE, SWAP_MAGIC};
/// Errors returned by bootloader
#[derive(PartialEq, Eq, Debug)]
@ -371,6 +371,8 @@ impl<ACTIVE: NorFlash, DFU: NorFlash, STATE: NorFlash> BootLoader<ACTIVE, DFU, S
if !state_word.iter().any(|&b| b != SWAP_MAGIC) {
Ok(State::Swap)
} else if !state_word.iter().any(|&b| b != DFU_DETACH_MAGIC) {
Ok(State::DfuDetach)
} else {
Ok(State::Boot)
}

View File

@ -6,7 +6,7 @@ use embassy_sync::blocking_mutex::raw::NoopRawMutex;
use embedded_storage_async::nor_flash::NorFlash;
use super::FirmwareUpdaterConfig;
use crate::{FirmwareUpdaterError, State, BOOT_MAGIC, STATE_ERASE_VALUE, SWAP_MAGIC};
use crate::{FirmwareUpdaterError, State, BOOT_MAGIC, DFU_DETACH_MAGIC, STATE_ERASE_VALUE, SWAP_MAGIC};
/// FirmwareUpdater is an application API for interacting with the BootLoader without the ability to
/// 'mess up' the internal bootloader state
@ -161,6 +161,12 @@ impl<'d, DFU: NorFlash, STATE: NorFlash> FirmwareUpdater<'d, DFU, STATE> {
self.state.mark_updated().await
}
/// Mark to trigger USB DFU on next boot.
pub async fn mark_dfu(&mut self) -> Result<(), FirmwareUpdaterError> {
self.state.verify_booted().await?;
self.state.mark_dfu().await
}
/// Mark firmware boot successful and stop rollback on reset.
pub async fn mark_booted(&mut self) -> Result<(), FirmwareUpdaterError> {
self.state.mark_booted().await
@ -207,6 +213,16 @@ pub struct FirmwareState<'d, STATE> {
}
impl<'d, STATE: NorFlash> FirmwareState<'d, STATE> {
/// Create a firmware state instance from a FirmwareUpdaterConfig with a buffer for magic content and state partition.
///
/// # Safety
///
/// The `aligned` buffer must have a size of STATE::WRITE_SIZE, and follow the alignment rules for the flash being read from
/// and written to.
pub fn from_config<DFU: NorFlash>(config: FirmwareUpdaterConfig<DFU, STATE>, aligned: &'d mut [u8]) -> Self {
Self::new(config.state, aligned)
}
/// Create a firmware state instance with a buffer for magic content and state partition.
///
/// # Safety
@ -247,6 +263,11 @@ impl<'d, STATE: NorFlash> FirmwareState<'d, STATE> {
self.set_magic(SWAP_MAGIC).await
}
/// Mark to trigger USB DFU on next boot.
pub async fn mark_dfu(&mut self) -> Result<(), FirmwareUpdaterError> {
self.set_magic(DFU_DETACH_MAGIC).await
}
/// Mark firmware boot successful and stop rollback on reset.
pub async fn mark_booted(&mut self) -> Result<(), FirmwareUpdaterError> {
self.set_magic(BOOT_MAGIC).await

View File

@ -6,7 +6,7 @@ use embassy_sync::blocking_mutex::raw::NoopRawMutex;
use embedded_storage::nor_flash::NorFlash;
use super::FirmwareUpdaterConfig;
use crate::{FirmwareUpdaterError, State, BOOT_MAGIC, STATE_ERASE_VALUE, SWAP_MAGIC};
use crate::{FirmwareUpdaterError, State, BOOT_MAGIC, DFU_DETACH_MAGIC, STATE_ERASE_VALUE, SWAP_MAGIC};
/// Blocking FirmwareUpdater is an application API for interacting with the BootLoader without the ability to
/// 'mess up' the internal bootloader state
@ -168,6 +168,12 @@ impl<'d, DFU: NorFlash, STATE: NorFlash> BlockingFirmwareUpdater<'d, DFU, STATE>
self.state.mark_updated()
}
/// Mark to trigger USB DFU device on next boot.
pub fn mark_dfu(&mut self) -> Result<(), FirmwareUpdaterError> {
self.state.verify_booted()?;
self.state.mark_dfu()
}
/// Mark firmware boot successful and stop rollback on reset.
pub fn mark_booted(&mut self) -> Result<(), FirmwareUpdaterError> {
self.state.mark_booted()
@ -213,6 +219,16 @@ pub struct BlockingFirmwareState<'d, STATE> {
}
impl<'d, STATE: NorFlash> BlockingFirmwareState<'d, STATE> {
/// Creates a firmware state instance from a FirmwareUpdaterConfig, with a buffer for magic content and state partition.
///
/// # Safety
///
/// The `aligned` buffer must have a size of STATE::WRITE_SIZE, and follow the alignment rules for the flash being read from
/// and written to.
pub fn from_config<DFU: NorFlash>(config: FirmwareUpdaterConfig<DFU, STATE>, aligned: &'d mut [u8]) -> Self {
Self::new(config.state, aligned)
}
/// Create a firmware state instance with a buffer for magic content and state partition.
///
/// # Safety
@ -226,7 +242,7 @@ impl<'d, STATE: NorFlash> BlockingFirmwareState<'d, STATE> {
// Make sure we are running a booted firmware to avoid reverting to a bad state.
fn verify_booted(&mut self) -> Result<(), FirmwareUpdaterError> {
if self.get_state()? == State::Boot {
if self.get_state()? == State::Boot || self.get_state()? == State::DfuDetach {
Ok(())
} else {
Err(FirmwareUpdaterError::BadState)
@ -243,6 +259,8 @@ impl<'d, STATE: NorFlash> BlockingFirmwareState<'d, STATE> {
if !self.aligned.iter().any(|&b| b != SWAP_MAGIC) {
Ok(State::Swap)
} else if !self.aligned.iter().any(|&b| b != DFU_DETACH_MAGIC) {
Ok(State::DfuDetach)
} else {
Ok(State::Boot)
}
@ -253,6 +271,11 @@ impl<'d, STATE: NorFlash> BlockingFirmwareState<'d, STATE> {
self.set_magic(SWAP_MAGIC)
}
/// Mark to trigger USB DFU on next boot.
pub fn mark_dfu(&mut self) -> Result<(), FirmwareUpdaterError> {
self.set_magic(DFU_DETACH_MAGIC)
}
/// Mark firmware boot successful and stop rollback on reset.
pub fn mark_booted(&mut self) -> Result<(), FirmwareUpdaterError> {
self.set_magic(BOOT_MAGIC)

View File

@ -23,6 +23,7 @@ pub use firmware_updater::{
pub(crate) const BOOT_MAGIC: u8 = 0xD0;
pub(crate) const SWAP_MAGIC: u8 = 0xF0;
pub(crate) const DFU_DETACH_MAGIC: u8 = 0xE0;
/// The state of the bootloader after running prepare.
#[derive(PartialEq, Eq, Debug)]
@ -32,6 +33,8 @@ pub enum State {
Boot,
/// Bootloader has swapped the active partition with the dfu partition and will attempt boot.
Swap,
/// Application has received a request to reboot into DFU mode to apply an update.
DfuDetach,
}
/// Buffer aligned to 32 byte boundary, largest known alignment requirement for embassy-boot.

View File

@ -10,7 +10,10 @@ pub use embassy_boot::{
use embedded_storage::nor_flash::NorFlash;
/// A bootloader for STM32 devices.
pub struct BootLoader;
pub struct BootLoader {
/// The reported state of the bootloader after preparing for boot
pub state: State,
}
impl BootLoader {
/// Inspect the bootloader state and perform actions required before booting, such as swapping firmware
@ -19,8 +22,8 @@ impl BootLoader {
) -> Self {
let mut aligned_buf = AlignedBuffer([0; BUFFER_SIZE]);
let mut boot = embassy_boot::BootLoader::new(config);
boot.prepare_boot(aligned_buf.as_mut()).expect("Boot prepare error");
Self
let state = boot.prepare_boot(aligned_buf.as_mut()).expect("Boot prepare error");
Self { state }
}
/// Boots the application.

View File

@ -0,0 +1,32 @@
[package]
edition = "2021"
name = "embassy-usb-dfu"
version = "0.1.0"
description = "An implementation of the USB DFU 1.1 protocol, using embassy-boot"
license = "MIT OR Apache-2.0"
repository = "https://github.com/embassy-rs/embassy"
categories = [
"embedded",
"no-std",
"asynchronous"
]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bitflags = "2.4.1"
cortex-m = { version = "0.7.7", features = ["inline-asm"], optional = true }
defmt = { version = "0.3.5", optional = true }
embassy-boot = { version = "0.1.1", path = "../embassy-boot/boot" }
# embassy-embedded-hal = { version = "0.1.0", path = "../embassy-embedded-hal" }
embassy-futures = { version = "0.1.1", path = "../embassy-futures" }
embassy-sync = { version = "0.5.0", path = "../embassy-sync" }
embassy-time = { version = "0.2.0", path = "../embassy-time" }
embassy-usb = { version = "0.1.0", path = "../embassy-usb", default-features = false }
embedded-storage = { version = "0.3.1" }
esp32c3-hal = { version = "0.13.0", optional = true, default-features = false }
[features]
dfu = []
application = []
defmt = ["dep:defmt"]

View File

@ -0,0 +1,135 @@
use core::marker::PhantomData;
use embassy_boot::BlockingFirmwareState;
use embassy_time::{Duration, Instant};
use embassy_usb::control::{InResponse, OutResponse, Recipient, RequestType};
use embassy_usb::driver::Driver;
use embassy_usb::{Builder, Handler};
use embedded_storage::nor_flash::NorFlash;
use crate::consts::{
DfuAttributes, Request, State, Status, APPN_SPEC_SUBCLASS_DFU, DESC_DFU_FUNCTIONAL, DFU_PROTOCOL_RT,
USB_CLASS_APPN_SPEC,
};
use crate::Reset;
/// Internal state for the DFU class
pub struct Control<'d, STATE: NorFlash, RST: Reset> {
firmware_state: BlockingFirmwareState<'d, STATE>,
attrs: DfuAttributes,
state: State,
timeout: Option<Duration>,
detach_start: Option<Instant>,
_rst: PhantomData<RST>,
}
impl<'d, STATE: NorFlash, RST: Reset> Control<'d, STATE, RST> {
pub fn new(firmware_state: BlockingFirmwareState<'d, STATE>, attrs: DfuAttributes) -> Self {
Control {
firmware_state,
attrs,
state: State::AppIdle,
detach_start: None,
timeout: None,
_rst: PhantomData,
}
}
}
impl<'d, STATE: NorFlash, RST: Reset> Handler for Control<'d, STATE, RST> {
fn reset(&mut self) {
if let Some(start) = self.detach_start {
let delta = Instant::now() - start;
let timeout = self.timeout.unwrap();
trace!(
"Received RESET with delta = {}, timeout = {}",
delta.as_millis(),
timeout.as_millis()
);
if delta < timeout {
self.firmware_state
.mark_dfu()
.expect("Failed to mark DFU mode in bootloader");
RST::sys_reset()
}
}
}
fn control_out(
&mut self,
req: embassy_usb::control::Request,
_: &[u8],
) -> Option<embassy_usb::control::OutResponse> {
if (req.request_type, req.recipient) != (RequestType::Class, Recipient::Interface) {
return None;
}
trace!("Received request {}", req);
match Request::try_from(req.request) {
Ok(Request::Detach) => {
trace!("Received DETACH, awaiting USB reset");
self.detach_start = Some(Instant::now());
self.timeout = Some(Duration::from_millis(req.value as u64));
self.state = State::AppDetach;
Some(OutResponse::Accepted)
}
_ => None,
}
}
fn control_in<'a>(
&'a mut self,
req: embassy_usb::control::Request,
buf: &'a mut [u8],
) -> Option<embassy_usb::control::InResponse<'a>> {
if (req.request_type, req.recipient) != (RequestType::Class, Recipient::Interface) {
return None;
}
trace!("Received request {}", req);
match Request::try_from(req.request) {
Ok(Request::GetStatus) => {
buf[0..6].copy_from_slice(&[Status::Ok as u8, 0x32, 0x00, 0x00, self.state as u8, 0x00]);
Some(InResponse::Accepted(buf))
}
_ => None,
}
}
}
/// An implementation of the USB DFU 1.1 runtime protocol
///
/// This function will add a DFU interface descriptor to the provided Builder, and register the provided Control as a handler for the USB device. The USB builder can be used as normal once this is complete.
/// The handler is responsive to DFU GetStatus and Detach commands.
///
/// Once a detach command, followed by a USB reset is received by the host, a magic number will be written into the bootloader state partition to indicate that
/// it should expose a DFU device, and a software reset will be issued.
///
/// To apply USB DFU updates, the bootloader must be capable of recognizing the DFU magic and exposing a device to handle the full DFU transaction with the host.
pub fn usb_dfu<'d, D: Driver<'d>, STATE: NorFlash, RST: Reset>(
builder: &mut Builder<'d, D>,
handler: &'d mut Control<'d, STATE, RST>,
timeout: Duration,
) {
let mut func = builder.function(0x00, 0x00, 0x00);
let mut iface = func.interface();
let mut alt = iface.alt_setting(USB_CLASS_APPN_SPEC, APPN_SPEC_SUBCLASS_DFU, DFU_PROTOCOL_RT, None);
let timeout = timeout.as_millis() as u16;
alt.descriptor(
DESC_DFU_FUNCTIONAL,
&[
handler.attrs.bits(),
(timeout & 0xff) as u8,
((timeout >> 8) & 0xff) as u8,
0x40,
0x00, // 64B control buffer size for application side
0x10,
0x01, // DFU 1.1
],
);
drop(func);
builder.handler(handler);
}

View File

@ -0,0 +1,189 @@
use core::marker::PhantomData;
use embassy_boot::{AlignedBuffer, BlockingFirmwareUpdater};
use embassy_usb::control::{InResponse, OutResponse, Recipient, RequestType};
use embassy_usb::driver::Driver;
use embassy_usb::{Builder, Handler};
use embedded_storage::nor_flash::{NorFlash, NorFlashErrorKind};
use crate::consts::{
DfuAttributes, Request, State, Status, APPN_SPEC_SUBCLASS_DFU, DESC_DFU_FUNCTIONAL, DFU_PROTOCOL_DFU,
USB_CLASS_APPN_SPEC,
};
use crate::Reset;
/// Internal state for USB DFU
pub struct Control<'d, DFU: NorFlash, STATE: NorFlash, RST: Reset, const BLOCK_SIZE: usize> {
updater: BlockingFirmwareUpdater<'d, DFU, STATE>,
attrs: DfuAttributes,
state: State,
status: Status,
offset: usize,
_rst: PhantomData<RST>,
}
impl<'d, DFU: NorFlash, STATE: NorFlash, RST: Reset, const BLOCK_SIZE: usize> Control<'d, DFU, STATE, RST, BLOCK_SIZE> {
pub fn new(updater: BlockingFirmwareUpdater<'d, DFU, STATE>, attrs: DfuAttributes) -> Self {
Self {
updater,
attrs,
state: State::DfuIdle,
status: Status::Ok,
offset: 0,
_rst: PhantomData,
}
}
fn reset_state(&mut self) {
self.offset = 0;
self.state = State::DfuIdle;
self.status = Status::Ok;
}
}
impl<'d, DFU: NorFlash, STATE: NorFlash, RST: Reset, const BLOCK_SIZE: usize> Handler
for Control<'d, DFU, STATE, RST, BLOCK_SIZE>
{
fn control_out(
&mut self,
req: embassy_usb::control::Request,
data: &[u8],
) -> Option<embassy_usb::control::OutResponse> {
if (req.request_type, req.recipient) != (RequestType::Class, Recipient::Interface) {
return None;
}
match Request::try_from(req.request) {
Ok(Request::Abort) => {
self.reset_state();
Some(OutResponse::Accepted)
}
Ok(Request::Dnload) if self.attrs.contains(DfuAttributes::CAN_DOWNLOAD) => {
if req.value == 0 {
self.state = State::Download;
self.offset = 0;
}
let mut buf = AlignedBuffer([0; BLOCK_SIZE]);
buf.as_mut()[..data.len()].copy_from_slice(data);
if req.length == 0 {
match self.updater.mark_updated() {
Ok(_) => {
self.status = Status::Ok;
self.state = State::ManifestSync;
}
Err(e) => {
self.state = State::Error;
match e {
embassy_boot::FirmwareUpdaterError::Flash(e) => match e {
NorFlashErrorKind::NotAligned => self.status = Status::ErrWrite,
NorFlashErrorKind::OutOfBounds => self.status = Status::ErrAddress,
_ => self.status = Status::ErrUnknown,
},
embassy_boot::FirmwareUpdaterError::Signature(_) => self.status = Status::ErrVerify,
embassy_boot::FirmwareUpdaterError::BadState => self.status = Status::ErrUnknown,
}
}
}
} else {
if self.state != State::Download {
// Unexpected DNLOAD while chip is waiting for a GETSTATUS
self.status = Status::ErrUnknown;
self.state = State::Error;
return Some(OutResponse::Rejected);
}
match self.updater.write_firmware(self.offset, buf.as_ref()) {
Ok(_) => {
self.status = Status::Ok;
self.state = State::DlSync;
self.offset += data.len();
}
Err(e) => {
self.state = State::Error;
match e {
embassy_boot::FirmwareUpdaterError::Flash(e) => match e {
NorFlashErrorKind::NotAligned => self.status = Status::ErrWrite,
NorFlashErrorKind::OutOfBounds => self.status = Status::ErrAddress,
_ => self.status = Status::ErrUnknown,
},
embassy_boot::FirmwareUpdaterError::Signature(_) => self.status = Status::ErrVerify,
embassy_boot::FirmwareUpdaterError::BadState => self.status = Status::ErrUnknown,
}
}
}
}
Some(OutResponse::Accepted)
}
Ok(Request::Detach) => Some(OutResponse::Accepted), // Device is already in DFU mode
Ok(Request::ClrStatus) => {
self.reset_state();
Some(OutResponse::Accepted)
}
_ => None,
}
}
fn control_in<'a>(
&'a mut self,
req: embassy_usb::control::Request,
buf: &'a mut [u8],
) -> Option<embassy_usb::control::InResponse<'a>> {
if (req.request_type, req.recipient) != (RequestType::Class, Recipient::Interface) {
return None;
}
match Request::try_from(req.request) {
Ok(Request::GetStatus) => {
//TODO: Configurable poll timeout, ability to add string for Vendor error
buf[0..6].copy_from_slice(&[self.status as u8, 0x32, 0x00, 0x00, self.state as u8, 0x00]);
match self.state {
State::DlSync => self.state = State::Download,
State::ManifestSync => RST::sys_reset(),
_ => {}
}
Some(InResponse::Accepted(&buf[0..6]))
}
Ok(Request::GetState) => {
buf[0] = self.state as u8;
Some(InResponse::Accepted(&buf[0..1]))
}
Ok(Request::Upload) if self.attrs.contains(DfuAttributes::CAN_UPLOAD) => {
//TODO: FirmwareUpdater does not provide a way of reading the active partition, can't upload.
Some(InResponse::Rejected)
}
_ => None,
}
}
}
/// An implementation of the USB DFU 1.1 protocol
///
/// This function will add a DFU interface descriptor to the provided Builder, and register the provided Control as a handler for the USB device
/// The handler is responsive to DFU GetState, GetStatus, Abort, and ClrStatus commands, as well as Download if configured by the user.
///
/// Once the host has initiated a DFU download operation, the chunks sent by the host will be written to the DFU partition.
/// Once the final sync in the manifestation phase has been received, the handler will trigger a system reset to swap the new firmware.
pub fn usb_dfu<'d, D: Driver<'d>, DFU: NorFlash, STATE: NorFlash, RST: Reset, const BLOCK_SIZE: usize>(
builder: &mut Builder<'d, D>,
handler: &'d mut Control<'d, DFU, STATE, RST, BLOCK_SIZE>,
) {
let mut func = builder.function(0x00, 0x00, 0x00);
let mut iface = func.interface();
let mut alt = iface.alt_setting(USB_CLASS_APPN_SPEC, APPN_SPEC_SUBCLASS_DFU, DFU_PROTOCOL_DFU, None);
alt.descriptor(
DESC_DFU_FUNCTIONAL,
&[
handler.attrs.bits(),
0xc4,
0x09, // 2500ms timeout, doesn't affect operation as DETACH not necessary in bootloader code
(BLOCK_SIZE & 0xff) as u8,
((BLOCK_SIZE & 0xff00) >> 8) as u8,
0x10,
0x01, // DFU 1.1
],
);
drop(func);
builder.handler(handler);
}

View File

@ -0,0 +1,95 @@
pub(crate) const USB_CLASS_APPN_SPEC: u8 = 0xFE;
pub(crate) const APPN_SPEC_SUBCLASS_DFU: u8 = 0x01;
#[allow(unused)]
pub(crate) const DFU_PROTOCOL_DFU: u8 = 0x02;
#[allow(unused)]
pub(crate) const DFU_PROTOCOL_RT: u8 = 0x01;
pub(crate) const DESC_DFU_FUNCTIONAL: u8 = 0x21;
#[cfg(feature = "defmt")]
defmt::bitflags! {
pub struct DfuAttributes: u8 {
const WILL_DETACH = 0b0000_1000;
const MANIFESTATION_TOLERANT = 0b0000_0100;
const CAN_UPLOAD = 0b0000_0010;
const CAN_DOWNLOAD = 0b0000_0001;
}
}
#[cfg(not(feature = "defmt"))]
bitflags::bitflags! {
pub struct DfuAttributes: u8 {
const WILL_DETACH = 0b0000_1000;
const MANIFESTATION_TOLERANT = 0b0000_0100;
const CAN_UPLOAD = 0b0000_0010;
const CAN_DOWNLOAD = 0b0000_0001;
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
#[repr(u8)]
#[allow(unused)]
pub enum State {
AppIdle = 0,
AppDetach = 1,
DfuIdle = 2,
DlSync = 3,
DlBusy = 4,
Download = 5,
ManifestSync = 6,
Manifest = 7,
ManifestWaitReset = 8,
UploadIdle = 9,
Error = 10,
}
#[derive(Copy, Clone, PartialEq, Eq)]
#[repr(u8)]
#[allow(unused)]
pub enum Status {
Ok = 0x00,
ErrTarget = 0x01,
ErrFile = 0x02,
ErrWrite = 0x03,
ErrErase = 0x04,
ErrCheckErased = 0x05,
ErrProg = 0x06,
ErrVerify = 0x07,
ErrAddress = 0x08,
ErrNotDone = 0x09,
ErrFirmware = 0x0A,
ErrVendor = 0x0B,
ErrUsbr = 0x0C,
ErrPor = 0x0D,
ErrUnknown = 0x0E,
ErrStalledPkt = 0x0F,
}
#[derive(Copy, Clone, PartialEq, Eq)]
#[repr(u8)]
pub enum Request {
Detach = 0,
Dnload = 1,
Upload = 2,
GetStatus = 3,
ClrStatus = 4,
GetState = 5,
Abort = 6,
}
impl TryFrom<u8> for Request {
type Error = ();
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(Request::Detach),
1 => Ok(Request::Dnload),
2 => Ok(Request::Upload),
3 => Ok(Request::GetStatus),
4 => Ok(Request::ClrStatus),
5 => Ok(Request::GetState),
6 => Ok(Request::Abort),
_ => Err(()),
}
}
}

258
embassy-usb-dfu/src/fmt.rs Normal file
View File

@ -0,0 +1,258 @@
#![macro_use]
#![allow(unused_macros)]
use core::fmt::{Debug, Display, LowerHex};
#[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)*);
}
};
}
#[cfg(not(feature = "defmt"))]
macro_rules! unreachable {
($($x:tt)*) => {
::core::unreachable!($($x)*)
};
}
#[cfg(feature = "defmt")]
macro_rules! unreachable {
($($x:tt)*) => {
::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
}
}
#[allow(unused)]
pub(crate) struct Bytes<'a>(pub &'a [u8]);
impl<'a> Debug for Bytes<'a> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{:#02x?}", self.0)
}
}
impl<'a> Display for Bytes<'a> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{:#02x?}", self.0)
}
}
impl<'a> LowerHex for Bytes<'a> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{:#02x?}", self.0)
}
}
#[cfg(feature = "defmt")]
impl<'a> defmt::Format for Bytes<'a> {
fn format(&self, fmt: defmt::Formatter) {
defmt::write!(fmt, "{:02x}", self.0)
}
}

View File

@ -0,0 +1,51 @@
#![no_std]
mod fmt;
pub mod consts;
#[cfg(feature = "dfu")]
mod bootloader;
#[cfg(feature = "dfu")]
pub use self::bootloader::*;
#[cfg(feature = "application")]
mod application;
#[cfg(feature = "application")]
pub use self::application::*;
#[cfg(any(
all(feature = "dfu", feature = "application"),
not(any(feature = "dfu", feature = "application"))
))]
compile_error!("usb-dfu must be compiled with exactly one of `bootloader`, or `application` features");
/// Provides a platform-agnostic interface for initiating a system reset.
///
/// This crate exposes `ResetImmediate` when compiled with cortex-m or esp32c3 support, which immediately issues a
/// reset request without interfacing with any other peripherals.
///
/// If alternate behaviour is desired, a custom implementation of Reset can be provided as a type argument to the usb_dfu function.
pub trait Reset {
fn sys_reset() -> !;
}
#[cfg(feature = "esp32c3-hal")]
pub struct ResetImmediate;
#[cfg(feature = "esp32c3-hal")]
impl Reset for ResetImmediate {
fn sys_reset() -> ! {
esp32c3_hal::reset::software_reset();
loop {}
}
}
#[cfg(feature = "cortex-m")]
pub struct ResetImmediate;
#[cfg(feature = "cortex-m")]
impl Reset for ResetImmediate {
fn sys_reset() -> ! {
cortex_m::peripheral::SCB::sys_reset()
}
}

View File

@ -0,0 +1,9 @@
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
# replace your chip as listed in `probe-rs chip list`
runner = "probe-rs run --chip STM32WLE5JCIx"
[build]
target = "thumbv7em-none-eabihf"
[env]
DEFMT_LOG = "trace"

View File

@ -0,0 +1,32 @@
[package]
edition = "2021"
name = "embassy-boot-stm32wb-dfu-examples"
version = "0.1.0"
license = "MIT OR Apache-2.0"
[dependencies]
embassy-sync = { version = "0.5.0", path = "../../../../embassy-sync" }
embassy-executor = { version = "0.4.0", path = "../../../../embassy-executor", features = ["arch-cortex-m", "executor-thread", "nightly", "integrated-timers"] }
embassy-time = { version = "0.2", path = "../../../../embassy-time", features = [ "tick-hz-32_768"] }
embassy-stm32 = { version = "0.1.0", path = "../../../../embassy-stm32", features = ["stm32wb55rg", "time-driver-any", "exti"] }
embassy-boot-stm32 = { version = "0.1.0", path = "../../../../embassy-boot/stm32", features = [] }
embassy-embedded-hal = { version = "0.1.0", path = "../../../../embassy-embedded-hal" }
embassy-usb = { version = "0.1.0", path = "../../../../embassy-usb" }
embassy-usb-dfu = { version = "0.1.0", path = "../../../../embassy-usb-dfu", features = ["application", "cortex-m"] }
defmt = { version = "0.3", optional = true }
defmt-rtt = { version = "0.4", optional = true }
panic-reset = { version = "0.1.1" }
embedded-hal = { version = "0.2.6" }
cortex-m = { version = "0.7.6", features = ["inline-asm", "critical-section-single-core"] }
cortex-m-rt = "0.7.0"
[features]
defmt = [
"dep:defmt",
"dep:defmt-rtt",
"embassy-stm32/defmt",
"embassy-boot-stm32/defmt",
"embassy-sync/defmt",
]

View File

@ -0,0 +1,29 @@
# Examples using bootloader
Example for STM32WL 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-stm32`
## Usage
```
# Flash bootloader
cargo flash --manifest-path ../../bootloader/stm32/Cargo.toml --release --features embassy-stm32/stm32wl55jc-cm4 --chip STM32WLE5JCIx
# Build 'b'
cargo build --release --bin b
# Generate binary for 'b'
cargo objcopy --release --bin b -- -O binary b.bin
```
# Flash `a` (which includes b.bin)
```
cargo flash --release --bin a --chip STM32WLE5JCIx
```

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,15 @@
MEMORY
{
/* NOTE 1 K = 1 KiBi = 1024 bytes */
BOOTLOADER : ORIGIN = 0x08000000, LENGTH = 24K
BOOTLOADER_STATE : ORIGIN = 0x08006000, LENGTH = 4K
FLASH : ORIGIN = 0x08008000, LENGTH = 128K
DFU : ORIGIN = 0x08028000, LENGTH = 132K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 32K
}
__bootloader_state_start = ORIGIN(BOOTLOADER_STATE) - ORIGIN(BOOTLOADER);
__bootloader_state_end = ORIGIN(BOOTLOADER_STATE) + LENGTH(BOOTLOADER_STATE) - ORIGIN(BOOTLOADER);
__bootloader_dfu_start = ORIGIN(DFU) - ORIGIN(BOOTLOADER);
__bootloader_dfu_end = ORIGIN(DFU) + LENGTH(DFU) - ORIGIN(BOOTLOADER);

View File

@ -0,0 +1,64 @@
#![no_std]
#![no_main]
#![feature(type_alias_impl_trait)]
use core::cell::RefCell;
#[cfg(feature = "defmt-rtt")]
use defmt_rtt::*;
use embassy_boot_stm32::{AlignedBuffer, BlockingFirmwareState, FirmwareUpdaterConfig};
use embassy_executor::Spawner;
use embassy_stm32::flash::{Flash, WRITE_SIZE};
use embassy_stm32::rcc::WPAN_DEFAULT;
use embassy_stm32::usb::{self, Driver};
use embassy_stm32::{bind_interrupts, peripherals};
use embassy_sync::blocking_mutex::Mutex;
use embassy_time::Duration;
use embassy_usb::Builder;
use embassy_usb_dfu::consts::DfuAttributes;
use embassy_usb_dfu::{usb_dfu, Control, ResetImmediate};
use panic_reset as _;
bind_interrupts!(struct Irqs {
USB_LP => usb::InterruptHandler<peripherals::USB>;
});
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let mut config = embassy_stm32::Config::default();
config.rcc = WPAN_DEFAULT;
let p = embassy_stm32::init(config);
let flash = Flash::new_blocking(p.FLASH);
let flash = Mutex::new(RefCell::new(flash));
let config = FirmwareUpdaterConfig::from_linkerfile_blocking(&flash);
let mut magic = AlignedBuffer([0; WRITE_SIZE]);
let mut firmware_state = BlockingFirmwareState::from_config(config, &mut magic.0);
firmware_state.mark_booted().expect("Failed to mark booted");
let driver = Driver::new(p.USB, Irqs, p.PA12, p.PA11);
let mut config = embassy_usb::Config::new(0xc0de, 0xcafe);
config.manufacturer = Some("Embassy");
config.product = Some("USB-DFU Runtime example");
config.serial_number = Some("1235678");
let mut device_descriptor = [0; 256];
let mut config_descriptor = [0; 256];
let mut bos_descriptor = [0; 256];
let mut control_buf = [0; 64];
let mut state = Control::new(firmware_state, DfuAttributes::CAN_DOWNLOAD);
let mut builder = Builder::new(
driver,
config,
&mut device_descriptor,
&mut config_descriptor,
&mut bos_descriptor,
&mut [],
&mut control_buf,
);
usb_dfu::<_, _, ResetImmediate>(&mut builder, &mut state, Duration::from_millis(2500));
let mut dev = builder.build();
dev.run().await
}

View File

@ -0,0 +1,63 @@
[package]
edition = "2021"
name = "stm32wb-dfu-bootloader-example"
version = "0.1.0"
description = "Example USB DFUbootloader for the STM32WB series of chips"
license = "MIT OR Apache-2.0"
[dependencies]
defmt = { version = "0.3", optional = true }
defmt-rtt = { version = "0.4", optional = true }
embassy-stm32 = { path = "../../../../embassy-stm32", features = ["stm32wb55rg"] }
embassy-boot-stm32 = { path = "../../../../embassy-boot/stm32" }
cortex-m = { version = "0.7.6", features = ["inline-asm", "critical-section-single-core"] }
embassy-sync = { version = "0.5.0", path = "../../../../embassy-sync" }
cortex-m-rt = { version = "0.7" }
embedded-storage = "0.3.1"
embedded-storage-async = "0.4.0"
cfg-if = "1.0.0"
embassy-usb-dfu = { version = "0.1.0", path = "../../../../embassy-usb-dfu", features = ["dfu", "cortex-m"] }
embassy-usb = { version = "0.1.0", path = "../../../../embassy-usb", default-features = false }
embassy-futures = { version = "0.1.1", path = "../../../../embassy-futures" }
[features]
defmt = [
"dep:defmt",
"embassy-boot-stm32/defmt",
"embassy-stm32/defmt",
"embassy-usb/defmt",
"embassy-usb-dfu/defmt"
]
debug = ["defmt-rtt", "defmt"]
[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 STM32
The bootloader uses `embassy-boot` to interact with the flash.
# Usage
Flash the bootloader
```
cargo flash --features embassy-stm32/stm32wl55jc-cm4 --release --chip STM32WLE5JCIx
```

View File

@ -0,0 +1,27 @@
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 = 0x08000000, LENGTH = 24K
BOOTLOADER_STATE : ORIGIN = 0x08006000, LENGTH = 4K
ACTIVE : ORIGIN = 0x08008000, LENGTH = 128K
DFU : ORIGIN = 0x08028000, LENGTH = 132K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 16K
}
__bootloader_state_start = ORIGIN(BOOTLOADER_STATE) - ORIGIN(FLASH);
__bootloader_state_end = ORIGIN(BOOTLOADER_STATE) + LENGTH(BOOTLOADER_STATE) - ORIGIN(FLASH);
__bootloader_active_start = ORIGIN(ACTIVE) - ORIGIN(FLASH);
__bootloader_active_end = ORIGIN(ACTIVE) + LENGTH(ACTIVE) - ORIGIN(FLASH);
__bootloader_dfu_start = ORIGIN(DFU) - ORIGIN(FLASH);
__bootloader_dfu_end = ORIGIN(DFU) + LENGTH(DFU) - ORIGIN(FLASH);

View File

@ -0,0 +1,93 @@
#![no_std]
#![no_main]
use core::cell::RefCell;
use cortex_m_rt::{entry, exception};
#[cfg(feature = "defmt")]
use defmt_rtt as _;
use embassy_boot_stm32::*;
use embassy_stm32::flash::{Flash, BANK1_REGION, WRITE_SIZE};
use embassy_stm32::rcc::WPAN_DEFAULT;
use embassy_stm32::usb::Driver;
use embassy_stm32::{bind_interrupts, peripherals, usb};
use embassy_sync::blocking_mutex::Mutex;
use embassy_usb::Builder;
use embassy_usb_dfu::consts::DfuAttributes;
use embassy_usb_dfu::{usb_dfu, Control, ResetImmediate};
bind_interrupts!(struct Irqs {
USB_LP => usb::InterruptHandler<peripherals::USB>;
});
#[entry]
fn main() -> ! {
let mut config = embassy_stm32::Config::default();
config.rcc = WPAN_DEFAULT;
let p = embassy_stm32::init(config);
// Prevent a hard fault when accessing flash 'too early' after boot.
#[cfg(feature = "defmt")]
for _ in 0..10000000 {
cortex_m::asm::nop();
}
let layout = Flash::new_blocking(p.FLASH).into_blocking_regions();
let flash = Mutex::new(RefCell::new(layout.bank1_region));
let config = BootLoaderConfig::from_linkerfile_blocking(&flash);
let active_offset = config.active.offset();
let bl = BootLoader::prepare::<_, _, _, 2048>(config);
if bl.state == State::DfuDetach {
let driver = Driver::new(p.USB, Irqs, p.PA12, p.PA11);
let mut config = embassy_usb::Config::new(0xc0de, 0xcafe);
config.manufacturer = Some("Embassy");
config.product = Some("USB-DFU Bootloader example");
config.serial_number = Some("1235678");
let fw_config = FirmwareUpdaterConfig::from_linkerfile_blocking(&flash);
let mut buffer = AlignedBuffer([0; WRITE_SIZE]);
let updater = BlockingFirmwareUpdater::new(fw_config, &mut buffer.0[..]);
let mut device_descriptor = [0; 256];
let mut config_descriptor = [0; 256];
let mut bos_descriptor = [0; 256];
let mut control_buf = [0; 4096];
let mut state = Control::new(updater, DfuAttributes::CAN_DOWNLOAD);
let mut builder = Builder::new(
driver,
config,
&mut device_descriptor,
&mut config_descriptor,
&mut bos_descriptor,
&mut [],
&mut control_buf,
);
usb_dfu::<_, _, _, ResetImmediate, 4096>(&mut builder, &mut state);
let mut dev = builder.build();
embassy_futures::block_on(dev.run());
}
unsafe { bl.load(BANK1_REGION.base + active_offset) }
}
#[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) -> ! {
cortex_m::asm::udf();
}