USB descriptors

When your system is acting as the USB device in a link with another system acting as the USB host, you can specify which USB descriptors you want to expose to the host. These USB descriptors indicate your system's device function (e.g., mass storage, serial data transfer).

Defining your own USB descriptors allows you to override the default descriptors any time you use usblauncher to restart the USB stack in device mode. The descriptor overriding is done by issuing the start_stack::device,n command to the device control object. Here, n is an index into the one-based list of descriptors (i.e., 1 refers to the first entry). In the QNX SDP image, this list is found in the main configuration file (rules.lua) but the individual descriptors are stored in separate Lua files (e.g., umass.lua, usbser.lua).

You can change the USB descriptors exposed to a USB host by issuing a start_stack command after the USB stack is started but before the link with the host is enabled (i.e., before the host can detect the attachment and enumerate the device). Because usblauncher doesn't monitor hardware for device or cable attachments, it's up to client applications to detect when a USB host is trying to set up a link and to then decide on the appropriate USB device role for the system and expose the device descriptors based on this role (see "Support for USB On-The-Go (OTG)" for a typical device role-swapping scenario).

USB descriptor levels

USB descriptors are expressed in a hierarchy that defines different levels for storing the communication and data properties of a device. Each level of depth is more specific than the last, meaning it describes a more specialized set of USB properties. The four levels in USB descriptors are:

Represents the entire device. A device can have only one device descriptor.
Specifies details on device power usage and stores interface descriptors. Currently, usblauncher supports only one configuration per device.
Groups endpoint descriptors to define a single device feature.
Acts as a single channel for USB data, similar to a socket in a program. The USB host uses endpoint information to determine bandwidth requirements.
Note: You can find detailed information on these four descriptor levels, including lists of USB fields applicable to each level, on the USB Descriptors page of the USB in a NutShell online blog.


You can store common USB descriptor values in variables and later refer to these variables when defining USB fields. Here are the variables defined at the top of usbser.lua:

--  Sample USB descriptors for a USB Serial device

USB_MAX_CURRENT                = 0

Class-specific descriptors

Some interfaces require class-specific descriptors, which provide information specific to the communication or data class supported by the interface. Class-specific descriptors are expressed as the concatenation of all functional descriptors for the class. Functional descriptors provide information such as call management capabilities, supported network control notifications, and so on.

In usbser.lua, we define class-specific descriptors for a communication device. Because there are many class-specific descriptor formats, which are defined outside the core USB specification, we don't include any helper templates for them. Therefore, you need to define these descriptors as an array of bytes, in the same order they would be sent over the wire. You can refer to this array later when defining the descriptor for an interface.

There is one exception to the strict byte-by-byte copying and data transfering done by usblauncher: when it finds a string in your array of bytes, usblauncher converts the string to double-byte format, stores it in a table of strings, and then replaces the raw bytes in the array with the corresponding index from the strings table. For all other bytes, you must hand-code them:

comm_descriptors = {
-- Header Functional Descriptor
    0x05,                   -- bFunctionLength
    0x24,                   -- bDescriptorType
    0x00,                   -- bDescriptorSubType
    0x10,                   -- bcdCDC   (LSB)
    0x01,                   -- bcdCDC   (MSB)

-- Call Managment Functional Descriptor
    0x05,                   -- bFunctionLength
    0x24,                   -- bDescriptorType
    0x01,                   -- bDescriptorSubType
    0x00,                   -- bmCapabilities - ENOSUP
    0x01,                   -- bDataInterface

-- Abstract Control Model Function Descriptor
    0x04,                   -- bFunctionLength
    0x24,                   -- bDescriptorType
    0x02,                   -- bDescriptorSubType
    0x02,                   -- bmCapabilities

-- Abstract Control Model Union Descriptor
    0x05,                   -- bFunctionLength
    0x24,                   -- bDescriptorType
    0x06,                   -- bDescriptorSubType
    0x00,                   -- bControlInterface
    0x01,                   -- bSubordinate interface

Descriptor templates

Before you can fill in the templates that correspond to the four USB descriptor levels, you must first create an empty table:

usbser = {}

You can now define the USB device properties that you want to expose to a USB host. The Device{} construct lets you specify device descriptor fields, which list manufacturer information and the device's supported USB classes and protocols. The sample rules.lua file provides some predefined variables for USB class and subclass types. To improve readability, you can refer to these variables instead of putting in literal integer values. In all descriptor templates, the bLength and bDescriptorType fields are filled in for you. Depending on the descriptor type, other fields may also be filled in.

When you assign strings to the table entries for the iManufacturer, iProduct, and iSerialNumber device descriptor fields, usblauncher stores a double-byte version of each string in a special strings table and replaces the literal values of those entries with the associated indexes in the strings table. Most likely, you'll want to define strings for these previous three fields along with values for these other fields:

The USB Descriptors page explains the meaning of all device descriptor fields.

You must assign the filled-in Device{} template to the device entry in the table that you created:

usbser.device = Device{
    bDeviceClass = USB_CLASS_COMMS,
    bDeviceProtocol = 0x0,
    bMaxPacketSize = 64,
    idVendor = 0x1234,
    idProduct = 0x4,
    bcdDevice = 0x0100,
    manufacturer = 'Acme Corporation',
    product = 'CDC Serial Peripheral',
    serial = 'xxxx-xxxx-xxxx',
    bNumConfigurations = 1,
Note: This release supports only one configuration per device, so bNumConfigurations must be set to 1. You must define separate configuration descriptors for Full Speed and for High Speed connections, but each USB device will use only one configuration in an active link.

Next, specify the remaining descriptor types as part of one configuration, using the Config{} construct. Fields like bmAttributes and bMaxPower take values described in the configuration descriptor fields section of the USB Descriptors page. You can define a string for the iDescription field, in which case usblauncher stores a double-byte version of this string in the strings table and writes the appropriate table index in place of the string bytes when sending data over the wire. The wTotalLength and bNumInterfaces fields are also filled in by usblauncher.

To specify the full-speed configuration, you must assign the filled-in Config{} template to the fs_config entry in the main table:

usbser.fs_config = Config{  -- full speed
    bConfigurationValue = 1,
    bmAttributes = USB_CONFIG_SELFPOWERED,
    bMaxPower = USB_MAX_CURRENT,
    description = 'Default Configuration',
    interfaces = {

The high-speed configuration is specified in a similar manner; you must complete a Config{} template but assign it to the hs_config entry:

usbser.hs_config = Config{  -- high speed
    bConfigurationValue = 1,
    bmAttributes = USB_CONFIG_SELFPOWERED,
    bMaxPower = USB_MAX_CURRENT,
    description = 'Default Configuration',
    interfaces = {

Next, define a nested table in the interfaces entry, in each of the full-speed and high-speed configurations. You can group multiple interfaces together by defining an Interface Association Descriptor (IAD), using the Association{} construct. An IAD has a class, subclass, protocol, and optionally a string to describe it, followed by a list of interfaces, each of which is defined by a separate Iface{} template:

            bInterfaceClass = USB_CLASS_COMMS,
            bInterfaceSubClass = USB_COMMS_SUBCLASS_MODEM,
            bInterfaceProtocol = 0x0,
            description = 'Serial Port Interface',
            interfaces = {

When you assign a string to the IAD description entry, usblauncher writes the table index for this string into the iInterface descriptor field for each interface defined within the IAD.

An Iface{} defines an interface's USB class, subclass, and protocol as well as a bAlternateSetting entry, which determines the autogenerated value written into the bInterfaceNumber descriptor field. The interface number increases by 1 for each new interface whose bAlternateSetting value is 0. When this value is 1, the interface is an alternate interface and therefore has the same interface number as the previous interface.

In both the full-speed and high-speed configurations, you must define a Communication Class interface to handle device management and possibly call management. You can define any number of Data Class interfaces to support the transfer of data with a certain structure and usage. Each such interface is specified in its own Iface{} tag.

For the Communication Class interfaces, we assign class-specific descriptors by setting the class_specific entry to the array of bytes containing those descriptors (which we defined earlier):

                    bInterfaceClass = USB_CLASS_COMMS,
                    bInterfaceSubClass = USB_COMMS_SUBCLASS_MODEM,
                    bInterfaceProtocol = 0x0,
                    bAlternateSetting = 0,
                    description = 
                      'Serial Port Communication Class Interface',
                    class_specific = comm_descriptors,
                    endpoints = {
Note: In our example, the Data Class interfaces don't use class-specific descriptors, so the class_specific entry isn't set for those interfaces.

Finally, you must provide a list of endpoints in each interface. For the Communication Class interfaces, you can use the InterruptIn{} construct to set the wMaxPacketSize and bInterval endpoint descriptor fields (as defined in the USB specification):

                    endpoints = {
                        InterruptIn{wMaxPacketSize = 8, 
                                            bInterval = 8}

In this case, usblauncher fills in the bEndpointAddress and bmAttributes fields, which are sent along with the other endpoint information to the USB host.

For the Data Class interfaces, you can use the BulkIn{} and BulkOut{} constructs to set limits on the packet sizes for outgoing and incoming bulk data. Each construct takes only the wMaxPacketSize field:

                    endpoints = {
                        BulkOut{wMaxPacketSize = 64},
                        BulkIn{wMaxPacketSize = 64},

When you define maximum packet sizes for bulk data transfer on an endpoint, usblauncher sets the bInterval descriptor field to 0 and calculates the bEndpointAddress and bmAttributes fields.

For all other endpoint properties, you can use either the EndpointIn{} or the EndpointOut{} construct and then set the bmAttributes, wMaxPacketSize, and bInterval fields according to the USB specification. In this case, usblauncher calculates the bEndpointAddress field.

Note: If any expected field is left undefined, usblauncher logs a warning message and assigns 0 in place of the missing field.

Bypassing helper templates to fully define USB descriptors

The helper templates don't cover every USB device scenario because usblauncher fills in many descriptor fields for you, at all hierarchy levels in the USB descriptors. For full control over what's stored in the descriptors, you can define their raw bytes, as demonstrated in the included raw_desc_usbser.lua file. If you want to fully define USB descriptors byte by byte, you must set certain keys in the descriptor table:

Any descriptor table that you create must be referenced in the list of descriptor overrides (i.e., the Device_Stack.descriptors list in the main configuration file). For example, to use the serial_raw_desc{} table defined in raw_desc_usbser.lua, you need to put an entry for that table in the descriptors list:

descriptors = { iap2, iap2ncm, serial_raw_desc };

For two-byte fields such as wMaxPacketSize and wTotalLength, you can access their least-significant and most-significant bytes by using the lsb() and msb() functions, which is necessary when defining these fields byte by byte.

The .device, .fs_config, and .hs_config keys must be Lua strings (which aren't null-terminated, as are C strings). You can use Lua's string.char() function to convert an array of bytes into a string, the expected type for these keys. However, the .strings key must be a table of strings. We provide the double_byte() helper function so you don't have to hand-code the double-byte representations of these strings when expressing them as raw bytes.