When you plug an Android smartphone into your PC, it doesn't just present itself as a single USB device. Simultaneously, it exposes a USB CDC-ACM serial debugging interface (ADB), an MTP media transfer filesystem, and perhaps an RNDIS Ethernet tethering interface over a single physical USB cable. This capability—exposing multiple distinct functional interfaces under one physical USB device connection—is made possible by the Linux USB Composite Gadget Subsystem.

Whether you are developing custom embedded firmware on an NXP i.MX8, Broadcom BCM2711 (Raspberry Pi), or Qualcomm Snapdragon target, understanding how the Linux kernel handles USB composite devices is essential for creating multi-function peripherals. In this guide, we dive deep into the Linux USB gadget architecture, trace the execution flow from ConfigFS down to the UDC (USB Device Controller) driver, and build a working multi-function USB device.

Kernel Configuration & Core Modules Cheatsheet

To enable composite gadget support in your custom Linux kernel build, verify that the following CONFIG_USB_* flags are enabled in your kernel .config:

.config (Kernel Options)ini
# Enable Core USB Gadget & Peripheral Support
CONFIG_USB_GADGET=y
CONFIG_USB_LIBCOMPOSITE=m
CONFIG_USB_CONFIGFS=y
 
# Enable Composite Gadget Function Drivers
CONFIG_USB_CONFIGFS_ACM=y
CONFIG_USB_CONFIGFS_MASS_STORAGE=y
CONFIG_USB_CONFIGFS_F_FS=y
CONFIG_USB_CONFIGFS_RNDIS=y
 
# Enable Hardware UDC Driver (SoC specific, e.g. DWC3 or ChipIdea)
CONFIG_USB_DWC3=y
CONFIG_USB_DWC3_GADGET=y

Key kernel modules involved in composite gadget creation: - libcomposite.ko: The core composite framework that manages USB descriptors, configurations, and function bindings. - configfs.ko: Provides the user-space virtual filesystem interface under /sys/kernel/config/usb_gadget/. - Function modules (usb_f_acm.ko, usb_f_mass_storage.ko, usb_f_hid.ko): Individual protocol implementations for specific USB device classes.

Visualizing the Linux USB Gadget Architecture Flow

The architecture of a Linux USB composite gadget is organized into four distinct abstraction layers. User space defines device descriptors via ConfigFS, while the composite core negotiates endpoint allocations with the low-level UDC hardware driver:

Linux USB Composite Gadget Stack Architecture Flowtext
+-----------------------------------------------------------------------+
| USER SPACE: ConfigFS Initialization (/sys/kernel/config/usb_gadget/)  |
| Creates: Gadget Instance -> Descriptors (VID/PID) -> Functions        |
+----------------------------------+------------------------------------+
                                   | (sysfs / configfs calls)
                                   v
+-----------------------------------------------------------------------+
| KERNEL SPACE: USB Composite Framework (drivers/usb/gadget/composite.c)|
|  - Manages struct usb_composite_dev & struct usb_configuration        |
|  - Handles SETUP requests (GET_DESCRIPTOR, SET_CONFIGURATION)         |
+-----------------+-----------------------------------+-----------------+
                  |                                   |
                  v                                   v
+------------------------------------+  +-------------------------------+
| Function Driver 1: CDC-ACM Serial  |  | Function Driver 2: Mass Storage|
| (drivers/usb/gadget/function/f_acm)|  | (function/f_mass_storage.c)   |
| Implements Endpoint IN/OUT Handlers|  | Implements SCSI RAM Disk I/O  |
+-----------------+------------------+  +-----------------+-------------+
                  |                                   |
                  +-----------------+-----------------+
                                    | (usb_ep allocation & queues)
                                    v
+-----------------------------------------------------------------------+
| CORE GADGET SUBSYSTEM: UDC Core (drivers/usb/gadget/udc/core.c)       |
| Binds active gadget configuration to physical hardware controller     |
+----------------------------------+------------------------------------+
                                   | (gadget_ops callbacks)
                                   v
+-----------------------------------------------------------------------+
| HARDWARE DRIVER: USB Device Controller (DWC3 / ChipIdea / MUSB)       |
| Hardware Interrupt Handling (IRQ), DMA Buffer Management, PHY Layers  |
+-----------------------------------------------------------------------+

Hands-on Implementation: Building a Dual ACM Serial + Mass Storage Gadget via ConfigFS

ConfigFS provides an elegant shell interface to create composite devices at runtime without writing custom kernel C modules. Below is a shell script (create_composite_gadget.sh) that builds a composite device containing both a CDC-ACM Virtual COM Port and a Mass Storage Flash Drive:

create_composite_gadget.shbash
#!/bin/sh
set -e
 
# 1. Mount configfs if not already present
if [ ! -d /sys/kernel/config/usb_gadget ]; then
    mount -t configfs none /sys/kernel/config
fi
 
