blob: 81b6152eed90eea7cb5a84c847ad84e58ee8d229 [file]
// Licensed under the Apache-2.0 license
// SPDX-License-Identifier: Apache-2.0
//! CDC-ACM (Serial) USB class implementation.
#![no_std]
pub use hal_usb::driver::{UsbDriver, UsbEvent, UsbPacket};
pub use hal_usb::{
Direction, EndpointDescriptor, FunctionalDescriptor, InterfaceDescriptor, Recipient, Request,
RequestType, SetupPacket, StringHandle, TransferType,
};
pub use usb_driver::{EpIn, EpOut};
use usb_stack::{UsbAction, UsbClass, EMPTY};
use util_ringbuffer::RingBuffer;
use zerocopy::{Immutable, IntoBytes};
/// CDC-ACM specific constants.
pub const USB_CLASS_CDC: u8 = 0x02;
pub const USB_CLASS_CDC_DATA: u8 = 0x0a;
pub const CDC_SUBCLASS_ACM: u8 = 0x02;
pub const CDC_PROTOCOL_NONE: u8 = 0x00;
pub const CS_INTERFACE: u8 = 0x24;
pub const CDC_TYPE_HEADER: u8 = 0x00;
pub const CDC_TYPE_ACM: u8 = 0x02;
pub const CDC_TYPE_UNION: u8 = 0x06;
/// CDC-ACM specific requests.
pub const REQ_SEND_ENCAPSULATED_COMMAND: Request = Request::new(
Direction::HostToDevice,
RequestType::Class,
Recipient::Interface,
0x00,
);
pub const REQ_GET_ENCAPSULATED_COMMAND: Request = Request::new(
Direction::DeviceToHost,
RequestType::Class,
Recipient::Interface,
0x01,
);
pub const REQ_SET_LINE_CODING: Request = Request::new(
Direction::HostToDevice,
RequestType::Class,
Recipient::Interface,
0x20,
);
pub const REQ_GET_LINE_CODING: Request = Request::new(
Direction::DeviceToHost,
RequestType::Class,
Recipient::Interface,
0x21,
);
pub const REQ_SET_CONTROL_LINE_STATE: Request = Request::new(
Direction::HostToDevice,
RequestType::Class,
Recipient::Interface,
0x22,
);
#[derive(Copy, Clone, PartialEq, Eq, Default, IntoBytes, Immutable)]
#[repr(u8)]
pub enum StopBits {
#[default]
One = 0,
OnePointFive = 1,
Two = 2,
}
impl From<u8> for StopBits {
fn from(x: u8) -> Self {
match x {
0 => StopBits::One,
1 => StopBits::OnePointFive,
2 => StopBits::Two,
_ => StopBits::One,
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, Default, IntoBytes, Immutable)]
#[repr(u8)]
pub enum Parity {
#[default]
None = 0,
Odd = 1,
Even = 2,
Mark = 3,
Space = 4,
}
impl From<u8> for Parity {
fn from(x: u8) -> Self {
match x {
0 => Parity::None,
1 => Parity::Odd,
2 => Parity::Even,
3 => Parity::Mark,
4 => Parity::Space,
_ => Parity::None,
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, IntoBytes, Immutable)]
#[repr(C)]
pub struct LineCoding {
pub data_rate: u32,
pub stop_bits: StopBits,
pub parity: Parity,
pub data_bits: u8,
// In order to be zerocopy, this struct needs to align and pad as u32.
pub _pad: u8,
}
impl TryFrom<&[u8]> for LineCoding {
type Error = ();
fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
if data.len() >= 7 {
Ok(LineCoding {
data_rate: u32::from_le_bytes(data[0..4].try_into().unwrap()),
stop_bits: StopBits::from(data[4]),
parity: Parity::from(data[5]),
data_bits: data[6],
_pad: 0,
})
} else {
Err(())
}
}
}
impl Default for LineCoding {
fn default() -> Self {
LineCoding {
data_rate: 9600,
stop_bits: StopBits::default(),
parity: Parity::default(),
data_bits: 8,
_pad: 0,
}
}
}
impl LineCoding {
pub fn as_bytes(&self) -> &[u8] {
// The size_of::<LineCoding>() is 8, but CDC-ACM uses only 7 bytes.
let bytes = <Self as IntoBytes>::as_bytes(self);
&bytes[..7]
}
}
/// A builder for CDC-ACM class configuration.
///
/// This builder encapsulates the constants and configuration logic for a
/// CDC-ACM instance. It provides methods to generate descriptor fragments
/// and the final `InterfaceDescriptor` structs, hiding class-specific details
/// from the application.
///
/// # Convention
/// USB classes should provide a `Builder` struct with `const` methods that:
/// 1. Return arrays of child descriptors (functional, endpoints).
/// 2. Construct fully-populated `InterfaceDescriptor`s from application-provided
/// handles and static fragments.
/// 3. Provide hardware configuration (`EpIn`, `EpOut`).
#[derive(Copy, Clone)]
pub struct CdcAcmBuilder {
pub comm_if: u8,
pub data_if: u8,
pub comm_ep: u8,
pub data_out_ep: u8,
pub data_in_ep: u8,
}
impl CdcAcmBuilder {
/// Creates a new CDC-ACM configuration.
pub const fn new(
comm_if: u8,
data_if: u8,
comm_ep: u8,
data_out_ep: u8,
data_in_ep: u8,
) -> Self {
Self {
comm_if,
data_if,
comm_ep,
data_out_ep,
data_in_ep,
}
}
/// Returns the functional descriptors for the control interface.
pub const fn comm_func_descs(&self) -> [FunctionalDescriptor; 3] {
[
FunctionalDescriptor::raw(CS_INTERFACE, &[CDC_TYPE_HEADER, 0x10, 0x01]),
FunctionalDescriptor::raw(CS_INTERFACE, &[CDC_TYPE_ACM, 0x02]),
FunctionalDescriptor::raw(CS_INTERFACE, &[CDC_TYPE_UNION, self.comm_if, self.data_if]),
]
}
/// Returns the endpoints for the control interface.
pub const fn comm_endpoints(&self) -> [EndpointDescriptor; 1] {
[EndpointDescriptor {
direction: Direction::DeviceToHost,
endpoint_num: self.comm_ep,
interval: 255,
max_packet_size: 8,
transfer_type: TransferType::Interrupt,
}]
}
/// Returns the endpoints for the data interface.
pub const fn data_endpoints(&self) -> [EndpointDescriptor; 2] {
[
EndpointDescriptor {
direction: Direction::HostToDevice,
endpoint_num: self.data_out_ep,
interval: 0,
max_packet_size: 64,
transfer_type: TransferType::Bulk,
},
EndpointDescriptor {
direction: Direction::DeviceToHost,
endpoint_num: self.data_in_ep,
interval: 0,
max_packet_size: 64,
transfer_type: TransferType::Bulk,
},
]
}
/// Constructs the CDC-ACM Communication (Control) interface descriptor.
pub const fn comm_interface(
&self,
name: StringHandle,
func_descs: &'static [FunctionalDescriptor],
endpoints: &'static [EndpointDescriptor],
) -> InterfaceDescriptor {
InterfaceDescriptor {
name,
interface_number: self.comm_if,
alternate_setting: 0,
interface_class: USB_CLASS_CDC,
interface_sub_class: CDC_SUBCLASS_ACM,
interface_protocol: CDC_PROTOCOL_NONE,
func_descs,
endpoints,
}
}
/// Constructs the CDC-ACM Data interface descriptor.
pub const fn data_interface(
&self,
name: StringHandle,
endpoints: &'static [EndpointDescriptor],
) -> InterfaceDescriptor {
InterfaceDescriptor {
name,
interface_number: self.data_if,
alternate_setting: 0,
interface_class: USB_CLASS_CDC_DATA,
interface_sub_class: 0,
interface_protocol: CDC_PROTOCOL_NONE,
func_descs: &[],
endpoints,
}
}
/// Returns the hardware endpoint configuration for this CDC-ACM instance.
pub const fn eps(&self) -> ([EpIn; 2], [EpOut; 1]) {
(
[
EpIn {
num: self.comm_ep,
buf_pool_size: 1,
},
EpIn {
num: self.data_in_ep,
buf_pool_size: 1,
},
],
[EpOut {
num: self.data_out_ep,
set_nak: false,
}],
)
}
}
/// CDC-ACM class handler.
pub struct CdcAcm<const RX_SIZE: usize, const TX_SIZE: usize> {
config: CdcAcmBuilder,
line_coding: LineCoding,
expecting_control_out: bool,
control_buf: [u8; 8],
pub rx_queue: RingBuffer<u8, RX_SIZE>,
pub tx_queue: RingBuffer<u8, TX_SIZE>,
}
impl<const RX_SIZE: usize, const TX_SIZE: usize> CdcAcm<RX_SIZE, TX_SIZE> {
/// Creates a new CDC-ACM class handler from a builder.
pub fn new(config: CdcAcmBuilder) -> Self {
Self {
config,
line_coding: LineCoding::default(),
expecting_control_out: false,
control_buf: [0u8; 8],
rx_queue: RingBuffer::default(),
tx_queue: RingBuffer::default(),
}
}
/// Returns the configuration builder for this instance.
pub fn config(&self) -> &CdcAcmBuilder {
&self.config
}
fn handle_setup<'a>(&'a mut self, pkt: SetupPacket) -> (UsbAction<'a>, bool) {
if !(pkt.request().recipient() == Recipient::Interface
&& (pkt.index() as u8) == self.config.comm_if)
{
return (UsbAction::None, false);
}
match pkt.request() {
REQ_SEND_ENCAPSULATED_COMMAND => (
UsbAction::TransferIn {
endpoint: 0,
data: EMPTY,
zlp: true,
},
false,
),
REQ_GET_ENCAPSULATED_COMMAND => (UsbAction::StallInAndOut { endpoint: 0 }, false),
REQ_SET_LINE_CODING => {
self.expecting_control_out = true;
(UsbAction::None, true)
}
REQ_GET_LINE_CODING => (
UsbAction::TransferInUnaligned {
endpoint: 0,
data: self.line_coding.as_bytes(),
zlp: true,
},
false,
),
REQ_SET_CONTROL_LINE_STATE => (
UsbAction::TransferIn {
endpoint: 0,
data: EMPTY,
zlp: true,
},
false,
),
_ => (UsbAction::StallInAndOut { endpoint: 0 }, false),
}
}
fn handle_control_out<'a>(&'a mut self, pkt: impl UsbPacket) -> UsbAction<'a> {
let buf = pkt.copy_to_unaligned(&mut self.control_buf);
self.expecting_control_out = false;
match LineCoding::try_from(buf) {
Ok(x) => {
self.line_coding = x;
UsbAction::TransferIn {
endpoint: 0,
data: EMPTY,
zlp: true,
}
}
Err(_) => UsbAction::StallInAndOut { endpoint: 0 },
}
}
/// Polls the send buffer and initiates IN transfers if data is available.
pub fn poll_transmit<D: UsbDriver>(&mut self, driver: &mut D) {
let data = self.tx_queue.as_slice();
if !data.is_empty() {
let n = driver.transfer_in_unaligned(self.config.data_in_ep, data, true);
self.tx_queue.consume(n);
}
}
}
impl<const RX_SIZE: usize, const TX_SIZE: usize> UsbClass for CdcAcm<RX_SIZE, TX_SIZE> {
fn handle_event<'a, P: UsbPacket>(
&'a mut self,
event: UsbEvent<P>,
) -> Result<UsbAction<'a>, UsbEvent<P>> {
match event {
UsbEvent::SetupPacket { pkt, endpoint } if endpoint == 0 => {
let (action, claimed) = self.handle_setup(pkt);
if action != UsbAction::None || claimed {
Ok(action)
} else {
Err(UsbEvent::SetupPacket { pkt, endpoint })
}
}
UsbEvent::DataOutPacket(pkt) => {
if pkt.endpoint_index() == 0 && self.expecting_control_out {
Ok(self.handle_control_out(pkt))
} else if pkt.endpoint_index() == self.config.data_out_ep as usize {
let mut tmp = [0u8; 64];
let buf = pkt.copy_to_unaligned(&mut tmp);
let _ = self.rx_queue.push_slice(buf);
Ok(UsbAction::None)
} else {
Err(UsbEvent::DataOutPacket(pkt))
}
}
_ => Err(event),
}
}
}