Add embassy-usb-dfu

This commit is contained in:
Kaitlyn Kenwell 2023-12-13 14:40:49 -05:00 committed by Dario Nieuwenhuis
parent 08203d4c04
commit 0b26b2d360
10 changed files with 492 additions and 7 deletions

View File

@ -5,7 +5,7 @@ use embassy_sync::blocking_mutex::raw::NoopRawMutex;
use embassy_sync::blocking_mutex::Mutex;
use embedded_storage::nor_flash::{NorFlash, NorFlashError, NorFlashErrorKind};
use crate::{State, BOOT_MAGIC, STATE_ERASE_VALUE, SWAP_MAGIC};
use crate::{State, BOOT_MAGIC, STATE_ERASE_VALUE, SWAP_MAGIC, DFU_DETACH_MAGIC};
/// Errors returned by bootloader
#[derive(PartialEq, Eq, Debug)]
@ -371,6 +371,8 @@ impl<ACTIVE: NorFlash, DFU: NorFlash, STATE: NorFlash> BootLoader<ACTIVE, DFU, S
if !state_word.iter().any(|&b| b != SWAP_MAGIC) {
Ok(State::Swap)
} else if !state_word.iter().any(|&b| b != DFU_DETACH_MAGIC) {
Ok(State::DfuDetach)
} else {
Ok(State::Boot)
}

View File

