wip: add standalone USB DFU implementation.

This commit is contained in:
Dario Nieuwenhuis 2024-01-05 23:53:06 +01:00
parent 1e16661e0a
commit d51b191c9e
6 changed files with 388 additions and 0 deletions

View File

@ -54,6 +54,7 @@ embassy-net-driver-channel = { version = "0.2.0", path = "../embassy-net-driver-
defmt = { version = "0.3", optional = true } defmt = { version = "0.3", optional = true }
log = { version = "0.4.14", optional = true } log = { version = "0.4.14", optional = true }
heapless = "0.8" heapless = "0.8"
bitflags = "2.4.1"
# for HID # for HID
usbd-hid = { version = "0.6.0", optional = true } usbd-hid = { version = "0.6.0", optional = true }

View File

@ -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<H: Handler> {
handler: H,
state: State,
attrs: DfuAttributes,
}
impl<H: Handler> DfuState<H> {
/// 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<H: Handler> crate::Handler for DfuState<H> {
fn reset(&mut self) {
self.handler.reset()
}
fn control_out(&mut self, req: ControlRequest, _: &[u8]) -> Option<OutResponse> {
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<InResponse<'a>> {
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<H>) {
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);
}

View File

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

View File

@ -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<H: Handler> {
handler: H,
attrs: DfuAttributes,
state: State,
status: Status,
next_block_num: usize,
}
impl<'d, H: Handler> DfuState<H> {
/// 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<H: Handler> crate::Handler for DfuState<H> {
fn reset(&mut self) {
self.handler.reset();
}
fn control_out(&mut self, req: ControlRequest, data: &[u8]) -> Option<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 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<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 => 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<H>,
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);
}

View File

@ -0,0 +1,4 @@
pub mod consts;
pub mod app_mode;
pub mod dfu_mode;

View File

@ -1,5 +1,6 @@
//! Implementations of well-known USB classes. //! Implementations of well-known USB classes.
pub mod cdc_acm; pub mod cdc_acm;
pub mod cdc_ncm; pub mod cdc_ncm;
pub mod dfu;
pub mod hid; pub mod hid;
pub mod midi; pub mod midi;