Composite Device Solution

Overview

The USB Composite Device specification defines the device architecture and enumeration standards for carrying multiple independent function classes (such as Audio, HID, Storage, Serial, etc.) through a single physical USB interface.

Based on the USB specification officially released by the USB-IF, Ameba implements a flexible composite device functional framework. It supports aggregating multiple functional interfaces via Interface Descriptors or Interface Association Descriptors (IAD), providing capabilities for parallel enumeration, independent driver loading, and collaborative operation of multiple logical devices on the host side.

../../../_images/usb_device_composite_overview.svg

Features

  • Supports the following function combinations:

    • CDC ACM + HID

    • CDC ACM + MSC

    • CDC ACM + UAC

    • HID + UAC

  • Supports USB hot-plug

  • Supports fully customizable descriptors

  • Supports configuration of parameters such as transfer buffer size and speed mode

Application Scenarios

As a USB composite device, Ameba can simultaneously enumerate multiple device classes through a single USB physical interface, enabling parallel processing of data transmission and control interaction. It is suitable for a wide range of complex application scenarios. For example,

  • USB Audio Device with Remote Control (UAC + HID): Ameba provides USB audio input/output functionality (UAC) while utilizing the HID interface for media control. Users can enjoy a high-quality audio streaming experience (such as listening to music through headphones or recording with a microphone), and also interact with features like volume adjustment, muting, song switching, or RGB lighting effects control through the HID channel.

  • Smart Industry and 3D Printing Control (MSC + CDC ACM): Ameba combines the MSC high-capacity storage and CDC virtual serial port functions. In 3D printer or data logger scenarios, the MSC interface can be simulated as a USB flash drive for storing G-code slicing files or sensor historical data, while the CDC interface simultaneously serves as a console for the host computer to send AT commands in real time, monitor temperature, or calibrate parameters.

  • Automated Testing and Assistive Input (HID Mouse + CDC ACM): Ameba combines the HID mouse and CDC serial port functions. In this scenario, the CDC interface is responsible for receiving raw data (such as coordinate instructions, head tracking data) from backend scripts or sensors, while the HID interface simulates cursor movement and click actions based on these instructions. It is widely used in hardware automated testing tools or assistive input devices for the disabled.

Protocol Introduction

The USB Composite Device specification defines a device architecture capable of supporting multiple independent function interfaces via a single physical interface within the USB framework.

This mechanism enables the host to identify and enumerate a single physical device as a collection of logical functions, facilitating parallel processing and independent control across different device classes (e.g., HID, MSC, CDC).

Common implementations include wireless keyboard/mouse receivers, USB headsets with integrated audio cards, and industrial devices combining storage and debugging capabilities.

Descriptor Structure

While adhering to standard USB descriptors (Device and Configuration Descriptors), composite devices define multi-functionality by aggregating multiple Interface Descriptors within the Configuration Descriptor set.

Single-Interface Function Class

When a single interface represents a standalone function, the composite device simply aggregates these functional classes. Typical examples of single-interface function classes include HID and MSC.

Taking a HID Keyboard + HID Mouse composite device as an example, the descriptor topology is illustrated below:

Device Descriptor
|  bDeviceClass: 0xEF (Miscellaneous)
|  bDeviceSubClass: 0x02 (Common Class)
|  bDeviceProtocol: 0x01 (Interface Association Descriptor)
│
└── Configuration Descriptor
   |   bNumInterfaces: 2  (2 Interfaces)
   │   ...
   ├── Interface Descriptor 0 (HID Keyboard)
   │   bInterfaceNumber: 0
   │   bInterfaceClass: 0x03 (HID)
   │   bInterfaceSubClass: 0x01 (Boot Interface)
   │   bInterfaceProtocol: 0x01 (Keyboard)
   │
   └── Interface Descriptor 1 (HID Mouse)
       bInterfaceNumber: 1
       bInterfaceClass: 0x03 (HID)
       bInterfaceSubClass: 0x01 (Boot Interface)
       bInterfaceProtocol: 0x02 (Mouse)

HID Keyboard + HID Mouse composite device compared to individual keyboard/mouse devices, with descriptor characteristics as follows:

Descriptor Level

Single Function Device

Composite Device

Device Descriptor

bDeviceClass defines the type

bDeviceClass is typically 0xEF (Misc) or 0x00 (Defined by Interface)

Configuration Descriptor

1 Configuration

1 Configuration

Interface Descriptor

1 Interface

2 (or more) Interfaces

Endpoint Descriptor

Belong to the single interface

Each interface has independent endpoints

Note

bDeviceClass = 0xEF: This is a standard flag for composite devices, which triggers the host driver to parse the IAD (optional) and multiple interface descriptors in the configuration descriptor, decompose different device functions, and create these logical sub-devices. Then load the keyboard driver for Interface 0 and the mouse driver for Interface 1.

Multi-Interface Function Class

If a logical function requires the use of multiple interfaces to complete, the Interface Association Descriptor (IAD) must be included when using this type of function to associate these interfaces.

Typical multi-interface functional class

  • CDC (Communication Device Class): Typically requires 1 Control Interface (CCI) + 1 Data Interface (DCI).

  • UVC (USB Video Class): Requires 1 Video Control Interface (VC) + 1 or more Video Streaming Interfaces (VS).

  • UAC (USB Audio Class): Requires 1 Audio Control Interface (AC) + 1 or more Audio Stream Interfaces (AS).

