diff options
author | Dario Nieuwenhuis <dirbaio@dirbaio.net> | 2022-09-26 13:00:21 +0200 |
---|---|---|
committer | Dario Nieuwenhuis <dirbaio@dirbaio.net> | 2022-09-26 13:00:21 +0200 |
commit | f27a47a37b59bf3b9079f4d4d5f43caf7b7872f8 (patch) | |
tree | 732f73b4da7a2e726203f2876651a2141d9468be /embassy-usb | |
parent | f4f58249722bc656a13865e06535d208440c3e4a (diff) | |
download | embassy-f27a47a37b59bf3b9079f4d4d5f43caf7b7872f8.zip |
usb: move classes into the `embassy-usb` crate.
Diffstat (limited to 'embassy-usb')
-rw-r--r-- | embassy-usb/Cargo.toml | 9 | ||||
-rw-r--r-- | embassy-usb/src/class/cdc_acm.rs | 354 | ||||
-rw-r--r-- | embassy-usb/src/class/cdc_ncm.rs | 478 | ||||
-rw-r--r-- | embassy-usb/src/class/hid.rs | 504 | ||||
-rw-r--r-- | embassy-usb/src/class/mod.rs | 3 | ||||
-rw-r--r-- | embassy-usb/src/lib.rs | 1 |
6 files changed, 1348 insertions, 1 deletions
diff --git a/embassy-usb/Cargo.toml b/embassy-usb/Cargo.toml index 660ecc8c..aad54dba 100644 --- a/embassy-usb/Cargo.toml +++ b/embassy-usb/Cargo.toml @@ -11,11 +11,18 @@ target = "thumbv7em-none-eabi" [features] defmt = ["dep:defmt", "embassy-usb-driver/defmt"] +usbd-hid = ["dep:usbd-hid", "dep:ssmarshal"] +default = ["usbd-hid"] [dependencies] embassy-futures = { version = "0.1.0", path = "../embassy-futures" } embassy-usb-driver = { version = "0.1.0", path = "../embassy-usb-driver" } +embassy-sync = { version = "0.1.0", path = "../embassy-sync" } defmt = { version = "0.3", optional = true } log = { version = "0.4.14", optional = true } -heapless = "0.7.10"
\ No newline at end of file +heapless = "0.7.10" + +# for HID +usbd-hid = { version = "0.6.0", optional = true } +ssmarshal = { version = "1.0", default-features = false, optional = true } diff --git a/embassy-usb/src/class/cdc_acm.rs b/embassy-usb/src/class/cdc_acm.rs new file mode 100644 index 00000000..09bb1cc8 --- /dev/null +++ b/embassy-usb/src/class/cdc_acm.rs @@ -0,0 +1,354 @@ +use core::cell::Cell; +use core::mem::{self, MaybeUninit}; +use core::sync::atomic::{AtomicBool, Ordering}; + +use embassy_sync::blocking_mutex::CriticalSectionMutex; + +use crate::control::{self, ControlHandler, InResponse, OutResponse, Request}; +use crate::driver::{Driver, Endpoint, EndpointError, EndpointIn, EndpointOut}; +use crate::types::*; +use crate::Builder; + +/// 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<Control<'a>>, + 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. +/// - `write_packet` must not be called with a buffer larger than max_packet_size bytes. +/// - 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>> { + _comm_ep: D::EndpointIn, + _data_if: InterfaceNumber, + read_ep: D::EndpointOut, + write_ep: D::EndpointIn, + control: &'d ControlShared, +} + +struct Control<'a> { + shared: &'a ControlShared, +} + +/// Shared data between Control and CdcAcmClass +struct ControlShared { + line_coding: CriticalSectionMutex<Cell<LineCoding>>, + 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)); + debug!("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); + debug!("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 => { + debug!("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 Builder<'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 mut func = builder.function(USB_CLASS_CDC, CDC_SUBCLASS_ACM, CDC_PROTOCOL_NONE); + + // Control interface + let mut iface = func.interface(); + iface.handler(control); + let comm_if = iface.interface_number(); + let data_if = u8::from(comm_if) + 1; + let mut alt = iface.alt_setting(USB_CLASS_CDC, CDC_SUBCLASS_ACM, CDC_PROTOCOL_NONE); + + alt.descriptor( + CS_INTERFACE, + &[ + CDC_TYPE_HEADER, // bDescriptorSubtype + 0x10, + 0x01, // bcdCDC (1.10) + ], + ); + alt.descriptor( + CS_INTERFACE, + &[ + CDC_TYPE_ACM, // bDescriptorSubtype + 0x00, // bmCapabilities + ], + ); + alt.descriptor( + CS_INTERFACE, + &[ + CDC_TYPE_UNION, // bDescriptorSubtype + comm_if.into(), // bControlInterface + data_if.into(), // bSubordinateInterface + ], + ); + alt.descriptor( + CS_INTERFACE, + &[ + CDC_TYPE_CALL_MANAGEMENT, // bDescriptorSubtype + 0x00, // bmCapabilities + data_if.into(), // bDataInterface + ], + ); + + let comm_ep = alt.endpoint_interrupt_in(8, 255); + + // Data interface + let mut iface = func.interface(); + let data_if = iface.interface_number(); + let mut alt = iface.alt_setting(USB_CLASS_CDC_DATA, 0x00, CDC_PROTOCOL_NONE); + let read_ep = alt.endpoint_bulk_out(max_packet_size); + let write_ep = alt.endpoint_bulk_in(max_packet_size); + + CdcAcmClass { + _comm_ep: comm_ep, + _data_if: 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<(), EndpointError> { + 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<usize, EndpointError> { + self.read_ep.read(data).await + } + + /// Waits for the USB host to enable this interface + pub async fn wait_connection(&mut self) { + self.read_ep.wait_enabled().await + } +} + +/// Number of stop bits for LineCoding +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum StopBits { + /// 1 stop bit + One = 0, + + /// 1.5 stop bits + OnePointFive = 1, + + /// 2 stop bits + Two = 2, +} + +impl From<u8> for StopBits { + fn from(value: u8) -> Self { + if value <= 2 { + unsafe { mem::transmute(value) } + } else { + StopBits::One + } + } +} + +/// Parity for LineCoding +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ParityType { + None = 0, + Odd = 1, + Even = 2, + Mark = 3, + Space = 4, +} + +impl From<u8> 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, Debug)] +#[cfg_attr(feature = "defmt", derive(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/src/class/cdc_ncm.rs b/embassy-usb/src/class/cdc_ncm.rs new file mode 100644 index 00000000..a39b87e9 --- /dev/null +++ b/embassy-usb/src/class/cdc_ncm.rs @@ -0,0 +1,478 @@ +use core::intrinsics::copy_nonoverlapping; +use core::mem::{size_of, MaybeUninit}; + +use crate::control::{self, ControlHandler, InResponse, OutResponse, Request}; +use crate::driver::{Driver, Endpoint, EndpointError, EndpointIn, EndpointOut}; +use crate::types::*; +use crate::Builder; + +/// 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_NCM: u8 = 0x0d; + +const CDC_PROTOCOL_NONE: u8 = 0x00; +const CDC_PROTOCOL_NTB: u8 = 0x01; + +const CS_INTERFACE: u8 = 0x24; +const CDC_TYPE_HEADER: u8 = 0x00; +const CDC_TYPE_UNION: u8 = 0x06; +const CDC_TYPE_ETHERNET: u8 = 0x0F; +const CDC_TYPE_NCM: u8 = 0x1A; + +const REQ_SEND_ENCAPSULATED_COMMAND: u8 = 0x00; +//const REQ_GET_ENCAPSULATED_COMMAND: u8 = 0x01; +//const REQ_SET_ETHERNET_MULTICAST_FILTERS: u8 = 0x40; +//const REQ_SET_ETHERNET_POWER_MANAGEMENT_PATTERN_FILTER: u8 = 0x41; +//const REQ_GET_ETHERNET_POWER_MANAGEMENT_PATTERN_FILTER: u8 = 0x42; +//const REQ_SET_ETHERNET_PACKET_FILTER: u8 = 0x43; +//const REQ_GET_ETHERNET_STATISTIC: u8 = 0x44; +const REQ_GET_NTB_PARAMETERS: u8 = 0x80; +//const REQ_GET_NET_ADDRESS: u8 = 0x81; +//const REQ_SET_NET_ADDRESS: u8 = 0x82; +//const REQ_GET_NTB_FORMAT: u8 = 0x83; +//const REQ_SET_NTB_FORMAT: u8 = 0x84; +//const REQ_GET_NTB_INPUT_SIZE: u8 = 0x85; +const REQ_SET_NTB_INPUT_SIZE: u8 = 0x86; +//const REQ_GET_MAX_DATAGRAM_SIZE: u8 = 0x87; +//const REQ_SET_MAX_DATAGRAM_SIZE: u8 = 0x88; +//const REQ_GET_CRC_MODE: u8 = 0x89; +//const REQ_SET_CRC_MODE: u8 = 0x8A; + +//const NOTIF_MAX_PACKET_SIZE: u16 = 8; +//const NOTIF_POLL_INTERVAL: u8 = 20; + +const NTB_MAX_SIZE: usize = 2048; +const SIG_NTH: u32 = 0x484d434e; +const SIG_NDP_NO_FCS: u32 = 0x304d434e; +const SIG_NDP_WITH_FCS: u32 = 0x314d434e; + +const ALTERNATE_SETTING_DISABLED: u8 = 0x00; +const ALTERNATE_SETTING_ENABLED: u8 = 0x01; + +/// Simple NTB header (NTH+NDP all in one) for sending packets +#[repr(packed)] +#[allow(unused)] +struct NtbOutHeader { + // NTH + nth_sig: u32, + nth_len: u16, + nth_seq: u16, + nth_total_len: u16, + nth_first_index: u16, + + // NDP + ndp_sig: u32, + ndp_len: u16, + ndp_next_index: u16, + ndp_datagram_index: u16, + ndp_datagram_len: u16, + ndp_term1: u16, + ndp_term2: u16, +} + +#[repr(packed)] +#[allow(unused)] +struct NtbParameters { + length: u16, + formats_supported: u16, + in_params: NtbParametersDir, + out_params: NtbParametersDir, +} + +#[repr(packed)] +#[allow(unused)] +struct NtbParametersDir { + max_size: u32, + divisor: u16, + payload_remainder: u16, + out_alignment: u16, + max_datagram_count: u16, +} + +fn byteify<T>(buf: &mut [u8], data: T) -> &[u8] { + let len = size_of::<T>(); + unsafe { copy_nonoverlapping(&data as *const _ as *const u8, buf.as_mut_ptr(), len) } + &buf[..len] +} + +pub struct State<'a> { + comm_control: MaybeUninit<CommControl<'a>>, + data_control: MaybeUninit<DataControl>, + shared: ControlShared, +} + +impl<'a> State<'a> { + pub fn new() -> Self { + Self { + comm_control: MaybeUninit::uninit(), + data_control: MaybeUninit::uninit(), + shared: Default::default(), + } + } +} + +/// Shared data between Control and CdcAcmClass +struct ControlShared { + mac_addr: [u8; 6], +} + +impl Default for ControlShared { + fn default() -> Self { + ControlShared { mac_addr: [0; 6] } + } +} + +struct CommControl<'a> { + mac_addr_string: StringIndex, + shared: &'a ControlShared, + mac_addr_str: [u8; 12], +} + +impl<'d> ControlHandler for CommControl<'d> { + 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_NTB_INPUT_SIZE => { + // TODO + OutResponse::Accepted + } + _ => OutResponse::Rejected, + } + } + + fn control_in<'a>(&'a mut self, req: Request, buf: &'a mut [u8]) -> InResponse<'a> { + match req.request { + REQ_GET_NTB_PARAMETERS => { + let res = NtbParameters { + length: size_of::<NtbParameters>() as _, + formats_supported: 1, // only 16bit, + in_params: NtbParametersDir { + max_size: NTB_MAX_SIZE as _, + divisor: 4, + payload_remainder: 0, + out_alignment: 4, + max_datagram_count: 0, // not used + }, + out_params: NtbParametersDir { + max_size: NTB_MAX_SIZE as _, + divisor: 4, + payload_remainder: 0, + out_alignment: 4, + max_datagram_count: 1, // We only decode 1 packet per NTB + }, + }; + InResponse::Accepted(byteify(buf, res)) + } + _ => InResponse::Rejected, + } + } + + fn get_string(&mut self, index: StringIndex, _lang_id: u16) -> Option<&str> { + if index == self.mac_addr_string { + let mac_addr = self.shared.mac_addr; + let s = &mut self.mac_addr_str; + for i in 0..12 { + let n = (mac_addr[i / 2] >> ((1 - i % 2) * 4)) & 0xF; + s[i] = match n { + 0x0..=0x9 => b'0' + n, + 0xA..=0xF => b'A' + n - 0xA, + _ => unreachable!(), + } + } + + Some(unsafe { core::str::from_utf8_unchecked(s) }) + } else { + warn!("unknown string index requested"); + None + } + } +} + +struct DataControl {} + +impl ControlHandler for DataControl { + fn set_alternate_setting(&mut self, alternate_setting: u8) { + match alternate_setting { + ALTERNATE_SETTING_ENABLED => info!("ncm: interface enabled"), + ALTERNATE_SETTING_DISABLED => info!("ncm: interface disabled"), + _ => unreachable!(), + } + } +} + +pub struct CdcNcmClass<'d, D: Driver<'d>> { + _comm_if: InterfaceNumber, + comm_ep: D::EndpointIn, + + data_if: InterfaceNumber, + read_ep: D::EndpointOut, + write_ep: D::EndpointIn, + + _control: &'d ControlShared, +} + +impl<'d, D: Driver<'d>> CdcNcmClass<'d, D> { + pub fn new( + builder: &mut Builder<'d, D>, + state: &'d mut State<'d>, + mac_address: [u8; 6], + max_packet_size: u16, + ) -> Self { + state.shared.mac_addr = mac_address; + + let mut func = builder.function(USB_CLASS_CDC, CDC_SUBCLASS_NCM, CDC_PROTOCOL_NONE); + + // Control interface + let mut iface = func.interface(); + let mac_addr_string = iface.string(); + iface.handler(state.comm_control.write(CommControl { + mac_addr_string, + shared: &state.shared, + mac_addr_str: [0; 12], + })); + let comm_if = iface.interface_number(); + let mut alt = iface.alt_setting(USB_CLASS_CDC, CDC_SUBCLASS_NCM, CDC_PROTOCOL_NONE); + + alt.descriptor( + CS_INTERFACE, + &[ + CDC_TYPE_HEADER, // bDescriptorSubtype + 0x10, + 0x01, // bcdCDC (1.10) + ], + ); + alt.descriptor( + CS_INTERFACE, + &[ + CDC_TYPE_UNION, // bDescriptorSubtype + comm_if.into(), // bControlInterface + u8::from(comm_if) + 1, // bSubordinateInterface + ], + ); + alt.descriptor( + CS_INTERFACE, + &[ + CDC_TYPE_ETHERNET, // bDescriptorSubtype + mac_addr_string.into(), // iMACAddress + 0, // bmEthernetStatistics + 0, // | + 0, // | + 0, // | + 0xea, // wMaxSegmentSize = 1514 + 0x05, // | + 0, // wNumberMCFilters + 0, // | + 0, // bNumberPowerFilters + ], + ); + alt.descriptor( + CS_INTERFACE, + &[ + CDC_TYPE_NCM, // bDescriptorSubtype + 0x00, // bcdNCMVersion + 0x01, // | + 0, // bmNetworkCapabilities + ], + ); + + let comm_ep = alt.endpoint_interrupt_in(8, 255); + + // Data interface + let mut iface = func.interface(); + iface.handler(state.data_control.write(DataControl {})); + let data_if = iface.interface_number(); + let _alt = iface.alt_setting(USB_CLASS_CDC_DATA, 0x00, CDC_PROTOCOL_NTB); + let mut alt = iface.alt_setting(USB_CLASS_CDC_DATA, 0x00, CDC_PROTOCOL_NTB); + let read_ep = alt.endpoint_bulk_out(max_packet_size); + let write_ep = alt.endpoint_bulk_in(max_packet_size); + + CdcNcmClass { + _comm_if: comm_if, + comm_ep, + data_if, + read_ep, + write_ep, + _control: &state.shared, + } + } + + pub fn split(self) -> (Sender<'d, D>, Receiver<'d, D>) { + ( + Sender { + write_ep: self.write_ep, + seq: 0, + }, + Receiver { + data_if: self.data_if, + comm_ep: self.comm_ep, + read_ep: self.read_ep, + }, + ) + } +} + +pub struct Sender<'d, D: Driver<'d>> { + write_ep: D::EndpointIn, + seq: u16, +} + +impl<'d, D: Driver<'d>> Sender<'d, D> { + pub async fn write_packet(&mut self, data: &[u8]) -> Result<(), EndpointError> { + let seq = self.seq; + self.seq = self.seq.wrapping_add(1); + + const MAX_PACKET_SIZE: usize = 64; // TODO unhardcode + const OUT_HEADER_LEN: usize = 28; + + let header = NtbOutHeader { + nth_sig: SIG_NTH, + nth_len: 0x0c, + nth_seq: seq, + nth_total_len: (data.len() + OUT_HEADER_LEN) as u16, + nth_first_index: 0x0c, + + ndp_sig: SIG_NDP_NO_FCS, + ndp_len: 0x10, + ndp_next_index: 0x00, + ndp_datagram_index: OUT_HEADER_LEN as u16, + ndp_datagram_len: data.len() as u16, + ndp_term1: 0x00, + ndp_term2: 0x00, + }; + + // Build first packet on a buffer, send next packets straight from `data`. + let mut buf = [0; MAX_PACKET_SIZE]; + let n = byteify(&mut buf, header); + assert_eq!(n.len(), OUT_HEADER_LEN); + + if OUT_HEADER_LEN + data.len() < MAX_PACKET_SIZE { + // First packet is not full, just send it. + // No need to send ZLP because it's short for sure. + buf[OUT_HEADER_LEN..][..data.len()].copy_from_slice(data); + self.write_ep.write(&buf[..OUT_HEADER_LEN + data.len()]).await?; + } else { + let (d1, d2) = data.split_at(MAX_PACKET_SIZE - OUT_HEADER_LEN); + + buf[OUT_HEADER_LEN..].copy_from_slice(d1); + self.write_ep.write(&buf).await?; + + for chunk in d2.chunks(MAX_PACKET_SIZE) { + self.write_ep.write(&chunk).await?; + } + + // Send ZLP if needed. + if d2.len() % MAX_PACKET_SIZE == 0 { + self.write_ep.write(&[]).await?; + } + } + + Ok(()) + } +} + +pub struct Receiver<'d, D: Driver<'d>> { + data_if: InterfaceNumber, + comm_ep: D::EndpointIn, + read_ep: D::EndpointOut, +} + +impl<'d, D: Driver<'d>> Receiver<'d, D> { + /// Reads a single packet from the OUT endpoint. + pub async fn read_packet(&mut self, buf: &mut [u8]) -> Result<usize, EndpointError> { + // Retry loop + loop { + // read NTB + let mut ntb = [0u8; NTB_MAX_SIZE]; + let mut pos = 0; + loop { + let n = self.read_ep.read(&mut ntb[pos..]).await?; + pos += n; + if n < self.read_ep.info().max_packet_size as usize || pos == NTB_MAX_SIZE { + break; + } + } + + let ntb = &ntb[..pos]; + + // Process NTB header (NTH) + let nth = match ntb.get(..12) { + Some(x) => x, + None => { + warn!("Received too short NTB"); + continue; + } + }; + let sig = u32::from_le_bytes(nth[0..4].try_into().unwrap()); + if sig != SIG_NTH { + warn!("Received bad NTH sig."); + continue; + } + let ndp_idx = u16::from_le_bytes(nth[10..12].try_into().unwrap()) as usize; + + // Process NTB Datagram Pointer (NDP) + let ndp = match ntb.get(ndp_idx..ndp_idx + 12) { + Some(x) => x, + None => { + warn!("NTH has an NDP pointer out of range."); + continue; + } + }; + let sig = u32::from_le_bytes(ndp[0..4].try_into().unwrap()); + if sig != SIG_NDP_NO_FCS && sig != SIG_NDP_WITH_FCS { + warn!("Received bad NDP sig."); + continue; + } + let datagram_index = u16::from_le_bytes(ndp[8..10].try_into().unwrap()) as usize; + let datagram_len = u16::from_le_bytes(ndp[10..12].try_into().unwrap()) as usize; + + if datagram_index == 0 || datagram_len == 0 { + // empty, ignore. This is allowed by the spec, so don't warn. + continue; + } + + // Process actual datagram, finally. + let datagram = match ntb.get(datagram_index..datagram_index + datagram_len) { + Some(x) => x, + None => { + warn!("NDP has a datagram pointer out of range."); + continue; + } + }; + buf[..datagram_len].copy_from_slice(datagram); + + return Ok(datagram_len); + } + } + + /// Waits for the USB host to enable this interface + pub async fn wait_connection(&mut self) -> Result<(), EndpointError> { + loop { + self.read_ep.wait_enabled().await; + self.comm_ep.wait_enabled().await; + + let buf = [ + 0xA1, //bmRequestType + 0x00, //bNotificationType = NETWORK_CONNECTION + 0x01, // wValue = connected + 0x00, + self.data_if.into(), // wIndex = interface + 0x00, + 0x00, // wLength + 0x00, + ]; + match self.comm_ep.write(&buf).await { + Ok(()) => break, // Done! + Err(EndpointError::Disabled) => {} // Got disabled again, wait again. + Err(e) => return Err(e), + } + } + + Ok(()) + } +} diff --git a/embassy-usb/src/class/hid.rs b/embassy-usb/src/class/hid.rs new file mode 100644 index 00000000..4d1fa995 --- /dev/null +++ b/embassy-usb/src/class/hid.rs @@ -0,0 +1,504 @@ +use core::mem::MaybeUninit; +use core::ops::Range; +use core::sync::atomic::{AtomicUsize, Ordering}; + +#[cfg(feature = "usbd-hid")] +use ssmarshal::serialize; +#[cfg(feature = "usbd-hid")] +use usbd_hid::descriptor::AsInputReport; + +use crate::control::{ControlHandler, InResponse, OutResponse, Request, RequestType}; +use crate::driver::{Driver, Endpoint, EndpointError, EndpointIn, EndpointOut}; +use crate::Builder; + +const USB_CLASS_HID: u8 = 0x03; +const USB_SUBCLASS_NONE: u8 = 0x00; +const USB_PROTOCOL_NONE: u8 = 0x00; + +// HID +const HID_DESC_DESCTYPE_HID: u8 = 0x21; +const HID_DESC_DESCTYPE_HID_REPORT: u8 = 0x22; +const HID_DESC_SPEC_1_10: [u8; 2] = [0x10, 0x01]; +const HID_DESC_COUNTRY_UNSPEC: u8 = 0x00; + +const HID_REQ_SET_IDLE: u8 = 0x0a; +const HID_REQ_GET_IDLE: u8 = 0x02; +const HID_REQ_GET_REPORT: u8 = 0x01; +const HID_REQ_SET_REPORT: u8 = 0x09; +const HID_REQ_GET_PROTOCOL: u8 = 0x03; +const HID_REQ_SET_PROTOCOL: u8 = 0x0b; + +pub struct Config<'d> { + /// HID report descriptor. + pub report_descriptor: &'d [u8], + + /// Handler for control requests. + pub request_handler: Option<&'d dyn RequestHandler>, + + /// Configures how frequently the host should poll for reading/writing HID reports. + /// + /// A lower value means better throughput & latency, at the expense + /// of CPU on the device & bandwidth on the bus. A value of 10 is reasonable for + /// high performance uses, and a value of 255 is good for best-effort usecases. + pub poll_ms: u8, + + /// Max packet size for both the IN and OUT endpoints. + pub max_packet_size: u16, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ReportId { + In(u8), + Out(u8), + Feature(u8), +} + +impl ReportId { + fn try_from(value: u16) -> Result<Self, ()> { + match value >> 8 { + 1 => Ok(ReportId::In(value as u8)), + 2 => Ok(ReportId::Out(value as u8)), + 3 => Ok(ReportId::Feature(value as u8)), + _ => Err(()), + } + } +} + +pub struct State<'d> { + control: MaybeUninit<Control<'d>>, + out_report_offset: AtomicUsize, +} + +impl<'d> State<'d> { + pub fn new() -> Self { + State { + control: MaybeUninit::uninit(), + out_report_offset: AtomicUsize::new(0), + } + } +} + +pub struct HidReaderWriter<'d, D: Driver<'d>, const READ_N: usize, const WRITE_N: usize> { + reader: HidReader<'d, D, READ_N>, + writer: HidWriter<'d, D, WRITE_N>, +} + +fn build<'d, D: Driver<'d>>( + builder: &mut Builder<'d, D>, + state: &'d mut State<'d>, + config: Config<'d>, + with_out_endpoint: bool, +) -> (Option<D::EndpointOut>, D::EndpointIn, &'d AtomicUsize) { + let control = state.control.write(Control::new( + config.report_descriptor, + config.request_handler, + &state.out_report_offset, + )); + + let len = config.report_descriptor.len(); + + let mut func = builder.function(USB_CLASS_HID, USB_SUBCLASS_NONE, USB_PROTOCOL_NONE); + let mut iface = func.interface(); + iface.handler(control); + let mut alt = iface.alt_setting(USB_CLASS_HID, USB_SUBCLASS_NONE, USB_PROTOCOL_NONE); + + // HID descriptor + alt.descriptor( + HID_DESC_DESCTYPE_HID, + &[ + // HID Class spec version + HID_DESC_SPEC_1_10[0], + HID_DESC_SPEC_1_10[1], + // Country code not supported + HID_DESC_COUNTRY_UNSPEC, + // Number of following descriptors + 1, + // We have a HID report descriptor the host should read + HID_DESC_DESCTYPE_HID_REPORT, + // HID report descriptor size, + (len & 0xFF) as u8, + (len >> 8 & 0xFF) as u8, + ], + ); + + let ep_in = alt.endpoint_interrupt_in(config.max_packet_size, config.poll_ms); + let ep_out = if with_out_endpoint { + Some(alt.endpoint_interrupt_out(config.max_packet_size, config.poll_ms)) + } else { + None + }; + + (ep_out, ep_in, &state.out_report_offset) +} + +impl<'d, D: Driver<'d>, const READ_N: usize, const WRITE_N: usize> HidReaderWriter<'d, D, READ_N, WRITE_N> { + /// Creates a new HidReaderWriter. + /// + /// This will allocate one IN and one OUT endpoints. If you only need writing (sending) + /// HID reports, consider using [`HidWriter::new`] instead, which allocates an IN endpoint only. + /// + pub fn new(builder: &mut Builder<'d, D>, state: &'d mut State<'d>, config: Config<'d>) -> Self { + let (ep_out, ep_in, offset) = build(builder, state, config, true); + + Self { + reader: HidReader { + ep_out: ep_out.unwrap(), + offset, + }, + writer: HidWriter { ep_in }, + } + } + + /// Splits into seperate readers/writers for input and output reports. + pub fn split(self) -> (HidReader<'d, D, READ_N>, HidWriter<'d, D, WRITE_N>) { + (self.reader, self.writer) + } + + /// Waits for both IN and OUT endpoints to be enabled. + pub async fn ready(&mut self) -> () { + self.reader.ready().await; + self.writer.ready().await; + } + + /// Writes an input report by serializing the given report structure. + #[cfg(feature = "usbd-hid")] + pub async fn write_serialize<IR: AsInputReport>(&mut self, r: &IR) -> Result<(), EndpointError> { + self.writer.write_serialize(r).await + } + + /// Writes `report` to its interrupt endpoint. + pub async fn write(&mut self, report: &[u8]) -> Result<(), EndpointError> { + self.writer.write(report).await + } + + /// Reads an output report from the Interrupt Out pipe. + /// + /// See [`HidReader::read`]. + pub async fn read(&mut self, buf: &mut [u8]) -> Result<usize, ReadError> { + self.reader.read(buf).await + } +} + +pub struct HidWriter<'d, D: Driver<'d>, const N: usize> { + ep_in: D::EndpointIn, +} + +pub struct HidReader<'d, D: Driver<'d>, const N: usize> { + ep_out: D::EndpointOut, + offset: &'d AtomicUsize, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ReadError { + BufferOverflow, + Disabled, + Sync(Range<usize>), +} + +impl From<EndpointError> for ReadError { + fn from(val: EndpointError) -> Self { + use EndpointError::*; + match val { + BufferOverflow => ReadError::BufferOverflow, + Disabled => ReadError::Disabled, + } + } +} + +impl<'d, D: Driver<'d>, const N: usize> HidWriter<'d, D, N> { + /// Creates a new HidWriter. + /// + /// This will allocate one IN endpoint only, so the host won't be able to send + /// reports to us. If you need that, consider using [`HidReaderWriter::new`] instead. + /// + /// poll_ms configures how frequently the host should poll for reading/writing + /// HID reports. A lower value means better throughput & latency, at the expense + /// of CPU on the device & bandwidth on the bus. A value of 10 is reasonable for + /// high performance uses, and a value of 255 is good for best-effort usecases. + pub fn new(builder: &mut Builder<'d, D>, state: &'d mut State<'d>, config: Config<'d>) -> Self { + let (ep_out, ep_in, _offset) = build(builder, state, config, false); + + assert!(ep_out.is_none()); + + Self { ep_in } + } + + /// Waits for the interrupt in endpoint to be enabled. + pub async fn ready(&mut self) -> () { + self.ep_in.wait_enabled().await + } + + /// Writes an input report by serializing the given report structure. + #[cfg(feature = "usbd-hid")] + pub async fn write_serialize<IR: AsInputReport>(&mut self, r: &IR) -> Result<(), EndpointError> { + let mut buf: [u8; N] = [0; N]; + let size = match serialize(&mut buf, r) { + Ok(size) => size, + Err(_) => return Err(EndpointError::BufferOverflow), + }; + self.write(&buf[0..size]).await + } + + /// Writes `report` to its interrupt endpoint. + pub async fn write(&mut self, report: &[u8]) -> Result<(), EndpointError> { + assert!(report.len() <= N); + + let max_packet_size = usize::from(self.ep_in.info().max_packet_size); + let zlp_needed = report.len() < N && (report.len() % max_packet_size == 0); + for chunk in report.chunks(max_packet_size) { + self.ep_in.write(chunk).await?; + } + + if zlp_needed { + self.ep_in.write(&[]).await?; + } + + Ok(()) + } +} + +impl<'d, D: Driver<'d>, const N: usize> HidReader<'d, D, N> { + /// Waits for the interrupt out endpoint to be enabled. + pub async fn ready(&mut self) -> () { + self.ep_out.wait_enabled().await + } + + /// Delivers output reports from the Interrupt Out pipe to `handler`. + /// + /// If `use_report_ids` is true, the first byte of the report will be used as + /// the `ReportId` value. Otherwise the `ReportId` value will be 0. + pub async fn run<T: RequestHandler>(mut self, use_report_ids: bool, handler: &T) -> ! { + let offset = self.offset.load(Ordering::Acquire); + assert!(offset == 0); + let mut buf = [0; N]; + loop { + match self.read(&mut buf).await { + Ok(len) => { + let id = if use_report_ids { buf[0] } else { 0 }; + handler.set_report(ReportId::Out(id), &buf[..len]); + } + Err(ReadError::BufferOverflow) => warn!( + "Host sent output report larger than the configured maximum output report length ({})", + N + ), + Err(ReadError::Disabled) => self.ep_out.wait_enabled().await, + Err(ReadError::Sync(_)) => unreachable!(), + } + } + } + + /// Reads an output report from the Interrupt Out pipe. + /// + /// **Note:** Any reports sent from the host over the control pipe will be + /// passed to [`RequestHandler::set_report()`] for handling. The application + /// is responsible for ensuring output reports from both pipes are handled + /// correctly. + /// + /// **Note:** If `N` > the maximum packet size of the endpoint (i.e. output + /// reports may be split across multiple packets) and this method's future + /// is dropped after some packets have been read, the next call to `read()` + /// will return a [`ReadError::SyncError()`]. The range in the sync error + /// indicates the portion `buf` that was filled by the current call to + /// `read()`. If the dropped future used the same `buf`, then `buf` will + /// contain the full report. + pub async fn read(&mut self, buf: &mut [u8]) -> Result<usize, ReadError> { + assert!(N != 0); + assert!(buf.len() >= N); + + // Read packets from the endpoint + let max_packet_size = usize::from(self.ep_out.info().max_packet_size); + let starting_offset = self.offset.load(Ordering::Acquire); + let mut total = starting_offset; + loop { + for chunk in buf[starting_offset..N].chunks_mut(max_packet_size) { + match self.ep_out.read(chunk).await { + Ok(size) => { + total += size; + if size < max_packet_size || total == N { + self.offset.store(0, Ordering::Release); + break; + } else { + self.offset.store(total, Ordering::Release); + } + } + Err(err) => { + self.offset.store(0, Ordering::Release); + return Err(err.into()); + } + } + } + + // Some hosts may send ZLPs even when not required by the HID spec, so we'll loop as long as total == 0. + if total > 0 { + break; + } + } + + if starting_offset > 0 { + Err(ReadError::Sync(starting_offset..total)) + } else { + Ok(total) + } + } +} + +pub trait RequestHandler { + /// Reads the value of report `id` into `buf` returning the size. + /// + /// Returns `None` if `id` is invalid or no data is available. + fn get_report(&self, id: ReportId, buf: &mut [u8]) -> Option<usize> { + let _ = (id, buf); + None + } + + /// Sets the value of report `id` to `data`. + fn set_report(&self, id: ReportId, data: &[u8]) -> OutResponse { + let _ = (id, data); + OutResponse::Rejected + } + + /// Get the idle rate for `id`. + /// + /// If `id` is `None`, get the idle rate for all reports. Returning `None` + /// will reject the control request. Any duration at or above 1.024 seconds + /// or below 4ms will be returned as an indefinite idle rate. + fn get_idle_ms(&self, id: Option<ReportId>) -> Option<u32> { + let _ = id; + None + } + + /// Set the idle rate for `id` to `dur`. + /// + /// If `id` is `None`, set the idle rate of all input reports to `dur`. If + /// an indefinite duration is requested, `dur` will be set to `u32::MAX`. + fn set_idle_ms(&self, id: Option<ReportId>, duration_ms: u32) { + let _ = (id, duration_ms); + } +} + +struct Control<'d> { + report_descriptor: &'d [u8], + request_handler: Option<&'d dyn RequestHandler>, + out_report_offset: &'d AtomicUsize, + hid_descriptor: [u8; 9], +} + +impl<'d> Control<'d> { + fn new( + report_descriptor: &'d [u8], + request_handler: Option<&'d dyn RequestHandler>, + out_report_offset: &'d AtomicUsize, + ) -> Self { + Control { + report_descriptor, + request_handler, + out_report_offset, + hid_descriptor: [ + // Length of buf inclusive of size prefix + 9, + // Descriptor type + HID_DESC_DESCTYPE_HID, + // HID Class spec version + HID_DESC_SPEC_1_10[0], + HID_DESC_SPEC_1_10[1], + // Country code not supported + HID_DESC_COUNTRY_UNSPEC, + // Number of following descriptors + 1, + // We have a HID report descriptor the host should read + HID_DESC_DESCTYPE_HID_REPORT, + // HID report descriptor size, + (report_descriptor.len() & 0xFF) as u8, + (report_descriptor.len() >> 8 & 0xFF) as u8, + ], + } + } +} + +impl<'d> ControlHandler for Control<'d> { + fn reset(&mut self) { + self.out_report_offset.store(0, Ordering::Release); + } + + fn get_descriptor<'a>(&'a mut self, req: Request, _buf: &'a mut [u8]) -> InResponse<'a> { + match (req.value >> 8) as u8 { + HID_DESC_DESCTYPE_HID_REPORT => InResponse::Accepted(self.report_descriptor), + HID_DESC_DESCTYPE_HID => InResponse::Accepted(&self.hid_descriptor), + _ => InResponse::Rejected, + } + } + + fn control_out(&mut self, req: Request, data: &[u8]) -> OutResponse { + trace!("HID control_out {:?} {=[u8]:x}", req, data); + if let RequestType::Class = req.request_type { + match req.request { + HID_REQ_SET_IDLE => { + if let Some(handler) = self.request_handler { + let id = req.value as u8; + let id = (id != 0).then(|| ReportId::In(id)); + let dur = u32::from(req.value >> 8); + let dur = if dur == 0 { u32::MAX } else { 4 * dur }; + handler.set_idle_ms(id, dur); + } + OutResponse::Accepted + } + HID_REQ_SET_REPORT => match (ReportId::try_from(req.value), self.request_handler) { + (Ok(id), Some(handler)) => handler.set_report(id, data), + _ => OutResponse::Rejected, + }, + HID_REQ_SET_PROTOCOL => { + if req.value == 1 { + OutResponse::Accepted + } else { + warn!("HID Boot Protocol is unsupported."); + OutResponse::Rejected // UNSUPPORTED: Boot Protocol + } + } + _ => OutResponse::Rejected, + } + } else { + OutResponse::Rejected // UNSUPPORTED: SET_DESCRIPTOR + } + } + + fn control_in<'a>(&'a mut self, req: Request, buf: &'a mut [u8]) -> InResponse<'a> { + trace!("HID control_in {:?}", req); + match req.request { + HID_REQ_GET_REPORT => { + let size = match ReportId::try_from(req.value) { + Ok(id) => self.request_handler.and_then(|x| x.get_report(id, buf)), + Err(_) => None, + }; + + if let Some(size) = size { + InResponse::Accepted(&buf[0..size]) + } else { + InResponse::Rejected + } + } + HID_REQ_GET_IDLE => { + if let Some(handler) = self.request_handler { + let id = req.value as u8; + let id = (id != 0).then(|| ReportId::In(id)); + if let Some(dur) = handler.get_idle_ms(id) { + let dur = u8::try_from(dur / 4).unwrap_or(0); + buf[0] = dur; + InResponse::Accepted(&buf[0..1]) + } else { + InResponse::Rejected + } + } else { + InResponse::Rejected + } + } + HID_REQ_GET_PROTOCOL => { + // UNSUPPORTED: Boot Protocol + buf[0] = 1; + InResponse::Accepted(&buf[0..1]) + } + _ => InResponse::Rejected, + } + } +} diff --git a/embassy-usb/src/class/mod.rs b/embassy-usb/src/class/mod.rs new file mode 100644 index 00000000..af27577a --- /dev/null +++ b/embassy-usb/src/class/mod.rs @@ -0,0 +1,3 @@ +pub mod cdc_acm; +pub mod cdc_ncm; +pub mod hid; diff --git a/embassy-usb/src/lib.rs b/embassy-usb/src/lib.rs index e1a99cfa..661b8411 100644 --- a/embassy-usb/src/lib.rs +++ b/embassy-usb/src/lib.rs @@ -7,6 +7,7 @@ pub(crate) mod fmt; pub use embassy_usb_driver as driver; mod builder; +pub mod class; pub mod control; pub mod descriptor; mod descriptor_reader; |