diff options
| -rw-r--r-- | embassy-boot/boot/src/boot_loader.rs | 4 | ||||
| -rw-r--r-- | embassy-boot/boot/src/firmware_updater/asynch.rs | 13 | ||||
| -rw-r--r-- | embassy-boot/boot/src/firmware_updater/blocking.rs | 17 | ||||
| -rw-r--r-- | embassy-boot/boot/src/lib.rs | 3 | ||||
| -rw-r--r-- | embassy-boot/stm32/src/lib.rs | 9 | ||||
| -rw-r--r-- | embassy-usb-dfu/Cargo.toml | 31 | ||||
| -rw-r--r-- | embassy-usb-dfu/src/application.rs | 114 | ||||
| -rw-r--r-- | embassy-usb-dfu/src/bootloader.rs | 196 | ||||
| -rw-r--r-- | embassy-usb-dfu/src/consts.rs | 96 | ||||
| -rw-r--r-- | embassy-usb-dfu/src/lib.rs | 16 |
10 files changed, 492 insertions, 7 deletions
diff --git a/embassy-boot/boot/src/boot_loader.rs b/embassy-boot/boot/src/boot_loader.rs index a8c19197b..c0deca22b 100644 --- a/embassy-boot/boot/src/boot_loader.rs +++ b/embassy-boot/boot/src/boot_loader.rs | |||
| @@ -5,7 +5,7 @@ use embassy_sync::blocking_mutex::raw::NoopRawMutex; | |||
| 5 | use embassy_sync::blocking_mutex::Mutex; | 5 | use embassy_sync::blocking_mutex::Mutex; |
| 6 | use embedded_storage::nor_flash::{NorFlash, NorFlashError, NorFlashErrorKind}; | 6 | use embedded_storage::nor_flash::{NorFlash, NorFlashError, NorFlashErrorKind}; |
| 7 | 7 | ||
| 8 | use crate::{State, BOOT_MAGIC, STATE_ERASE_VALUE, SWAP_MAGIC}; | 8 | use crate::{State, BOOT_MAGIC, STATE_ERASE_VALUE, SWAP_MAGIC, DFU_DETACH_MAGIC}; |
| 9 | 9 | ||
| 10 | /// Errors returned by bootloader | 10 | /// Errors returned by bootloader |
| 11 | #[derive(PartialEq, Eq, Debug)] | 11 | #[derive(PartialEq, Eq, Debug)] |
| @@ -384,6 +384,8 @@ impl<ACTIVE: NorFlash, DFU: NorFlash, STATE: NorFlash> BootLoader<ACTIVE, DFU, S | |||
| 384 | 384 | ||
| 385 | if !state_word.iter().any(|&b| b != SWAP_MAGIC) { | 385 | if !state_word.iter().any(|&b| b != SWAP_MAGIC) { |
| 386 | Ok(State::Swap) | 386 | Ok(State::Swap) |
| 387 | } else if !state_word.iter().any(|&b| b != DFU_DETACH_MAGIC) { | ||
| 388 | Ok(State::DfuDetach) | ||
| 387 | } else { | 389 | } else { |
| 388 | Ok(State::Boot) | 390 | Ok(State::Boot) |
| 389 | } | 391 | } |
diff --git a/embassy-boot/boot/src/firmware_updater/asynch.rs b/embassy-boot/boot/src/firmware_updater/asynch.rs index ae713bb6f..0a3cbc136 100644 --- a/embassy-boot/boot/src/firmware_updater/asynch.rs +++ b/embassy-boot/boot/src/firmware_updater/asynch.rs | |||
| @@ -6,7 +6,7 @@ use embassy_sync::blocking_mutex::raw::NoopRawMutex; | |||
| 6 | use embedded_storage_async::nor_flash::NorFlash; | 6 | use embedded_storage_async::nor_flash::NorFlash; |
| 7 | 7 | ||
| 8 | use super::FirmwareUpdaterConfig; | 8 | use super::FirmwareUpdaterConfig; |
| 9 | use crate::{FirmwareUpdaterError, State, BOOT_MAGIC, STATE_ERASE_VALUE, SWAP_MAGIC}; | 9 | use crate::{FirmwareUpdaterError, State, BOOT_MAGIC, STATE_ERASE_VALUE, SWAP_MAGIC, DFU_DETACH_MAGIC}; |
| 10 | 10 | ||
| 11 | /// FirmwareUpdater is an application API for interacting with the BootLoader without the ability to | 11 | /// FirmwareUpdater is an application API for interacting with the BootLoader without the ability to |
| 12 | /// 'mess up' the internal bootloader state | 12 | /// 'mess up' the internal bootloader state |
| @@ -161,6 +161,12 @@ impl<'d, DFU: NorFlash, STATE: NorFlash> FirmwareUpdater<'d, DFU, STATE> { | |||
| 161 | self.state.mark_updated().await | 161 | self.state.mark_updated().await |
| 162 | } | 162 | } |
| 163 | 163 | ||
| 164 | /// Mark to trigger USB DFU on next boot. | ||
| 165 | pub async fn mark_dfu(&mut self) -> Result<(), FirmwareUpdaterError> { | ||
| 166 | self.state.verify_booted().await?; | ||
| 167 | self.state.mark_dfu().await | ||
| 168 | } | ||
| 169 | |||
| 164 | /// Mark firmware boot successful and stop rollback on reset. | 170 | /// Mark firmware boot successful and stop rollback on reset. |
| 165 | pub async fn mark_booted(&mut self) -> Result<(), FirmwareUpdaterError> { | 171 | pub async fn mark_booted(&mut self) -> Result<(), FirmwareUpdaterError> { |
| 166 | self.state.mark_booted().await | 172 | self.state.mark_booted().await |
| @@ -247,6 +253,11 @@ impl<'d, STATE: NorFlash> FirmwareState<'d, STATE> { | |||
| 247 | self.set_magic(SWAP_MAGIC).await | 253 | self.set_magic(SWAP_MAGIC).await |
| 248 | } | 254 | } |
| 249 | 255 | ||
| 256 | /// Mark to trigger USB DFU on next boot. | ||
| 257 | pub async fn mark_dfu(&mut self) -> Result<(), FirmwareUpdaterError> { | ||
| 258 | self.set_magic(DFU_DETACH_MAGIC).await | ||
| 259 | } | ||
| 260 | |||
| 250 | /// Mark firmware boot successful and stop rollback on reset. | 261 | /// Mark firmware boot successful and stop rollback on reset. |
| 251 | pub async fn mark_booted(&mut self) -> Result<(), FirmwareUpdaterError> { | 262 | pub async fn mark_booted(&mut self) -> Result<(), FirmwareUpdaterError> { |
| 252 | self.set_magic(BOOT_MAGIC).await | 263 | self.set_magic(BOOT_MAGIC).await |
diff --git a/embassy-boot/boot/src/firmware_updater/blocking.rs b/embassy-boot/boot/src/firmware_updater/blocking.rs index 76e4264a0..b2a633d1e 100644 --- a/embassy-boot/boot/src/firmware_updater/blocking.rs +++ b/embassy-boot/boot/src/firmware_updater/blocking.rs | |||
| @@ -6,7 +6,7 @@ use embassy_sync::blocking_mutex::raw::NoopRawMutex; | |||
| 6 | use embedded_storage::nor_flash::NorFlash; | 6 | use embedded_storage::nor_flash::NorFlash; |
| 7 | 7 | ||
| 8 | use super::FirmwareUpdaterConfig; | 8 | use super::FirmwareUpdaterConfig; |
| 9 | use crate::{FirmwareUpdaterError, State, BOOT_MAGIC, STATE_ERASE_VALUE, SWAP_MAGIC}; | 9 | use crate::{FirmwareUpdaterError, State, BOOT_MAGIC, STATE_ERASE_VALUE, SWAP_MAGIC, DFU_DETACH_MAGIC}; |
| 10 | 10 | ||
| 11 | /// Blocking FirmwareUpdater is an application API for interacting with the BootLoader without the ability to | 11 | /// Blocking FirmwareUpdater is an application API for interacting with the BootLoader without the ability to |
| 12 | /// 'mess up' the internal bootloader state | 12 | /// 'mess up' the internal bootloader state |
| @@ -168,6 +168,12 @@ impl<'d, DFU: NorFlash, STATE: NorFlash> BlockingFirmwareUpdater<'d, DFU, STATE> | |||
| 168 | self.state.mark_updated() | 168 | self.state.mark_updated() |
| 169 | } | 169 | } |
| 170 | 170 | ||
| 171 | /// Mark to trigger USB DFU device on next boot. | ||
| 172 | pub fn mark_dfu(&mut self) -> Result<(), FirmwareUpdaterError> { | ||
| 173 | self.state.verify_booted()?; | ||
| 174 | self.state.mark_dfu() | ||
| 175 | } | ||
| 176 | |||
| 171 | /// Mark firmware boot successful and stop rollback on reset. | 177 | /// Mark firmware boot successful and stop rollback on reset. |
| 172 | pub fn mark_booted(&mut self) -> Result<(), FirmwareUpdaterError> { | 178 | pub fn mark_booted(&mut self) -> Result<(), FirmwareUpdaterError> { |
| 173 | self.state.mark_booted() | 179 | self.state.mark_booted() |
| @@ -226,7 +232,7 @@ impl<'d, STATE: NorFlash> BlockingFirmwareState<'d, STATE> { | |||
| 226 | 232 | ||
| 227 | // Make sure we are running a booted firmware to avoid reverting to a bad state. | 233 | // Make sure we are running a booted firmware to avoid reverting to a bad state. |
| 228 | fn verify_booted(&mut self) -> Result<(), FirmwareUpdaterError> { | 234 | fn verify_booted(&mut self) -> Result<(), FirmwareUpdaterError> { |
| 229 | if self.get_state()? == State::Boot { | 235 | if self.get_state()? == State::Boot || self.get_state()? == State::DfuDetach { |
| 230 | Ok(()) | 236 | Ok(()) |
| 231 | } else { | 237 | } else { |
| 232 | Err(FirmwareUpdaterError::BadState) | 238 | Err(FirmwareUpdaterError::BadState) |
| @@ -243,6 +249,8 @@ impl<'d, STATE: NorFlash> BlockingFirmwareState<'d, STATE> { | |||
| 243 | 249 | ||
| 244 | if !self.aligned.iter().any(|&b| b != SWAP_MAGIC) { | 250 | if !self.aligned.iter().any(|&b| b != SWAP_MAGIC) { |
| 245 | Ok(State::Swap) | 251 | Ok(State::Swap) |
| 252 | } else if !self.aligned.iter().any(|&b| b != DFU_DETACH_MAGIC) { | ||
| 253 | Ok(State::DfuDetach) | ||
| 246 | } else { | 254 | } else { |
| 247 | Ok(State::Boot) | 255 | Ok(State::Boot) |
| 248 | } | 256 | } |
| @@ -253,6 +261,11 @@ impl<'d, STATE: NorFlash> BlockingFirmwareState<'d, STATE> { | |||
| 253 | self.set_magic(SWAP_MAGIC) | 261 | self.set_magic(SWAP_MAGIC) |
| 254 | } | 262 | } |
| 255 | 263 | ||
| 264 | /// Mark to trigger USB DFU on next boot. | ||
| 265 | pub fn mark_dfu(&mut self) -> Result<(), FirmwareUpdaterError> { | ||
| 266 | self.set_magic(DFU_DETACH_MAGIC) | ||
| 267 | } | ||
| 268 | |||
| 256 | /// Mark firmware boot successful and stop rollback on reset. | 269 | /// Mark firmware boot successful and stop rollback on reset. |
| 257 | pub fn mark_booted(&mut self) -> Result<(), FirmwareUpdaterError> { | 270 | pub fn mark_booted(&mut self) -> Result<(), FirmwareUpdaterError> { |
| 258 | self.set_magic(BOOT_MAGIC) | 271 | self.set_magic(BOOT_MAGIC) |
diff --git a/embassy-boot/boot/src/lib.rs b/embassy-boot/boot/src/lib.rs index 9e70a4dca..451992945 100644 --- a/embassy-boot/boot/src/lib.rs +++ b/embassy-boot/boot/src/lib.rs | |||
| @@ -23,6 +23,7 @@ pub use firmware_updater::{ | |||
| 23 | 23 | ||
| 24 | pub(crate) const BOOT_MAGIC: u8 = 0xD0; | 24 | pub(crate) const BOOT_MAGIC: u8 = 0xD0; |
| 25 | pub(crate) const SWAP_MAGIC: u8 = 0xF0; | 25 | pub(crate) const SWAP_MAGIC: u8 = 0xF0; |
| 26 | pub(crate) const DFU_DETACH_MAGIC: u8 = 0xE0; | ||
| 26 | 27 | ||
| 27 | /// The state of the bootloader after running prepare. | 28 | /// The state of the bootloader after running prepare. |
| 28 | #[derive(PartialEq, Eq, Debug)] | 29 | #[derive(PartialEq, Eq, Debug)] |
| @@ -32,6 +33,8 @@ pub enum State { | |||
| 32 | Boot, | 33 | Boot, |
| 33 | /// Bootloader has swapped the active partition with the dfu partition and will attempt boot. | 34 | /// Bootloader has swapped the active partition with the dfu partition and will attempt boot. |
| 34 | Swap, | 35 | Swap, |
| 36 | /// Application has received a DFU_DETACH request over USB, and is rebooting into the bootloader to apply a DFU. | ||
| 37 | DfuDetach, | ||
| 35 | } | 38 | } |
| 36 | 39 | ||
| 37 | /// Buffer aligned to 32 byte boundary, largest known alignment requirement for embassy-boot. | 40 | /// Buffer aligned to 32 byte boundary, largest known alignment requirement for embassy-boot. |
diff --git a/embassy-boot/stm32/src/lib.rs b/embassy-boot/stm32/src/lib.rs index c418cb262..4b4091ac9 100644 --- a/embassy-boot/stm32/src/lib.rs +++ b/embassy-boot/stm32/src/lib.rs | |||
| @@ -10,7 +10,10 @@ pub use embassy_boot::{ | |||
| 10 | use embedded_storage::nor_flash::NorFlash; | 10 | use embedded_storage::nor_flash::NorFlash; |
| 11 | 11 | ||
| 12 | /// A bootloader for STM32 devices. | 12 | /// A bootloader for STM32 devices. |
| 13 | pub struct BootLoader; | 13 | pub struct BootLoader { |
| 14 | /// The reported state of the bootloader after preparing for boot | ||
| 15 | pub state: State, | ||
| 16 | } | ||
| 14 | 17 | ||
| 15 | impl BootLoader { | 18 | impl BootLoader { |
| 16 | /// Inspect the bootloader state and perform actions required before booting, such as swapping firmware | 19 | /// Inspect the bootloader state and perform actions required before booting, such as swapping firmware |
| @@ -19,8 +22,8 @@ impl BootLoader { | |||
| 19 | ) -> Self { | 22 | ) -> Self { |
| 20 | let mut aligned_buf = AlignedBuffer([0; BUFFER_SIZE]); | 23 | let mut aligned_buf = AlignedBuffer([0; BUFFER_SIZE]); |
| 21 | let mut boot = embassy_boot::BootLoader::new(config); | 24 | let mut boot = embassy_boot::BootLoader::new(config); |
| 22 | boot.prepare_boot(aligned_buf.as_mut()).expect("Boot prepare error"); | 25 | let state = boot.prepare_boot(aligned_buf.as_mut()).expect("Boot prepare error"); |
| 23 | Self | 26 | Self { state } |
| 24 | } | 27 | } |
| 25 | 28 | ||
| 26 | /// Boots the application. | 29 | /// Boots the application. |
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] | ||
| 2 | edition = "2021" | ||
| 3 | name = "embassy-usb-dfu" | ||
| 4 | version = "0.1.0" | ||
| 5 | description = "An implementation of the USB DFU 1.1 protocol, using embassy-boot" | ||
| 6 | license = "MIT OR Apache-2.0" | ||
| 7 | repository = "https://github.com/embassy-rs/embassy" | ||
| 8 | categories = [ | ||
| 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] | ||
| 17 | bitflags = "2.4.1" | ||
| 18 | cortex-m = { version = "0.7.7", features = ["inline-asm"] } | ||
| 19 | defmt = { version = "0.3.5", optional = true } | ||
| 20 | embassy-boot = { version = "0.1.1", path = "../embassy-boot/boot" } | ||
| 21 | embassy-embedded-hal = { version = "0.1.0", path = "../embassy-embedded-hal" } | ||
| 22 | embassy-futures = { version = "0.1.1", path = "../embassy-futures" } | ||
| 23 | embassy-sync = { version = "0.5.0", path = "../embassy-sync" } | ||
| 24 | embassy-time = { version = "0.2.0", path = "../embassy-time" } | ||
| 25 | embassy-usb = { version = "0.1.0", path = "../embassy-usb", default-features = false } | ||
| 26 | embedded-storage = { version = "0.3.1" } | ||
| 27 | |||
| 28 | [features] | ||
| 29 | bootloader = [] | ||
| 30 | application = [] | ||
| 31 | defmt = ["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 | |||
| 2 | use embassy_boot::BlockingFirmwareUpdater; | ||
| 3 | use embassy_time::{Instant, Duration}; | ||
| 4 | use embassy_usb::{Handler, control::{RequestType, Recipient, OutResponse, InResponse}, Builder, driver::Driver}; | ||
| 5 | use embedded_storage::nor_flash::NorFlash; | ||
| 6 | |||
| 7 | use 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 | ||
| 10 | pub 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 | |||
| 18 | impl<'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 | |||
| 24 | impl<'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. | ||
| 89 | 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) { | ||
| 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 @@ | |||
| 1 | use embassy_boot::BlockingFirmwareUpdater; | ||
| 2 | use embassy_usb::{ | ||
| 3 | control::{InResponse, OutResponse, Recipient, RequestType}, | ||
| 4 | driver::Driver, | ||
| 5 | Builder, Handler, | ||
| 6 | }; | ||
| 7 | use embedded_storage::nor_flash::{NorFlashErrorKind, NorFlash}; | ||
| 8 | |||
| 9 | use 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 | ||
| 12 | pub 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 | |||
| 20 | impl<'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 | |||
| 38 | impl<'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. | ||
| 171 | pub 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 | |||
| 2 | pub(crate) const USB_CLASS_APPN_SPEC: u8 = 0xFE; | ||
| 3 | pub(crate) const APPN_SPEC_SUBCLASS_DFU: u8 = 0x01; | ||
| 4 | #[allow(unused)] | ||
| 5 | pub(crate) const DFU_PROTOCOL_DFU: u8 = 0x02; | ||
| 6 | #[allow(unused)] | ||
| 7 | pub(crate) const DFU_PROTOCOL_RT: u8 = 0x01; | ||
| 8 | pub(crate) const DESC_DFU_FUNCTIONAL: u8 = 0x21; | ||
| 9 | |||
| 10 | #[cfg(feature = "defmt")] | ||
| 11 | defmt::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"))] | ||
| 21 | bitflags::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)] | ||
| 33 | pub 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)] | ||
| 50 | pub 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)] | ||
| 71 | pub 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 | |||
| 81 | impl 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 | |||
| 3 | pub mod consts; | ||
| 4 | |||
| 5 | #[cfg(feature = "bootloader")] | ||
| 6 | mod bootloader; | ||
| 7 | #[cfg(feature = "bootloader")] | ||
| 8 | pub use self::bootloader::*; | ||
| 9 | |||
| 10 | #[cfg(feature = "application")] | ||
| 11 | mod application; | ||
| 12 | #[cfg(feature = "application")] | ||
| 13 | pub use self::application::*; | ||
| 14 | |||
| 15 | #[cfg(any(all(feature = "bootloader", feature = "application"), not(any(feature = "bootloader", feature = "application"))))] | ||
| 16 | compile_error!("usb-dfu must be compiled with exactly one of `bootloader`, or `application` features"); | ||