Interface Association Descriptor (IAD)

IAD declares to the host that a set of consecutive interfaces that follow closely belong to the same function and should be loaded and managed by the same driver. Without IAD, the host sometimes recognizes them as two separate devices, or the driver fails to load.

Interface Association Descriptor (IAD)
├── bLength                 : 1 byte  → 0x08 (Fixed Length)
├── bDescriptorType         : 1 byte  → 0x0B (IAD type code)
├── bFirstInterface         : 1 byte  → First Interface Number
├── bInterfaceCount         : 1 bytes → Count of associated interfaces
├── bFunctionClass          : 1 byte  → Function Class Code
├── bFunctionSubClass       : 1 byte  → Function SubClass Code
├── bFunctionProtocol       : 1 byte  → Function Protocol Code
└── iFunction               : 1 byte  → Function String Descriptor Index

IAD Usage Example

../../../_images/usb_iad_descriptor.svg
  • Function 1: Class utilize two interfaces. In order for the host to recognize them as a single logical function, it is necessary to use IAD to associate these two interfaces.

  • Function 2: Class utilize a separate interface, eliminating the need for IAD association.

Class-specific request

The USB protocol specification does not define a specific “composite class request”. Their core logic lies in precisely directing Class-Specific Requests to the designated interface through the addressing mechanism in USB standard requests.

In the bmRequestType field of the SETUP packet, the lower 5 bits (Bits 0..4) represent the Recipient.

  • General single-function device: Control requests are typically sent to the “entire device”, meaning the Recipient is set to Device (00000).

  • Composite device: A large number of control requests must be precisely sent to a “specific interface”, which means setting the Recipient to Interface (00001).

The following are the most notable key points regarding the handling of control requests by composite devices:

Field

Value

Meaning

Implementation in Composite Devices

bmRequestType

0x21 / 0xA1

Class Request, Recipient=Interface

Most common scenario. Examples include setting CDC baud rates or controlling HID keyboard LEDs.

wIndex

Interface Number

Interface Number

When the Recipient is Interface, wIndex must specify the target interface index (e.g., 0, 1, 2). The driver routes the request to the corresponding function driver based on this value.

Class Driver

This section provides a detailed analysis of how to design and implement a USB composite device class driver. The composite class driver acts as a “parent class,” responsible for managing resource scheduling, request dispatching, and data processing for multiple “child classes” (such as CDC ACM, HID, MSC, etc.).

../../../_images/usb_device_composite_driver_arch.png

Descriptor Structure

To allow the host to correctly identify the composite device and the multiple functions it contains, the driver must dynamically assemble a complete and precise set of descriptors at runtime. The following points must be noted when generating device descriptors:

  • Device Descriptor

    bDeviceClass: Usually set to 0xEF (Miscellaneous) or 0x00 (defined by the interface).

  • Configuration Descriptor

    This is a dynamically generated “aggregate” that contains the descriptors for all sub-functions.

    • bNumInterfaces: Must be the sum of the number of interfaces for all sub-functions. For example, for a composite device consisting of a CDC function (occupying 2 interfaces) and an MSC function (occupying 1 interface), this value should be 3.

    • wTotalLength: Must be the sum of the lengths of all descriptors (Configuration, IAD, Interface, Endpoint). This value needs to be calculated precisely at runtime.

    • Interface Association Descriptor (IAD): If a sub-function contains multiple interfaces (e.g., CDC ACM), an IAD must be used to “bundle” these interfaces together, declaring that they belong to the same function.

  • Endpoint Descriptor

    The configuration of endpoints must be tailored to the hardware capabilities of the chip. Please refer to Hardware Configuration for details.

    • Endpoint selection: Available endpoints must be selected based on functional requirements (IN/OUT) and hardware support.

    • Maximum Packet Size (MPS): Hardware limitations need to be considered. Especially when using a dedicated transmit buffer, it is important to ensure that the size of the transmit buffer area of the IN endpoint can accommodate at least one maximum packet.

CDC ACM + MSC Example

The following section illustrates the descriptor topology of the CDC ACM + MSC composite device. These structures correspond to the standard descriptor definitions in the USB protocol specifications.

  • CDC ACM (Virtual Serial Port): Occupies two interfaces. To ensure the host recognizes them as a single logical function, an IAD (Interface Association Descriptor) must be used to associate these two interfaces.

  • MSC (Mass Storage): Operates as an independent interface and does not require IAD association.

The composite device (CDC ACM + MSC) solution utilizes 5 non-zero endpoints (excluding the default control endpoint EP0).

Interface number

Interface Class

Endpoints

Description

Interface 0

CDC Control (ACM)

1x Interrupt IN

Used to notify Serial State and management commands.

Interface 1

CDC Data

1x Bulk OUT, 1x Bulk IN

Responsible for sending (OUT) and receiving (IN) virtual serial port data.

Interface 2

MSC (Mass Storage)

1x Bulk OUT, 1x Bulk IN

Responsible for data read/write of the mass storage device (SCSI commands/data).

