aboutsummaryrefslogtreecommitdiff
path: root/embassy-usb-dfu
diff options
context:
space:
mode:
authorKaitlyn Kenwell <[email protected]>2023-12-13 14:40:49 -0500
committerKaitlyn Kenwell <[email protected]>2023-12-13 14:40:49 -0500
commit976a7ae22aa222213861c12d515115aac87bd2e0 (patch)
tree88f802c7d57df011c02142ef309ff5cffe8bd3b0 /embassy-usb-dfu
parent14f41a71b6ea9dedb4ee5b9c741fe10575772c7d (diff)
Add embassy-usb-dfu
Diffstat (limited to 'embassy-usb-dfu')
-rw-r--r--embassy-usb-dfu/Cargo.toml31
-rw-r--r--embassy-usb-dfu/src/application.rs114
-rw-r--r--embassy-usb-dfu/src/bootloader.rs196
-rw-r--r--embassy-usb-dfu/src/consts.rs96
-rw-r--r--embassy-usb-dfu/src/lib.rs16
5 files changed, 453 insertions, 0 deletions
diff --git a/embassy-usb-dfu/Cargo.toml b/embassy-usb-dfu/Cargo.toml
new file mode 100644
index 000000000..62398afbc
--- /dev/null
+++ b/embassy-usb-dfu/Cargo.toml
@@ -0,0 +1,31 @@
1[package]
2edition = "2021"
3name = "embassy-usb-dfu"
4version = "0.1.0"
5description = "An implementation of the USB DFU 1.1 protocol, using embassy-boot"
6license = "MIT OR Apache-2.0"
7repository = "https://github.com/embassy-rs/embassy"
8categories = [
9 "embedded",
10 "no-std",
11 "asynchronous"
12]
13
14# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
15
16[dependencies]
17bitflags = "2.4.1"
18cortex-m = { version = "0.7.7", features = ["inline-asm"] }
19defmt = { version = "0.3.5", optional = true }
20embassy-boot = { version = "0.1.1", path = "../embassy-boot/boot" }
21embassy-embedded-hal = { version = "0.1.0", path = "../embassy-embedded-hal" }
22embassy-futures = { version = "0.1.1", path = "../embassy-futures" }
23embassy-sync = { version = "0.5.0", path = "../embassy-sync" }
24embassy-time = { version = "0.2.0", path = "../embassy-time" }
25embassy-usb = { version = "0.1.0", path = "../embassy-usb", default-features = false }
26embedded-storage = { version = "0.3.1" }
27
28[features]
29bootloader = []
30application = []
31defmt = ["dep:defmt"]
diff --git a/embassy-usb-dfu/src/application.rs b/embassy-usb-dfu/src/application.rs
new file mode 100644
index 000000000..5a52a9fed
--- /dev/null
+++ b/embassy-usb-dfu/src/application.rs
@@ -0,0 +1,114 @@
1
2use embassy_boot::BlockingFirmwareUpdater;
3use embassy_time::{Instant, Duration};
4use embassy_usb::{Handler, control::{RequestType, Recipient, OutResponse, InResponse}, Builder, driver::Driver};
5use embedded_storage::nor_flash::NorFlash;
6
7use crate::consts::{DfuAttributes, Request, Status, State, USB_CLASS_APPN_SPEC, APPN_SPEC_SUBCLASS_DFU, DESC_DFU_FUNCTIONAL, DFU_PROTOCOL_RT};
8
9/// Internal state for the DFU class
10pub struct Control<'d, DFU: NorFlash, STATE: NorFlash> {
11 updater: BlockingFirmwareUpdater<'d, DFU, STATE>,
12 attrs: DfuAttributes,
13 state: State,
14 timeout: Option<Duration>,
15 detach_start: Option<Instant>,
16}
17
18impl<'d, DFU: NorFlash, STATE: NorFlash> Control<'d, DFU, STATE> {
19 pub fn new(updater: BlockingFirmwareUpdater<'d, DFU, STATE>, attrs: DfuAttributes) -> Self {
20 Control { updater, attrs, state: State::AppIdle, detach_start: None, timeout: None }
21 }
22}
23
24impl<'d, DFU: NorFlash, STATE: NorFlash> Handler for Control<'d, DFU, STATE> {
25 fn reset(&mut self) {
26 if let Some(start) = self.detach_start {
27 let delta = Instant::now() - start;
28 let timeout = self.timeout.unwrap();
29 #[cfg(feature = "defmt")]
30 defmt::info!("Received RESET with delta = {}, timeout = {}", delta.as_millis(), timeout.as_millis());
31 if delta < timeout {
32 self.updater.mark_dfu().expect("Failed to mark DFU mode in bootloader");
33 cortex_m::asm::dsb();
34 cortex_m::peripheral::SCB::sys_reset();
35 }
36 }
37 }
38
39 fn control_out(&mut self, req: embassy_usb::control::Request, _: &[u8]) -> Option<embassy_usb::control::OutResponse> {
40 if (req.request_type, req.recipient) != (RequestType::Class, Recipient::Interface) {
41 return None;
42 }
43
44 #[cfg(feature = "defmt")]
45 defmt::info!("Received request {}", req);
46
47 match Request::try_from(req.request) {
48 Ok(Request::Detach) => {
49 #[cfg(feature = "defmt")]
50 defmt::info!("Received DETACH, awaiting USB reset");
51 self.detach_start = Some(Instant::now());
52 self.timeout = Some(Duration::from_millis(req.value as u64));
53 self.state = State::AppDetach;
54 Some(OutResponse::Accepted)
55 }
56 _ => {
57 None
58 }
59 }
60 }
61
62 fn control_in<'a>(&'a mut self, req: embassy_usb::control::Request, buf: &'a mut [u8]) -> Option<embassy_usb::control::InResponse<'a>> {
63 if (req.request_type, req.recipient) != (RequestType::Class, Recipient::Interface) {
64 return None;
65 }
66
67 #[cfg(feature = "defmt")]
68 defmt::info!("Received request {}", req);
69
70 match Request::try_from(req.request) {
71 Ok(Request::GetStatus) => {
72 buf[0..6].copy_from_slice(&[Status::Ok as u8, 0x32, 0x00, 0x00, self.state as u8, 0x00]);
73 Some(InResponse::Accepted(buf))
74 }
75 _ => None
76 }
77 }
78}
79
80/// An implementation of the USB DFU 1.1 runtime protocol
81///
82/// 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.
83/// The handler is responsive to DFU GetStatus and Detach commands.
84///
85/// 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
86/// it should expose a DFU device, and a software reset will be issued.
87///
88/// 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.
89pub 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) {
90 #[cfg(feature = "defmt")]
91 defmt::info!("Application USB DFU initializing");
92 let mut func = builder.function(0x00, 0x00, 0x00);
93 let mut iface = func.interface();
94 let mut alt = iface.alt_setting(
95 USB_CLASS_APPN_SPEC,
96 APPN_SPEC_SUBCLASS_DFU,
97 DFU_PROTOCOL_RT,
98 None,
99 );
100 let timeout = timeout.as_millis() as u16;
101 alt.descriptor(
102 DESC_DFU_FUNCTIONAL,
103 &[
104 handler.attrs.bits(),
105 (timeout & 0xff) as u8,
106 ((timeout >> 8) & 0xff) as u8,
107 0x40, 0x00, // 64B control buffer size for application side
108 0x10, 0x01, // DFU 1.1
109 ],
110 );
111
112 drop(func);
113 builder.handler(handler);
114} \ No newline at end of file
diff --git a/embassy-usb-dfu/src/bootloader.rs b/embassy-usb-dfu/src/bootloader.rs
new file mode 100644
index 000000000..7bcb0b258
--- /dev/null
+++ b/embassy-usb-dfu/src/bootloader.rs
@@ -0,0 +1,196 @@
1use embassy_boot::BlockingFirmwareUpdater;
2use embassy_usb::{
3 control::{InResponse, OutResponse, Recipient, RequestType},
4 driver::Driver,
5 Builder, Handler,
6};
7use embedded_storage::nor_flash::{NorFlashErrorKind, NorFlash};
8
9use crate::consts::{DfuAttributes, Request, State, Status, USB_CLASS_APPN_SPEC, APPN_SPEC_SUBCLASS_DFU, DFU_PROTOCOL_DFU, DESC_DFU_FUNCTIONAL};
10
11/// Internal state for USB DFU
12pub struct Control<'d, DFU: NorFlash, STATE: NorFlash, const BLOCK_SIZE: usize> {
13 updater: BlockingFirmwareUpdater<'d, DFU, STATE>,
14 attrs: DfuAttributes,
15 state: State,
16 status: Status,
17 offset: usize,
18}
19
20impl<'d, DFU: NorFlash, STATE: NorFlash, const BLOCK_SIZE: usize> Control<'d, DFU, STATE, BLOCK_SIZE> {
21 pub fn new(updater: BlockingFirmwareUpdater<'d, DFU, STATE>, attrs: DfuAttributes) -> Self {
22 Self {
23 updater,
24 attrs,
25 state: State::DfuIdle,
26 status: Status::Ok,
27 offset: 0,
28 }
29 }
30
31 fn reset_state(&mut self) {
32 self.offset = 0;
33 self.state = State::DfuIdle;
34 self.status = Status::Ok;
35 }
36}
37
38impl<'d, DFU: NorFlash, STATE: NorFlash, const BLOCK_SIZE: usize> Handler for Control<'d, DFU, STATE, BLOCK_SIZE> {
39 fn control_out(
40 &mut self,
41 req: embassy_usb::control::Request,
42 data: &[u8],
43 ) -> Option<embassy_usb::control::OutResponse> {
44 if (req.request_type, req.recipient) != (RequestType::Class, Recipient::Interface) {
45 return None;
46 }
47 match Request::try_from(req.request) {
48 Ok(Request::Abort) => {
49 self.reset_state();
50 Some(OutResponse::Accepted)
51 }
52 Ok(Request::Dnload) if self.attrs.contains(DfuAttributes::CAN_DOWNLOAD) => {
53 if req.value == 0 {
54 self.state = State::Download;
55 self.offset = 0;
56 }
57
58 let mut buf = [0; BLOCK_SIZE];
59 buf[..data.len()].copy_from_slice(data);
60
61 if req.length == 0 {
62 match self.updater.mark_updated() {
63 Ok(_) => {
64 self.status = Status::Ok;
65 self.state = State::ManifestSync;
66 }
67 Err(e) => {
68 self.state = State::Error;
69 match e {
70 embassy_boot::FirmwareUpdaterError::Flash(e) => match e {
71 NorFlashErrorKind::NotAligned => self.status = Status::ErrWrite,
72 NorFlashErrorKind::OutOfBounds => {
73 self.status = Status::ErrAddress
74 }
75 _ => self.status = Status::ErrUnknown,
76 },
77 embassy_boot::FirmwareUpdaterError::Signature(_) => {
78 self.status = Status::ErrVerify
79 }
80 embassy_boot::FirmwareUpdaterError::BadState => {
81 self.status = Status::ErrUnknown
82 }
83 }
84 }
85 }
86 } else {
87 if self.state != State::Download {
88 // Unexpected DNLOAD while chip is waiting for a GETSTATUS
89 self.status = Status::ErrUnknown;
90 self.state = State::Error;
91 return Some(OutResponse::Rejected);
92 }
93 match self.updater.write_firmware(self.offset, &buf[..]) {
94 Ok(_) => {
95 self.status = Status::Ok;
96 self.state = State::DlSync;
97 self.offset += data.len();
98 }
99 Err(e) => {
100 self.state = State::Error;
101 match e {
102 embassy_boot::FirmwareUpdaterError::Flash(e) => match e {
103 NorFlashErrorKind::NotAligned => self.status = Status::ErrWrite,
104 NorFlashErrorKind::OutOfBounds => {
105 self.status = Status::ErrAddress
106 }
107 _ => self.status = Status::ErrUnknown,
108 },
109 embassy_boot::FirmwareUpdaterError::Signature(_) => {
110 self.status = Status::ErrVerify
111 }
112 embassy_boot::FirmwareUpdaterError::BadState => {
113 self.status = Status::ErrUnknown
114 }
115 }
116 }
117 }
118 }
119
120 Some(OutResponse::Accepted)
121 }
122 Ok(Request::Detach) => Some(OutResponse::Accepted), // Device is already in DFU mode
123 Ok(Request::ClrStatus) => {
124 self.reset_state();
125 Some(OutResponse::Accepted)
126 }
127 _ => None,
128 }
129 }
130
131 fn control_in<'a>(
132 &'a mut self,
133 req: embassy_usb::control::Request,
134 buf: &'a mut [u8],
135 ) -> Option<embassy_usb::control::InResponse<'a>> {
136 if (req.request_type, req.recipient) != (RequestType::Class, Recipient::Interface) {
137 return None;
138 }
139 match Request::try_from(req.request) {
140 Ok(Request::GetStatus) => {
141 //TODO: Configurable poll timeout, ability to add string for Vendor error
142 buf[0..6].copy_from_slice(&[self.status as u8, 0x32, 0x00, 0x00, self.state as u8, 0x00]);
143 match self.state {
144 State::DlSync => self.state = State::Download,
145 State::ManifestSync => cortex_m::peripheral::SCB::sys_reset(),
146 _ => {}
147 }
148
149 Some(InResponse::Accepted(&buf[0..6]))
150 }
151 Ok(Request::GetState) => {
152 buf[0] = self.state as u8;
153 Some(InResponse::Accepted(&buf[0..1]))
154 }
155 Ok(Request::Upload) if self.attrs.contains(DfuAttributes::CAN_UPLOAD) => {
156 //TODO: FirmwareUpdater does not provide a way of reading the active partition, can't upload.
157 Some(InResponse::Rejected)
158 }
159 _ => None,
160 }
161 }
162}
163
164/// An implementation of the USB DFU 1.1 protocol
165///
166/// This function will add a DFU interface descriptor to the provided Builder, and register the provided Control as a handler for the USB device
167/// The handler is responsive to DFU GetState, GetStatus, Abort, and ClrStatus commands, as well as Download if configured by the user.
168///
169/// Once the host has initiated a DFU download operation, the chunks sent by the host will be written to the DFU partition.
170/// Once the final sync in the manifestation phase has been received, the handler will trigger a system reset to swap the new firmware.
171pub fn usb_dfu<'d, D: Driver<'d>, DFU: NorFlash, STATE: NorFlash, const BLOCK_SIZE: usize>(
172 builder: &mut Builder<'d, D>,
173 handler: &'d mut Control<'d, DFU, STATE, BLOCK_SIZE>,
174) {
175 let mut func = builder.function(0x00, 0x00, 0x00);
176 let mut iface = func.interface();
177 let mut alt = iface.alt_setting(
178 USB_CLASS_APPN_SPEC,
179 APPN_SPEC_SUBCLASS_DFU,
180 DFU_PROTOCOL_DFU,
181 None,
182 );
183 alt.descriptor(
184 DESC_DFU_FUNCTIONAL,
185 &[
186 handler.attrs.bits(),
187 0xc4, 0x09, // 2500ms timeout, doesn't affect operation as DETACH not necessary in bootloader code
188 (BLOCK_SIZE & 0xff) as u8,
189 ((BLOCK_SIZE & 0xff00) >> 8) as u8,
190 0x10, 0x01, // DFU 1.1
191 ],
192 );
193
194 drop(func);
195 builder.handler(handler);
196}
diff --git a/embassy-usb-dfu/src/consts.rs b/embassy-usb-dfu/src/consts.rs
new file mode 100644
index 000000000..b083af9de
--- /dev/null
+++ b/embassy-usb-dfu/src/consts.rs
@@ -0,0 +1,96 @@
1
2pub(crate) const USB_CLASS_APPN_SPEC: u8 = 0xFE;
3pub(crate) const APPN_SPEC_SUBCLASS_DFU: u8 = 0x01;
4#[allow(unused)]
5pub(crate) const DFU_PROTOCOL_DFU: u8 = 0x02;
6#[allow(unused)]
7pub(crate) const DFU_PROTOCOL_RT: u8 = 0x01;
8pub(crate) const DESC_DFU_FUNCTIONAL: u8 = 0x21;
9
10#[cfg(feature = "defmt")]
11defmt::bitflags! {
12 pub struct DfuAttributes: u8 {
13 const WILL_DETACH = 0b0000_1000;
14 const MANIFESTATION_TOLERANT = 0b0000_0100;
15 const CAN_UPLOAD = 0b0000_0010;
16 const CAN_DOWNLOAD = 0b0000_0001;
17 }
18}
19
20#[cfg(not(feature = "defmt"))]
21bitflags::bitflags! {
22 pub struct DfuAttributes: u8 {
23 const WILL_DETACH = 0b0000_1000;
24 const MANIFESTATION_TOLERANT = 0b0000_0100;
25 const CAN_UPLOAD = 0b0000_0010;
26 const CAN_DOWNLOAD = 0b0000_0001;
27 }
28}
29
30#[derive(Copy, Clone, PartialEq, Eq)]
31#[repr(u8)]
32#[allow(unused)]
33pub enum State {
34 AppIdle = 0,
35 AppDetach = 1,
36 DfuIdle = 2,
37 DlSync = 3,
38 DlBusy = 4,
39 Download = 5,
40 ManifestSync = 6,
41 Manifest = 7,
42 ManifestWaitReset = 8,
43 UploadIdle = 9,
44 Error = 10,
45}
46
47#[derive(Copy, Clone, PartialEq, Eq)]
48#[repr(u8)]
49#[allow(unused)]
50pub enum Status {
51 Ok = 0x00,
52 ErrTarget = 0x01,
53 ErrFile = 0x02,
54 ErrWrite = 0x03,
55 ErrErase = 0x04,
56 ErrCheckErased = 0x05,
57 ErrProg = 0x06,
58 ErrVerify = 0x07,
59 ErrAddress = 0x08,
60 ErrNotDone = 0x09,
61 ErrFirmware = 0x0A,
62 ErrVendor = 0x0B,
63 ErrUsbr = 0x0C,
64 ErrPor = 0x0D,
65 ErrUnknown = 0x0E,
66 ErrStalledPkt = 0x0F,
67}
68
69#[derive(Copy, Clone, PartialEq, Eq)]
70#[repr(u8)]
71pub enum Request {
72 Detach = 0,
73 Dnload = 1,
74 Upload = 2,
75 GetStatus = 3,
76 ClrStatus = 4,
77 GetState = 5,
78 Abort = 6,
79}
80
81impl TryFrom<u8> for Request {
82 type Error = ();
83
84 fn try_from(value: u8) -> Result<Self, Self::Error> {
85 match value {
86 0 => Ok(Request::Detach),
87 1 => Ok(Request::Dnload),
88 2 => Ok(Request::Upload),
89 3 => Ok(Request::GetStatus),
90 4 => Ok(Request::ClrStatus),
91 5 => Ok(Request::GetState),
92 6 => Ok(Request::Abort),
93 _ => Err(()),
94 }
95 }
96}
diff --git a/embassy-usb-dfu/src/lib.rs b/embassy-usb-dfu/src/lib.rs
new file mode 100644
index 000000000..dcdb11b2a
--- /dev/null
+++ b/embassy-usb-dfu/src/lib.rs
@@ -0,0 +1,16 @@
1#![no_std]
2
3pub mod consts;
4
5#[cfg(feature = "bootloader")]
6mod bootloader;
7#[cfg(feature = "bootloader")]
8pub use self::bootloader::*;
9
10#[cfg(feature = "application")]
11mod application;
12#[cfg(feature = "application")]
13pub use self::application::*;
14
15#[cfg(any(all(feature = "bootloader", feature = "application"), not(any(feature = "bootloader", feature = "application"))))]
16compile_error!("usb-dfu must be compiled with exactly one of `bootloader`, or `application` features");