# 2. Create a new USB gadget directory instance
GADGET_DIR="/sys/kernel/config/usb_gadget/g1"
mkdir -p $GADGET_DIR
cd $GADGET_DIR
 
# 3. Configure USB Vendor & Product Identifiers (e.g. Linux Foundation IDs)
echo 0x1d6b > idVendor  # Linux Foundation
echo 0x0104 > idProduct # Multifunction Composite Gadget
echo 0x0200 > bcdUSB    # USB 2.0 Specification
echo 0x0100 > bcdDevice # Device Release 1.0.0
 
# 4. Define Device Descriptor Strings (English 0x409)
mkdir -p strings/0x409
echo "6789012345"             > strings/0x409/serialnumber
echo "Lynxbee Systems Inc."   > strings/0x409/manufacturer
echo "Composite USB Appliance"> strings/0x409/product
 
# 5. Create USB Configuration Instance (c.1)
mkdir -p configs/c.1
mkdir -p configs/c.1/strings/0x409
echo "Serial + Storage Config"> configs/c.1/strings/0x409/configuration
echo 250                      > configs/c.1/MaxPower  # 500mA power allocation
 
# 6. Create Function Instances
# Function 1: CDC-ACM Virtual Serial Port
mkdir -p functions/acm.usb0
 
# Function 2: USB Mass Storage Disk (e.g. Backed by /dev/ram0 or image file)
mkdir -p functions/mass_storage.usb0
echo 1 > functions/mass_storage.usb0/stall
echo /tmp/backing_disk.img > functions/mass_storage.usb0/lun.0/file
echo 1 > functions/mass_storage.usb0/lun.0/removable
 
# 7. Symlink Functions into Configuration c.1
ln -s functions/acm.usb0 configs/c.1/
ln -s functions/mass_storage.usb0 configs/c.1/
 
# 8. Bind Composite Gadget to Hardware USB Device Controller (UDC)
UDC_NAME=$(ls /sys/class/udc | head -n 1)
echo $UDC_NAME > UDC
 
echo "Successfully bound Linux Composite Gadget to UDC: $UDC_NAME"

### Key Script Execution Takeaways - Function Symlinking: In ConfigFS, creating directories under functions/ instantiates function drivers. Symlinking those directories into configs/c.1/ binds them to specific USB configuration descriptors. - UDC Binding: Writing the hardware controller name (e.g. 3880000.usb or dwc3.0.auto) into the UDC attribute file triggers physical USB enumeration with the host PC. - Disabling the Gadget: To tear down the gadget safely, write an empty string to UDC (echo "" > UDC) before removing symlinks and directories.

Deep Kernel Architecture: Data Structures & Descriptor Binding

Inside the Linux kernel source code (drivers/usb/gadget/), the composite framework relies on three fundamental C structures defined in <linux/usb/composite.h>:

1. struct usb_composite_dev

Represents the overall USB physical device. It holds pointers to the underlying struct usb_gadget (the UDC hardware representation), the list of active configurations (struct usb_configuration), and manages control endpoint zero (ep0) transfers during initial USB enumeration.

2. struct usb_configuration

Represents a specific USB configuration descriptor (e.g. Configuration 1). A single device can support multiple configurations (such as high-power vs low-power modes), though only one configuration is active at any given time. It contains a list of associated struct usb_function instances.

3. struct usb_function

Represents an individual functional interface (e.g., ACM serial port, HID keyboard, or Mass Storage). Each usb_function defines its own interface descriptors, requests its required hardware endpoints (struct usb_ep), and registers callback functions: - bind(): Called when the function is bound to a configuration. Allocates endpoints (usb_ep_autoconfig). - set_alt(): Called when the host selects an interface or alt setting. Enables hardware endpoints (usb_ep_enable). - setup(): Handles class-specific control requests on endpoint zero.

Endpoint Allocation & Bandwidth Management

Physical USB Device Controllers (UDCs) have a finite number of hardware IN/OUT endpoints (typically 4 to 16 endpoints depending on the SoC controller IP). When a composite device binds multiple functions (e.g., ACM serial requires 3 endpoints, Mass Storage requires 2 endpoints), the kernel uses usb_ep_autoconfig() during function binding to map virtual function endpoints to available physical UDC hardware endpoints.

If a composite gadget requests more endpoints than the physical UDC hardware supports, binding fails with -ENOSPC (No space left on device).

Troubleshooting & Diagnostic Commands

- `echo: write error: Device or resource busy` when writing to UDC: The controller is already bound to another gadget driver (e.g. legacy g_serial). Unbind existing drivers or ensure /sys/class/udc is free. - Host Fails to Enumerate Device (`Device Descriptor Request Failed`): Check kernel dmesg logs for UDC endpoint allocation errors. Verify that idVendor and idProduct are valid hex strings. - Debugging USB Packets: Load the kernel module usbmon (sudo modprobe usbmon) and capture raw USB control requests on endpoint zero using Wireshark.