In the class driver, various descriptors are generally defined as arrays. When obtaining configuration descriptors, the callback function get_descriptor of each sub-function class driver is called sequentially to aggregate them together.

 /* USB Standard Device Descriptor */
 static const u8 usbd_composite_dev_desc[USB_LEN_DEV_DESC] = {
    //...
    0xEF,       /* bDeviceClass: Miscellaneous */
    0x02,       /* bDeviceSubClass: Common Class */
    0x01,       /* bDeviceProtocol: Interface Association Descriptor */
    //...
 };

 /* USB Standard Configuration Descriptor */
 static const u8 usbd_composite_config_desc[USB_LEN_CFG_DESC] = {
    //...
    0x00, 0x00,  /* wTotalLength: calculated at runtime */
    0x03,        /* bNumInterfaces */
    //...
 };

/**
 * @brief  Get descriptor callback
 * @param  dev: USB device instance
 * @param  req: Setup request handle
 * @param  buf: Poniter to Buffer
 * @return Descriptor length
 */
 static u16 usbd_composite_get_descriptor(usb_dev_t *dev, usb_setup_req_t *req, u8 *buf)
 {
    usbd_composite_dev_t *cdev = &usbd_composite_dev;
    usb_speed_type_t speed = dev->dev_speed;
    u16 len = 0;
    u16 desc_len;
    u16 total_len = 0;

    switch (USB_HIGH_BYTE(req->wValue)) {
    //...
    case USB_DESC_TYPE_CONFIGURATION:
    case USB_DESC_TYPE_OTHER_SPEED_CONFIGURATION:
       usb_os_memcpy((void *)buf, (void *)usbd_composite_config_desc, USB_LEN_CFG_DESC);
       buf += USB_LEN_CFG_DESC;
       total_len += USB_LEN_CFG_DESC;
       desc_len = cdev->cdc->get_descriptor(dev, req, buf);
       buf += desc_len;
       total_len += desc_len;
       desc_len = cdev->msc->get_descriptor(dev, req, buf);
       total_len += desc_len;
       buf = dev->ep0_in.xfer_buf;
       if (USB_HIGH_BYTE(req->wValue) == USB_DESC_TYPE_OTHER_SPEED_CONFIGURATION) {
          buf[USB_CFG_DESC_OFFSET_TYPE] = USB_DESC_TYPE_OTHER_SPEED_CONFIGURATION;
       }
       buf[USB_CFG_DESC_OFFSET_TOTAL_LEN] = USB_LOW_BYTE(total_len);
       buf[USB_CFG_DESC_OFFSET_TOTAL_LEN + 1] = USB_HIGH_BYTE(total_len);
       len = total_len;
       break;
       }

    return len;
 }

The complete topology structure of the device descriptor is as follows:

Device Descriptor
|  bDeviceClass: 0xEF (Miscellaneous)
|  bDeviceSubClass: 0x02 (Common Class)
|  bDeviceProtocol: 0x01 (Interface Association Descriptor)
|
└── Configuration descriptor
    |   bNumInterfaces: 3 (3 Interfaces)
    |
    |   /* Function 1: CDC ACM */
    |
    ├── Interface Association Descriptor (IAD)
    |    bFirstInterface: 0
    |    bInterfaceCount: 2 (Associate Interface 0 with Interface 1)
    |    bFunctionClass:  0x02 (CDC Control)
    |    bFunctionSubClass: 0x02 (ACM)
    |
    ├── Interface Descriptor 0 (CDC Control Interface)
    |   |  bInterfaceNumber: 0
    |   |  bInterfaceClass:  0x02 (CDC Control)
    |   |  bInterfaceSubClass: 0x02 (ACM)
    |   |  ...
    |   ├── CDC Class Specific Descriptors (Header, Call Mgmt, ACM...)
    |   └── Endpoint Descriptor (Interrupt IN)
    |
    ├── Interface Descriptor 1 (CDC Data Interface)
    |   |  bInterfaceNumber: 1
    |   |  bInterfaceClass:  0x0A (CDC Data)
    |   |  ...
    |   ├── Endpoint Descriptor (Bulk OUT)
    |   └── Endpoint Descriptor (Bulk IN)
    |
    |   /* Function 2: MSC */
    |
    └── Interface Descriptor 2 (MSC Interface)
        |  bInterfaceNumber: 2
        |  bInterfaceClass:  0x08 (Mass Storage)
        |  bInterfaceSubClass: 0x06 (SCSI Transparent Command Set)
        |  bInterfaceProtocol: 0x50 (Bulk-Only Transport / BBB)
        |   ...
        ├── Endpoint Descriptor (Bulk OUT)
        └── Endpoint Descriptor (Bulk IN)

Note

Composite Class Driver Implementation

