Skip to content

Virtual COM Port

Nathanael Schneider edited this page Jan 9, 2023 · 10 revisions

A CDC device consists of two interfaces and two endpoints. One endpoint is used for device management (setting the baud-rate, parity, stop bits, flow control, reporting changes of the serial line, etc.) and the other is used for the actual data.

The documentation for the definition is found in the CDC120 and PSTN120 documents from the USBorg. They are pretty readable at only ~30 pages each.

Device Descriptor / IAD

The device needs to mark itself as a CDC device, define which kind of control is required (modem, telephone, serial, ethernet, etc.) and depending on the control type also the version of the control sequences. Serial devices use the ACM (Abstract Control Model) for device management.

Field Value Meaning
Class 0x02 Communication Device Class
SubClass 0x00 Refer to interface for value
Protocol 0x00 Refer to interface for value

Attention: When using an IAD, you'll need to define the SubClass & Protocol correctly (ie. as in the control interface) and can't defer the resolution to the interface.

Interfaces

We need to define two interfaces. One will contain the CDC-Definition with the management endpoint while the other will contain the data endpoint. The Control Interface will contain quite a bit of metadata to bundle everything together.

Control Interface

Field Value Meaning
Class 0x02 CDC
SubClass 0x02 Use ACM
Protocol 0x01 Use V.250 commands for ACM

Functional Descriptors

The control interface will include functional descriptors before the endpoint descriptors to define how the CDC-Device behaves, which features are supported and which interfaces belong to this function.

Header Functional Descriptor

Source: CDC120, Table 15
This descriptor starts the list of function descriptors.

Field Size Value Meaning
Length 1 5 Length of the Header descriptor
Descriptor Type 1 0x24 This descriptor belongs to an interface descriptor
Descriptor Subtype 1 0x00 This is a header descriptor
CDC-Version 2 0x0110 Version 1.1 of the CDC definition

ACM Descriptor

Source: PSTN120, Table 4
This descriptor defines which control capabilities this CDC-Device supports (Network, Break, Flow control, Device specific communication). A Virtual COM Port only needs to support the Line Coding and Serial/Control line state.

Field Size Value Meaning
Length 1 4
Descriptor Type 1 0x24 The descriptor belongs to an interface
Descriptor SubType 1 0x02 This is an ACM descriptor
Capabilities 1 0x02 Which capabilities this ACM descriptor supports; 0x02 for COM ports

Union Descriptor

Source: CDC120, Table 16
This descriptor combines several interfaces into one functionality, in our case the notification and data interface.

Field Size Value Meaning
Length 1 5
Descriptor Type 1 0x24 This descriptor belongs to an interface
Descriptor SubType 1 0x06 This is a Union functional descriptor
Management Interface ID 1 0x00 The ID of this interface, which contains all the declaration stuff and management endpoints
Subinterface ID 1 0x01 The ID of the Data Interface that belongs to this CDC-Instance

Endpoints

