From d1e4b3d7d5a931cd6e04b7e2fe467945ef862477 Mon Sep 17 00:00:00 2001 From: Dario Nieuwenhuis Date: Wed, 30 Mar 2022 01:18:37 +0200 Subject: usb: add -usb-serial crate, fix warnings and stable build. --- embassy-nrf/Cargo.toml | 4 +- embassy-nrf/src/chips/nrf52820.rs | 1 + embassy-nrf/src/chips/nrf52833.rs | 1 + embassy-nrf/src/chips/nrf52840.rs | 1 + embassy-nrf/src/chips/nrf5340_app.rs | 1 + embassy-nrf/src/lib.rs | 1 + embassy-nrf/src/usb.rs | 11 +- embassy-usb-serial/Cargo.toml | 11 + embassy-usb-serial/src/fmt.rs | 225 ++++++++++++++++++++ embassy-usb-serial/src/lib.rs | 388 +++++++++++++++++++++++++++++++++++ embassy-usb/Cargo.toml | 2 - embassy-usb/src/descriptor.rs | 1 + embassy-usb/src/lib.rs | 10 +- examples/nrf/Cargo.toml | 1 + examples/nrf/src/bin/usb/cdc_acm.rs | 387 ---------------------------------- examples/nrf/src/bin/usb/main.rs | 94 --------- examples/nrf/src/bin/usb_serial.rs | 91 ++++++++ 17 files changed, 734 insertions(+), 496 deletions(-) create mode 100644 embassy-usb-serial/Cargo.toml create mode 100644 embassy-usb-serial/src/fmt.rs create mode 100644 embassy-usb-serial/src/lib.rs delete mode 100644 examples/nrf/src/bin/usb/cdc_acm.rs delete mode 100644 examples/nrf/src/bin/usb/main.rs create mode 100644 examples/nrf/src/bin/usb_serial.rs diff --git a/embassy-nrf/Cargo.toml b/embassy-nrf/Cargo.toml index 36c61c651..858ff1f6e 100644 --- a/embassy-nrf/Cargo.toml +++ b/embassy-nrf/Cargo.toml @@ -18,7 +18,7 @@ flavors = [ [features] # Enable nightly-only features -nightly = ["embassy/nightly", "embedded-hal-1", "embedded-hal-async"] +nightly = ["embassy/nightly", "embedded-hal-1", "embedded-hal-async", "embassy-usb"] # Reexport the PAC for the currently enabled chip at `embassy_nrf::pac`. # This is unstable because semver-minor (non-breaking) releases of embassy-nrf may major-bump (breaking) the PAC version. @@ -64,7 +64,7 @@ _gpio-p1 = [] embassy = { version = "0.1.0", path = "../embassy" } embassy-macros = { version = "0.1.0", path = "../embassy-macros", features = ["nrf"]} embassy-hal-common = {version = "0.1.0", path = "../embassy-hal-common" } -embassy-usb = {version = "0.1.0", path = "../embassy-usb" } +embassy-usb = {version = "0.1.0", path = "../embassy-usb", optional=true } embedded-hal-02 = { package = "embedded-hal", version = "0.2.6", features = ["unproven"] } embedded-hal-1 = { package = "embedded-hal", version = "1.0.0-alpha.7", git = "https://github.com/embassy-rs/embedded-hal", branch = "embassy2", optional = true} diff --git a/embassy-nrf/src/chips/nrf52820.rs b/embassy-nrf/src/chips/nrf52820.rs index aa2b2e61d..136ef4ec9 100644 --- a/embassy-nrf/src/chips/nrf52820.rs +++ b/embassy-nrf/src/chips/nrf52820.rs @@ -125,6 +125,7 @@ embassy_hal_common::peripherals! { TEMP, } +#[cfg(feature = "nightly")] impl_usb!(USBD, USBD, USBD); impl_uarte!(UARTE0, UARTE0, UARTE0_UART0); diff --git a/embassy-nrf/src/chips/nrf52833.rs b/embassy-nrf/src/chips/nrf52833.rs index 498a3c307..35cf4224d 100644 --- a/embassy-nrf/src/chips/nrf52833.rs +++ b/embassy-nrf/src/chips/nrf52833.rs @@ -157,6 +157,7 @@ embassy_hal_common::peripherals! { TEMP, } +#[cfg(feature = "nightly")] impl_usb!(USBD, USBD, USBD); impl_uarte!(UARTE0, UARTE0, UARTE0_UART0); diff --git a/embassy-nrf/src/chips/nrf52840.rs b/embassy-nrf/src/chips/nrf52840.rs index 411768146..d20abbfbd 100644 --- a/embassy-nrf/src/chips/nrf52840.rs +++ b/embassy-nrf/src/chips/nrf52840.rs @@ -160,6 +160,7 @@ embassy_hal_common::peripherals! { TEMP, } +#[cfg(feature = "nightly")] impl_usb!(USBD, USBD, USBD); impl_uarte!(UARTE0, UARTE0, UARTE0_UART0); diff --git a/embassy-nrf/src/chips/nrf5340_app.rs b/embassy-nrf/src/chips/nrf5340_app.rs index ae6887b3c..89579b69f 100644 --- a/embassy-nrf/src/chips/nrf5340_app.rs +++ b/embassy-nrf/src/chips/nrf5340_app.rs @@ -348,6 +348,7 @@ embassy_hal_common::peripherals! { P1_15, } +#[cfg(feature = "nightly")] impl_usb!(USBD, USBD, USBD); impl_uarte!(UARTETWISPI0, UARTE0, SERIAL0); diff --git a/embassy-nrf/src/lib.rs b/embassy-nrf/src/lib.rs index 06e8235e3..667e8ea38 100644 --- a/embassy-nrf/src/lib.rs +++ b/embassy-nrf/src/lib.rs @@ -91,6 +91,7 @@ pub mod uarte; feature = "nrf52833", feature = "nrf52840" ))] +#[cfg(feature = "nightly")] pub mod usb; #[cfg(not(feature = "_nrf5340"))] pub mod wdt; diff --git a/embassy-nrf/src/usb.rs b/embassy-nrf/src/usb.rs index 1cd5a9ebb..b26e40272 100644 --- a/embassy-nrf/src/usb.rs +++ b/embassy-nrf/src/usb.rs @@ -4,6 +4,7 @@ use core::marker::PhantomData; use core::mem::MaybeUninit; use core::sync::atomic::{compiler_fence, AtomicU32, Ordering}; use core::task::Poll; +use cortex_m::peripheral::NVIC; use embassy::interrupt::InterruptExt; use embassy::time::{with_timeout, Duration}; use embassy::util::Unborrow; @@ -14,7 +15,6 @@ use embassy_usb::driver::{self, Event, ReadError, WriteError}; use embassy_usb::types::{EndpointAddress, EndpointInfo, EndpointType, UsbDirection}; use futures::future::poll_fn; use futures::Future; -use pac::NVIC; pub use embassy_usb; @@ -617,7 +617,7 @@ impl<'d, T: Instance> driver::ControlPipe for ControlPipe<'d, T> { fn data_out<'a>(&'a mut self, buf: &'a mut [u8]) -> Self::DataOutFuture<'a> { async move { let req = self.request.unwrap(); - assert_eq!(req.direction, UsbDirection::Out); + assert!(req.direction == UsbDirection::Out); assert!(req.length > 0); let req_length = usize::from(req.length); @@ -644,9 +644,12 @@ impl<'d, T: Instance> driver::ControlPipe for ControlPipe<'d, T> { fn accept_in<'a>(&'a mut self, buf: &'a [u8]) -> Self::AcceptInFuture<'a> { async move { - info!("control accept {=[u8]:x}", buf); + #[cfg(feature = "defmt")] + info!("control accept {:x}", buf); + #[cfg(not(feature = "defmt"))] + info!("control accept {:x?}", buf); let req = self.request.unwrap(); - assert_eq!(req.direction, UsbDirection::In); + assert!(req.direction == UsbDirection::In); let req_len = usize::from(req.length); let len = buf.len().min(req_len); diff --git a/embassy-usb-serial/Cargo.toml b/embassy-usb-serial/Cargo.toml new file mode 100644 index 000000000..16e03046e --- /dev/null +++ b/embassy-usb-serial/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "embassy-usb-serial" +version = "0.1.0" +edition = "2021" + +[dependencies] +embassy = { version = "0.1.0", path = "../embassy" } +embassy-usb = { version = "0.1.0", path = "../embassy-usb" } + +defmt = { version = "0.3", optional = true } +log = { version = "0.4.14", optional = true } diff --git a/embassy-usb-serial/src/fmt.rs b/embassy-usb-serial/src/fmt.rs new file mode 100644 index 000000000..066970813 --- /dev/null +++ b/embassy-usb-serial/src/fmt.rs @@ -0,0 +1,225 @@ +#![macro_use] +#![allow(unused_macros)] + +#[cfg(all(feature = "defmt", feature = "log"))] +compile_error!("You may not enable both `defmt` and `log` features."); + +macro_rules! assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert!($($x)*); + } + }; +} + +macro_rules! assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_eq!($($x)*); + } + }; +} + +macro_rules! assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_ne!($($x)*); + } + }; +} + +macro_rules! debug_assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert!($($x)*); + } + }; +} + +macro_rules! debug_assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert_eq!($($x)*); + } + }; +} + +macro_rules! debug_assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert_ne!($($x)*); + } + }; +} + +macro_rules! todo { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::todo!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::todo!($($x)*); + } + }; +} + +macro_rules! unreachable { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::unreachable!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::unreachable!($($x)*); + } + }; +} + +macro_rules! panic { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::panic!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::panic!($($x)*); + } + }; +} + +macro_rules! trace { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::trace!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::trace!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! debug { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::debug!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::debug!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! info { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::info!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::info!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! warn { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::warn!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::warn!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! error { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::error!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::error!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[cfg(feature = "defmt")] +macro_rules! unwrap { + ($($x:tt)*) => { + ::defmt::unwrap!($($x)*) + }; +} + +#[cfg(not(feature = "defmt"))] +macro_rules! unwrap { + ($arg:expr) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {:?}", ::core::stringify!($arg), e); + } + } + }; + ($arg:expr, $($msg:expr),+ $(,)? ) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {}: {:?}", ::core::stringify!($arg), ::core::format_args!($($msg,)*), e); + } + } + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct NoneError; + +pub trait Try { + type Ok; + type Error; + fn into_result(self) -> Result; +} + +impl Try for Option { + type Ok = T; + type Error = NoneError; + + #[inline] + fn into_result(self) -> Result { + self.ok_or(NoneError) + } +} + +impl Try for Result { + type Ok = T; + type Error = E; + + #[inline] + fn into_result(self) -> Self { + self + } +} diff --git a/embassy-usb-serial/src/lib.rs b/embassy-usb-serial/src/lib.rs new file mode 100644 index 000000000..d6c31c86e --- /dev/null +++ b/embassy-usb-serial/src/lib.rs @@ -0,0 +1,388 @@ +#![no_std] +#![feature(generic_associated_types)] +#![feature(type_alias_impl_trait)] + +// This mod MUST go first, so that the others see its macros. +pub(crate) mod fmt; + +use core::cell::Cell; +use core::mem::{self, MaybeUninit}; +use core::sync::atomic::{AtomicBool, Ordering}; +use embassy::blocking_mutex::CriticalSectionMutex; +use embassy_usb::control::{self, ControlHandler, InResponse, OutResponse, Request}; +use embassy_usb::driver::{Endpoint, EndpointIn, EndpointOut, ReadError, WriteError}; +use embassy_usb::{driver::Driver, types::*, UsbDeviceBuilder}; + +/// This should be used as `device_class` when building the `UsbDevice`. +pub const USB_CLASS_CDC: u8 = 0x02; + +const USB_CLASS_CDC_DATA: u8 = 0x0a; +const CDC_SUBCLASS_ACM: u8 = 0x02; +const CDC_PROTOCOL_NONE: u8 = 0x00; + +const CS_INTERFACE: u8 = 0x24; +const CDC_TYPE_HEADER: u8 = 0x00; +const CDC_TYPE_CALL_MANAGEMENT: u8 = 0x01; +const CDC_TYPE_ACM: u8 = 0x02; +const CDC_TYPE_UNION: u8 = 0x06; + +const REQ_SEND_ENCAPSULATED_COMMAND: u8 = 0x00; +#[allow(unused)] +const REQ_GET_ENCAPSULATED_COMMAND: u8 = 0x01; +const REQ_SET_LINE_CODING: u8 = 0x20; +const REQ_GET_LINE_CODING: u8 = 0x21; +const REQ_SET_CONTROL_LINE_STATE: u8 = 0x22; + +pub struct State<'a> { + control: MaybeUninit>, + shared: ControlShared, +} + +impl<'a> State<'a> { + pub fn new() -> Self { + Self { + control: MaybeUninit::uninit(), + shared: Default::default(), + } + } +} + +/// Packet level implementation of a CDC-ACM serial port. +/// +/// This class can be used directly and it has the least overhead due to directly reading and +/// writing USB packets with no intermediate buffers, but it will not act like a stream-like serial +/// port. The following constraints must be followed if you use this class directly: +/// +/// - `read_packet` must be called with a buffer large enough to hold max_packet_size bytes, and the +/// method will return a `WouldBlock` error if there is no packet to be read. +/// - `write_packet` must not be called with a buffer larger than max_packet_size bytes, and the +/// method will return a `WouldBlock` error if the previous packet has not been sent yet. +/// - If you write a packet that is exactly max_packet_size bytes long, it won't be processed by the +/// host operating system until a subsequent shorter packet is sent. A zero-length packet (ZLP) +/// can be sent if there is no other data to send. This is because USB bulk transactions must be +/// terminated with a short packet, even if the bulk endpoint is used for stream-like data. +pub struct CdcAcmClass<'d, D: Driver<'d>> { + // TODO not pub + pub comm_ep: D::EndpointIn, + pub data_if: InterfaceNumber, + pub read_ep: D::EndpointOut, + pub write_ep: D::EndpointIn, + control: &'d ControlShared, +} + +struct Control<'a> { + shared: &'a ControlShared, +} + +/// Shared data between Control and CdcAcmClass +struct ControlShared { + line_coding: CriticalSectionMutex>, + dtr: AtomicBool, + rts: AtomicBool, +} + +impl Default for ControlShared { + fn default() -> Self { + ControlShared { + dtr: AtomicBool::new(false), + rts: AtomicBool::new(false), + line_coding: CriticalSectionMutex::new(Cell::new(LineCoding { + stop_bits: StopBits::One, + data_bits: 8, + parity_type: ParityType::None, + data_rate: 8_000, + })), + } + } +} + +impl<'a> Control<'a> { + fn shared(&mut self) -> &'a ControlShared { + self.shared + } +} + +impl<'d> ControlHandler for Control<'d> { + fn reset(&mut self) { + let shared = self.shared(); + shared.line_coding.lock(|x| x.set(LineCoding::default())); + shared.dtr.store(false, Ordering::Relaxed); + shared.rts.store(false, Ordering::Relaxed); + } + + fn control_out(&mut self, req: control::Request, data: &[u8]) -> OutResponse { + match req.request { + REQ_SEND_ENCAPSULATED_COMMAND => { + // We don't actually support encapsulated commands but pretend we do for standards + // compatibility. + OutResponse::Accepted + } + REQ_SET_LINE_CODING if data.len() >= 7 => { + let coding = LineCoding { + data_rate: u32::from_le_bytes(data[0..4].try_into().unwrap()), + stop_bits: data[4].into(), + parity_type: data[5].into(), + data_bits: data[6], + }; + self.shared().line_coding.lock(|x| x.set(coding)); + info!("Set line coding to: {:?}", coding); + + OutResponse::Accepted + } + REQ_SET_CONTROL_LINE_STATE => { + let dtr = (req.value & 0x0001) != 0; + let rts = (req.value & 0x0002) != 0; + + let shared = self.shared(); + shared.dtr.store(dtr, Ordering::Relaxed); + shared.rts.store(rts, Ordering::Relaxed); + info!("Set dtr {}, rts {}", dtr, rts); + + OutResponse::Accepted + } + _ => OutResponse::Rejected, + } + } + + fn control_in<'a>(&'a mut self, req: Request, buf: &'a mut [u8]) -> InResponse<'a> { + match req.request { + // REQ_GET_ENCAPSULATED_COMMAND is not really supported - it will be rejected below. + REQ_GET_LINE_CODING if req.length == 7 => { + info!("Sending line coding"); + let coding = self.shared().line_coding.lock(|x| x.get()); + assert!(buf.len() >= 7); + buf[0..4].copy_from_slice(&coding.data_rate.to_le_bytes()); + buf[4] = coding.stop_bits as u8; + buf[5] = coding.parity_type as u8; + buf[6] = coding.data_bits; + InResponse::Accepted(&buf[0..7]) + } + _ => InResponse::Rejected, + } + } +} + +impl<'d, D: Driver<'d>> CdcAcmClass<'d, D> { + /// Creates a new CdcAcmClass with the provided UsbBus and max_packet_size in bytes. For + /// full-speed devices, max_packet_size has to be one of 8, 16, 32 or 64. + pub fn new( + builder: &mut UsbDeviceBuilder<'d, D>, + state: &'d mut State<'d>, + max_packet_size: u16, + ) -> Self { + let control = state.control.write(Control { + shared: &state.shared, + }); + + let control_shared = &state.shared; + + assert!(builder.control_buf_len() >= 7); + + let comm_if = builder.alloc_interface_with_handler(control); + let comm_ep = builder.alloc_interrupt_endpoint_in(8, 255); + let data_if = builder.alloc_interface(); + let read_ep = builder.alloc_bulk_endpoint_out(max_packet_size); + let write_ep = builder.alloc_bulk_endpoint_in(max_packet_size); + + builder + .config_descriptor + .iad( + comm_if, + 2, + USB_CLASS_CDC, + CDC_SUBCLASS_ACM, + CDC_PROTOCOL_NONE, + ) + .unwrap(); + + builder + .config_descriptor + .interface(comm_if, USB_CLASS_CDC, CDC_SUBCLASS_ACM, CDC_PROTOCOL_NONE) + .unwrap(); + + builder + .config_descriptor + .write( + CS_INTERFACE, + &[ + CDC_TYPE_HEADER, // bDescriptorSubtype + 0x10, + 0x01, // bcdCDC (1.10) + ], + ) + .unwrap(); + + builder + .config_descriptor + .write( + CS_INTERFACE, + &[ + CDC_TYPE_ACM, // bDescriptorSubtype + 0x00, // bmCapabilities + ], + ) + .unwrap(); + + builder + .config_descriptor + .write( + CS_INTERFACE, + &[ + CDC_TYPE_UNION, // bDescriptorSubtype + comm_if.into(), // bControlInterface + data_if.into(), // bSubordinateInterface + ], + ) + .unwrap(); + + builder + .config_descriptor + .write( + CS_INTERFACE, + &[ + CDC_TYPE_CALL_MANAGEMENT, // bDescriptorSubtype + 0x00, // bmCapabilities + data_if.into(), // bDataInterface + ], + ) + .unwrap(); + + builder.config_descriptor.endpoint(comm_ep.info()).unwrap(); + + builder + .config_descriptor + .interface(data_if, USB_CLASS_CDC_DATA, 0x00, 0x00) + .unwrap(); + + builder.config_descriptor.endpoint(write_ep.info()).unwrap(); + builder.config_descriptor.endpoint(read_ep.info()).unwrap(); + + CdcAcmClass { + comm_ep, + data_if, + read_ep, + write_ep, + control: control_shared, + } + } + + /// Gets the maximum packet size in bytes. + pub fn max_packet_size(&self) -> u16 { + // The size is the same for both endpoints. + self.read_ep.info().max_packet_size + } + + /// Gets the current line coding. The line coding contains information that's mainly relevant + /// for USB to UART serial port emulators, and can be ignored if not relevant. + pub fn line_coding(&self) -> LineCoding { + self.control.line_coding.lock(|x| x.get()) + } + + /// Gets the DTR (data terminal ready) state + pub fn dtr(&self) -> bool { + self.control.dtr.load(Ordering::Relaxed) + } + + /// Gets the RTS (request to send) state + pub fn rts(&self) -> bool { + self.control.rts.load(Ordering::Relaxed) + } + + /// Writes a single packet into the IN endpoint. + pub async fn write_packet(&mut self, data: &[u8]) -> Result<(), WriteError> { + self.write_ep.write(data).await + } + + /// Reads a single packet from the OUT endpoint. + pub async fn read_packet(&mut self, data: &mut [u8]) -> Result { + self.read_ep.read(data).await + } +} + +/// Number of stop bits for LineCoding +#[derive(Copy, Clone, PartialEq, Eq, defmt::Format)] +pub enum StopBits { + /// 1 stop bit + One = 0, + + /// 1.5 stop bits + OnePointFive = 1, + + /// 2 stop bits + Two = 2, +} + +impl From for StopBits { + fn from(value: u8) -> Self { + if value <= 2 { + unsafe { mem::transmute(value) } + } else { + StopBits::One + } + } +} + +/// Parity for LineCoding +#[derive(Copy, Clone, PartialEq, Eq, defmt::Format)] +pub enum ParityType { + None = 0, + Odd = 1, + Event = 2, + Mark = 3, + Space = 4, +} + +impl From for ParityType { + fn from(value: u8) -> Self { + if value <= 4 { + unsafe { mem::transmute(value) } + } else { + ParityType::None + } + } +} + +/// Line coding parameters +/// +/// This is provided by the host for specifying the standard UART parameters such as baud rate. Can +/// be ignored if you don't plan to interface with a physical UART. +#[derive(Clone, Copy, defmt::Format)] +pub struct LineCoding { + stop_bits: StopBits, + data_bits: u8, + parity_type: ParityType, + data_rate: u32, +} + +impl LineCoding { + /// Gets the number of stop bits for UART communication. + pub fn stop_bits(&self) -> StopBits { + self.stop_bits + } + + /// Gets the number of data bits for UART communication. + pub fn data_bits(&self) -> u8 { + self.data_bits + } + + /// Gets the parity type for UART communication. + pub fn parity_type(&self) -> ParityType { + self.parity_type + } + + /// Gets the data rate in bits per second for UART communication. + pub fn data_rate(&self) -> u32 { + self.data_rate + } +} + +impl Default for LineCoding { + fn default() -> Self { + LineCoding { + stop_bits: StopBits::One, + data_bits: 8, + parity_type: ParityType::None, + data_rate: 8_000, + } + } +} diff --git a/embassy-usb/Cargo.toml b/embassy-usb/Cargo.toml index af5986c15..01bf5ef6f 100644 --- a/embassy-usb/Cargo.toml +++ b/embassy-usb/Cargo.toml @@ -8,6 +8,4 @@ embassy = { version = "0.1.0", path = "../embassy" } defmt = { version = "0.3", optional = true } log = { version = "0.4.14", optional = true } -cortex-m = "0.7.3" -num-traits = { version = "0.2.14", default-features = false } heapless = "0.7.10" \ No newline at end of file diff --git a/embassy-usb/src/descriptor.rs b/embassy-usb/src/descriptor.rs index 746c6b828..5f8b0d560 100644 --- a/embassy-usb/src/descriptor.rs +++ b/embassy-usb/src/descriptor.rs @@ -136,6 +136,7 @@ impl<'a> DescriptorWriter<'a> { ) } + #[allow(unused)] pub(crate) fn end_class(&mut self) { self.num_endpoints_mark = None; } diff --git a/embassy-usb/src/lib.rs b/embassy-usb/src/lib.rs index cbb909244..5a6b21906 100644 --- a/embassy-usb/src/lib.rs +++ b/embassy-usb/src/lib.rs @@ -302,7 +302,6 @@ impl<'d, D: Driver<'d>> UsbDevice<'d, D> { async fn handle_get_descriptor(&mut self, req: Request) { let (dtype, index) = req.descriptor_type_index(); - let config = self.config.clone(); match dtype { descriptor_type::BOS => self.control.accept_in(self.bos_descriptor).await, @@ -321,13 +320,10 @@ impl<'d, D: Driver<'d>> UsbDevice<'d, D> { 2 => self.config.product, 3 => self.config.serial_number, _ => { - let index = StringIndex::new(index); - let lang_id = req.index; + let _index = StringIndex::new(index); + let _lang_id = req.index; + // TODO None - //classes - // .iter() - // .filter_map(|cls| cls.get_string(index, lang_id)) - // .nth(0) } }; diff --git a/examples/nrf/Cargo.toml b/examples/nrf/Cargo.toml index 59e5de026..58450a045 100644 --- a/examples/nrf/Cargo.toml +++ b/examples/nrf/Cargo.toml @@ -12,6 +12,7 @@ nightly = ["embassy-nrf/nightly", "embassy-nrf/unstable-traits"] embassy = { version = "0.1.0", path = "../../embassy", features = ["defmt", "defmt-timestamp-uptime"] } embassy-nrf = { version = "0.1.0", path = "../../embassy-nrf", features = ["defmt", "nrf52840", "time-driver-rtc1", "gpiote", "unstable-pac"] } embassy-usb = { version = "0.1.0", path = "../../embassy-usb", features = ["defmt"] } +embassy-usb-serial = { version = "0.1.0", path = "../../embassy-usb-serial", features = ["defmt"] } defmt = "0.3" defmt-rtt = "0.3" diff --git a/examples/nrf/src/bin/usb/cdc_acm.rs b/examples/nrf/src/bin/usb/cdc_acm.rs deleted file mode 100644 index c28681dc4..000000000 --- a/examples/nrf/src/bin/usb/cdc_acm.rs +++ /dev/null @@ -1,387 +0,0 @@ -use core::cell::Cell; -use core::mem::{self, MaybeUninit}; -use core::sync::atomic::{AtomicBool, Ordering}; -use defmt::info; -use embassy::blocking_mutex::CriticalSectionMutex; -use embassy_usb::control::{self, ControlHandler, InResponse, OutResponse, Request}; -use embassy_usb::driver::{Endpoint, EndpointIn, EndpointOut, ReadError, WriteError}; -use embassy_usb::{driver::Driver, types::*, UsbDeviceBuilder}; - -/// This should be used as `device_class` when building the `UsbDevice`. -pub const USB_CLASS_CDC: u8 = 0x02; - -const USB_CLASS_CDC_DATA: u8 = 0x0a; -const CDC_SUBCLASS_ACM: u8 = 0x02; -const CDC_PROTOCOL_NONE: u8 = 0x00; - -const CS_INTERFACE: u8 = 0x24; -const CDC_TYPE_HEADER: u8 = 0x00; -const CDC_TYPE_CALL_MANAGEMENT: u8 = 0x01; -const CDC_TYPE_ACM: u8 = 0x02; -const CDC_TYPE_UNION: u8 = 0x06; - -const REQ_SEND_ENCAPSULATED_COMMAND: u8 = 0x00; -#[allow(unused)] -const REQ_GET_ENCAPSULATED_COMMAND: u8 = 0x01; -const REQ_SET_LINE_CODING: u8 = 0x20; -const REQ_GET_LINE_CODING: u8 = 0x21; -const REQ_SET_CONTROL_LINE_STATE: u8 = 0x22; - -pub struct State<'a> { - control: MaybeUninit>, - shared: ControlShared, -} - -impl<'a> State<'a> { - pub fn new() -> Self { - Self { - control: MaybeUninit::uninit(), - shared: Default::default(), - } - } -} - -/// Packet level implementation of a CDC-ACM serial port. -/// -/// This class can be used directly and it has the least overhead due to directly reading and -/// writing USB packets with no intermediate buffers, but it will not act like a stream-like serial -/// port. The following constraints must be followed if you use this class directly: -/// -/// - `read_packet` must be called with a buffer large enough to hold max_packet_size bytes, and the -/// method will return a `WouldBlock` error if there is no packet to be read. -/// - `write_packet` must not be called with a buffer larger than max_packet_size bytes, and the -/// method will return a `WouldBlock` error if the previous packet has not been sent yet. -/// - If you write a packet that is exactly max_packet_size bytes long, it won't be processed by the -/// host operating system until a subsequent shorter packet is sent. A zero-length packet (ZLP) -/// can be sent if there is no other data to send. This is because USB bulk transactions must be -/// terminated with a short packet, even if the bulk endpoint is used for stream-like data. -pub struct CdcAcmClass<'d, D: Driver<'d>> { - // TODO not pub - pub comm_ep: D::EndpointIn, - pub data_if: InterfaceNumber, - pub read_ep: D::EndpointOut, - pub write_ep: D::EndpointIn, - control: &'d ControlShared, -} - -struct Control<'a> { - shared: &'a ControlShared, -} - -/// Shared data between Control and CdcAcmClass -struct ControlShared { - line_coding: CriticalSectionMutex>, - dtr: AtomicBool, - rts: AtomicBool, -} - -impl Default for ControlShared { - fn default() -> Self { - ControlShared { - dtr: AtomicBool::new(false), - rts: AtomicBool::new(false), - line_coding: CriticalSectionMutex::new(Cell::new(LineCoding { - stop_bits: StopBits::One, - data_bits: 8, - parity_type: ParityType::None, - data_rate: 8_000, - })), - } - } -} - -impl<'a> Control<'a> { - fn shared(&mut self) -> &'a ControlShared { - self.shared - } -} - -impl<'d> ControlHandler for Control<'d> { - fn reset(&mut self) { - let shared = self.shared(); - shared.line_coding.lock(|x| x.set(LineCoding::default())); - shared.dtr.store(false, Ordering::Relaxed); - shared.rts.store(false, Ordering::Relaxed); - } - - fn control_out(&mut self, req: control::Request, data: &[u8]) -> OutResponse { - match req.request { - REQ_SEND_ENCAPSULATED_COMMAND => { - // We don't actually support encapsulated commands but pretend we do for standards - // compatibility. - OutResponse::Accepted - } - REQ_SET_LINE_CODING if data.len() >= 7 => { - let coding = LineCoding { - data_rate: u32::from_le_bytes(data[0..4].try_into().unwrap()), - stop_bits: data[4].into(), - parity_type: data[5].into(), - data_bits: data[6], - }; - self.shared().line_coding.lock(|x| x.set(coding)); - info!("Set line coding to: {:?}", coding); - - OutResponse::Accepted - } - REQ_SET_CONTROL_LINE_STATE => { - let dtr = (req.value & 0x0001) != 0; - let rts = (req.value & 0x0002) != 0; - - let shared = self.shared(); - shared.dtr.store(dtr, Ordering::Relaxed); - shared.rts.store(rts, Ordering::Relaxed); - info!("Set dtr {}, rts {}", dtr, rts); - - OutResponse::Accepted - } - _ => OutResponse::Rejected, - } - } - - fn control_in<'a>(&'a mut self, req: Request, buf: &'a mut [u8]) -> InResponse<'a> { - match req.request { - // REQ_GET_ENCAPSULATED_COMMAND is not really supported - it will be rejected below. - REQ_GET_LINE_CODING if req.length == 7 => { - info!("Sending line coding"); - let coding = self.shared().line_coding.lock(|x| x.get()); - assert!(buf.len() >= 7); - buf[0..4].copy_from_slice(&coding.data_rate.to_le_bytes()); - buf[4] = coding.stop_bits as u8; - buf[5] = coding.parity_type as u8; - buf[6] = coding.data_bits; - InResponse::Accepted(&buf[0..7]) - } - _ => InResponse::Rejected, - } - } -} - -impl<'d, D: Driver<'d>> CdcAcmClass<'d, D> { - /// Creates a new CdcAcmClass with the provided UsbBus and max_packet_size in bytes. For - /// full-speed devices, max_packet_size has to be one of 8, 16, 32 or 64. - pub fn new( - builder: &mut UsbDeviceBuilder<'d, D>, - state: &'d mut State<'d>, - max_packet_size: u16, - ) -> Self { - let control = state.control.write(Control { - shared: &state.shared, - }); - - let control_shared = &state.shared; - - assert!(builder.control_buf_len() >= 7); - - let comm_if = builder.alloc_interface_with_handler(control); - let comm_ep = builder.alloc_interrupt_endpoint_in(8, 255); - let data_if = builder.alloc_interface(); - let read_ep = builder.alloc_bulk_endpoint_out(max_packet_size); - let write_ep = builder.alloc_bulk_endpoint_in(max_packet_size); - - builder - .config_descriptor - .iad( - comm_if, - 2, - USB_CLASS_CDC, - CDC_SUBCLASS_ACM, - CDC_PROTOCOL_NONE, - ) - .unwrap(); - - builder - .config_descriptor - .interface(comm_if, USB_CLASS_CDC, CDC_SUBCLASS_ACM, CDC_PROTOCOL_NONE) - .unwrap(); - - builder - .config_descriptor - .write( - CS_INTERFACE, - &[ - CDC_TYPE_HEADER, // bDescriptorSubtype - 0x10, - 0x01, // bcdCDC (1.10) - ], - ) - .unwrap(); - - builder - .config_descriptor - .write( - CS_INTERFACE, - &[ - CDC_TYPE_ACM, // bDescriptorSubtype - 0x00, // bmCapabilities - ], - ) - .unwrap(); - - builder - .config_descriptor - .write( - CS_INTERFACE, - &[ - CDC_TYPE_UNION, // bDescriptorSubtype - comm_if.into(), // bControlInterface - data_if.into(), // bSubordinateInterface - ], - ) - .unwrap(); - - builder - .config_descriptor - .write( - CS_INTERFACE, - &[ - CDC_TYPE_CALL_MANAGEMENT, // bDescriptorSubtype - 0x00, // bmCapabilities - data_if.into(), // bDataInterface - ], - ) - .unwrap(); - - builder.config_descriptor.endpoint(comm_ep.info()).unwrap(); - - builder - .config_descriptor - .interface(data_if, USB_CLASS_CDC_DATA, 0x00, 0x00) - .unwrap(); - - builder.config_descriptor.endpoint(write_ep.info()).unwrap(); - builder.config_descriptor.endpoint(read_ep.info()).unwrap(); - - CdcAcmClass { - comm_ep, - data_if, - read_ep, - write_ep, - control: control_shared, - } - } - - /// Gets the maximum packet size in bytes. - pub fn max_packet_size(&self) -> u16 { - // The size is the same for both endpoints. - self.read_ep.info().max_packet_size - } - - /// Gets the current line coding. The line coding contains information that's mainly relevant - /// for USB to UART serial port emulators, and can be ignored if not relevant. - pub fn line_coding(&self) -> LineCoding { - self.control.line_coding.lock(|x| x.get()) - } - - /// Gets the DTR (data terminal ready) state - pub fn dtr(&self) -> bool { - self.control.dtr.load(Ordering::Relaxed) - } - - /// Gets the RTS (request to send) state - pub fn rts(&self) -> bool { - self.control.rts.load(Ordering::Relaxed) - } - - /// Writes a single packet into the IN endpoint. - pub async fn write_packet(&mut self, data: &[u8]) -> Result<(), WriteError> { - self.write_ep.write(data).await - } - - /// Reads a single packet from the OUT endpoint. - pub async fn read_packet(&mut self, data: &mut [u8]) -> Result { - self.read_ep.read(data).await - } - - /// Gets the address of the IN endpoint. - pub(crate) fn write_ep_address(&self) -> EndpointAddress { - self.write_ep.info().addr - } -} - -/// Number of stop bits for LineCoding -#[derive(Copy, Clone, PartialEq, Eq, defmt::Format)] -pub enum StopBits { - /// 1 stop bit - One = 0, - - /// 1.5 stop bits - OnePointFive = 1, - - /// 2 stop bits - Two = 2, -} - -impl From for StopBits { - fn from(value: u8) -> Self { - if value <= 2 { - unsafe { mem::transmute(value) } - } else { - StopBits::One - } - } -} - -/// Parity for LineCoding -#[derive(Copy, Clone, PartialEq, Eq, defmt::Format)] -pub enum ParityType { - None = 0, - Odd = 1, - Event = 2, - Mark = 3, - Space = 4, -} - -impl From for ParityType { - fn from(value: u8) -> Self { - if value <= 4 { - unsafe { mem::transmute(value) } - } else { - ParityType::None - } - } -} - -/// Line coding parameters -/// -/// This is provided by the host for specifying the standard UART parameters such as baud rate. Can -/// be ignored if you don't plan to interface with a physical UART. -#[derive(Clone, Copy, defmt::Format)] -pub struct LineCoding { - stop_bits: StopBits, - data_bits: u8, - parity_type: ParityType, - data_rate: u32, -} - -impl LineCoding { - /// Gets the number of stop bits for UART communication. - pub fn stop_bits(&self) -> StopBits { - self.stop_bits - } - - /// Gets the number of data bits for UART communication. - pub fn data_bits(&self) -> u8 { - self.data_bits - } - - /// Gets the parity type for UART communication. - pub fn parity_type(&self) -> ParityType { - self.parity_type - } - - /// Gets the data rate in bits per second for UART communication. - pub fn data_rate(&self) -> u32 { - self.data_rate - } -} - -impl Default for LineCoding { - fn default() -> Self { - LineCoding { - stop_bits: StopBits::One, - data_bits: 8, - parity_type: ParityType::None, - data_rate: 8_000, - } - } -} diff --git a/examples/nrf/src/bin/usb/main.rs b/examples/nrf/src/bin/usb/main.rs deleted file mode 100644 index c4b9c0176..000000000 --- a/examples/nrf/src/bin/usb/main.rs +++ /dev/null @@ -1,94 +0,0 @@ -#![no_std] -#![no_main] -#![feature(generic_associated_types)] -#![feature(type_alias_impl_trait)] - -#[path = "../../example_common.rs"] -mod example_common; - -mod cdc_acm; - -use core::mem; -use defmt::*; -use embassy::executor::Spawner; -use embassy::time::{Duration, Timer}; -use embassy_nrf::interrupt; -use embassy_nrf::pac; -use embassy_nrf::usb::Driver; -use embassy_nrf::Peripherals; -use embassy_usb::driver::{EndpointIn, EndpointOut}; -use embassy_usb::{Config, UsbDeviceBuilder}; -use futures::future::join3; - -use crate::cdc_acm::{CdcAcmClass, State}; - -#[embassy::main] -async fn main(_spawner: Spawner, p: Peripherals) { - let clock: pac::CLOCK = unsafe { mem::transmute(()) }; - let power: pac::POWER = unsafe { mem::transmute(()) }; - - info!("Enabling ext hfosc..."); - clock.tasks_hfclkstart.write(|w| unsafe { w.bits(1) }); - while clock.events_hfclkstarted.read().bits() != 1 {} - - info!("Waiting for vbus..."); - while !power.usbregstatus.read().vbusdetect().is_vbus_present() {} - info!("vbus OK"); - - // Create the driver, from the HAL. - let irq = interrupt::take!(USBD); - let driver = Driver::new(p.USBD, irq); - - // Create embassy-usb Config - let config = Config::new(0xc0de, 0xcafe); - - // Create embassy-usb DeviceBuilder using the driver and config. - // It needs some buffers for building the descriptors. - let mut device_descriptor = [0; 256]; - let mut config_descriptor = [0; 256]; - let mut bos_descriptor = [0; 256]; - let mut control_buf = [0; 7]; - - let mut state = State::new(); - - let mut builder = UsbDeviceBuilder::new( - driver, - config, - &mut device_descriptor, - &mut config_descriptor, - &mut bos_descriptor, - &mut control_buf, - ); - - // Create classes on the builder. - let mut class = CdcAcmClass::new(&mut builder, &mut state, 64); - - // Build the builder. - let mut usb = builder.build(); - - // Run the USB device. - let fut1 = usb.run(); - - // Do stuff with the classes - let fut2 = async { - let mut buf = [0; 64]; - loop { - let n = class.read_ep.read(&mut buf).await.unwrap(); - let data = &buf[..n]; - info!("data: {:x}", data); - } - }; - let fut3 = async { - loop { - info!("writing..."); - class.write_ep.write(b"Hello World!\r\n").await.unwrap(); - info!("written"); - - Timer::after(Duration::from_secs(1)).await; - } - }; - - // Run everything concurrently. - // If we had made everything `'static` above instead, we could do this using separate tasks instead. - join3(fut1, fut2, fut3).await; -} diff --git a/examples/nrf/src/bin/usb_serial.rs b/examples/nrf/src/bin/usb_serial.rs new file mode 100644 index 000000000..0a4ff9489 --- /dev/null +++ b/examples/nrf/src/bin/usb_serial.rs @@ -0,0 +1,91 @@ +#![no_std] +#![no_main] +#![feature(generic_associated_types)] +#![feature(type_alias_impl_trait)] + +use core::mem; +use defmt::*; +use embassy::executor::Spawner; +use embassy::time::{Duration, Timer}; +use embassy_nrf::interrupt; +use embassy_nrf::pac; +use embassy_nrf::usb::Driver; +use embassy_nrf::Peripherals; +use embassy_usb::driver::{EndpointIn, EndpointOut}; +use embassy_usb::{Config, UsbDeviceBuilder}; +use embassy_usb_serial::{CdcAcmClass, State}; +use futures::future::join3; + +use defmt_rtt as _; // global logger +use panic_probe as _; + +#[embassy::main] +async fn main(_spawner: Spawner, p: Peripherals) { + let clock: pac::CLOCK = unsafe { mem::transmute(()) }; + let power: pac::POWER = unsafe { mem::transmute(()) }; + + info!("Enabling ext hfosc..."); + clock.tasks_hfclkstart.write(|w| unsafe { w.bits(1) }); + while clock.events_hfclkstarted.read().bits() != 1 {} + + info!("Waiting for vbus..."); + while !power.usbregstatus.read().vbusdetect().is_vbus_present() {} + info!("vbus OK"); + + // Create the driver, from the HAL. + let irq = interrupt::take!(USBD); + let driver = Driver::new(p.USBD, irq); + + // Create embassy-usb Config + let config = Config::new(0xc0de, 0xcafe); + + // Create embassy-usb DeviceBuilder using the driver and config. + // It needs some buffers for building the descriptors. + let mut device_descriptor = [0; 256]; + let mut config_descriptor = [0; 256]; + let mut bos_descriptor = [0; 256]; + let mut control_buf = [0; 7]; + + let mut state = State::new(); + + let mut builder = UsbDeviceBuilder::new( + driver, + config, + &mut device_descriptor, + &mut config_descriptor, + &mut bos_descriptor, + &mut control_buf, + ); + + // Create classes on the builder. + let mut class = CdcAcmClass::new(&mut builder, &mut state, 64); + + // Build the builder. + let mut usb = builder.build(); + + // Run the USB device. + let fut1 = usb.run(); + + // Do stuff with the classes + let fut2 = async { + let mut buf = [0; 64]; + loop { + let n = class.read_ep.read(&mut buf).await.unwrap(); + let data = &buf[..n]; + info!("data: {:x}", data); + } + }; + let fut3 = async { + loop { + info!("writing..."); + class.write_ep.write(b"Hello World!\r\n").await.unwrap(); + info!("written"); + + Timer::after(Duration::from_secs(1)).await; + } + }; + + // Run everything concurrently. + // If we had made everything `'static` above instead, we could do this using separate tasks instead. + join3(fut1, fut2, fut3).await; +} -- cgit