It mainly involves defining composite devices and implementing class-driven callback functions.

  • Sub-function Class Driver

    Each sub-function (such as CDC, MSC, HID) is an independent class driver, please refer to the corresponding chapters of the sub-function solution for details.

    • Independent driver structure: Each sub-driver defines a standard usbd_class_driver_t structure to implement its own process logic.

    • Independent resource management: Each sub-driver is responsible for managing its own endpoints, data buffers, and data transmission and reception processing.

  • Composite Class Driver

    • The composite Class driver needs to define a standard usbd_class_driver_t structure. This structure serves as the unified entry point registered to the USB Core. The composite device driver is responsible for dispatching events to, or iterating through, the callback functions of the sub-function class drivers.

    • The composite Class driver defines a standard usbd_composite_dev_t structure. This is the core of the composite device instance, used to manage all sub-funcion class drivers.

    /* Composite Device */
    static usbd_composite_dev_t usbd_composite_dev;
    
    /* Composite Class Driver Interface */
    static const usbd_class_driver_t usbd_composite_driver = {
        .get_descriptor = usbd_composite_get_descriptor,    /* Iterate through all sub-function classes to obtain the aggregated configuration descriptor */
        .set_config = usbd_composite_set_config,            /* Iterate to initialize all sub-function class endpoints and resources */
        .clear_config = usbd_composite_clear_config,        /* Iterate to release all sub-function class endpoints and resources */
        .setup = usbd_composite_setup,                      /* Dispatch class control requests to different interfaces: wIndex = Interface xx */
        .sof = usbd_composite_sof,                          /* Called during SOF interrupt, used for processing logic with strict timing requirements */
        .ep0_data_out = usbd_composite_handle_ep0_data_out, /* After the device is ready, dispatch and handle sub-function class requests for control OUT endpoints */
        .ep0_data_in = usbd_composite_handle_ep0_data_in,   /* After the device is ready, dispatch and handle sub-function class request results for control IN endpoints */
        .ep_data_in = usbd_composite_handle_ep_data_in,     /* Dispatch IN endpoint data processing; use ep_addr to determine which sub-function class the data belongs to */
        .ep_data_out = usbd_composite_handle_ep_data_out,   /* Dispatch OUT endpoint data processing; use ep_addr to determine which sub-function class the data belongs to */
        .status_changed = usbd_composite_status_changed,    /* Monitor connection status and notify the application layer or all sub-function class state machines when necessary */
    };
    

CDC ACM + MSC Example

The following section uses CDC ACM + MSC as an example to detail the implementation of the composite device class driver.

/* Composite Device structure. */
typedef struct {
   usb_setup_req_t ctrl_req;  /* Control setup request */
   usbd_class_driver_t *cdc;  /* CDC ACM class */
   usbd_class_driver_t *msc;  /* MSC class */
   usbd_composite_cb_t *cb;   /* Composite user callback */
   usb_dev_t *dev;            /* USB device instance */
} usbd_composite_dev_t;
/* Composite Device */
static usbd_composite_dev_t usbd_composite_dev;

/* Composite Class Driver */
static const usbd_class_driver_t usbd_composite_driver = {
   .get_descriptor = usbd_composite_get_descriptor,
   .set_config = usbd_composite_set_config,
   .clear_config = usbd_composite_clear_config,
   .setup = usbd_composite_setup,
   .ep0_data_out = usbd_composite_handle_ep0_data_out,
   .ep_data_in = usbd_composite_handle_ep_data_in,
   .ep_data_out = usbd_composite_handle_ep_data_out,
   .status_changed = usbd_composite_status_changed,
};

/********************** Function 1: CDC ACM class *********************/
/* CDC ACM device structure. */
typedef struct {
   usbd_composite_dev_t *cdev;           /**< Pointer to the parent composite device structure. */
   usbd_composite_cdc_acm_usr_cb_t *cb;  /**< Pointer to the user-registered callback structure. */
   usbd_ep_t ep_bulk_in;                 /**< Bulk IN endpoint handler. */
   usbd_ep_t ep_bulk_out;                /**< Bulk OUT endpoint handler. */
#if CONFIG_COMP_CDC_ACM_NOTIFY
   usbd_ep_t ep_intr_in;                 /**< Interrupt IN endpoint handler (for notifications). */
#endif
} usbd_composite_cdc_acm_dev_t;
/* CDC ACM Device */
static usbd_composite_cdc_acm_dev_t composite_cdc_acm_dev;

/* CDC ACM Class Driver */
const usbd_class_driver_t usbd_composite_cdc_acm_driver = {
   .get_descriptor = composite_cdc_acm_get_descriptor,
   .set_config = composite_cdc_acm_set_config,
   .clear_config = composite_cdc_acm_clear_config,
   .setup = composite_cdc_acm_setup,
   .ep_data_in = composite_cdc_acm_handle_ep_data_in,
   .ep_data_out = composite_cdc_acm_handle_ep_data_out,
   .ep0_data_out = composite_cdc_acm_handle_ep0_data_out,
};

/********************** Function 2: MSC class *********************/
/* MSC device structure.  */
typedef struct {
   usbd_ep_t ep_bulk_in;                 /**< Bulk IN endpoint handler. */
   usbd_ep_t ep_bulk_out;                /**< Bulk OUT endpoint handler. */
   usbd_composite_dev_t *cdev;           /**< Pointer to the parent composite device structure. */
   //...
} usbd_composite_msc_dev_t;
/* MSC Device */
static usbd_composite_msc_dev_t usbd_composite_msc_dev;

