From 9ddf8b08e448caca3825fc47aa737247323d8725 Mon Sep 17 00:00:00 2001 From: Ulf Lilleengen Date: Tue, 19 Dec 2023 16:33:05 +0100 Subject: docs: document usb-logger and usb-dfu --- embassy-usb-dfu/README.md | 20 ++++ embassy-usb-dfu/src/application.rs | 1 + embassy-usb-dfu/src/bootloader.rs | 189 ------------------------------------ embassy-usb-dfu/src/consts.rs | 12 ++- embassy-usb-dfu/src/dfu.rs | 190 +++++++++++++++++++++++++++++++++++++ embassy-usb-dfu/src/lib.rs | 11 ++- embassy-usb-logger/README.md | 14 +++ 7 files changed, 242 insertions(+), 195 deletions(-) create mode 100644 embassy-usb-dfu/README.md delete mode 100644 embassy-usb-dfu/src/bootloader.rs create mode 100644 embassy-usb-dfu/src/dfu.rs diff --git a/embassy-usb-dfu/README.md b/embassy-usb-dfu/README.md new file mode 100644 index 000000000..d8bc19bfd --- /dev/null +++ b/embassy-usb-dfu/README.md @@ -0,0 +1,20 @@ +# embassy-usb-dfu + +An implementation of the USB DFU 1.1 protocol using embassy-boot. It has 2 components depending on which feature is enabled by the user. + +* DFU protocol mode, enabled by the `dfu` feature. This mode corresponds to the transfer phase DFU protocol described by the USB IF. It supports DFU_DNLOAD requests if marked by the user, and will automatically reset the chip once a DFU transaction has been completed. It also responds to DFU_GETSTATUS, DFU_GETSTATE, DFU_ABORT, and DFU_CLRSTATUS with no user intervention. +* DFU runtime mode, enabled by the `application feature`. This mode allows users to expose a DFU interface on their USB device, informing the host of the capability to DFU over USB, and allowing the host to reset the device into its bootloader to complete a DFU operation. Supports DFU_GETSTATUS and DFU_DETACH. When detach/reset is seen by the device as described by the standard, will write a new DFU magic number into the bootloader state in flash, and reset the system. + +## Minimum supported Rust version (MSRV) + +Embassy is guaranteed to compile on the latest stable Rust version at the time of release. It might compile with older versions but that may change in any new patch release. + +## License + +This work is licensed under either of + +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or + ) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or ) + +at your option. diff --git a/embassy-usb-dfu/src/application.rs b/embassy-usb-dfu/src/application.rs index 75689db26..f0d7626f6 100644 --- a/embassy-usb-dfu/src/application.rs +++ b/embassy-usb-dfu/src/application.rs @@ -24,6 +24,7 @@ pub struct Control<'d, STATE: NorFlash, RST: Reset> { } impl<'d, STATE: NorFlash, RST: Reset> Control<'d, STATE, RST> { + /// Create a new DFU instance to expose a DFU interface. pub fn new(firmware_state: BlockingFirmwareState<'d, STATE>, attrs: DfuAttributes) -> Self { Control { firmware_state, diff --git a/embassy-usb-dfu/src/bootloader.rs b/embassy-usb-dfu/src/bootloader.rs deleted file mode 100644 index d41e6280d..000000000 --- a/embassy-usb-dfu/src/bootloader.rs +++ /dev/null @@ -1,189 +0,0 @@ -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 index b359a107e..f8a056e5c 100644 --- a/embassy-usb-dfu/src/consts.rs +++ b/embassy-usb-dfu/src/consts.rs @@ -1,3 +1,4 @@ +//! USB DFU constants. pub(crate) const USB_CLASS_APPN_SPEC: u8 = 0xFE; pub(crate) const APPN_SPEC_SUBCLASS_DFU: u8 = 0x01; #[allow(unused)] @@ -18,10 +19,15 @@ defmt::bitflags! { #[cfg(not(feature = "defmt"))] bitflags::bitflags! { + /// Attributes supported by the DFU controller. pub struct DfuAttributes: u8 { + /// Generate WillDetache sequence on bus. const WILL_DETACH = 0b0000_1000; + /// Device can communicate during manifestation phase. const MANIFESTATION_TOLERANT = 0b0000_0100; + /// Capable of upload. const CAN_UPLOAD = 0b0000_0010; + /// Capable of download. const CAN_DOWNLOAD = 0b0000_0001; } } @@ -29,7 +35,7 @@ bitflags::bitflags! { #[derive(Copy, Clone, PartialEq, Eq)] #[repr(u8)] #[allow(unused)] -pub enum State { +pub(crate) enum State { AppIdle = 0, AppDetach = 1, DfuIdle = 2, @@ -46,7 +52,7 @@ pub enum State { #[derive(Copy, Clone, PartialEq, Eq)] #[repr(u8)] #[allow(unused)] -pub enum Status { +pub(crate) enum Status { Ok = 0x00, ErrTarget = 0x01, ErrFile = 0x02, @@ -67,7 +73,7 @@ pub enum Status { #[derive(Copy, Clone, PartialEq, Eq)] #[repr(u8)] -pub enum Request { +pub(crate) enum Request { Detach = 0, Dnload = 1, Upload = 2, diff --git a/embassy-usb-dfu/src/dfu.rs b/embassy-usb-dfu/src/dfu.rs new file mode 100644 index 000000000..e99aa70c3 --- /dev/null +++ b/embassy-usb-dfu/src/dfu.rs @@ -0,0 +1,190 @@ +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> { + /// Create a new DFU instance to handle DFU transfers. + 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/lib.rs b/embassy-usb-dfu/src/lib.rs index 389bb33f2..eaa4b6e33 100644 --- a/embassy-usb-dfu/src/lib.rs +++ b/embassy-usb-dfu/src/lib.rs @@ -1,12 +1,14 @@ #![no_std] +#![doc = include_str!("../README.md")] +#![warn(missing_docs)] mod fmt; pub mod consts; #[cfg(feature = "dfu")] -mod bootloader; +mod dfu; #[cfg(feature = "dfu")] -pub use self::bootloader::*; +pub use self::dfu::*; #[cfg(feature = "application")] mod application; @@ -17,7 +19,7 @@ pub use self::application::*; 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"); +compile_error!("usb-dfu must be compiled with exactly one of `dfu`, or `application` features"); /// Provides a platform-agnostic interface for initiating a system reset. /// @@ -26,9 +28,11 @@ compile_error!("usb-dfu must be compiled with exactly one of `bootloader`, or `a /// /// 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 { + /// Reset the device. fn sys_reset() -> !; } +/// Reset immediately. #[cfg(feature = "esp32c3-hal")] pub struct ResetImmediate; @@ -40,6 +44,7 @@ impl Reset for ResetImmediate { } } +/// Reset immediately. #[cfg(feature = "cortex-m")] pub struct ResetImmediate; diff --git a/embassy-usb-logger/README.md b/embassy-usb-logger/README.md index 81b0dcd0e..6cb18e87d 100644 --- a/embassy-usb-logger/README.md +++ b/embassy-usb-logger/README.md @@ -13,3 +13,17 @@ async fn logger_task(driver: Driver<'static, USB>) { embassy_usb_logger::run!(1024, log::LevelFilter::Info, driver); } ``` + +## Minimum supported Rust version (MSRV) + +Embassy is guaranteed to compile on the latest stable Rust version at the time of release. It might compile with older versions but that may change in any new patch release. + +## License + +This work is licensed under either of + +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or + ) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or ) + +at your option. -- cgit