Skip to main content

CDC Device Stack

This section introduces XRUSB’s USB CDC ACM (virtual serial port) device class implementation, focusing on:

  • Descriptor organization (IAD + Communication Interface + Data Interface)
  • Endpoint resource allocation, configuration, and callback dispatch
  • CDC ACM standard class request handling (Line Coding / Control Line State)
  • Serial State notification format and sending strategy
  • Upper-layer adaptation (CDCUart) and throughput test classes (CDCWriteTest / CDCReadTest)

The current CDC stack consists of the following header files (source code is included in the repository):

  • cdc_base.hpp: CDC ACM common base (descriptors / class requests / endpoint management / callback dispatch)
  • cdc_uart.hpp: CDC ↔ UART semantic adapter (exposes LibXR::UART-style Read/Write to the upper layer)
  • cdc_test.hpp: Throughput test classes (continuous write-out / continuous read-in)

Component Overview

LibXR::USB::CDCBase

CDCBase inherits from DeviceClass and implements the common CDC ACM functionality:

  • Endpoint resource allocation and configuration (Data IN / Data OUT / Comm IN)
  • Filling a configuration-descriptor block for IAD + Communication Interface + Data Interface
  • CDC ACM standard class request handling (GET_LINE_CODING / SET_LINE_CODING / SET_CONTROL_LINE_STATE)
  • Notifying the upper layer via callbacks for control line changes (DTR/RTS) and line parameter changes (Line Coding)
  • Providing TX/RX completion hooks (OnDataOutComplete / OnDataInComplete) for derived classes to implement concrete data paths

CDCBase does not implement a data path directly; derived classes must implement:

virtual void OnDataOutComplete(bool in_isr, ConstRawData& data) = 0;
virtual void OnDataInComplete(bool in_isr, ConstRawData& data) = 0;

These hooks are triggered by endpoint transfer completion, and dispatched after an inited_ guard via CDCBase’s static trampoline.

LibXR::USB::CDCUart

CDCUart inherits from CDCBase and also from LibXR::UART, exposing typical UART semantics to the upper layer:

  • Read(): reads data from OUT transactions sent by the host
  • Write(): sends IN data to the host
  • SetConfig(): maps UART configuration to CDC Line Coding, and sends one Serial State notification

Internally it uses LibXR::ReadPort / LibXR::WritePort as a software RX buffer and TX queue manager, and performs enqueue/dequeue in endpoint callbacks.

LibXR::USB::CDCWriteTest / LibXR::USB::CDCReadTest

Both derive from CDCBase and are used to validate link throughput and driver stability:

  • CDCWriteTest: when the host sends any data, continuously returns data through Data IN (tests device → host path)
  • CDCReadTest: continuously arms the OUT endpoint and immediately restarts it upon completion (tests host → device path)

Interfaces and Endpoint Layout

Interfaces

A CDC ACM device is exposed as two interfaces (Communication + Data) and includes an IAD (Interface Association Descriptor), allowing the host to recognize it as a single CDC composite function.

  • Communication Interface: includes 1 Interrupt IN endpoint (Notification Endpoint)
  • Data Interface: includes 1 Bulk OUT + 1 Bulk IN endpoint (data RX/TX)

CDCBase::GetInterfaceNum() always returns 2, and HasIAD() always returns true.

Notes:

  • The IAD’s bFirstInterface is derived from the start_itf_num offset
  • The Communication Interface is typically the target interface for class requests (the request wIndex points to this interface number)

Endpoints

CDCBase::Init() requests and configures the following endpoints from EndpointPool:

EndpointDirectionTypeTypical Use
Data OUTOUTBULKHost → device data reception
Data ININBULKDevice → host data transmission
Comm INININTERRUPTCDC notifications (Serial State, etc)

The Comm IN endpoint max packet size is fixed at 16 bytes; the Serial State notification itself is a 10-byte structure (see below).

Endpoint numbers can be specified when constructing CDCBase / CDCUart / the test classes; the default is Endpoint::EPNumber::EP_AUTO, which lets the endpoint pool auto-assign numbers.

Speed and Max Packet Size

The endpoint descriptor wMaxPacketSize for Data IN/OUT is taken from the endpoint object’s MaxPacketSize().

  • Full-Speed Bulk is typically 64 bytes/packet
  • High-Speed Bulk is typically 512 bytes/packet (if the platform supports HS)

The actual value depends on the platform USB controller and endpoint implementation. The stack writes whatever value is reported by the endpoint into the descriptor.


Key Capabilities of CDCBase

DTR/RTS Control Line State

CDCBase maintains a control line state control_line_state_ and provides:

bool IsDtrSet() const;
bool IsRtsSet() const;

When receiving the SET_CONTROL_LINE_STATE class request, the behavior is:

  • Update control_line_state_ (from wValue)
  • Return a ZLP (Zero-Length Packet) as acknowledgement
  • Call SendSerialState() to attempt reporting the current serial state via Comm IN
  • Trigger the user callback set by SetOnSetControlLineStateCallback(cb), with (DTR, RTS) as arguments

Engineering recommendation:

  • Treat DTR as the key signal indicating “the host has opened the serial port / is ready to communicate”
  • When DTR is deasserted, avoid continuing to transmit to prevent upper-layer blocking or meaningless queue buildup