/* MSC Class Driver */
const usbd_class_driver_t usbd_composite_msc_driver = {
   .get_descriptor = usbd_composite_msc_get_descriptor,
   .set_config = usbd_composite_msc_set_config,
   .clear_config = usbd_composite_msc_clear_config,
   .setup = usbd_composite_msc_setup,
   .ep_data_in = usbd_composite_msc_handle_ep_data_in,
   .ep_data_out = usbd_composite_msc_handle_ep_data_out,
};

The specific callbak implementation of the composite driver usbd_composite_driver:

/**
* @brief  Set composite class configuration
* @param  dev: USB device instance
* @param  config: USB configuration index
* @return Status
*/
static int usbd_composite_set_config(usb_dev_t *dev, u8 config)
{
   int ret = HAL_OK;
   usbd_composite_dev_t *cdev = &usbd_composite_dev;

   cdev->dev = dev;

   cdev->cdc->set_config(dev, config);
   cdev->msc->set_config(dev, config);

   return ret;
}

/**
* @brief  Clear composite configuration
* @param  dev: USB device instance
* @param  config: USB configuration index
* @return Status
*/
static int usbd_composite_clear_config(usb_dev_t *dev, u8 config)
{
   int ret = 0U;
   usbd_composite_dev_t *cdev = &usbd_composite_dev;

   cdev->cdc->clear_config(dev, config);
   cdev->msc->clear_config(dev, config);

   return ret;
}

/**
* @brief  Handle class specific control requests
* @param  dev: USB device instance
* @param  req: USB control requests
* @return Status
*/
static int usbd_composite_setup(usb_dev_t *dev, usb_setup_req_t *req)
{
   usbd_composite_dev_t *cdev = &usbd_composite_dev;
   usbd_ep_t *ep0_in = &dev->ep0_in;
   int ret = HAL_OK;

   switch (req->bmRequestType & USB_REQ_TYPE_MASK) {
   //...
   case USB_REQ_TYPE_CLASS:
      if ((req->wIndex == USBD_COMP_CDC_COM_ITF) || (req->wIndex == USBD_COMP_CDC_DAT_ITF)) {
         ret = cdev->cdc->setup(dev, req);
      } else if (req->wIndex == USBD_COMP_MSC_ITF) {
         ret = cdev->msc->setup(dev, req);
      } else {
         RTK_LOGS(TAG, RTK_LOG_WARN, "Invalid class req\n");
      }
      break;
   }

   return ret;
}

/**
* @brief  Data sent on non-control IN endpoint
* @param  dev: USB device instance
* @param  ep_addr: endpoint address
* @return Status
*/
static int usbd_composite_handle_ep_data_in(usb_dev_t *dev, u8 ep_addr, u8 status)
{
   int ret = HAL_OK;
   usbd_composite_dev_t *cdev = &usbd_composite_dev;

   if ((ep_addr == USBD_COMP_CDC_BULK_IN_EP) || (ep_addr == USBD_COMP_CDC_INTR_IN_EP)) {
      if (cdev->cdc->ep_data_in != NULL) {
         ret = cdev->cdc->ep_data_in(dev, ep_addr, status);
      }
   } else if (ep_addr == USBD_COMP_MSC_BULK_IN_EP) {
      if (cdev->msc->ep_data_in != NULL) {
         ret = cdev->msc->ep_data_in(dev, ep_addr, status);
      }
   }

   return ret;
}

/**
* @brief  Data received on non-control OUT endpoint
* @param  dev: USB device instance
* @param  ep_addr: endpoint address
* @return Status
*/
static int usbd_composite_handle_ep_data_out(usb_dev_t *dev, u8 ep_addr, u16 len)
{
   int ret = HAL_OK;
   usbd_composite_dev_t *cdev = &usbd_composite_dev;

   if (ep_addr == USBD_COMP_CDC_BULK_OUT_EP) {
      if (cdev->cdc->ep_data_out != NULL) {
         ret = cdev->cdc->ep_data_out(dev, ep_addr, len);
      }
   } else if (ep_addr == USBD_COMP_MSC_BULK_OUT_EP) {
      if (cdev->msc->ep_data_out != NULL) {
         ret = cdev->msc->ep_data_out(dev, ep_addr, len);
      }
   }

   return ret;
}

/**
* @brief  Handle EP0 Rx Ready event
* @param  dev: USB device instance
* @return Status
*/
static int usbd_composite_handle_ep0_data_out(usb_dev_t *dev)
{
   int ret = HAL_OK;
   usbd_composite_dev_t *cdev = &usbd_composite_dev;

   cdev->cdc->ep0_data_out(dev);

   return ret;
}

/**
* @brief  USB attach status change
* @param  dev: USB device instance
* @param  status: USB attach status
* @return void
*/
static void usbd_composite_status_changed(usb_dev_t *dev, u8 old_status, u8 status)
{
   usbd_composite_dev_t *cdev = &usbd_composite_dev;

   UNUSED(dev);

   if (cdev->cb->status_changed) {
      cdev->cb->status_changed(old_status, status);
   }
}

