diff --git a/ci.sh b/ci.sh index 8a5e206d2..83c05d1b9 100755 --- a/ci.sh +++ b/ci.sh @@ -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 \ diff --git a/embassy-boot/boot/src/boot_loader.rs b/embassy-boot/boot/src/boot_loader.rs index 65b12dc5f..e568001bc 100644 --- a/embassy-boot/boot/src/boot_loader.rs +++ b/embassy-boot/boot/src/boot_loader.rs @@ -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 BootLoader 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(config: FirmwareUpdaterConfig, 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 diff --git a/embassy-boot/boot/src/firmware_updater/blocking.rs b/embassy-boot/boot/src/firmware_updater/blocking.rs index 76e4264a0..c4c142169 100644 --- a/embassy-boot/boot/src/firmware_updater/blocking.rs +++ b/embassy-boot/boot/src/firmware_updater/blocking.rs @@ -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(config: FirmwareUpdaterConfig, 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) diff --git a/embassy-boot/boot/src/lib.rs b/embassy-boot/boot/src/lib.rs index 9e70a4dca..15b69f69d 100644 --- a/embassy-boot/boot/src/lib.rs +++ b/embassy-boot/boot/src/lib.rs @@ -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. diff --git a/embassy-boot/stm32/src/lib.rs b/embassy-boot/stm32/src/lib.rs index c418cb262..4b4091ac9 100644 --- a/embassy-boot/stm32/src/lib.rs +++ b/embassy-boot/stm32/src/lib.rs @@ -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. diff --git a/embassy-usb-dfu/Cargo.toml b/embassy-usb-dfu/Cargo.toml new file mode 100644 index 000000000..ee110ee87 --- /dev/null +++ b/embassy-usb-dfu/Cargo.toml @@ -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"] diff --git a/embassy-usb-dfu/src/application.rs b/embassy-usb-dfu/src/application.rs new file mode 100644 index 000000000..75689db26 --- /dev/null +++ b/embassy-usb-dfu/src/application.rs @@ -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, + detach_start: Option, + _rst: PhantomData, +} + +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 { + 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> { + 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); +} diff --git a/embassy-usb-dfu/src/bootloader.rs b/embassy-usb-dfu/src/bootloader.rs new file mode 100644 index 000000000..d41e6280d --- /dev/null +++ b/embassy-usb-dfu/src/bootloader.rs @@ -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, +} + +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 { + 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> { + 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); +} diff --git a/embassy-usb-dfu/src/consts.rs b/embassy-usb-dfu/src/consts.rs new file mode 100644 index 000000000..b359a107e --- /dev/null +++ b/embassy-usb-dfu/src/consts.rs @@ -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 for Request { + type Error = (); + + fn try_from(value: u8) -> Result { + 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(()), + } + } +} diff --git a/embassy-usb-dfu/src/fmt.rs b/embassy-usb-dfu/src/fmt.rs new file mode 100644 index 000000000..78e583c1c --- /dev/null +++ b/embassy-usb-dfu/src/fmt.rs @@ -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; +} + +impl Try for Option { + type Ok = T; + type Error = NoneError; + + #[inline] + fn into_result(self) -> Result { + self.ok_or(NoneError) + } +} + +impl Try for Result { + type Ok = T; + type Error = E; + + #[inline] + fn into_result(self) -> Self { + self + } +} + +#[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) + } +} diff --git a/embassy-usb-dfu/src/lib.rs b/embassy-usb-dfu/src/lib.rs new file mode 100644 index 000000000..389bb33f2 --- /dev/null +++ b/embassy-usb-dfu/src/lib.rs @@ -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() + } +} diff --git a/examples/boot/application/stm32wb-dfu/.cargo/config.toml b/examples/boot/application/stm32wb-dfu/.cargo/config.toml new file mode 100644 index 000000000..4f8094ff2 --- /dev/null +++ b/examples/boot/application/stm32wb-dfu/.cargo/config.toml @@ -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" diff --git a/examples/boot/application/stm32wb-dfu/Cargo.toml b/examples/boot/application/stm32wb-dfu/Cargo.toml new file mode 100644 index 000000000..f6beea498 --- /dev/null +++ b/examples/boot/application/stm32wb-dfu/Cargo.toml @@ -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", +] diff --git a/examples/boot/application/stm32wb-dfu/README.md b/examples/boot/application/stm32wb-dfu/README.md new file mode 100644 index 000000000..c8dce0387 --- /dev/null +++ b/examples/boot/application/stm32wb-dfu/README.md @@ -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 +``` diff --git a/examples/boot/application/stm32wb-dfu/build.rs b/examples/boot/application/stm32wb-dfu/build.rs new file mode 100644 index 000000000..e1da69328 --- /dev/null +++ b/examples/boot/application/stm32wb-dfu/build.rs @@ -0,0 +1,37 @@ +//! This build script copies the `memory.x` file from the crate root into +//! a directory where the linker can always find it at build time. +//! For many projects this is optional, as the linker always searches the +//! project root directory -- wherever `Cargo.toml` is. However, if you +//! are using a workspace or have a more complicated build setup, this +//! build script becomes required. Additionally, by requesting that +//! Cargo re-run the build script whenever `memory.x` is changed, +//! updating `memory.x` ensures a rebuild of the application with the +//! new memory settings. + +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +fn main() { + // Put `memory.x` in our output directory and ensure it's + // on the linker search path. + let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); + File::create(out.join("memory.x")) + .unwrap() + .write_all(include_bytes!("memory.x")) + .unwrap(); + println!("cargo:rustc-link-search={}", out.display()); + + // By default, Cargo will re-run a build script whenever + // any file in the project changes. By specifying `memory.x` + // here, we ensure the build script is only re-run when + // `memory.x` is changed. + println!("cargo:rerun-if-changed=memory.x"); + + println!("cargo:rustc-link-arg-bins=--nmagic"); + println!("cargo:rustc-link-arg-bins=-Tlink.x"); + if env::var("CARGO_FEATURE_DEFMT").is_ok() { + println!("cargo:rustc-link-arg-bins=-Tdefmt.x"); + } +} diff --git a/examples/boot/application/stm32wb-dfu/memory.x b/examples/boot/application/stm32wb-dfu/memory.x new file mode 100644 index 000000000..ff1b800d2 --- /dev/null +++ b/examples/boot/application/stm32wb-dfu/memory.x @@ -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); diff --git a/examples/boot/application/stm32wb-dfu/src/main.rs b/examples/boot/application/stm32wb-dfu/src/main.rs new file mode 100644 index 000000000..fbecbf23b --- /dev/null +++ b/examples/boot/application/stm32wb-dfu/src/main.rs @@ -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; +}); + +#[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 +} diff --git a/examples/boot/bootloader/stm32wb-dfu/Cargo.toml b/examples/boot/bootloader/stm32wb-dfu/Cargo.toml new file mode 100644 index 000000000..ada073970 --- /dev/null +++ b/examples/boot/bootloader/stm32wb-dfu/Cargo.toml @@ -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 diff --git a/examples/boot/bootloader/stm32wb-dfu/README.md b/examples/boot/bootloader/stm32wb-dfu/README.md new file mode 100644 index 000000000..a82b730b9 --- /dev/null +++ b/examples/boot/bootloader/stm32wb-dfu/README.md @@ -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 +``` diff --git a/examples/boot/bootloader/stm32wb-dfu/build.rs b/examples/boot/bootloader/stm32wb-dfu/build.rs new file mode 100644 index 000000000..fd605991f --- /dev/null +++ b/examples/boot/bootloader/stm32wb-dfu/build.rs @@ -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"); + } +} diff --git a/examples/boot/bootloader/stm32wb-dfu/memory.x b/examples/boot/bootloader/stm32wb-dfu/memory.x new file mode 100644 index 000000000..858062631 --- /dev/null +++ b/examples/boot/bootloader/stm32wb-dfu/memory.x @@ -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); diff --git a/examples/boot/bootloader/stm32wb-dfu/src/main.rs b/examples/boot/bootloader/stm32wb-dfu/src/main.rs new file mode 100644 index 000000000..a7ab813b6 --- /dev/null +++ b/examples/boot/bootloader/stm32wb-dfu/src/main.rs @@ -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; +}); + +#[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(); +}