mirror of
https://github.com/embassy-rs/embassy.git
synced 2024-11-21 22:32:29 +00:00
wip: add standalone USB DFU implementation.
This commit is contained in:
parent
1e16661e0a
commit
d51b191c9e
@ -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 }
|
||||
|
105
embassy-usb/src/class/dfu/app_mode.rs
Normal file
105
embassy-usb/src/class/dfu/app_mode.rs
Normal 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);
|
||||
}
|
101
embassy-usb/src/class/dfu/consts.rs
Normal file
101
embassy-usb/src/class/dfu/consts.rs
Normal 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(()),
|
||||
}
|
||||
}
|
||||
}
|
176
embassy-usb/src/class/dfu/dfu_mode.rs
Normal file
176
embassy-usb/src/class/dfu/dfu_mode.rs
Normal 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);
|
||||
}
|
4
embassy-usb/src/class/dfu/mod.rs
Normal file
4
embassy-usb/src/class/dfu/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub mod consts;
|
||||
|
||||
pub mod app_mode;
|
||||
pub mod dfu_mode;
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user