/**
* @brief  Get descriptor callback
* @param  dev: USB device instance
* @param  req: Setup request handle
* @param  buf: Poniter to Buffer
* @return Descriptor length
*/
static u16 usbd_composite_get_descriptor(usb_dev_t *dev, usb_setup_req_t *req, u8 *buf)
{
   usbd_composite_dev_t *cdev = &usbd_composite_dev;
   usb_speed_type_t speed = dev->dev_speed;
   u16 len = 0;
   u16 desc_len;
   u16 total_len = 0;

   switch (USB_HIGH_BYTE(req->wValue)) {
   //...
   case USB_DESC_TYPE_CONFIGURATION:
   case USB_DESC_TYPE_OTHER_SPEED_CONFIGURATION:
      usb_os_memcpy((void *)buf, (void *)usbd_composite_config_desc, USB_LEN_CFG_DESC);
      buf += USB_LEN_CFG_DESC;
      total_len += USB_LEN_CFG_DESC;
      desc_len = cdev->cdc->get_descriptor(dev, req, buf);
      buf += desc_len;
      total_len += desc_len;
      desc_len = cdev->msc->get_descriptor(dev, req, buf);
      total_len += desc_len;
      buf = dev->ep0_in.xfer_buf;
      if (USB_HIGH_BYTE(req->wValue) == USB_DESC_TYPE_OTHER_SPEED_CONFIGURATION) {
         buf[USB_CFG_DESC_OFFSET_TYPE] = USB_DESC_TYPE_OTHER_SPEED_CONFIGURATION;
      }
      buf[USB_CFG_DESC_OFFSET_TOTAL_LEN] = USB_LOW_BYTE(total_len);
      buf[USB_CFG_DESC_OFFSET_TOTAL_LEN + 1] = USB_HIGH_BYTE(total_len);
      len = total_len;
      break;
      }

   return len;
}

Application Callback API

The driver provides callback function interfaces for the application layer, allowing application code to respond to USB events and handle business logic.

  • Composite Application Callback API

    usbd_composite_cb_t is a callback structure for the status of the entire composite device, where the application layer implements specific business logic.

  • Function Class Application Callback API

    This is the callback customized for each sub-function, This is a callback customized for each function, implemented by the application layer. Generally, the following can be implemented:

    typedef struct {
       int(* init)(void);
       int(* deinit)(void);
       int(* setup)(usb_setup_req_t *req, u8 *buf);
       int(* set_config)(void);
       void (*status_changed)(u8 old_status, u8 status);
       int(* sof)(void);
       int(* received)(u8 *buf, u32 len);
       void(* transmitted)(u8 status);
    } usbd_composite_function_class_xx_usr_cb_t;
    

    API

    Description

    init

    Called during class driver initialization; used to initialize application-specific resources.

    deinit

    Called during class driver de-initialization; used to release application-specific resources.

    setup

    Called during the setup or data stage of a control transfer; used to handle application-specific control requests.

    set_config

    Called within the class driver’s set_config callback; used to notify the application layer that the UAC class driver is ready.

    status_changed

    Called when the USB connection status changes; used by the application layer to handle USB hot-plug events.

    sof

    Called when an SOF interrupt is received; used by the application layer to handle clock synchronization.

    transmitted

    Called upon completion of an IN transfer; used by the application layer to asynchronously obtain the IN transfer status.

    received

    Called upon completion of an OUT transfer; used by the application layer to asynchronously obtain the OUT transfer status.

CDC ACM + MSC Example

The following section takes CDC ACM + MSC as an example to introduce the customized sub-function application layer callback (MSC does not use application layer callback).

/**
* @brief User callback structure for CDC ACM events.
* @details This structure allows the application layer to handle CDC ACM events.
*/
typedef struct {
   int(* init)(void);                             /**< Called during class driver initialization for application resource setup. */
   int(* deinit)(void);                           /**< Called during class driver deinitialization for resource cleanup. */
   int(* setup)(usb_setup_req_t *req, u8 *buf);   /**< Called during control transfer SETUP/DATA phases to handle application-specific control requests. */
   int(* received)(u8 *buf, u32 len);             /**< Called when new data is received on the Bulk OUT endpoint. */
   void(* transmitted)(u8 status);                /**< Called after data transmission on the Bulk IN endpoint is complete. */
} usbd_composite_cdc_acm_usr_cb_t;

API for Application

The application layer controls the lifecycle of the entire composite device driver through the following two main functions:

  • usbd_composite_init(): Initialization Function

    • Receives parameters passed by the application layer, such as endpoint buffer size and callback function sets for each sub-function.

    • Calls the initialization function for each sub-function.

    • Links the sub-function driver instances to the usbd_composite_dev structure.

    • Finally, calls usbd_register_class() to register the composite device driver with the USB Core, making it effective.

  • usbd_composite_deinit(): De-initialization Function

    • Calls usbd_unregister_class() to unregister the composite device driver from the USB Core.

    • Sequentially calls the de-initialization function for each sub-function to release all resources.

CDC ACM + MSC Example

The following section takes CDC ACM + MSC as an example to introduce the implementation of application-layer-oriented APIs for composite device class drivers.

