mirror of
https://github.com/embassy-rs/embassy.git
synced 2026-03-14 02:18:19 +00:00
Merge pull request #5216 from embassy-rs/usb-dfu-2
usb: add standalone dfu class implementation, use it in embassy-usb-dfu.
This commit is contained in:
commit
8abb6ab381
@ -1,160 +1,4 @@
|
||||
//! Application part of DFU logic
|
||||
use embassy_boot::BlockingFirmwareState;
|
||||
use embassy_time::{Duration, Instant};
|
||||
use embassy_usb::control::{InResponse, OutResponse, Recipient, RequestType};
|
||||
use embassy_usb::driver::Driver;
|
||||
use embassy_usb::{Builder, FunctionBuilder, Handler};
|
||||
use embedded_storage::nor_flash::NorFlash;
|
||||
|
||||
use crate::Reset;
|
||||
use crate::consts::{
|
||||
APPN_SPEC_SUBCLASS_DFU, DESC_DFU_FUNCTIONAL, DFU_PROTOCOL_RT, DfuAttributes, Request, State, Status,
|
||||
USB_CLASS_APPN_SPEC,
|
||||
};
|
||||
|
||||
/// Generic interface for a system that can signal to the bootloader that USB DFU mode is needed on the next boot.
|
||||
///
|
||||
/// By default this trait is implemented for `BlockingFirmwareState<'d, STATE>` but you could also implement this generic
|
||||
/// interface yourself instead in more complex situations. This could for instance be when you cannot hand ownership of a
|
||||
/// `BlockingFirmwareState` instance over directly to the DFU `Control` instance and need to use a more complex mechanism.
|
||||
pub trait DfuMarker {
|
||||
/// Signal to the bootloader that DFU mode should be used on the next boot.
|
||||
fn mark_dfu(&mut self);
|
||||
}
|
||||
|
||||
impl<'d, STATE: NorFlash> DfuMarker for BlockingFirmwareState<'d, STATE> {
|
||||
fn mark_dfu(&mut self) {
|
||||
self.mark_dfu().expect("Failed to mark DFU mode in bootloader")
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal state for the DFU class
|
||||
pub struct Control<MARK: DfuMarker, RST: Reset> {
|
||||
dfu_marker: MARK,
|
||||
attrs: DfuAttributes,
|
||||
state: State,
|
||||
timeout: Option<Duration>,
|
||||
detach_start: Option<Instant>,
|
||||
reset: RST,
|
||||
}
|
||||
|
||||
impl<MARK: DfuMarker, RST: Reset> Control<MARK, RST> {
|
||||
/// Create a new DFU instance to expose a DFU interface.
|
||||
pub fn new(dfu_marker: MARK, attrs: DfuAttributes, reset: RST) -> Self {
|
||||
Control {
|
||||
dfu_marker,
|
||||
attrs,
|
||||
state: State::AppIdle,
|
||||
detach_start: None,
|
||||
timeout: None,
|
||||
reset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<MARK: DfuMarker, RST: Reset> Handler for Control<MARK, RST> {
|
||||
fn reset(&mut self) {
|
||||
if let Some(start) = self.detach_start {
|
||||
let delta = Instant::now() - start;
|
||||
let timeout = self.timeout.unwrap();
|
||||
trace!(
|
||||
"Received RESET with delta = {}, timeout = {}",
|
||||
delta.as_millis(),
|
||||
timeout.as_millis()
|
||||
);
|
||||
if delta < timeout {
|
||||
self.dfu_marker.mark_dfu();
|
||||
self.reset.sys_reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn control_out(
|
||||
&mut self,
|
||||
req: embassy_usb::control::Request,
|
||||
_: &[u8],
|
||||
) -> Option<embassy_usb::control::OutResponse> {
|
||||
if (req.request_type, req.recipient) != (RequestType::Class, Recipient::Interface) {
|
||||
return None;
|
||||
}
|
||||
|
||||
trace!("Received request {}", req);
|
||||
|
||||
match Request::try_from(req.request) {
|
||||
Ok(Request::Detach) => {
|
||||
self.detach_start = Some(Instant::now());
|
||||
self.timeout = Some(Duration::from_millis(req.value as u64));
|
||||
self.state = State::AppDetach;
|
||||
if self.attrs.contains(DfuAttributes::WILL_DETACH) {
|
||||
trace!("Received DETACH, performing reset");
|
||||
self.reset();
|
||||
} else {
|
||||
trace!("Received DETACH, awaiting USB reset");
|
||||
}
|
||||
Some(OutResponse::Accepted)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn control_in<'a>(
|
||||
&'a mut self,
|
||||
req: embassy_usb::control::Request,
|
||||
buf: &'a mut [u8],
|
||||
) -> Option<embassy_usb::control::InResponse<'a>> {
|
||||
if (req.request_type, req.recipient) != (RequestType::Class, Recipient::Interface) {
|
||||
return None;
|
||||
}
|
||||
|
||||
trace!("Received 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>, MARK: DfuMarker, RST: Reset>(
|
||||
builder: &mut Builder<'d, D>,
|
||||
handler: &'d mut Control<MARK, RST>,
|
||||
timeout: Duration,
|
||||
func_modifier: impl Fn(&mut FunctionBuilder<'_, 'd, D>),
|
||||
) {
|
||||
let mut func = builder.function(0x00, 0x00, 0x00);
|
||||
|
||||
// Here we give users the opportunity to add their own function level MSOS headers for instance.
|
||||
// This is useful when DFU functionality is part of a composite USB device.
|
||||
func_modifier(&mut func);
|
||||
|
||||
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 = timeout.as_millis() as u16;
|
||||
alt.descriptor(
|
||||
DESC_DFU_FUNCTIONAL,
|
||||
&[
|
||||
handler.attrs.bits(),
|
||||
(timeout & 0xff) as u8,
|
||||
((timeout >> 8) & 0xff) as u8,
|
||||
0x40,
|
||||
0x00, // 64B control buffer size for application side
|
||||
0x10,
|
||||
0x01, // DFU 1.1
|
||||
],
|
||||
);
|
||||
|
||||
drop(func);
|
||||
builder.handler(handler);
|
||||
}
|
||||
pub use embassy_usb::class::dfu::app_mode::{DfuState, Handler, usb_dfu};
|
||||
pub use embassy_usb::class::dfu::consts::DfuAttributes;
|
||||
|
||||
@ -1,144 +0,0 @@
|
||||
//! USB DFU constants, taken from
|
||||
//! https://www.usb.org/sites/default/files/DFU_1.1.pdf
|
||||
/// Device Firmware Upgrade class, App specific
|
||||
pub(crate) const USB_CLASS_APPN_SPEC: u8 = 0xFE;
|
||||
/// Device Firmware Upgrade subclass, App specific
|
||||
pub(crate) const APPN_SPEC_SUBCLASS_DFU: u8 = 0x01;
|
||||
#[allow(unused)]
|
||||
/// USB interface alternative setting
|
||||
pub(crate) const DFU_PROTOCOL_DFU: u8 = 0x02;
|
||||
#[allow(unused)]
|
||||
/// DFU runtime class
|
||||
pub(crate) const DFU_PROTOCOL_RT: u8 = 0x01;
|
||||
/// DFU functional descriptor
|
||||
pub(crate) const DESC_DFU_FUNCTIONAL: u8 = 0x21;
|
||||
|
||||
macro_rules! define_dfu_attributes {
|
||||
($macro:path) => {
|
||||
$macro! {
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "defmt")]
|
||||
define_dfu_attributes!(defmt::bitflags);
|
||||
|
||||
#[cfg(not(feature = "defmt"))]
|
||||
define_dfu_attributes!(bitflags::bitflags);
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
#[allow(unused)]
|
||||
/// An indication of the state that the device is going to enter immediately following transmission of this response.
|
||||
pub(crate) enum State {
|
||||
/// Device is running its normal application.
|
||||
AppIdle = 0,
|
||||
/// Device is running its normal application, has received the DFU_DETACH request, and is waiting for a USB reset.
|
||||
AppDetach = 1,
|
||||
/// Device is operating in the DFU mode and is waiting for requests.
|
||||
DfuIdle = 2,
|
||||
/// Device has received a block and is waiting for the host to solicit the status via DFU_GETSTATUS.
|
||||
DlSync = 3,
|
||||
/// Device is programming a control-write block into its nonvolatile memories.
|
||||
DlBusy = 4,
|
||||
/// Device is processing a download operation. Expecting DFU_DNLOAD requests.
|
||||
Download = 5,
|
||||
/// Device has received the final block of firmware from the host, waits for DFU_GETSTATUS to start Manifestation phase or completed this phase
|
||||
ManifestSync = 6,
|
||||
/// Device is in the Manifestation phase. Not all devices will be able to respond to DFU_GETSTATUS when in this state.
|
||||
Manifest = 7,
|
||||
/// Device has programmed its memories and is waiting for a USB reset or a power on reset.
|
||||
ManifestWaitReset = 8,
|
||||
/// The device is processing an upload operation. Expecting DFU_UPLOAD requests.
|
||||
UploadIdle = 9,
|
||||
/// An error has occurred. Awaiting the DFU_CLRSTATUS request.
|
||||
Error = 10,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
#[allow(unused)]
|
||||
/// An indication of the status resulting from the execution of the most recent request.
|
||||
pub(crate) enum Status {
|
||||
/// No error
|
||||
Ok = 0x00,
|
||||
/// File is not targeted for use by this device
|
||||
ErrTarget = 0x01,
|
||||
/// File is for this device but fails some vendor-specific verification test
|
||||
ErrFile = 0x02,
|
||||
/// Device is unable to write memory
|
||||
ErrWrite = 0x03,
|
||||
/// Memory erase function failed
|
||||
ErrErase = 0x04,
|
||||
/// Memory erase check failed
|
||||
ErrCheckErased = 0x05,
|
||||
/// Program memory function failed
|
||||
ErrProg = 0x06,
|
||||
/// Programmed memory failed verification
|
||||
ErrVerify = 0x07,
|
||||
/// Cannot program memory due to received address that is out of range
|
||||
ErrAddress = 0x08,
|
||||
/// Received DFU_DNLOAD with wLength = 0, but device does not think it has all of the data yet
|
||||
ErrNotDone = 0x09,
|
||||
/// Device’s firmware is corrupt. It cannot return to run-time (non-DFU) operations
|
||||
ErrFirmware = 0x0A,
|
||||
/// iString indicates a vendor-specific error
|
||||
ErrVendor = 0x0B,
|
||||
/// Device detected unexpected USB reset signaling
|
||||
ErrUsbr = 0x0C,
|
||||
/// Device detected unexpected power on reset
|
||||
ErrPor = 0x0D,
|
||||
/// Something went wrong, but the device does not know what
|
||||
ErrUnknown = 0x0E,
|
||||
/// Device stalled an unexpected request
|
||||
ErrStalledPkt = 0x0F,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
/// DFU requests
|
||||
pub(crate) enum Request {
|
||||
/// Host instructs the device to generate a detach-attach sequence
|
||||
Detach = 0,
|
||||
/// Host initiates control-write transfers with this request, and sends a DFU_DNLOAD request
|
||||
/// with wLength = 0 to indicate that it has completed transferring the firmware image file
|
||||
Dnload = 1,
|
||||
/// The DFU_UPLOAD request is employed by the host to solicit firmware from the device.
|
||||
Upload = 2,
|
||||
/// The host employs the DFU_GETSTATUS request to facilitate synchronization with the device.
|
||||
GetStatus = 3,
|
||||
/// Any time the device detects an error, it waits with transition until ClrStatus
|
||||
ClrStatus = 4,
|
||||
/// Requests a report about a state of the device
|
||||
GetState = 5,
|
||||
/// Enables the host to exit from certain states and return to the DFU_IDLE state
|
||||
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(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,22 +1,21 @@
|
||||
//! DFU bootloader part of DFU logic
|
||||
use embassy_boot::{AlignedBuffer, BlockingFirmwareUpdater, FirmwareUpdaterError};
|
||||
use embassy_usb::control::{InResponse, OutResponse, Recipient, RequestType};
|
||||
use embassy_usb::class::dfu::consts::{DfuAttributes, Status};
|
||||
/// Re-export DfuState from embassy-usb for convenience.
|
||||
pub use embassy_usb::class::dfu::dfu_mode::DfuState as UsbDfuState;
|
||||
use embassy_usb::class::dfu::dfu_mode::{self, DfuState};
|
||||
use embassy_usb::driver::Driver;
|
||||
use embassy_usb::{Builder, FunctionBuilder, Handler};
|
||||
use embassy_usb::{Builder, FunctionBuilder};
|
||||
use embedded_storage::nor_flash::{NorFlash, NorFlashErrorKind};
|
||||
|
||||
use crate::Reset;
|
||||
use crate::consts::{
|
||||
APPN_SPEC_SUBCLASS_DFU, DESC_DFU_FUNCTIONAL, DFU_PROTOCOL_DFU, DfuAttributes, Request, State, Status,
|
||||
USB_CLASS_APPN_SPEC,
|
||||
};
|
||||
|
||||
/// Internal state for USB DFU
|
||||
pub struct Control<'d, DFU: NorFlash, STATE: NorFlash, RST: Reset, const BLOCK_SIZE: usize> {
|
||||
/// Internal handler for USB DFU firmware updates.
|
||||
///
|
||||
/// This implements the `embassy_usb::class::dfu::dfu_mode::Handler` trait,
|
||||
/// providing the firmware write logic using `BlockingFirmwareUpdater`.
|
||||
pub struct FirmwareHandler<'d, DFU: NorFlash, STATE: NorFlash, RST: Reset, const BLOCK_SIZE: usize> {
|
||||
updater: BlockingFirmwareUpdater<'d, DFU, STATE>,
|
||||
attrs: DfuAttributes,
|
||||
state: State,
|
||||
status: Status,
|
||||
offset: usize,
|
||||
buf: AlignedBuffer<BLOCK_SIZE>,
|
||||
reset: RST,
|
||||
@ -25,19 +24,17 @@ pub struct Control<'d, DFU: NorFlash, STATE: NorFlash, RST: Reset, const BLOCK_S
|
||||
public_key: &'static [u8; 32],
|
||||
}
|
||||
|
||||
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.
|
||||
impl<'d, DFU: NorFlash, STATE: NorFlash, RST: Reset, const BLOCK_SIZE: usize>
|
||||
FirmwareHandler<'d, DFU, STATE, RST, BLOCK_SIZE>
|
||||
{
|
||||
/// Create a new firmware handler.
|
||||
pub fn new(
|
||||
updater: BlockingFirmwareUpdater<'d, DFU, STATE>,
|
||||
attrs: DfuAttributes,
|
||||
reset: RST,
|
||||
#[cfg(feature = "_verify")] public_key: &'static [u8; 32],
|
||||
) -> Self {
|
||||
Self {
|
||||
updater,
|
||||
attrs,
|
||||
state: State::DfuIdle,
|
||||
status: Status::Ok,
|
||||
offset: 0,
|
||||
buf: AlignedBuffer([0; BLOCK_SIZE]),
|
||||
reset,
|
||||
@ -46,170 +43,109 @@ impl<'d, DFU: NorFlash, STATE: NorFlash, RST: Reset, const BLOCK_SIZE: usize> Co
|
||||
public_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_state(&mut self) {
|
||||
self.offset = 0;
|
||||
self.state = State::DfuIdle;
|
||||
self.status = Status::Ok;
|
||||
fn firmware_error_to_status(e: FirmwareUpdaterError) -> Status {
|
||||
match e {
|
||||
FirmwareUpdaterError::Flash(e) => match e {
|
||||
NorFlashErrorKind::NotAligned => Status::ErrWrite,
|
||||
NorFlashErrorKind::OutOfBounds => Status::ErrAddress,
|
||||
_ => Status::ErrUnknown,
|
||||
},
|
||||
FirmwareUpdaterError::Signature(_) => Status::ErrVerify,
|
||||
FirmwareUpdaterError::BadState => Status::ErrUnknown,
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FirmwareUpdaterError> for Status {
|
||||
fn from(e: FirmwareUpdaterError) -> Self {
|
||||
match e {
|
||||
FirmwareUpdaterError::Flash(e) => match e {
|
||||
NorFlashErrorKind::NotAligned => Status::ErrWrite,
|
||||
NorFlashErrorKind::OutOfBounds => Status::ErrAddress,
|
||||
_ => Status::ErrUnknown,
|
||||
},
|
||||
FirmwareUpdaterError::Signature(_) => Status::ErrVerify,
|
||||
FirmwareUpdaterError::BadState => Status::ErrUnknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'d, DFU: NorFlash, STATE: NorFlash, RST: Reset, const BLOCK_SIZE: usize> Handler
|
||||
for Control<'d, DFU, STATE, RST, BLOCK_SIZE>
|
||||
impl<'d, DFU: NorFlash, STATE: NorFlash, RST: Reset, const BLOCK_SIZE: usize> dfu_mode::Handler
|
||||
for FirmwareHandler<'d, DFU, STATE, RST, BLOCK_SIZE>
|
||||
{
|
||||
fn control_out(
|
||||
&mut self,
|
||||
req: embassy_usb::control::Request,
|
||||
data: &[u8],
|
||||
) -> Option<embassy_usb::control::OutResponse> {
|
||||
if (req.request_type, req.recipient) != (RequestType::Class, Recipient::Interface) {
|
||||
debug!("Unknown out request: {:?}", req);
|
||||
return None;
|
||||
fn start(&mut self) {
|
||||
info!("Download starting");
|
||||
self.offset = 0;
|
||||
}
|
||||
|
||||
fn write(&mut self, data: &[u8]) -> Result<(), Status> {
|
||||
if data.len() > BLOCK_SIZE {
|
||||
error!("USB data len exceeded block size");
|
||||
return Err(Status::ErrUnknown);
|
||||
}
|
||||
match Request::try_from(req.request) {
|
||||
Ok(Request::Abort) => {
|
||||
info!("Abort requested");
|
||||
self.reset_state();
|
||||
Some(OutResponse::Accepted)
|
||||
|
||||
debug!("Copying {} bytes to buffer", data.len());
|
||||
self.buf.as_mut()[..data.len()].copy_from_slice(data);
|
||||
|
||||
debug!("Writing {} bytes at {}", data.len(), self.offset);
|
||||
match self.updater.write_firmware(self.offset, self.buf.as_ref()) {
|
||||
Ok(_) => {
|
||||
self.offset += data.len();
|
||||
Ok(())
|
||||
}
|
||||
Ok(Request::Dnload) if self.attrs.contains(DfuAttributes::CAN_DOWNLOAD) => {
|
||||
if req.value == 0 {
|
||||
info!("Download starting");
|
||||
self.state = State::Download;
|
||||
self.offset = 0;
|
||||
}
|
||||
|
||||
if self.state != State::Download {
|
||||
error!("Unexpected DNLOAD while chip is waiting for a GETSTATUS");
|
||||
self.status = Status::ErrUnknown;
|
||||
self.state = State::Error;
|
||||
return Some(OutResponse::Rejected);
|
||||
}
|
||||
|
||||
if data.len() > BLOCK_SIZE {
|
||||
error!("USB data len exceeded block size");
|
||||
self.status = Status::ErrUnknown;
|
||||
self.state = State::Error;
|
||||
return Some(OutResponse::Rejected);
|
||||
}
|
||||
|
||||
debug!("Copying {} bytes to buffer", data.len());
|
||||
self.buf.as_mut()[..data.len()].copy_from_slice(data);
|
||||
|
||||
let final_transfer = req.length == 0;
|
||||
if final_transfer {
|
||||
debug!("Receiving final transfer");
|
||||
|
||||
#[cfg(feature = "_verify")]
|
||||
let update_res: Result<(), FirmwareUpdaterError> = {
|
||||
const SIGNATURE_LEN: usize = 64;
|
||||
|
||||
let mut signature = [0; SIGNATURE_LEN];
|
||||
let update_len = (self.offset - SIGNATURE_LEN) as u32;
|
||||
|
||||
self.updater.read_dfu(update_len, &mut signature).and_then(|_| {
|
||||
self.updater
|
||||
.verify_and_mark_updated(self.public_key, &signature, update_len)
|
||||
})
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "_verify"))]
|
||||
let update_res = self.updater.mark_updated();
|
||||
|
||||
match update_res {
|
||||
Ok(_) => {
|
||||
self.status = Status::Ok;
|
||||
self.state = State::ManifestSync;
|
||||
info!("Update complete");
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error completing update: {}", e);
|
||||
self.state = State::Error;
|
||||
self.status = e.into();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug!("Writing {} bytes at {}", data.len(), self.offset);
|
||||
match self.updater.write_firmware(self.offset, self.buf.as_ref()) {
|
||||
Ok(_) => {
|
||||
self.status = Status::Ok;
|
||||
self.state = State::DlSync;
|
||||
self.offset += data.len();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error writing firmware: {:?}", e);
|
||||
self.state = State::Error;
|
||||
self.status = e.into();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(OutResponse::Accepted)
|
||||
Err(e) => {
|
||||
error!("Error writing firmware: {:?}", e);
|
||||
Err(firmware_error_to_status(e))
|
||||
}
|
||||
Ok(Request::Detach) => Some(OutResponse::Accepted), // Device is already in DFU mode
|
||||
Ok(Request::ClrStatus) => {
|
||||
info!("Clear status requested");
|
||||
self.reset_state();
|
||||
Some(OutResponse::Accepted)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn control_in<'a>(
|
||||
&'a mut self,
|
||||
req: embassy_usb::control::Request,
|
||||
buf: &'a mut [u8],
|
||||
) -> Option<embassy_usb::control::InResponse<'a>> {
|
||||
if (req.request_type, req.recipient) != (RequestType::Class, Recipient::Interface) {
|
||||
debug!("Unknown in request: {:?}", req);
|
||||
return None;
|
||||
}
|
||||
match Request::try_from(req.request) {
|
||||
Ok(Request::GetStatus) => {
|
||||
match self.state {
|
||||
State::DlSync => self.state = State::Download,
|
||||
State::ManifestSync => self.state = State::ManifestWaitReset,
|
||||
_ => {}
|
||||
}
|
||||
fn finish(&mut self) -> Result<(), Status> {
|
||||
debug!("Receiving final transfer");
|
||||
|
||||
//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]);
|
||||
Some(InResponse::Accepted(&buf[0..6]))
|
||||
#[cfg(feature = "_verify")]
|
||||
let update_res: Result<(), FirmwareUpdaterError> = {
|
||||
const SIGNATURE_LEN: usize = 64;
|
||||
|
||||
let mut signature = [0; SIGNATURE_LEN];
|
||||
let update_len = (self.offset - SIGNATURE_LEN) as u32;
|
||||
|
||||
self.updater.read_dfu(update_len, &mut signature).and_then(|_| {
|
||||
self.updater
|
||||
.verify_and_mark_updated(self.public_key, &signature, update_len)
|
||||
})
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "_verify"))]
|
||||
let update_res = self.updater.mark_updated();
|
||||
|
||||
match update_res {
|
||||
Ok(_) => {
|
||||
info!("Update complete");
|
||||
Ok(())
|
||||
}
|
||||
Ok(Request::GetState) => {
|
||||
buf[0] = self.state as u8;
|
||||
Some(InResponse::Accepted(&buf[0..1]))
|
||||
Err(e) => {
|
||||
error!("Error completing update: {}", e);
|
||||
Err(firmware_error_to_status(e))
|
||||
}
|
||||
Ok(Request::Upload) if self.attrs.contains(DfuAttributes::CAN_UPLOAD) => {
|
||||
//TODO: FirmwareUpdater provides a way of reading the active partition so we could in theory add functionality to upload firmware.
|
||||
Some(InResponse::Rejected)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
if matches!(self.state, State::ManifestSync | State::ManifestWaitReset) {
|
||||
self.reset.sys_reset()
|
||||
}
|
||||
fn system_reset(&mut self) {
|
||||
self.reset.sys_reset()
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience type alias for the DFU state with firmware handler.
|
||||
pub type State<'d, DFU, STATE, RST, const BLOCK_SIZE: usize> =
|
||||
DfuState<FirmwareHandler<'d, DFU, STATE, RST, BLOCK_SIZE>>;
|
||||
|
||||
/// Create a new DFU state instance.
|
||||
///
|
||||
/// This creates a `DfuState` with a `FirmwareHandler` inside, ready to be
|
||||
/// used with `usb_dfu`.
|
||||
pub fn new_state<'d, DFU: NorFlash, STATE: NorFlash, RST: Reset, const BLOCK_SIZE: usize>(
|
||||
updater: BlockingFirmwareUpdater<'d, DFU, STATE>,
|
||||
attrs: DfuAttributes,
|
||||
reset: RST,
|
||||
#[cfg(feature = "_verify")] public_key: &'static [u8; 32],
|
||||
) -> State<'d, DFU, STATE, RST, BLOCK_SIZE> {
|
||||
let handler = FirmwareHandler::new(
|
||||
updater,
|
||||
reset,
|
||||
#[cfg(feature = "_verify")]
|
||||
public_key,
|
||||
);
|
||||
DfuState::new(handler, attrs)
|
||||
}
|
||||
|
||||
/// 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
|
||||
@ -219,30 +155,8 @@ impl<'d, DFU: NorFlash, STATE: NorFlash, RST: Reset, const BLOCK_SIZE: usize> Ha
|
||||
/// 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>,
|
||||
state: &'d mut State<'d, DFU, STATE, RST, BLOCK_SIZE>,
|
||||
func_modifier: impl Fn(&mut FunctionBuilder<'_, 'd, D>),
|
||||
) {
|
||||
let mut func = builder.function(USB_CLASS_APPN_SPEC, APPN_SPEC_SUBCLASS_DFU, DFU_PROTOCOL_DFU);
|
||||
|
||||
// Here we give users the opportunity to add their own function level MSOS headers for instance.
|
||||
// This is useful when DFU functionality is part of a composite USB device.
|
||||
func_modifier(&mut func);
|
||||
|
||||
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);
|
||||
dfu_mode::usb_dfu(builder, state, BLOCK_SIZE, func_modifier);
|
||||
}
|
||||
|
||||
@ -3,7 +3,10 @@
|
||||
#![warn(missing_docs)]
|
||||
mod fmt;
|
||||
|
||||
pub mod consts;
|
||||
/// Re-export DFU constants from embassy-usb.
|
||||
pub mod consts {
|
||||
pub use embassy_usb::class::dfu::consts::*;
|
||||
}
|
||||
|
||||
#[cfg(feature = "dfu")]
|
||||
pub mod dfu;
|
||||
|
||||
@ -62,11 +62,13 @@ embassy-futures = { version = "0.1.2", path = "../embassy-futures" }
|
||||
embassy-usb-driver = { version = "0.2.0", path = "../embassy-usb-driver" }
|
||||
embassy-sync = { version = "0.7.2", path = "../embassy-sync" }
|
||||
embassy-net-driver-channel = { version = "0.3.2", path = "../embassy-net-driver-channel" }
|
||||
embassy-time = { version = "0.5.0", path = "../embassy-time" }
|
||||
|
||||
defmt = { version = "1", optional = true }
|
||||
log = { version = "0.4.14", optional = true }
|
||||
heapless = "0.8"
|
||||
embedded-io-async = { version = "0.7.0" }
|
||||
bitflags = "2.4.1"
|
||||
|
||||
# for HID
|
||||
usbd-hid = { version = "0.9.0", optional = true }
|
||||
|
||||
152
embassy-usb/src/class/dfu/app_mode.rs
Normal file
152
embassy-usb/src/class/dfu/app_mode.rs
Normal file
@ -0,0 +1,152 @@
|
||||
use embassy_time::{Duration, Instant};
|
||||
use embassy_usb_driver::Driver;
|
||||
|
||||
use super::consts::{
|
||||
APPN_SPEC_SUBCLASS_DFU, DESC_DFU_FUNCTIONAL, DFU_PROTOCOL_RT, DfuAttributes, Request, State, Status,
|
||||
USB_CLASS_APPN_SPEC,
|
||||
};
|
||||
use crate::control::{InResponse, OutResponse, Recipient, Request as ControlRequest, RequestType};
|
||||
use crate::{Builder, FunctionBuilder};
|
||||
|
||||
/// Handler trait for DFU runtime mode.
|
||||
///
|
||||
/// Implement this trait to handle entering DFU mode.
|
||||
pub trait Handler {
|
||||
/// Called when the device should enter DFU mode.
|
||||
///
|
||||
/// This is called after a valid detach sequence (detach request followed by
|
||||
/// USB reset within the timeout period). The implementation should mark the
|
||||
/// device for DFU mode and perform a system reset.
|
||||
fn enter_dfu(&mut self);
|
||||
}
|
||||
|
||||
/// Internal state for the DFU class
|
||||
pub struct DfuState<H: Handler> {
|
||||
handler: H,
|
||||
state: State,
|
||||
attrs: DfuAttributes,
|
||||
detach_start: Option<Instant>,
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
impl<H: Handler> DfuState<H> {
|
||||
/// Create a new DFU instance to expose a DFU interface.
|
||||
pub fn new(handler: H, attrs: DfuAttributes, timeout: Duration) -> Self {
|
||||
DfuState {
|
||||
handler,
|
||||
state: State::AppIdle,
|
||||
attrs,
|
||||
detach_start: None,
|
||||
timeout,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the handler.
|
||||
pub fn handler_mut(&mut self) -> &mut H {
|
||||
&mut self.handler
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: Handler> crate::Handler for DfuState<H> {
|
||||
fn reset(&mut self) {
|
||||
if let Some(start) = self.detach_start {
|
||||
let delta = Instant::now() - start;
|
||||
trace!(
|
||||
"Received RESET with delta = {}, timeout = {}",
|
||||
delta.as_millis(),
|
||||
self.timeout.as_millis()
|
||||
);
|
||||
if delta < self.timeout {
|
||||
self.handler.enter_dfu();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.detach_start = Some(Instant::now());
|
||||
if self.attrs.contains(DfuAttributes::WILL_DETACH) {
|
||||
trace!("WILL_DETACH set, performing reset");
|
||||
self.handler.enter_dfu();
|
||||
} else {
|
||||
trace!("Awaiting USB reset");
|
||||
}
|
||||
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) => {
|
||||
let timeout_ms = self.timeout.as_millis() as u16;
|
||||
buf[0..6].copy_from_slice(&[
|
||||
Status::Ok as u8,
|
||||
(timeout_ms & 0xff) as u8,
|
||||
((timeout_ms >> 8) & 0xff) as u8,
|
||||
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>,
|
||||
func_modifier: impl Fn(&mut FunctionBuilder<'_, 'd, D>),
|
||||
) {
|
||||
let mut func = builder.function(0x00, 0x00, 0x00);
|
||||
|
||||
// Here we give users the opportunity to add their own function level MSOS headers for instance.
|
||||
// This is useful when DFU functionality is part of a composite USB device.
|
||||
func_modifier(&mut func);
|
||||
|
||||
let timeout_ms = state.timeout.as_millis() as u16;
|
||||
let mut iface = func.interface();
|
||||
let mut alt = iface.alt_setting(USB_CLASS_APPN_SPEC, APPN_SPEC_SUBCLASS_DFU, DFU_PROTOCOL_RT, None);
|
||||
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);
|
||||
}
|
||||
124
embassy-usb/src/class/dfu/consts.rs
Normal file
124
embassy-usb/src/class/dfu/consts.rs
Normal file
@ -0,0 +1,124 @@
|
||||
//! USB DFU constants and types.
|
||||
|
||||
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! {
|
||||
/// Attributes supported by the DFU controller.
|
||||
pub struct DfuAttributes: u8 {
|
||||
/// Generate WillDetach 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;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "defmt"))]
|
||||
bitflags::bitflags! {
|
||||
/// Attributes supported by the DFU controller.
|
||||
pub struct DfuAttributes: u8 {
|
||||
/// Generate WillDetach 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,
|
||||
}
|
||||
|
||||
/// DFU status codes indicating the result of the most recent request.
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
#[allow(unused)]
|
||||
pub enum Status {
|
||||
/// No error.
|
||||
Ok = 0x00,
|
||||
/// File is not targeted for use by this device.
|
||||
ErrTarget = 0x01,
|
||||
/// File is for this device but fails some vendor-specific verification test.
|
||||
ErrFile = 0x02,
|
||||
/// Device is unable to write memory.
|
||||
ErrWrite = 0x03,
|
||||
/// Memory erase function failed.
|
||||
ErrErase = 0x04,
|
||||
/// Memory erase check failed.
|
||||
ErrCheckErased = 0x05,
|
||||
/// Program memory function failed.
|
||||
ErrProg = 0x06,
|
||||
/// Programmed memory failed verification.
|
||||
ErrVerify = 0x07,
|
||||
/// Cannot program memory due to received address that is out of range.
|
||||
ErrAddress = 0x08,
|
||||
/// Received DFU_DNLOAD with wLength = 0, but device does not think it has all of the data yet.
|
||||
ErrNotDone = 0x09,
|
||||
/// Device's firmware is corrupt. It cannot return to run-time (non-DFU) operations.
|
||||
ErrFirmware = 0x0A,
|
||||
/// iString indicates a vendor-specific error.
|
||||
ErrVendor = 0x0B,
|
||||
/// Device detected unexpected USB reset signaling.
|
||||
ErrUsbr = 0x0C,
|
||||
/// Device detected unexpected power on reset.
|
||||
ErrPor = 0x0D,
|
||||
/// Something went wrong, but the device does not know what.
|
||||
ErrUnknown = 0x0E,
|
||||
/// Device stalled an unexpected request.
|
||||
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(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
212
embassy-usb/src/class/dfu/dfu_mode.rs
Normal file
212
embassy-usb/src/class/dfu/dfu_mode.rs
Normal file
@ -0,0 +1,212 @@
|
||||
use embassy_usb_driver::Driver;
|
||||
|
||||
use super::consts::{
|
||||
APPN_SPEC_SUBCLASS_DFU, DESC_DFU_FUNCTIONAL, DFU_PROTOCOL_DFU, DfuAttributes, Request, State, Status,
|
||||
USB_CLASS_APPN_SPEC,
|
||||
};
|
||||
use crate::control::{InResponse, OutResponse, Recipient, Request as ControlRequest, RequestType};
|
||||
use crate::{Builder, FunctionBuilder};
|
||||
|
||||
/// Handler trait for DFU bootloader mode.
|
||||
///
|
||||
/// Implement this trait to handle firmware download operations.
|
||||
pub trait Handler {
|
||||
/// Called when a firmware download starts.
|
||||
///
|
||||
/// This is called when the first DFU_DNLOAD request is received.
|
||||
fn start(&mut self);
|
||||
|
||||
/// Called to write a chunk of firmware data.
|
||||
///
|
||||
/// Returns `Ok(())` on success, or a `Status` error on failure.
|
||||
fn write(&mut self, data: &[u8]) -> Result<(), Status>;
|
||||
|
||||
/// Called when the firmware download is complete.
|
||||
///
|
||||
/// This is called when a zero-length DFU_DNLOAD is received, indicating
|
||||
/// the end of the firmware data. This is where you would typically
|
||||
/// mark the firmware as ready to boot.
|
||||
///
|
||||
/// Returns `Ok(())` on success, or a `Status` error on failure.
|
||||
fn finish(&mut self) -> Result<(), Status>;
|
||||
|
||||
/// Called at the end of the DFU procedure.
|
||||
///
|
||||
/// This is typically where you would perform a system reset to boot
|
||||
/// the new firmware after a successful download.
|
||||
fn system_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) {
|
||||
if matches!(self.state, State::ManifestSync | State::ManifestWaitReset) {
|
||||
self.handler.system_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) => {
|
||||
info!("Abort requested");
|
||||
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 {
|
||||
error!("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) => {
|
||||
info!("Clear status requested");
|
||||
self.reset_state();
|
||||
Some(OutResponse::Accepted)
|
||||
}
|
||||
_ => {
|
||||
debug!("Unknown OUT request {:?}", req);
|
||||
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::ManifestWaitReset,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
_ => {
|
||||
debug!("Unknown IN request {:?}", req);
|
||||
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,
|
||||
func_modifier: impl Fn(&mut FunctionBuilder<'_, 'd, D>),
|
||||
) {
|
||||
let mut func = builder.function(0x00, 0x00, 0x00);
|
||||
|
||||
// Here we give users the opportunity to add their own function level MSOS headers for instance.
|
||||
// This is useful when DFU functionality is part of a composite USB device.
|
||||
func_modifier(&mut func);
|
||||
|
||||
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);
|
||||
}
|
||||
12
embassy-usb/src/class/dfu/mod.rs
Normal file
12
embassy-usb/src/class/dfu/mod.rs
Normal file
@ -0,0 +1,12 @@
|
||||
//! USB Device Firmware Upgrade (DFU) class implementation.
|
||||
//!
|
||||
//! This module provides USB DFU 1.1 protocol support, split into two modes:
|
||||
//! - `app_mode`: Runtime mode for applications to support detach requests
|
||||
//! - `dfu_mode`: Bootloader mode for handling firmware downloads
|
||||
|
||||
pub mod consts;
|
||||
|
||||
/// DFU runtime mode (application side).
|
||||
pub mod app_mode;
|
||||
/// DFU bootloader mode (firmware download).
|
||||
pub mod dfu_mode;
|
||||
@ -2,6 +2,7 @@
|
||||
pub mod cdc_acm;
|
||||
pub mod cdc_ncm;
|
||||
pub mod cmsis_dap_v2;
|
||||
pub mod dfu;
|
||||
pub mod hid;
|
||||
pub mod midi;
|
||||
pub mod uac1;
|
||||
|
||||
@ -19,6 +19,7 @@ defmt = { version = "1.0.1", optional = true }
|
||||
defmt-rtt = { version = "1.0.0", optional = true }
|
||||
panic-reset = { version = "0.1.1" }
|
||||
embedded-hal = { version = "0.2.6" }
|
||||
embedded-storage = "0.3.1"
|
||||
|
||||
cortex-m = { version = "0.7.6", features = ["inline-asm", "critical-section-single-core"] }
|
||||
cortex-m-rt = "0.7.0"
|
||||
|
||||
@ -14,8 +14,7 @@ use embassy_stm32::{bind_interrupts, peripherals};
|
||||
use embassy_sync::blocking_mutex::Mutex;
|
||||
use embassy_time::Duration;
|
||||
use embassy_usb::{Builder, msos};
|
||||
use embassy_usb_dfu::consts::DfuAttributes;
|
||||
use embassy_usb_dfu::{Control, ResetImmediate, usb_dfu};
|
||||
use embassy_usb_dfu::application::{DfuAttributes, DfuState, Handler, usb_dfu};
|
||||
use panic_reset as _;
|
||||
|
||||
bind_interrupts!(struct Irqs {
|
||||
@ -27,6 +26,17 @@ bind_interrupts!(struct Irqs {
|
||||
// N.B. update to a custom GUID for your own device!
|
||||
const DEVICE_INTERFACE_GUIDS: &[&str] = &["{EAA9A5DC-30BA-44BC-9232-606CDC875321}"];
|
||||
|
||||
struct DfuHandler<'d, FLASH: embedded_storage::nor_flash::NorFlash> {
|
||||
firmware_state: BlockingFirmwareState<'d, FLASH>,
|
||||
}
|
||||
|
||||
impl<FLASH: embedded_storage::nor_flash::NorFlash> Handler for DfuHandler<'_, FLASH> {
|
||||
fn enter_dfu(&mut self) {
|
||||
self.firmware_state.mark_dfu().expect("Failed to mark DFU mode");
|
||||
cortex_m::peripheral::SCB::sys_reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[embassy_executor::main]
|
||||
async fn main(_spawner: Spawner) {
|
||||
let mut config = embassy_stm32::Config::default();
|
||||
@ -49,7 +59,10 @@ async fn main(_spawner: Spawner) {
|
||||
let mut config_descriptor = [0; 256];
|
||||
let mut bos_descriptor = [0; 256];
|
||||
let mut control_buf = [0; 64];
|
||||
let mut state = Control::new(firmware_state, DfuAttributes::CAN_DOWNLOAD, ResetImmediate);
|
||||
|
||||
let handler = DfuHandler { firmware_state };
|
||||
let mut state = DfuState::new(handler, DfuAttributes::CAN_DOWNLOAD, Duration::from_millis(2500));
|
||||
|
||||
let mut builder = Builder::new(
|
||||
driver,
|
||||
config,
|
||||
@ -72,7 +85,7 @@ async fn main(_spawner: Spawner) {
|
||||
msos::PropertyData::RegMultiSz(DEVICE_INTERFACE_GUIDS),
|
||||
));
|
||||
|
||||
usb_dfu(&mut builder, &mut state, Duration::from_millis(2500), |func| {
|
||||
usb_dfu(&mut builder, &mut state, |func| {
|
||||
// You likely don't have to add these function level headers if your USB device is not composite
|
||||
// (i.e. if your device does not expose another interface in addition to DFU)
|
||||
func.msos_feature(msos::CompatibleIdFeatureDescriptor::new("WINUSB", ""));
|
||||
|
||||
@ -19,6 +19,7 @@ defmt = { version = "1.0.1", optional = true }
|
||||
defmt-rtt = { version = "1.0.0", optional = true }
|
||||
panic-reset = { version = "0.1.1" }
|
||||
embedded-hal = { version = "0.2.6" }
|
||||
embedded-storage = "0.3.1"
|
||||
|
||||
cortex-m = { version = "0.7.6", features = ["inline-asm", "critical-section-single-core"] }
|
||||
cortex-m-rt = "0.7.0"
|
||||
|
||||
@ -13,8 +13,7 @@ use embassy_stm32::{bind_interrupts, peripherals};
|
||||
use embassy_sync::blocking_mutex::Mutex;
|
||||
use embassy_time::Duration;
|
||||
use embassy_usb::{Builder, msos};
|
||||
use embassy_usb_dfu::consts::DfuAttributes;
|
||||
use embassy_usb_dfu::{Control, ResetImmediate, usb_dfu};
|
||||
use embassy_usb_dfu::application::{DfuAttributes, DfuState, Handler, usb_dfu};
|
||||
use panic_reset as _;
|
||||
|
||||
bind_interrupts!(struct Irqs {
|
||||
@ -26,6 +25,17 @@ bind_interrupts!(struct Irqs {
|
||||
// N.B. update to a custom GUID for your own device!
|
||||
const DEVICE_INTERFACE_GUIDS: &[&str] = &["{EAA9A5DC-30BA-44BC-9232-606CDC875321}"];
|
||||
|
||||
struct DfuHandler<'d, FLASH: embedded_storage::nor_flash::NorFlash> {
|
||||
firmware_state: BlockingFirmwareState<'d, FLASH>,
|
||||
}
|
||||
|
||||
impl<FLASH: embedded_storage::nor_flash::NorFlash> Handler for DfuHandler<'_, FLASH> {
|
||||
fn enter_dfu(&mut self) {
|
||||
self.firmware_state.mark_dfu().expect("Failed to mark DFU mode");
|
||||
cortex_m::peripheral::SCB::sys_reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[embassy_executor::main]
|
||||
async fn main(_spawner: Spawner) {
|
||||
let mut config = embassy_stm32::Config::default();
|
||||
@ -76,7 +86,10 @@ async fn main(_spawner: Spawner) {
|
||||
let mut config_descriptor = [0; 256];
|
||||
let mut bos_descriptor = [0; 256];
|
||||
let mut control_buf = [0; 64];
|
||||
let mut state = Control::new(firmware_state, DfuAttributes::CAN_DOWNLOAD, ResetImmediate);
|
||||
|
||||
let handler = DfuHandler { firmware_state };
|
||||
let mut state = DfuState::new(handler, DfuAttributes::CAN_DOWNLOAD, Duration::from_millis(2500));
|
||||
|
||||
let mut builder = Builder::new(
|
||||
driver,
|
||||
config,
|
||||
@ -99,7 +112,7 @@ async fn main(_spawner: Spawner) {
|
||||
msos::PropertyData::RegMultiSz(DEVICE_INTERFACE_GUIDS),
|
||||
));
|
||||
|
||||
usb_dfu(&mut builder, &mut state, Duration::from_millis(1000), |func| {
|
||||
usb_dfu(&mut builder, &mut state, |func| {
|
||||
// You likely don't have to add these function level headers if your USB device is not composite
|
||||
// (i.e. if your device does not expose another interface in addition to DFU)
|
||||
func.msos_feature(msos::CompatibleIdFeatureDescriptor::new("WINUSB", ""));
|
||||
|
||||
@ -14,7 +14,7 @@ use embassy_stm32::{bind_interrupts, peripherals, usb};
|
||||
use embassy_sync::blocking_mutex::Mutex;
|
||||
use embassy_usb::{Builder, msos};
|
||||
use embassy_usb_dfu::consts::DfuAttributes;
|
||||
use embassy_usb_dfu::{Control, ResetImmediate, usb_dfu};
|
||||
use embassy_usb_dfu::{ResetImmediate, new_state, usb_dfu};
|
||||
|
||||
bind_interrupts!(struct Irqs {
|
||||
USB_LP => usb::InterruptHandler<peripherals::USB>;
|
||||
@ -65,10 +65,10 @@ fn main() -> ! {
|
||||
let mut control_buf = [0; 4096];
|
||||
|
||||
#[cfg(not(feature = "verify"))]
|
||||
let mut state = Control::new(updater, DfuAttributes::CAN_DOWNLOAD, ResetImmediate);
|
||||
let mut state = new_state(updater, DfuAttributes::CAN_DOWNLOAD, ResetImmediate);
|
||||
|
||||
#[cfg(feature = "verify")]
|
||||
let mut state = Control::new(updater, DfuAttributes::CAN_DOWNLOAD, ResetImmediate, PUBLIC_SIGNING_KEY);
|
||||
let mut state = new_state(updater, DfuAttributes::CAN_DOWNLOAD, ResetImmediate, PUBLIC_SIGNING_KEY);
|
||||
|
||||
let mut builder = Builder::new(
|
||||
driver,
|
||||
|
||||
@ -13,7 +13,7 @@ use embassy_stm32::{Config, bind_interrupts, peripherals, usb};
|
||||
use embassy_sync::blocking_mutex::Mutex;
|
||||
use embassy_usb::{Builder, msos};
|
||||
use embassy_usb_dfu::consts::DfuAttributes;
|
||||
use embassy_usb_dfu::{Control, ResetImmediate, usb_dfu};
|
||||
use embassy_usb_dfu::{ResetImmediate, new_state, usb_dfu};
|
||||
|
||||
bind_interrupts!(struct Irqs {
|
||||
USB_OTG_HS => usb::InterruptHandler<peripherals::USB_OTG_HS>;
|
||||
@ -94,10 +94,10 @@ fn main() -> ! {
|
||||
let mut control_buf = [0; 4096];
|
||||
|
||||
#[cfg(not(feature = "verify"))]
|
||||
let mut state = Control::new(updater, DfuAttributes::CAN_DOWNLOAD, ResetImmediate);
|
||||
let mut state = new_state(updater, DfuAttributes::CAN_DOWNLOAD, ResetImmediate);
|
||||
|
||||
#[cfg(feature = "verify")]
|
||||
let mut state = Control::new(updater, DfuAttributes::CAN_DOWNLOAD, ResetImmediate, PUBLIC_SIGNING_KEY);
|
||||
let mut state = new_state(updater, DfuAttributes::CAN_DOWNLOAD, ResetImmediate, PUBLIC_SIGNING_KEY);
|
||||
|
||||
let mut builder = Builder::new(
|
||||
driver,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user