Line Coding (Baud Rate / Parity / Stop Bits / Data Bits)

CDC ACM Line Coding is read/written via the class requests SET_LINE_CODING / GET_LINE_CODING.

  • GET_LINE_CODING: the device returns the current line_coding_ (7 bytes)
  • SET_LINE_CODING: the 7-byte line_coding_ is written in the control transfer data stage, then converted to LibXR::UART::Configuration and delivered to the upper layer in OnClassData()

The data length for SET_LINE_CODING must be exactly 7 bytes; otherwise it returns ErrorCode::ARG_ERR.

Line Coding Mapping Rules

CDCBase currently maps CDC Line Coding to LibXR::UART::Configuration as follows:

CDC FieldValueMapped UART Configuration
dwDTERateanycfg.baudrate = dwDTERate
bCharFormat0stop_bits = 1
bCharFormat2stop_bits = 2
bCharFormatotherfallback to stop_bits = 1 (1.5 stop bits not yet supported)
bParityType1parity = ODD
bParityType2parity = EVEN
bParityTypeotherfallback to NO_PARITY (Mark/Space will be downgraded)
bDataBits5/6/7/8/16data_bits = bDataBits (passthrough)

Tips:

  • On most desktop OSes, CDC Line Coding is more of a “negotiation / hint”; whether it truly affects host-side serial parameters depends on driver policy
  • If bridging a real UART peripheral, trust the callback parameters and validate legality on the peripheral side

Serial State Notification

SendSerialState() sends a Serial State notification to the host via the Comm IN (Interrupt IN) endpoint.

The notification is 10 bytes:

  • 8-byte CDC Notification Header
  • 2-byte UART state bitmap serialState

The code defines the structure as:

#pragma pack(push, 1)
struct SerialStateNotification
{
uint8_t bmRequestType; // fixed 0xA1
uint8_t bNotification; // fixed SERIAL_STATE (0x20)
uint16_t wValue; // fixed 0
uint16_t wIndex; // Interface number (Communication Interface)
uint16_t wLength; // fixed 2
uint16_t serialState; // UART state bitmap
};
#pragma pack(pop)

Callbacks and Execution Context

CDCBase exposes two upper-layer callbacks:

  • SetOnSetControlLineStateCallback(LibXR::Callback<bool, bool> cb)
  • SetOnSetLineCodingCallback(LibXR::Callback<LibXR::UART::Configuration> cb)

Their Run(in_isr, ...) is triggered from the control transfer handling path; in_isr indicates the calling context.


Initialization and Resource Release Behavior

Init Behavior

Key actions in CDCBase::Init(endpoint_pool, start_itf_num):

  • Clear control_line_state_
  • Request three endpoints from EndpointPool and Configure them
  • Fill the descriptor block for IAD, Communication Interface, Data Interface, and endpoint descriptors
  • Pass the descriptor block to the device framework via SetData(RawData{...}) to be stitched into the configuration descriptor
  • Register transfer-complete callbacks for Data OUT / Data IN endpoints
  • Set inited_ = true
  • Start pre-receiving on Data OUT: ep_data_out_->Transfer(ep_data_out_->MaxTransferSize())

Notes:

  • The pre-receive length depends on the endpoint’s MaxTransferSize() implementation and is used to continuously receive host data
  • CDCBase does not buffer received data; derived classes must consume the data in OnDataOutComplete and restart the OUT transfer (or restart according to their own strategy)

Deinit Behavior

Key actions in CDCBase::Deinit(endpoint_pool):

  • Set inited_ = false
  • Clear control_line_state_
  • Close three endpoints and clear active length
  • Return endpoints to the EndpointPool
  • Set endpoint pointers to null

Derived classes or upper-layer adapters should ensure in Deinit():

  • Terminate all asynchronous operations that depend on endpoint objects
  • Complete or fail any pending read/write requests to avoid upper layers waiting indefinitely

Usage Examples

#include "cdc_uart.hpp"

LibXR::USB::CDCUart cdc_uart(/*rx*/256, /*tx*/256, /*tx_queue*/8);

// When constructing the USB device, put &cdc_uart into the class list: {{&cdc_uart}}
// usb_dev.Init();
// usb_dev.Start();

Optional: listen to host changes for Line Coding and DTR/RTS:

cdc_uart.SetOnSetLineCodingCallback(
LibXR::Callback<LibXR::UART::Configuration>(
[](bool in_isr, LibXR::UART::Configuration cfg) {
(void)in_isr;
// You can synchronize this to a real UART peripheral here
// (do not block in ISR context)
}
)
);

cdc_uart.SetOnSetControlLineStateCallback(
LibXR::Callback<bool, bool>(
[](bool in_isr, bool dtr, bool rts) {
(void)in_isr;
(void)rts;
// dtr=true indicates the host has opened the serial port and you can start transmitting
}
)
);

Throughput Tests

Write test:

#include "cdc_test.hpp"
LibXR::USB::CDCWriteTest cdc_write_test;

Read test:

#include "cdc_test.hpp"
LibXR::USB::CDCReadTest cdc_read_test;

Pass them into the USB Device class list in the same way.