/**
* @brief  Init composite class
* @param  cdc_bulk_out_xfer_size: CDC ACM bulk out xfer buffer size
* @param  cdc_bulk_in_xfer_size: CDC ACM bulk in xfer buffer size
* @param  cdc_cb: CDC ACM user callback
* @param  cb: composite user callback
* @return Status
*/
int usbd_composite_init(u16 cdc_bulk_out_xfer_size, u16 cdc_bulk_in_xfer_size, usbd_composite_cdc_acm_usr_cb_t *cdc_cb, usbd_composite_cb_t *cb)
{
   int ret;
   usbd_composite_dev_t *cdev = &usbd_composite_dev;

   if (cdc_cb == NULL) {
      ret = HAL_ERR_PARA;
      RTK_LOGS(TAG, RTK_LOG_ERROR, "Invalid user cb\n");
      return ret;
   }

   if (cb != NULL) {
      cdev->cb = cb;
   }

   ret = usbd_composite_cdc_acm_init(cdev, cdc_bulk_out_xfer_size, cdc_bulk_in_xfer_size, cdc_cb);
   if (ret != HAL_OK) {
      RTK_LOGS(TAG, RTK_LOG_ERROR, "Init CDC ACM itf fail: %d\n", ret);
      return ret;
   }

   ret = usbd_composite_msc_init(cdev);
   if (ret != HAL_OK) {
      RTK_LOGS(TAG, RTK_LOG_ERROR, "Init MSC itf fail: %d\n", ret);
      usbd_composite_cdc_acm_deinit();
      return ret;
   }

   cdev->cdc = (usbd_class_driver_t *)&usbd_composite_cdc_acm_driver;
   cdev->msc = (usbd_class_driver_t *)&usbd_composite_msc_driver;

   usbd_register_class(&usbd_composite_driver);

   return ret;
}

/**
* @brief  DeInit composite class
* @param  void
* @return Status
*/
void usbd_composite_deinit(void)
{
   usbd_unregister_class();

   usbd_composite_msc_deinit();
   usbd_composite_cdc_acm_deinit();
}

Note

For detailed class driver descriptions, please refer to: Vendor-Specific Device Solution

API Reference

For detailed function prototypes and usage, please refer to the Driver API

Application Example

This section takes USB Composite (CDC ACM + MSC) as an example to introduce the complete application implementation and the method of running example application.

Application Design

This section provides a detailed introduction to the complete development and design process of composite device drivers, covering driver initialization, hotplug management, and resource release.

Driver Initialization

The initialization process involves sequentially completing USB Core initialization, and Composite class driver loading. Defining the configuration structure and registering user callback functions are essential steps.

  • Configuration: Configure USB speed mode and interrupt priority.

  • Callback Registration: Define the user callback structure:cpp:struct:usbd_composite_cb_tusbd_composite_fucntion_xx_usr_cb and mount handler functions for each stage.

  • Core Initialization: Call usbd_init() to initialize the USB core.

  • Class Driver Init: Call usbd_composite_init() to initialize the composite class driver.

The following section uses CDC ACM + MSC as an example to introduces the implementation of composite device driver initialization.

Most of the interactions with the MSC are automatically handled by the protocol stack, while the application layer primarily focuses on disk initialization and deinitialization. Prior to USB initialization, it is essential to ensure that the storage medium (such as an SD card or Flash) is ready and invoke the disk initialization interface.

static usbd_config_t composite_cfg = {
   .speed = CONFIG_USBD_COMPOSITE_SPEED,
   .isr_priority = CONFIG_USBD_COMPOSITE_ISR_THREAD_PRIORITY,
   .intr_use_ptx_fifo = 0U,
}

static usbd_composite_cdc_acm_usr_cb_t composite_cdc_acm_usr_cb = {
   .init = composite_cdc_acm_cb_init,
   .deinit = composite_cdc_acm_cb_deinit,
   .setup = composite_cdc_acm_cb_setup,
   .received = composite_cdc_acm_cb_received,
   .transmitted = composite_cdc_acm_cb_transmitted
};

static usbd_composite_cb_t composite_cb = {
   .status_changed = composite_cb_status_changed,
};

int ret = 0;

/* Initializes the underlying storage disk. */
ret = usbd_composite_msc_disk_init();
if (ret != HAL_OK) {
   return;
}

/* Initialize USB device core driver with configuration. */
ret = usbd_init(&composite_cfg);
if (ret != HAL_OK) {
   usbd_composite_msc_disk_deinit();
   return;
}

/* Initialize composite class driver.   */
ret = usbd_composite_init(CONFIG_USBD_COMPOSITE_CDC_ACM_MSC_BULK_OUT_XFER_SIZE,
                     CONFIG_USBD_COMPOSITE_CDC_ACM_MSC_BULK_IN_XFER_SIZE,
                     &composite_cdc_acm_usr_cb,
                     &composite_cb);
if (ret != HAL_OK) {
   usbd_composite_msc_disk_deinit();
   usbd_composite_deinit();
   return;
}

USB Hot-plug Event Handling

Monitor USB connection status changes (connected/disconnected) by registering the status_changed callback function.

Refer to Device Connection Status Detection for more details.

Note

It is recommended to use a semaphore to notify a dedicated task thread for processing, to avoid executing time-consuming operations within the interrupt context.

The following section uses CDC ACM + MSC as an example to introduces the USB Hot-plug Event Handling of composite device driver.

static u8 composite_attach_status;
static rtos_sema_t composite_attach_status_changed_sema;

/* USB status change callback */
static usbd_composite_cb_t composite_cb = {
   .status_changed = composite_cb_status_changed,
};