The Control Interface needs one Notification Interrupt IN-Endpoint when used with ATM to report changes in the serial connection (for virtual ports those don't have to be implemented).

Data Interface

Field Value Meaning
Class 0x0A Data Interface Class
SubClass 0x00 Unused
Protocol 0x00 No class specific protocol required

Endpoints

The Data Interface needs an IN- and an OUT-Endpoint of type Bulk or Isochronous. Interrupt-endpoints are not supported.

Complete Descriptor

Here is an example of a complete working CDC-Descriptor

Descriptor Configuration
// Example definition for a Virtual COM Port
static const USB_DESCRIPTOR_DEVICE DeviceDescriptor = {
    .Length = 18,
    .Type = 0x01,
    .USBVersion = 0x0200,
    .DeviceClass = 0x02,
    .DeviceSubClass = 0x00,
    .DeviceProtocol = 0x00,
    .MaxPacketSize = 64,
    .VendorID = 0xDEAD,  // 0x0483,
    .ProductID = 0xBEEF, // 0x5740, to show as STM32 Virtual Com Port
    .DeviceVersion = 0x0100,
    .strManufacturer = 0,
    .strProduct = 0,
    .strSerialNumber = 0,
    .Configurations = 1};

// We need two interfaces. One for the CDC-Definition and one for the Data
static const USB_DESCRIPTOR_CONFIG ConfigDescriptor = {
    .Length = 9,
    .Type = 0x02,
    .TotalLength = 62,
    .Interfaces = 2,
    .ConfigurationID = 1,
    .strConfiguration = 0,
    .Attributes = (1 << 7),
    .MaxPower = 50};

// The CDC-Management interface
static const USB_DESCRIPTOR_INTERFACE CDCManagementInterface = {
    .Length = 9,
    .Type = 0x04,
    .InterfaceID = 0,
    .AlternateID = 0,
    .Endpoints = 1,
    .Class = 0x02,
    .SubClass = 0x02,
    .Protocol = 0x01,
    .strInterface = 0};

// Some CDC Functional Headers
static const USB_DESC_FUNC_HEADER CDCFuncHeader = {
    .Length = 5,
    .Type = 0x24,
    .SubType = 0x00,
    .CDCVersion = 0x0110};

static const USB_DESC_FUNC_ACM CDCFuncACM = {
    .Length = 4,
    .Type = 0x24,
    .SubType = 0x02,
    .Capabilities = (1 << 1)};

static const USB_DESC_FUNC_UNION1 CDCFuncUnion = {
    .Length = 5,
    .Type = 0x24,
    .SubType = 0x06,
    .ControlInterface = 0,
    .SubInterface0 = 1};

static const USB_DESCRIPTOR_ENDPOINT CDCNotificationEndpoint = {
    .Length = 7,
    .Type = 0x05,
    .Address = (1 << 7) | 1,
    .Attributes = 0x03,
    .MaxPacketSize = 8,
    .Interval = 0x10};

// The Data-Interface with two Bulk-Endpoints
static const USB_DESCRIPTOR_INTERFACE CDCDataInterface = {
    .Length = 9,
    .Type = 0x04,
    .InterfaceID = 1,
    .AlternateID = 0,
    .Endpoints = 2,
    .Class = 0x0A,
    .SubClass = 0x00,
    .Protocol = 0x00,
    .strInterface = 0};

static const USB_DESCRIPTOR_ENDPOINT CDCDataEndpoints[2] = {
    {.Length = 7,
     .Type = 0x05,
     .Address = (1 << 7) | 2,
     .Attributes = 0x02,
     .MaxPacketSize = 64,
     .Interval = 0x00},
    {.Length = 7,
     .Type = 0x05,
     .Address = 2,
     .Attributes = 0x02,
     .MaxPacketSize = 64,
     .Interval = 0x00}};

char *USB_GetConfigDescriptor(short *length) {
    if (ConfigurationBuffer[0] == 0) {
        short offset = 0;
        AddToDescriptor(&ConfigDescriptor, &offset);
        AddToDescriptor(&CDCManagementInterface, &offset);
        AddToDescriptor(&CDCFuncHeader, &offset);
        AddToDescriptor(&CDCFuncACM, &offset);
        AddToDescriptor(&CDCFuncUnion, &offset);
        AddToDescriptor(&CDCNotificationEndpoint, &offset);
        AddToDescriptor(&CDCDataInterface, &offset);
        AddToDescriptor(&CDCDataEndpoints[0], &offset);
        AddToDescriptor(&CDCDataEndpoints[1], &offset);
    }

    *length = sizeof(ConfigurationBuffer);
    return ConfigurationBuffer;
}

Implementation

The only thing we need to do is to handle the SetLineCoding (0x20) and GetLineCoding (0x21) control packets. They send / expect a seven byte array to configure the baud rate, stop bits, parity etc.

// Setup packets that are specific to CDC Devices
#define CDC_CONFIG_SETLINECODING 0x20
#define CDC_CONFIG_GETLINECODING 0x21
#define CDC_CONFIG_CONTROLLINESTATE 0x22

char buffer[64];
char lineCoding[7];

char CDC_SetupPacket(USB_SETUP_PACKET *setup, char *data, short length) {
    // Windows requires us to remember the line coding
    switch (setup->Request) {
    case CDC_CONFIG_CONTROLLINESTATE:
        break;
    case CDC_CONFIG_GETLINECODING:
        USB_Transmit(0, lineCoding, 7);
        break;
    case CDC_CONFIG_SETLINECODING:
        for (int i = 0; i < 7; i++) {
            lineCoding[i] = data[i];
        }
        return USB_OK;
        break;
    }
}

void CDC_HandlePacket(char ep, short length) {
    // Just mirror the text
    USB_Fetch(2, buffer, &length);

    // do NOT busy wait. We are still in the ISR, it will never clear.
    if (!USB_IsTransmitPending(2)) {
        USB_Transmit(2, buffer, length);
    }
}

Configuring the Class

Now the last part, wiring up the CDC-Device in usb_config:

char *USB_GetConfigDescriptor(short *length) {
    if (ConfigurationBuffer[0] == 0) {
        short offset = 0;
        AddToDescriptor(&ConfigDescriptor, &offset);
        AddToDescriptor(&CDCManagementInterface, &offset);
        AddToDescriptor(&CDCFuncHeader, &offset);
        AddToDescriptor(&CDCFuncACM, &offset);
        AddToDescriptor(&CDCFuncUnion, &offset);
        AddToDescriptor(&CDCNotificationEndpoint, &offset);
        AddToDescriptor(&CDCDataInterface, &offset);
        AddToDescriptor(&CDCDataEndpoints[0], &offset);
        AddToDescriptor(&CDCDataEndpoints[1], &offset);
    }

    *length = sizeof(ConfigurationBuffer);
    return ConfigurationBuffer;
}

void USB_ConfigureEndpoints() {
    // Configure all endpoints and route their reception to the functions that need them
    USB_CONFIG_EP Notification = {
        .EP = 1,
        .RxBufferSize = 0,
        .TxBufferSize = 8,
        .Type = USB_EP_INTERRUPT};

    USB_CONFIG_EP DataEP = {
        .EP = 2,
        .RxBufferSize = 64,
        .TxBufferSize = 64,
        .RxCallback = CDC_HandlePacket,
        .Type = USB_EP_BULK};

    USB_SetEPConfig(Notification);
    USB_SetEPConfig(DataEP);
}

char USB_HandleClassSetup(USB_SETUP_PACKET *setup, char *data, short length) {
    // Route the setup packets based on the Interface / Class Index
    return CDC_SetupPacket(setup, data, length);
}