@ -6,7 +6,7 @@ use embassy_sync::blocking_mutex::raw::NoopRawMutex;
use embedded_storage_async::nor_flash::NorFlash;
use super::FirmwareUpdaterConfig;
use crate::{FirmwareUpdaterError, State, BOOT_MAGIC, STATE_ERASE_VALUE, SWAP_MAGIC};
use crate::{FirmwareUpdaterError, State, BOOT_MAGIC, STATE_ERASE_VALUE, SWAP_MAGIC, DFU_DETACH_MAGIC};
/// FirmwareUpdater is an application API for interacting with the BootLoader without the ability to
/// 'mess up' the internal bootloader state
@ -161,6 +161,12 @@ impl<'d, DFU: NorFlash, STATE: NorFlash> FirmwareUpdater<'d, DFU, STATE> {
self.state.mark_updated().await
}
/// Mark to trigger USB DFU on next boot.
pub async fn mark_dfu(&mut self) -> Result<(), FirmwareUpdaterError> {
self.state.verify_booted().await?;
self.state.mark_dfu().await
}
/// Mark firmware boot successful and stop rollback on reset.
pub async fn mark_booted(&mut self) -> Result<(), FirmwareUpdaterError> {
self.state.mark_booted().await
@ -247,6 +253,11 @@ impl<'d, STATE: NorFlash> FirmwareState<'d, STATE> {
self.set_magic(SWAP_MAGIC).await
}
/// Mark to trigger USB DFU on next boot.
pub async fn mark_dfu(&mut self) -> Result<(), FirmwareUpdaterError> {
self.set_magic(DFU_DETACH_MAGIC).await
}
/// Mark firmware boot successful and stop rollback on reset.
pub async fn mark_booted(&mut self) -> Result<(), FirmwareUpdaterError> {
self.set_magic(BOOT_MAGIC).await

View File

@ -6,7 +6,7 @@ use embassy_sync::blocking_mutex::raw::NoopRawMutex;
use embedded_storage::nor_flash::NorFlash;
use super::FirmwareUpdaterConfig;
use crate::{FirmwareUpdaterError, State, BOOT_MAGIC, STATE_ERASE_VALUE, SWAP_MAGIC};
use crate::{FirmwareUpdaterError, State, BOOT_MAGIC, STATE_ERASE_VALUE, SWAP_MAGIC, DFU_DETACH_MAGIC};
/// Blocking FirmwareUpdater is an application API for interacting with the BootLoader without the ability to
/// 'mess up' the internal bootloader state
@ -168,6 +168,12 @@ impl<'d, DFU: NorFlash, STATE: NorFlash> BlockingFirmwareUpdater<'d, DFU, STATE>
self.state.mark_updated()
}
/// Mark to trigger USB DFU device on next boot.
pub fn mark_dfu(&mut self) -> Result<(), FirmwareUpdaterError> {
self.state.verify_booted()?;
self.state.mark_dfu()
}
/// Mark firmware boot successful and stop rollback on reset.
pub fn mark_booted(&mut self) -> Result<(), FirmwareUpdaterError> {
self.state.mark_booted()
@ -226,7 +232,7 @@ impl<'d, STATE: NorFlash> BlockingFirmwareState<'d, STATE> {
// Make sure we are running a booted firmware to avoid reverting to a bad state.
fn verify_booted(&mut self) -> Result<(), FirmwareUpdaterError> {
if self.get_state()? == State::Boot {
if self.get_state()? == State::Boot || self.get_state()? == State::DfuDetach {
Ok(())
} else {
Err(FirmwareUpdaterError::BadState)
@ -243,6 +249,8 @@ impl<'d, STATE: NorFlash> BlockingFirmwareState<'d, STATE> {
if !self.aligned.iter().any(|&b| b != SWAP_MAGIC) {
Ok(State::Swap)
} else if !self.aligned.iter().any(|&b| b != DFU_DETACH_MAGIC) {
Ok(State::DfuDetach)
} else {
Ok(State::Boot)
}
@ -253,6 +261,11 @@ impl<'d, STATE: NorFlash> BlockingFirmwareState<'d, STATE> {
self.set_magic(SWAP_MAGIC)
}
/// Mark to trigger USB DFU on next boot.
pub fn mark_dfu(&mut self) -> Result<(), FirmwareUpdaterError> {
self.set_magic(DFU_DETACH_MAGIC)
}
/// Mark firmware boot successful and stop rollback on reset.
pub fn mark_booted(&mut self) -> Result<(), FirmwareUpdaterError> {
self.set_magic(BOOT_MAGIC)

View File

@ -23,6 +23,7 @@ pub use firmware_updater::{
pub(crate) const BOOT_MAGIC: u8 = 0xD0;
pub(crate) const SWAP_MAGIC: u8 = 0xF0;
pub(crate) const DFU_DETACH_MAGIC: u8 = 0xE0;
/// The state of the bootloader after running prepare.
#[derive(PartialEq, Eq, Debug)]
@ -32,6 +33,8 @@ pub enum State {
Boot,
/// Bootloader has swapped the active partition with the dfu partition and will attempt boot.
Swap,
/// Application has received a DFU_DETACH request over USB, and is rebooting into the bootloader to apply a DFU.
DfuDetach,
}
/// Buffer aligned to 32 byte boundary, largest known alignment requirement for embassy-boot.

View File

@ -10,7 +10,10 @@ pub use embassy_boot::{
use embedded_storage::nor_flash::NorFlash;
/// A bootloader for STM32 devices.
pub struct BootLoader;
pub struct BootLoader {
/// The reported state of the bootloader after preparing for boot
pub state: State,
}
impl BootLoader {
/// Inspect the bootloader state and perform actions required before booting, such as swapping firmware
@ -19,8 +22,8 @@ impl BootLoader {
) -> Self {
let mut aligned_buf = AlignedBuffer([0; BUFFER_SIZE]);
let mut boot = embassy_boot::BootLoader::new(config);
boot.prepare_boot(aligned_buf.as_mut()).expect("Boot prepare error");
Self
let state = boot.prepare_boot(aligned_buf.as_mut()).expect("Boot prepare error");
Self { state }
}
/// Boots the application.

View File

@ -0,0 +1,31 @@
[package]
edition = "2021"
name = "embassy-usb-dfu"
version = "0.1.0"
description = "An implementation of the USB DFU 1.1 protocol, using embassy-boot"
license = "MIT OR Apache-2.0"
repository = "https://github.com/embassy-rs/embassy"
categories = [
"embedded",
"no-std",
"asynchronous"
]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bitflags = "2.4.1"
cortex-m = { version = "0.7.7", features = ["inline-asm"] }
defmt = { version = "0.3.5", optional = true }
embassy-boot = { version = "0.1.1", path = "../embassy-boot/boot" }
embassy-embedded-hal = { version = "0.1.0", path = "../embassy-embedded-hal" }
embassy-futures = { version = "0.1.1", path = "../embassy-futures" }
embassy-sync = { version = "0.5.0", path = "../embassy-sync" }
embassy-time = { version = "0.2.0", path = "../embassy-time" }
embassy-usb = { version = "0.1.0", path = "../embassy-usb", default-features = false }
embedded-storage = { version = "0.3.1" }
[features]
bootloader = []
application = []
defmt = ["dep:defmt"]

View File

@ -0,0 +1,114 @@
use embassy_boot::BlockingFirmwareUpdater;
use embassy_time::{Instant, Duration};
use embassy_usb::{Handler, control::{RequestType, Recipient, OutResponse, InResponse}, Builder, driver::Driver};
use embedded_storage::nor_flash::NorFlash;
use crate::consts::{DfuAttributes, Request, Status, State, USB_CLASS_APPN_SPEC, APPN_SPEC_SUBCLASS_DFU, DESC_DFU_FUNCTIONAL, DFU_PROTOCOL_RT};
/// Internal state for the DFU class
pub struct Control<'d, DFU: NorFlash, STATE: NorFlash> {
updater: BlockingFirmwareUpdater<'d, DFU, STATE>,
attrs: DfuAttributes,
state: State,
timeout: Option<Duration>,
detach_start: Option<Instant>,
}
impl<'d, DFU: NorFlash, STATE: NorFlash> Control<'d, DFU, STATE> {
pub fn new(updater: BlockingFirmwareUpdater<'d, DFU, STATE>, attrs: DfuAttributes) -> Self {
Control { updater, attrs, state: State::AppIdle, detach_start: None, timeout: None }
}
}
impl<'d, DFU: NorFlash, STATE: NorFlash> Handler for Control<'d, DFU, STATE> {
fn reset(&mut self) {
if let Some(start) = self.detach_start {
let delta = Instant::now() - start;
let timeout = self.timeout.unwrap();
#[cfg(feature = "defmt")]
defmt::info!("Received RESET with delta = {}, timeout = {}", delta.as_millis(), timeout.as_millis());
if delta < timeout {
self.updater.mark_dfu().expect("Failed to mark DFU mode in bootloader");
cortex_m::asm::dsb();
cortex_m::peripheral::SCB::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;
}
#[cfg(feature = "defmt")]
defmt::info!("Received request {}", req);
match Request::try_from(req.request) {
Ok(Request::Detach) => {
#[cfg(feature = "defmt")]
defmt::info!("Received DETACH, awaiting USB reset");
self.detach_start = Some(Instant::now());
self.timeout = Some(Duration::from_millis(req.value as u64));
self.state = State::AppDetach;
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;
}
#[cfg(feature = "defmt")]
defmt::info!("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>, DFU: NorFlash, STATE: NorFlash>(builder: &mut Builder<'d, D>, handler: &'d mut Control<'d, DFU, STATE>, timeout: Duration) {
#[cfg(feature = "defmt")]
defmt::info!("Application USB DFU initializing");
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 = 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);
}

View File

@ -0,0 +1,196 @@
use embassy_boot::BlockingFirmwareUpdater;
use embassy_usb::{
control::{InResponse, OutResponse, Recipient, RequestType},
driver::Driver,
Builder, Handler,
};
use embedded_storage::nor_flash::{NorFlashErrorKind, NorFlash};
use crate::consts::{DfuAttributes, Request, State, Status, USB_CLASS_APPN_SPEC, APPN_SPEC_SUBCLASS_DFU, DFU_PROTOCOL_DFU, DESC_DFU_FUNCTIONAL};
/// Internal state for USB DFU
pub struct Control<'d, DFU: NorFlash, STATE: NorFlash, const BLOCK_SIZE: usize> {
updater: BlockingFirmwareUpdater<'d, DFU, STATE>,
attrs: DfuAttributes,
state: State,
status: Status,
offset: usize,
}
impl<'d, DFU: NorFlash, STATE: NorFlash, const BLOCK_SIZE: usize> Control<'d, DFU, STATE, BLOCK_SIZE> {
pub fn new(updater: BlockingFirmwareUpdater<'d, DFU, STATE>, attrs: DfuAttributes) -> Self {
Self {
updater,
attrs,
state: State::DfuIdle,
status: Status::Ok,
offset: 0,
}
}
fn reset_state(&mut self) {
self.offset = 0;
self.state = State::DfuIdle;
self.status = Status::Ok;
}
}
impl<'d, DFU: NorFlash, STATE: NorFlash, const BLOCK_SIZE: usize> Handler for Control<'d, DFU, STATE, 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) {
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 == 0 {
self.state = State::Download;
self.offset = 0;
}
let mut buf = [0; BLOCK_SIZE];
buf[..data.len()].copy_from_slice(data);
if req.length == 0 {
match self.updater.mark_updated() {
Ok(_) => {
self.status = Status::Ok;
self.state = State::ManifestSync;
}
Err(e) => {
self.state = State::Error;
match e {
embassy_boot::FirmwareUpdaterError::Flash(e) => match e {
NorFlashErrorKind::NotAligned => self.status = Status::ErrWrite,
NorFlashErrorKind::OutOfBounds => {
self.status = Status::ErrAddress
}
_ => self.status = Status::ErrUnknown,
},
embassy_boot::FirmwareUpdaterError::Signature(_) => {
self.status = Status::ErrVerify
}
embassy_boot::FirmwareUpdaterError::BadState => {
self.status = Status::ErrUnknown
}
}
}
}
} 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.updater.write_firmware(self.offset, &buf[..]) {
Ok(_) => {
self.status = Status::Ok;
self.state = State::DlSync;
self.offset += data.len();
}
Err(e) => {
self.state = State::Error;
match e {
embassy_boot::FirmwareUpdaterError::Flash(e) => match e {
NorFlashErrorKind::NotAligned => self.status = Status::ErrWrite,
NorFlashErrorKind::OutOfBounds => {
self.status = Status::ErrAddress
}
_ => self.status = Status::ErrUnknown,
},
embassy_boot::FirmwareUpdaterError::Signature(_) => {
self.status = Status::ErrVerify
}
embassy_boot::FirmwareUpdaterError::BadState => {
self.status = Status::ErrUnknown
}
}
}
}
}
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: 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;
}
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 => cortex_m::peripheral::SCB::sys_reset(),
_ => {}
}
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>, DFU: NorFlash, STATE: NorFlash, const BLOCK_SIZE: usize>(
builder: &mut Builder<'d, D>,
handler: &'d mut Control<'d, DFU, STATE, BLOCK_SIZE>,
) {
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,
&[
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);
}

View File

@ -0,0 +1,96 @@
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! {
pub struct DfuAttributes: u8 {
const WILL_DETACH = 0b0000_1000;
const MANIFESTATION_TOLERANT = 0b0000_0100;
const CAN_UPLOAD = 0b0000_0010;
const CAN_DOWNLOAD = 0b0000_0001;
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
#[repr(u8)]
#[allow(unused)]
pub 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 enum Request {
Detach = 0,
Dnload = 1,
Upload = 2,
GetStatus = 3,
ClrStatus = 4,
GetState = 5,
Abort = 6,
}
impl TryFrom<u8> for Request {
type Error = ();
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(Request::Detach),
1 => Ok(Request::Dnload),
2 => Ok(Request::Upload),
3 => Ok(Request::GetStatus),
4 => Ok(Request::ClrStatus),
5 => Ok(Request::GetState),
6 => Ok(Request::Abort),
_ => Err(()),
}
}
}

View File

@ -0,0 +1,16 @@
#![no_std]
pub mod consts;
#[cfg(feature = "bootloader")]
mod bootloader;
#[cfg(feature = "bootloader")]
pub use self::bootloader::*;
#[cfg(feature = "application")]
mod application;
#[cfg(feature = "application")]
pub use self::application::*;
#[cfg(any(all(feature = "bootloader", feature = "application"), not(any(feature = "bootloader", feature = "application"))))]
compile_error!("usb-dfu must be compiled with exactly one of `bootloader`, or `application` features");