/* Callback executed in ISR context */
static void composite_cb_status_changed(u8 old_status, u8 status)
{
   composite_attach_status = status;
   rtos_sema_give(composite_attach_status_changed_sema);
}

/* Thread Context: Handle the state machine */
static void composite_hotplug_thread(void *param)
{
   int ret = 0;

   UNUSED(param);

   for (;;) {
         /* Wait for status change signal */
      if (rtos_sema_take(composite_attach_status_changed_sema, RTOS_SEMA_MAX_COUNT) == RTK_SUCCESS) {
         if (composite_attach_status == USBD_ATTACH_STATUS_DETACHED) {
            RTK_LOGS(TAG, RTK_LOG_INFO, "DETACHED\n");
            /* 1. Clean up composite class resources */
            usbd_composite_deinit();
            /* 2. De-initialize USB core */
            ret = usbd_deinit();
            if (ret != 0) {
               break;
            }
            usbd_composite_msc_disk_deinit();
            /* 3. Re-initialize for next connection */
            usbd_composite_msc_disk_init();
            ret = usbd_init(&composite_cfg);
            if (ret != 0) {
               break;
            }
            ret = usbd_composite_init(CONFIG_USBD_COMPOSITE_CDC_ACM_MSC_BULK_OUT_XFER_SIZE,
                              CONFIG_USBD_COMPOSITE_CDC_ACM_MSC_BULK_IN_XFER_SIZE,
                              &composite_cdc_acm_usr_cb,
                              &composite_cb);
            if (ret != 0) {
               usbd_deinit();
               break;
            }
         } else if (composite_attach_status == USBD_ATTACH_STATUS_ATTACHED) {
            RTK_LOGS(TAG, RTK_LOG_INFO, "ATTACHED\n");
         } else {
            RTK_LOGS(TAG, RTK_LOG_INFO, "INIT\n");
         }
      }
   }
   RTK_LOGS(TAG, RTK_LOG_ERROR, "Hotplug thread fail\n");
   rtos_task_delete(NULL);
}

Driver Deinitialization

When the USB function is no longer needed or the system is shut down, resources need to be released in the reverse order of initialization. The following section uses CDC ACM + MSC as an example to introduces the implementation of composite device driver deinitialization.

/* De-initializes the underlying storage disk. */
usbd_composite_msc_disk_deinit();

/* Deinitialize composite class driver first */
usbd_composite_deinit();

/* Deinitialize USB device core driver */
usbd_deinit();

Operation method

This example demonstrates how to use the composite device protocol stack to configure the Ameba development board as a device featuring both CDC ACM (Virtual Serial Port) and MSC (Mass Storage) capabilities simultaneously.

When the development board is connected to a USB Host (e.g., a PC), the system will recognize two independent logical devices. Users can communicate with the board via a serial tool and, at the same time, read from and write to the on-board SD card just like operating a standard USB flash drive.

The example code path is: {SDK}/example/usb/usbd_composite_cdc_acm_msc. It provides provides a complete reference solution for developers to design custom Composite products.

Configuration and Compilation

  • Compilation and Flashing

    Execute the following commands in the SDK root directory to configure the environment, select the target SoC, compile the project, and flash the generated Image file to the development board.

    # Initialize environment (required for every new terminal)
    source env.sh or env.bat(Windows system)
    
    # Select Target SoC (replace xxx with your specific SoCs)
    ameba.py soc xxx
    
    ameba.py build -a usbd_composite_cdc_acm_msc -p
    
  • Confirmation of Menuconfig configuration

    If compilation fails, please execute ameba.py menuconfig and confirm that USB Composite Device and the specific composite class combination (CDC ACM + MSC) have been selected.

    - Choose `CONFIG USB --->`:
    
       [*] Enable USB
             USB Mode (Device) --->
       [*] Composite
             Select Composite Class (CDC ACM + MSC) --->
    
          (X) CDC ACM + MSC
             Select storage media (SD Card (SD mode))  --->
    

Verification

  • Device Startup

    Reset the development board and observe the serial log; it should display the following startup message:

    [COMP] USBD COMP demo start
    
  • Connect to Host

    Connect the development board to a PC using a USB cable.

    When the device is connected to a Windows PC host, the Device Manager will present the following hierarchical structure:

    • Under Universal Serial Bus Controller, there appears: USB Composite Device (driven by usbccgp.sys).

    • Under the Universal Serial Bus Controller, there appears: USB Mass Storage Device (corresponding to the MSC function).

    • Under Ports (COM and LPT), there appears: USB Serial Device (COMx) (corresponding to the CDC ACM function).

  • Function verification 1: CDC ACM (virtual serial port)

    • Open the serial port debugging tool (such as Realtek Trace Tool) on the PC.

    • Select the virtual serial port number enumerated by the development board.

    • Send any character, and the development board will echo the received data as is to verify normal communication.

  • Function verification 2: MSC (mass storage)

    A new removable disk drive letter should automatically pop up in the PC’s file explorer. Users can double-click to open the drive letter and perform file read and write operations on the inserted SD card.

    Note

    This example uses an SD card as the underlying storage medium for the MSC. Please ensure that a formatted SD card is inserted into the on-board SDIOH slot of the development board. Please avoid removing the SD card or disconnecting the USB connection during data reading and writing to prevent damage to the file system.