Skip to main content

embassy_usb_host/class/
cdc_acm.rs

1//! CDC ACM (Serial over USB) host class driver.
2//!
3//! This driver can communicate with USB CDC ACM devices (virtual serial ports).
4
5use embassy_usb_driver::host::{PipeError, UsbHostAllocator, UsbPipe, pipe};
6use embassy_usb_driver::{Direction as UsbDirection, EndpointAddress, EndpointInfo, EndpointType};
7
8use crate::control::SetupPacket;
9use crate::descriptor::ConfigurationDescriptor;
10use crate::handler::EnumerationInfo;
11
12/// CDC class code.
13const USB_CLASS_CDC: u8 = 0x02;
14/// CDC Data class code.
15const USB_CLASS_CDC_DATA: u8 = 0x0A;
16/// CDC ACM subclass.
17const CDC_SUBCLASS_ACM: u8 = 0x02;
18
19/// CDC ACM class request: SET_LINE_CODING.
20const REQ_SET_LINE_CODING: u8 = 0x20;
21/// CDC ACM class request: SET_CONTROL_LINE_STATE.
22const REQ_SET_CONTROL_LINE_STATE: u8 = 0x22;
23
24/// USB line coding (serial parameters).
25#[derive(Clone, Debug)]
26#[cfg_attr(feature = "defmt", derive(defmt::Format))]
27pub struct LineCoding {
28    /// Baud rate in bits per second.
29    pub baud_rate: u32,
30    /// Stop bits: 0=1, 1=1.5, 2=2.
31    pub stop_bits: u8,
32    /// Parity: 0=None, 1=Odd, 2=Even.
33    pub parity: u8,
34    /// Data bits (5, 6, 7, 8).
35    pub data_bits: u8,
36}
37
38impl Default for LineCoding {
39    fn default() -> Self {
40        Self {
41            baud_rate: 115200,
42            stop_bits: 0,
43            parity: 0,
44            data_bits: 8,
45        }
46    }
47}
48
49impl LineCoding {
50    fn to_bytes(&self) -> [u8; 7] {
51        let baud = self.baud_rate.to_le_bytes();
52        [
53            baud[0],
54            baud[1],
55            baud[2],
56            baud[3],
57            self.stop_bits,
58            self.parity,
59            self.data_bits,
60        ]
61    }
62}
63
64/// CDC ACM host class driver error.
65#[derive(Debug)]
66#[cfg_attr(feature = "defmt", derive(defmt::Format))]
67pub enum CdcAcmError {
68    /// Transfer error.
69    Transfer(PipeError),
70    /// No matching CDC ACM interface found in the device.
71    NoInterface,
72    /// Failed to allocate a pipe.
73    NoPipe,
74}
75
76impl From<PipeError> for CdcAcmError {
77    fn from(e: PipeError) -> Self {
78        Self::Transfer(e)
79    }
80}
81
82impl core::fmt::Display for CdcAcmError {
83    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
84        match self {
85            Self::Transfer(_e) => write!(f, "Transfer error"),
86            Self::NoInterface => write!(f, "No CDC ACM interface found"),
87            Self::NoPipe => write!(f, "No free pipe"),
88        }
89    }
90}
91
92impl core::error::Error for CdcAcmError {}
93
94impl embedded_io_async::Error for CdcAcmError {
95    fn kind(&self) -> embedded_io_async::ErrorKind {
96        match self {
97            Self::Transfer(e) => match e {
98                PipeError::Disconnected => embedded_io_async::ErrorKind::NotConnected,
99                PipeError::BufferOverflow => embedded_io_async::ErrorKind::OutOfMemory,
100                PipeError::Timeout => embedded_io_async::ErrorKind::TimedOut,
101                _ => embedded_io_async::ErrorKind::Other,
102            },
103            Self::NoInterface => embedded_io_async::ErrorKind::NotFound,
104            Self::NoPipe => embedded_io_async::ErrorKind::OutOfMemory,
105        }
106    }
107}
108
109/// Information about a CDC ACM interface found in a configuration descriptor.
110#[derive(Clone, Debug)]
111#[cfg_attr(feature = "defmt", derive(defmt::Format))]
112pub struct CdcAcmInfo {
113    /// CDC communication interface number.
114    pub comm_interface: u8,
115    /// CDC data interface number.
116    pub data_interface: u8,
117    /// Bulk IN endpoint address.
118    pub bulk_in_ep: u8,
119    /// Bulk IN max packet size.
120    pub bulk_in_mps: u16,
121    /// Bulk OUT endpoint address.
122    pub bulk_out_ep: u8,
123    /// Bulk OUT max packet size.
124    pub bulk_out_mps: u16,
125}
126
127/// Find CDC ACM interfaces in a configuration descriptor.
128pub fn find_cdc_acm(config_desc: &[u8]) -> Option<CdcAcmInfo> {
129    let cfg = ConfigurationDescriptor::try_from_slice(config_desc).ok()?;
130
131    let mut comm_iface: Option<u8> = None;
132    let mut data_iface: Option<u8> = None;
133    let mut bulk_in: Option<(u8, u16)> = None;
134    let mut bulk_out: Option<(u8, u16)> = None;
135
136    for iface in cfg.iter_interface() {
137        if iface.interface_class == USB_CLASS_CDC && iface.interface_subclass == CDC_SUBCLASS_ACM {
138            comm_iface = Some(iface.interface_number);
139        } else if iface.interface_class == USB_CLASS_CDC_DATA {
140            data_iface = Some(iface.interface_number);
141            for ep in iface.iter_endpoints() {
142                if ep.transfer_type() == 0x02 {
143                    // Bulk
144                    if ep.is_in() {
145                        bulk_in = Some((ep.endpoint_address, ep.max_packet_size));
146                    } else {
147                        bulk_out = Some((ep.endpoint_address, ep.max_packet_size));
148                    }
149                }
150            }
151        }
152    }
153
154    if let (Some(comm), Some(data), Some((in_ep, in_mps)), Some((out_ep, out_mps))) =
155        (comm_iface, data_iface, bulk_in, bulk_out)
156    {
157        Some(CdcAcmInfo {
158            comm_interface: comm,
159            data_interface: data,
160            bulk_in_ep: in_ep,
161            bulk_in_mps: in_mps,
162            bulk_out_ep: out_ep,
163            bulk_out_mps: out_mps,
164        })
165    } else {
166        None
167    }
168}
169
170/// CDC ACM host driver.
171///
172/// Provides read/write access to a CDC ACM (virtual serial port) USB device.
173pub struct CdcAcmHost<'d, A: UsbHostAllocator<'d>> {
174    ctrl_ch: A::Pipe<pipe::Control, pipe::InOut>,
175    in_ch: A::Pipe<pipe::Bulk, pipe::In>,
176    out_ch: A::Pipe<pipe::Bulk, pipe::Out>,
177    comm_interface: u8,
178    _phantom: core::marker::PhantomData<&'d ()>,
179}
180
181impl<'d, A: UsbHostAllocator<'d>> CdcAcmHost<'d, A> {
182    /// Create a new CDC ACM host driver.
183    ///
184    /// Parses the config descriptor to find CDC ACM endpoints and allocates channels.
185    pub fn new(alloc: &A, config_desc: &[u8], enum_info: &EnumerationInfo) -> Result<Self, CdcAcmError> {
186        let info = find_cdc_acm(config_desc).ok_or(CdcAcmError::NoInterface)?;
187
188        let ctrl_ep_info = EndpointInfo {
189            addr: EndpointAddress::from_parts(0, UsbDirection::In),
190            ep_type: EndpointType::Control,
191            max_packet_size: enum_info.device_desc.max_packet_size0 as u16,
192            interval_ms: 0,
193        };
194
195        let in_ep_info = EndpointInfo {
196            addr: EndpointAddress::from_parts((info.bulk_in_ep & 0x0F) as usize, UsbDirection::In),
197            ep_type: EndpointType::Bulk,
198            max_packet_size: info.bulk_in_mps,
199            interval_ms: 0,
200        };
201
202        let out_ep_info = EndpointInfo {
203            addr: EndpointAddress::from_parts((info.bulk_out_ep & 0x0F) as usize, UsbDirection::Out),
204            ep_type: EndpointType::Bulk,
205            max_packet_size: info.bulk_out_mps,
206            interval_ms: 0,
207        };
208
209        let device_address = enum_info.device_address;
210        let split = enum_info.split();
211
212        let ctrl_ch = alloc
213            .alloc_pipe::<pipe::Control, pipe::InOut>(device_address, &ctrl_ep_info, split)
214            .map_err(|_| CdcAcmError::NoPipe)?;
215        let in_ch = alloc
216            .alloc_pipe::<pipe::Bulk, pipe::In>(device_address, &in_ep_info, split)
217            .map_err(|_| CdcAcmError::NoPipe)?;
218        let out_ch = alloc
219            .alloc_pipe::<pipe::Bulk, pipe::Out>(device_address, &out_ep_info, split)
220            .map_err(|_| CdcAcmError::NoPipe)?;
221
222        Ok(Self {
223            ctrl_ch,
224            in_ch,
225            out_ch,
226            comm_interface: info.comm_interface,
227            _phantom: core::marker::PhantomData,
228        })
229    }
230
231    /// Set the line coding (baud rate, data bits, parity, stop bits).
232    pub async fn set_line_coding(&mut self, coding: &LineCoding) -> Result<(), CdcAcmError> {
233        let data = coding.to_bytes();
234        let setup =
235            SetupPacket::class_interface_out(REQ_SET_LINE_CODING, 0, self.comm_interface as u16, data.len() as u16);
236        self.ctrl_ch.control_out(&setup.to_bytes(), &data).await?;
237        Ok(())
238    }
239
240    /// Set the control line state (DTR, RTS).
241    pub async fn set_control_line_state(&mut self, dtr: bool, rts: bool) -> Result<(), CdcAcmError> {
242        let value = (dtr as u16) | ((rts as u16) << 1);
243        let setup = SetupPacket::class_interface_out(REQ_SET_CONTROL_LINE_STATE, value, self.comm_interface as u16, 0);
244        self.ctrl_ch.control_out(&setup.to_bytes(), &[]).await?;
245        Ok(())
246    }
247
248    /// Read data from the CDC ACM device.
249    pub async fn read(&mut self, buf: &mut [u8]) -> Result<usize, CdcAcmError> {
250        let n = self.in_ch.request_in(buf).await?;
251        Ok(n)
252    }
253
254    /// Write data to the CDC ACM device.
255    pub async fn write(&mut self, data: &[u8]) -> Result<usize, CdcAcmError> {
256        self.out_ch.request_out(data, false).await?;
257        Ok(data.len())
258    }
259}
260
261impl<'d, A: UsbHostAllocator<'d>> embedded_io_async::ErrorType for CdcAcmHost<'d, A> {
262    type Error = CdcAcmError;
263}
264
265impl<'d, A: UsbHostAllocator<'d>> embedded_io_async::Read for CdcAcmHost<'d, A> {
266    async fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
267        CdcAcmHost::read(self, buf).await
268    }
269}
270
271impl<'d, A: UsbHostAllocator<'d>> embedded_io_async::Write for CdcAcmHost<'d, A> {
272    async fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
273        CdcAcmHost::write(self, buf).await
274    }
275
276    async fn flush(&mut self) -> Result<(), Self::Error> {
277        // USB bulk transfers are flushed immediately
278        Ok(())
279    }
280}