blob: 3958d0a69cdd1a6eb4156d9cd35241b4aff6a0a6 [file]
// Licensed under the Apache-2.0 license
// SPDX-License-Identifier: Apache-2.0
//! USB Device Firmware Upgrade (DFU) 1.1 implementation.
#![no_std]
use hal_usb::driver::{UsbDriver, UsbEvent, UsbPacket};
use hal_usb::{
Direction, FunctionalDescriptor, InterfaceDescriptor, Recipient, Request, RequestType,
SetupPacket, StringHandle,
};
use usb_stack::{UsbAction, UsbClass, EMPTY};
/// DFU specific constants.
pub const USB_CLASS_APP_SPECIFIC: u8 = 0xFE;
pub const USB_SUBCLASS_DFU: u8 = 0x01;
pub const USB_PROTOCOL_DFU: u8 = 0x02;
pub const DFU_DESCRIPTOR_TYPE: u8 = 0x21;
/// DFU specific requests.
pub const DFU_DETACH: Request = Request::new(
Direction::HostToDevice,
RequestType::Class,
Recipient::Interface,
0,
);
pub const DFU_DNLOAD: Request = Request::new(
Direction::HostToDevice,
RequestType::Class,
Recipient::Interface,
1,
);
pub const DFU_UPLOAD: Request = Request::new(
Direction::DeviceToHost,
RequestType::Class,
Recipient::Interface,
2,
);
pub const DFU_GETSTATUS: Request = Request::new(
Direction::DeviceToHost,
RequestType::Class,
Recipient::Interface,
3,
);
pub const DFU_CLRSTATUS: Request = Request::new(
Direction::HostToDevice,
RequestType::Class,
Recipient::Interface,
4,
);
pub const DFU_GETSTATE: Request = Request::new(
Direction::DeviceToHost,
RequestType::Class,
Recipient::Interface,
5,
);
pub const DFU_ABORT: Request = Request::new(
Direction::HostToDevice,
RequestType::Class,
Recipient::Interface,
6,
);
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum DfuState {
AppIdle = 0,
AppDetach = 1,
DfuIdle = 2,
DfuDnloadSync = 3,
DfuDnloadBusy = 4,
DfuDnloadIdle = 5,
DfuManifestSync = 6,
DfuManifest = 7,
DfuManifestWaitReset = 8,
DfuUploadIdle = 9,
DfuError = 10,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum DfuStatus {
Ok = 0,
ErrTarget = 1,
ErrFile = 2,
ErrWrite = 3,
ErrErase = 4,
ErrCheckChunked = 5,
ErrProg = 6,
ErrVerify = 7,
ErrAddress = 8,
ErrNotDone = 9,
ErrFirmware = 10,
ErrVendor = 11,
ErrUsbr = 12,
ErrPor = 13,
ErrUnknown = 14,
ErrStalledPkt = 15,
}
/// A trait for providing the backend storage for the DFU implementation.
pub trait DfuHandler {
/// Write a block of data to the device.
fn dnload(&mut self, alt: u8, block_num: u16, data: &[u8]) -> Result<(), DfuStatus>;
/// Read a block of data from the device.
fn upload(&mut self, alt: u8, block_num: u16, data: &mut [u8]) -> Result<usize, DfuStatus>;
/// Finalize the download.
fn manifest(&mut self) -> Result<(), DfuStatus>;
/// Abort the current operation.
fn abort(&mut self);
}
/// A builder for DFU class configuration.
#[derive(Copy, Clone)]
pub struct DfuBuilder {
pub interface_num: u8,
pub alt_settings: u8,
pub transfer_size: u16,
pub attributes: u8,
pub detach_timeout: u16,
}
impl DfuBuilder {
pub const fn new(interface_num: u8, alt_settings: u8, transfer_size: u16) -> Self {
Self {
interface_num,
alt_settings,
transfer_size,
attributes: 0x07, // bitCanDnload | bitCanUpload | bitManifestationTolerant
detach_timeout: 0,
}
}
pub const fn functional_descriptor(&self) -> FunctionalDescriptor {
FunctionalDescriptor::raw(
DFU_DESCRIPTOR_TYPE,
&[
self.attributes,
(self.detach_timeout & 0xFF) as u8,
((self.detach_timeout >> 8) & 0xFF) as u8,
(self.transfer_size & 0xFF) as u8,
((self.transfer_size >> 8) & 0xFF) as u8,
0x10, // bcdDFUVersion 1.1 (0x0110)
0x01,
],
)
}
pub const fn interface(
&self,
alt: u8,
name: StringHandle,
func_descs: &'static [FunctionalDescriptor],
) -> InterfaceDescriptor {
InterfaceDescriptor {
name,
interface_number: self.interface_num,
alternate_setting: alt,
interface_class: USB_CLASS_APP_SPECIFIC,
interface_sub_class: USB_SUBCLASS_DFU,
interface_protocol: USB_PROTOCOL_DFU,
func_descs,
endpoints: &[],
}
}
}
pub struct DfuClass<H, const BLOCK_SIZE: usize>
where
H: DfuHandler,
{
config: DfuBuilder,
handler: H,
state: DfuState,
status: DfuStatus,
alt: u8,
expecting_dnload: bool,
block_num: u16,
buffer: [u8; BLOCK_SIZE],
transfer_offset: usize,
transfer_total: usize,
}
impl<H, const BLOCK_SIZE: usize> DfuClass<H, BLOCK_SIZE>
where
H: DfuHandler,
{
pub fn new(config: DfuBuilder, handler: H) -> Self {
assert!(BLOCK_SIZE >= config.transfer_size as usize);
Self {
config,
handler,
state: DfuState::DfuIdle,
status: DfuStatus::Ok,
alt: 0,
expecting_dnload: false,
block_num: 0,
buffer: [0u8; BLOCK_SIZE],
transfer_offset: 0,
transfer_total: 0,
}
}
/// Polls the send buffer and initiates IN transfers if data is available.
pub fn poll<D: UsbDriver>(&mut self, driver: &mut D) {
if !(self.state == DfuState::DfuUploadIdle || self.state == DfuState::DfuIdle) {
return;
}
if self.transfer_offset >= self.transfer_total {
return;
}
if let Some(data) = self.buffer.get(self.transfer_offset..self.transfer_total) {
let n = driver.transfer_in_unaligned(0, data, true);
self.transfer_offset += n;
if self.transfer_offset == self.transfer_total {
if self.transfer_total < self.config.transfer_size as usize {
self.state = DfuState::DfuIdle;
} else {
self.state = DfuState::DfuUploadIdle;
}
}
}
}
fn handle_setup<'a>(&'a mut self, pkt: SetupPacket) -> (UsbAction<'a>, bool) {
if pkt.request().recipient() != Recipient::Interface
|| (pkt.index() as u8) != self.config.interface_num
{
return (UsbAction::None, false);
}
match pkt.request() {
DFU_DETACH => {
// In DFU mode, DETACH is a no-op or transitions back to APP mode.
// We'll just ACK it.
(
UsbAction::TransferIn {
endpoint: 0,
data: EMPTY,
zlp: true,
},
true,
)
}
DFU_DNLOAD => {
if self.state == DfuState::DfuError {
return (UsbAction::StallInAndOut { endpoint: 0 }, true);
}
let len = pkt.length() as usize;
if len > BLOCK_SIZE {
self.state = DfuState::DfuError;
self.status = DfuStatus::ErrStalledPkt;
return (UsbAction::StallInAndOut { endpoint: 0 }, true);
}
self.block_num = pkt.value();
self.transfer_offset = 0;
self.transfer_total = len;
if len == 0 {
// Transition to manifest sync
self.state = DfuState::DfuManifestSync;
(
UsbAction::TransferIn {
endpoint: 0,
data: EMPTY,
zlp: true,
},
true,
)
} else {
self.expecting_dnload = true;
self.state = DfuState::DfuDnloadSync;
(UsbAction::None, true)
}
}
DFU_UPLOAD => {
if self.state != DfuState::DfuIdle && self.state != DfuState::DfuUploadIdle {
return (UsbAction::StallInAndOut { endpoint: 0 }, true);
}
self.block_num = pkt.value();
match self
.handler
.upload(self.alt, self.block_num, &mut self.buffer)
{
Ok(0) => {
self.transfer_offset = 0;
self.transfer_total = 0;
self.state = DfuState::DfuIdle;
(
UsbAction::TransferIn {
endpoint: 0,
data: EMPTY,
zlp: true,
},
true,
)
}
Ok(n) => {
self.transfer_offset = 0;
self.transfer_total = n;
// poll_transmit will handle the actual transfer
(UsbAction::None, true)
}
Err(s) => {
self.state = DfuState::DfuError;
self.status = s;
(UsbAction::StallInAndOut { endpoint: 0 }, true)
}
}
}
DFU_GETSTATUS => {
// status[0]: bStatus
// status[1-3]: bwPollTimeout (24-bit, little endian)
// status[4]: bState
// status[5]: iString
self.buffer[0] = self.status as u8;
self.buffer[1] = 0; // bwPollTimeout = 0
self.buffer[2] = 0;
self.buffer[3] = 0;
self.buffer[4] = self.state as u8;
self.buffer[5] = 0;
// State transitions after GETSTATUS
match self.state {
DfuState::DfuDnloadSync => self.state = DfuState::DfuDnloadIdle,
DfuState::DfuManifestSync => {
match self.handler.manifest() {
Ok(()) => self.state = DfuState::DfuIdle, // ManifestationTolerant = 1
Err(s) => {
self.state = DfuState::DfuError;
self.status = s;
}
}
}
_ => {}
}
(
UsbAction::TransferInUnaligned {
endpoint: 0,
data: &self.buffer[..6],
zlp: true,
},
true,
)
}
DFU_CLRSTATUS => {
self.state = DfuState::DfuIdle;
self.status = DfuStatus::Ok;
(
UsbAction::TransferIn {
endpoint: 0,
data: EMPTY,
zlp: true,
},
true,
)
}
DFU_GETSTATE => {
self.buffer[0] = self.state as u8;
(
UsbAction::TransferInUnaligned {
endpoint: 0,
data: &self.buffer[..1],
zlp: true,
},
true,
)
}
DFU_ABORT => {
self.handler.abort();
self.state = DfuState::DfuIdle;
self.status = DfuStatus::Ok;
self.transfer_total = 0;
self.transfer_offset = 0;
(
UsbAction::TransferIn {
endpoint: 0,
data: EMPTY,
zlp: true,
},
true,
)
}
Request::INTERFACE_SET_INTERFACE => {
self.alt = pkt.value() as u8;
(
UsbAction::TransferIn {
endpoint: 0,
data: EMPTY,
zlp: true,
},
true,
)
}
_ => (UsbAction::None, false),
}
}
fn handle_control_out<'a>(&'a mut self, pkt: impl UsbPacket) -> UsbAction<'a> {
if !self.expecting_dnload {
return UsbAction::StallInAndOut { endpoint: 0 };
}
let dest = match self.buffer.get_mut(self.transfer_offset..) {
Some(d) => d,
None => return UsbAction::StallInAndOut { endpoint: 0 },
};
let data = pkt.copy_to_unaligned(dest);
self.transfer_offset += data.len();
if self.transfer_offset >= self.transfer_total {
self.expecting_dnload = false;
let data = match self.buffer.get(..self.transfer_total) {
Some(d) => d,
None => {
self.state = DfuState::DfuError;
self.status = DfuStatus::ErrAddress;
return UsbAction::StallInAndOut { endpoint: 0 };
}
};
match self.handler.dnload(self.alt, self.block_num, data) {
Ok(()) => UsbAction::TransferIn {
endpoint: 0,
data: EMPTY,
zlp: true,
},
Err(s) => {
self.state = DfuState::DfuError;
self.status = s;
UsbAction::StallInAndOut { endpoint: 0 }
}
}
} else {
UsbAction::None
}
}
}
impl<H, const BLOCK_SIZE: usize> UsbClass for DfuClass<H, BLOCK_SIZE>
where
H: DfuHandler,
{
fn handle_event<'a, P: UsbPacket>(
&'a mut self,
event: UsbEvent<P>,
) -> Result<UsbAction<'a>, UsbEvent<P>> {
match event {
UsbEvent::SetupPacket { pkt, endpoint } if endpoint == 0 => {
let (action, claimed) = self.handle_setup(pkt);
if claimed {
Ok(action)
} else {
Err(UsbEvent::SetupPacket { pkt, endpoint })
}
}
UsbEvent::DataOutPacket(pkt) if pkt.endpoint_index() == 0 && self.expecting_dnload => {
Ok(self.handle_control_out(pkt))
}
_ => Err(event),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use aligned::{Aligned, A4};
use hal_usb::driver::UsbEvent;
use usb_stack::testing::FakeUsbPacket;
struct MockHandler {
dnload_called: bool,
upload_called: bool,
manifest_called: bool,
abort_called: bool,
}
impl MockHandler {
fn new() -> Self {
Self {
dnload_called: false,
upload_called: false,
manifest_called: false,
abort_called: false,
}
}
}
impl DfuHandler for MockHandler {
fn dnload(&mut self, _alt: u8, _block_num: u16, _data: &[u8]) -> Result<(), DfuStatus> {
self.dnload_called = true;
Ok(())
}
fn upload(
&mut self,
_alt: u8,
_block_num: u16,
data: &mut [u8],
) -> Result<usize, DfuStatus> {
self.upload_called = true;
data[0] = 0xAA;
Ok(1)
}
fn manifest(&mut self) -> Result<(), DfuStatus> {
self.manifest_called = true;
Ok(())
}
fn abort(&mut self) {
self.abort_called = true;
}
}
fn setup_packet(req: Request, val: u16, idx: u16, len: u16) -> SetupPacket {
SetupPacket::new([
(u16::from(req) as u32) | ((val as u32) << 16),
(idx as u32) | ((len as u32) << 16),
])
}
#[test]
fn test_dfu_dnload_sequence() {
let config = DfuBuilder::new(1, 1, 64);
let mut dfu = DfuClass::<_, 64>::new(config, MockHandler::new());
// 1. DNLOAD request (block 0, 4 bytes)
let setup = setup_packet(DFU_DNLOAD, 0, 1, 4);
let event: UsbEvent<FakeUsbPacket<'_>> = UsbEvent::SetupPacket {
endpoint: 0,
pkt: setup,
};
let action = dfu.handle_event(event).map_err(|_| ()).unwrap();
assert!(matches!(action, UsbAction::None));
assert_eq!(dfu.state, DfuState::DfuDnloadSync);
// 2. Data OUT packet
let data = [1, 2, 3, 4];
let pkt = FakeUsbPacket { ep: 0, data: &data };
let action = dfu
.handle_event(UsbEvent::DataOutPacket(pkt))
.map_err(|_| ())
.unwrap();
assert!(matches!(action, UsbAction::TransferIn { endpoint: 0, .. }));
assert!(dfu.handler.dnload_called);
// 3. GETSTATUS
let setup = setup_packet(DFU_GETSTATUS, 0, 1, 6);
let event: UsbEvent<FakeUsbPacket<'_>> = UsbEvent::SetupPacket {
endpoint: 0,
pkt: setup,
};
let action = dfu.handle_event(event).map_err(|_| ()).unwrap();
assert!(matches!(action, UsbAction::TransferInUnaligned { .. }));
assert_eq!(dfu.state, DfuState::DfuDnloadIdle);
// 4. DNLOAD ZLP (Manifestation)
let setup = setup_packet(DFU_DNLOAD, 1, 1, 0);
let event: UsbEvent<FakeUsbPacket<'_>> = UsbEvent::SetupPacket {
endpoint: 0,
pkt: setup,
};
let action = dfu.handle_event(event).map_err(|_| ()).unwrap();
assert!(matches!(action, UsbAction::TransferIn { endpoint: 0, .. }));
assert_eq!(dfu.state, DfuState::DfuManifestSync);
// 5. GETSTATUS (Manifest)
let setup = setup_packet(DFU_GETSTATUS, 0, 1, 6);
let event: UsbEvent<FakeUsbPacket<'_>> = UsbEvent::SetupPacket {
endpoint: 0,
pkt: setup,
};
let _ = dfu.handle_event(event).map_err(|_| ()).unwrap();
assert!(dfu.handler.manifest_called);
assert_eq!(dfu.state, DfuState::DfuIdle);
}
#[test]
fn test_dfu_upload() {
let config = DfuBuilder::new(1, 1, 64);
let mut dfu = DfuClass::<_, 64>::new(config, MockHandler::new());
let setup = setup_packet(DFU_UPLOAD, 0, 1, 64);
let event: UsbEvent<FakeUsbPacket<'_>> = UsbEvent::SetupPacket {
endpoint: 0,
pkt: setup,
};
let action = dfu.handle_event(event).map_err(|_| ()).unwrap();
assert!(matches!(action, UsbAction::None));
// Mock driver for poll_transmit
struct MockDriver {
transferred: usize,
}
impl UsbDriver for MockDriver {
const MAX_PACKET_SIZE: usize = 64;
type Packet<'a> = FakeUsbPacket<'a>;
fn transfer_in(&mut self, _ep: u8, _data: &Aligned<A4, [u8]>, _zlp: bool) -> usize {
0
}
fn transfer_in_unaligned(&mut self, _ep: u8, data: &[u8], _zlp: bool) -> usize {
self.transferred = data.len();
data.len()
}
fn stall(&mut self, _ep: u8, _stall: bool) {}
fn is_stalled(&mut self, _ep: u8) -> bool {
false
}
fn set_address(&mut self, _addr: u8) {}
fn poll(&mut self) -> Option<UsbEvent<Self::Packet<'_>>> {
None
}
}
let mut driver = MockDriver { transferred: 0 };
dfu.poll(&mut driver);
assert_eq!(driver.transferred, 1);
assert!(dfu.handler.upload_called);
assert_eq!(dfu.state, DfuState::DfuIdle); // n < transfer_size
}
#[test]
fn test_dfu_abort() {
let config = DfuBuilder::new(1, 1, 64);
let mut dfu = DfuClass::<_, 64>::new(config, MockHandler::new());
dfu.state = DfuState::DfuDnloadIdle;
let setup = setup_packet(DFU_ABORT, 0, 1, 0);
let event: UsbEvent<FakeUsbPacket<'_>> = UsbEvent::SetupPacket {
endpoint: 0,
pkt: setup,
};
let action = dfu.handle_event(event).map_err(|_| ()).unwrap();
assert!(matches!(action, UsbAction::TransferIn { endpoint: 0, .. }));
assert!(dfu.handler.abort_called);
assert_eq!(dfu.state, DfuState::DfuIdle);
}
}