mirror of
https://github.com/embassy-rs/embassy.git
synced 2024-11-25 08:12:30 +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 }
|
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 }
|
||||||
|
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.
|
//! 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;
|
||||||
|
Loading…
Reference in New Issue
Block a user