diff --git a/embassy-usb/Cargo.toml b/embassy-usb/Cargo.toml index 1b31b6145..3ef5c5684 100644 --- a/embassy-usb/Cargo.toml +++ b/embassy-usb/Cargo.toml @@ -54,6 +54,7 @@ embassy-net-driver-channel = { version = "0.2.0", path = "../embassy-net-driver- defmt = { version = "0.3", optional = true } log = { version = "0.4.14", optional = true } heapless = "0.8" +bitflags = "2.4.1" # for HID usbd-hid = { version = "0.6.0", optional = true } diff --git a/embassy-usb/src/class/dfu/app_mode.rs b/embassy-usb/src/class/dfu/app_mode.rs new file mode 100644 index 000000000..9ca272f0b --- /dev/null +++ b/embassy-usb/src/class/dfu/app_mode.rs @@ -0,0 +1,105 @@ +use embassy_usb_driver::Driver; + +use crate::{ + control::{InResponse, OutResponse, Recipient, Request as ControlRequest, RequestType}, + Builder, +}; + +use super::consts::{ + DfuAttributes, Request, State, Status, APPN_SPEC_SUBCLASS_DFU, DESC_DFU_FUNCTIONAL, DFU_PROTOCOL_RT, + USB_CLASS_APPN_SPEC, +}; + +pub trait Handler { + fn detach(&mut self) {} + fn reset(&mut self) {} +} + +/// Internal state for the DFU class +pub struct DfuState { + handler: H, + state: State, + attrs: DfuAttributes, +} + +impl DfuState { + /// Create a new DFU instance to expose a DFU interface. + pub fn new(handler: H, attrs: DfuAttributes) -> Self { + DfuState { + handler, + state: State::AppIdle, + attrs, + } + } +} + +impl crate::Handler for DfuState { + fn reset(&mut self) { + self.handler.reset() + } + + fn control_out(&mut self, req: ControlRequest, _: &[u8]) -> Option { + if (req.request_type, req.recipient) != (RequestType::Class, Recipient::Interface) { + return None; + } + + trace!("Received out request {}", req); + + match Request::try_from(req.request) { + Ok(Request::Detach) => { + trace!("Received DETACH"); + self.state = State::AppDetach; + self.handler.detach(); + Some(OutResponse::Accepted) + } + _ => None, + } + } + + fn control_in<'a>(&'a mut self, req: ControlRequest, buf: &'a mut [u8]) -> Option> { + if (req.request_type, req.recipient) != (RequestType::Class, Recipient::Interface) { + return None; + } + + trace!("Received in 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>, H: Handler>(builder: &mut Builder<'d, D>, state: &'d mut DfuState) { + 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_ms = 1000; + alt.descriptor( + DESC_DFU_FUNCTIONAL, + &[ + state.attrs.bits(), + (timeout_ms & 0xff) as u8, + ((timeout_ms >> 8) & 0xff) as u8, + 0x40, + 0x00, // 64B control buffer size for application side + 0x10, + 0x01, // DFU 1.1 + ], + ); + + drop(func); + builder.handler(state); +} diff --git a/embassy-usb/src/class/dfu/consts.rs b/embassy-usb/src/class/dfu/consts.rs new file mode 100644 index 000000000..858d75883 --- /dev/null +++ b/embassy-usb/src/class/dfu/consts.rs @@ -0,0 +1,101 @@ +//! USB DFU constants. +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! { + /// 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; + } +} + +#[derive(Copy, Clone, PartialEq, Eq)] +#[repr(u8)] +#[allow(unused)] +pub(crate) 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(crate) 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/src/class/dfu/dfu_mode.rs b/embassy-usb/src/class/dfu/dfu_mode.rs new file mode 100644 index 000000000..9fdf67093 --- /dev/null +++ b/embassy-usb/src/class/dfu/dfu_mode.rs @@ -0,0 +1,176 @@ +use embassy_usb_driver::Driver; + +use crate::{ + control::{InResponse, OutResponse, Recipient, Request as ControlRequest, RequestType}, + Builder, +}; + +use super::consts::{ + DfuAttributes, Request, State, Status, APPN_SPEC_SUBCLASS_DFU, DESC_DFU_FUNCTIONAL, DFU_PROTOCOL_DFU, + DFU_PROTOCOL_RT, USB_CLASS_APPN_SPEC, +}; + +pub trait Handler { + fn start(&mut self); + fn write(&mut self, data: &[u8]) -> Result<(), Status>; + fn finish(&mut self) -> Result<(), Status>; + fn reset(&mut self); +} + +/// Internal state for USB DFU +pub struct DfuState { + handler: H, + attrs: DfuAttributes, + state: State, + status: Status, + next_block_num: usize, +} + +impl<'d, H: Handler> DfuState { + /// Create a new DFU instance to handle DFU transfers. + pub fn new(handler: H, attrs: DfuAttributes) -> Self { + Self { + handler, + attrs, + state: State::DfuIdle, + status: Status::Ok, + next_block_num: 0, + } + } + + fn reset_state(&mut self) { + self.next_block_num = 0; + self.state = State::DfuIdle; + self.status = Status::Ok; + } +} + +impl crate::Handler for DfuState { + fn reset(&mut self) { + self.handler.reset(); + } + + fn control_out(&mut self, req: ControlRequest, 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 as usize != self.next_block_num { + error!("expected next block num {}, got {}", self.next_block_num, req.value); + self.state = State::Error; + self.status = Status::ErrUnknown; + return Some(OutResponse::Rejected); + } + + if req.value == 0 { + self.state = State::Download; + self.handler.start(); + } + + if req.length == 0 { + match self.handler.finish() { + Ok(_) => { + self.status = Status::Ok; + self.state = State::ManifestSync; + } + Err(e) => { + self.state = State::Error; + self.status = e; + } + } + } 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.handler.write(data) { + Ok(_) => { + self.status = Status::Ok; + self.state = State::DlSync; + self.next_block_num += 1; + } + Err(e) => { + self.state = State::Error; + self.status = e; + } + } + } + + 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: ControlRequest, 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 => self.state = State::DfuIdle, + _ => {} + } + + 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>, H: Handler>( + builder: &mut Builder<'d, D>, + state: &'d mut DfuState, + max_write_size: usize, +) { + 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, + &[ + state.attrs.bits(), + 0xc4, + 0x09, // 2500ms timeout, doesn't affect operation as DETACH not necessary in bootloader code + (max_write_size & 0xff) as u8, + ((max_write_size & 0xff00) >> 8) as u8, + 0x10, + 0x01, // DFU 1.1 + ], + ); + + drop(func); + builder.handler(state); +} diff --git a/embassy-usb/src/class/dfu/mod.rs b/embassy-usb/src/class/dfu/mod.rs new file mode 100644 index 000000000..f4655c8e8 --- /dev/null +++ b/embassy-usb/src/class/dfu/mod.rs @@ -0,0 +1,4 @@ +pub mod consts; + +pub mod app_mode; +pub mod dfu_mode; diff --git a/embassy-usb/src/class/mod.rs b/embassy-usb/src/class/mod.rs index 452eedf17..4d792a786 100644 --- a/embassy-usb/src/class/mod.rs +++ b/embassy-usb/src/class/mod.rs @@ -1,5 +1,6 @@ //! Implementations of well-known USB classes. pub mod cdc_acm; pub mod cdc_ncm; +pub mod dfu; pub mod hid; pub mod midi;