Now we need a few descriptors

Since the control endpoint has been successfully initialized, this section will deal with the structure and definition of the standard descriptors.

This part of the tutorial only deals with the standard descriptors, i.e. the device, configuration, interface and endpoint descriptor, so that the host gets a rough picture of the connected USB device. The class-specific descriptors follow in a later part and allow the host to round off the rough picture of the connected USB device and finally load the appropriate driver for the device. But first we need to understand how a descriptor is structured and how we can communicate with the host.

Each USB-enabled device must have at least one device and at least one complete configuration descriptor. The configuration descriptor is further subdivided into the configuration descriptor itself, at least one interface descriptor and at least one end point descriptor. The software must therefore contain the following descriptors:

  • Device descriptor

  • Configuration descriptor

  • Interface descriptor

  • Endpoint descriptor

The descriptors all have a fixed structure, which is described in Chapter 9.6 of the USB specification.

Let's take a look at how the individual descriptors are structured ...

The device descriptor is always the first descriptor requested by the host as soon as the USB device is connected to the bus. It has the task of providing the host with general information about the connected device. Each USB-enabled device has a control endpoint, the size of which is described in the device descriptor. The host must know this size early enough so that communication with the device can run without errors. Each USB-capable device may only have one device descriptor and it should be described by the following structure:

typedef struct
{
	uint8_t bLength;
	uint8_t bDescriptorType;
	uint16_t bcdUSB;
	uint8_t bDeviceClass;
	uint8_t bDeviceSubClass;
	uint8_t bDeviceProtocol;
	uint8_t bMaxPacketSize0;
	uint16_t idVendor;
	uint16_t idProduct;
	uint16_t bcdDevice;
	uint8_t iManufacturer;
	uint8_t iProduct;
	uint8_t iSerialNumber;
	uint8_t bNumConfigurations; 
} __attribute__((packed)) USB_DeviceDescriptor_t;

The addition __attribute__ ((packed)) instructs the compiler to reserve only the memory that is actually required and not to insert any padding bytes. In this way, the data sent by the host can be copied into the memory to cast them into a corresponding structure variable.

Now we need a variable for the device descriptor:

const USB_DeviceDescriptor_t PROGMEM DeviceDescriptor;

With the help of the PROGMEM attribute, the descriptor is not copied from the flash to the SRAM by the startup code when the program is started.

As a consequence, the data must be read directly from the program memory, which means that other assembler instructions must be used.

The PROGMEM attribute is not neccessary needed for the function of the USB driver. Rather, it represents an optimization that ensures that large data structures are not copied into the SRAM. Ideally, large data structures (such as bitmaps for displays) or constant strings are declared via PROGMEM to save SRAM and to shorten the start time of the program.

The initialized device descriptor must now be fed with some values:

const USB_DeviceDescriptor_t PROGMEM DeviceDescriptor =
{
	.bLength            = sizeof(USB_DeviceDescriptor_t), 
	.bDescriptorType	  = DESCRIPTOR_TYPE_DEVICE,
	.bcdUSB			        = USB_VERSION(1, 1, 0),
	.bDeviceClass		    = USB_CLASS_VENDOR,
	.bDeviceSubClass	  = USB_SUBCLASS_NONE,
	.bDeviceProtocol	  = USB_PROTOCOL_NONE,
	.bMaxPacketSize0	  = 8,
	.idVendor		        = 0x0123,
	.idProduct		      = 0x4567,
	.bcdDevice		      = USB_VERSION(1, 0, 0),
	.iManufacturer		  = STRING_ID_MANUFACTURER,
	.iProduct		        = STRING_ID_PRODUCT,
	.iSerialNumber      = STRING_ID_SERIAL,
	.bNumConfigurations	= 1

I have entered the individual constants, definitions and macros of the standard descriptors in a corresponding include file so that the entered values ​​are easier to understand. Since this descriptor is not yet a complete mouse, but an example descriptor, the class Vendor, i.e. manufacturer-specific, is entered as the device class. The vendor and product ID can be freely selected if it is not a commercial product. However, you should be careful that other devices do not already use the same id, otherwise there may be problems with the drivers.

The other descriptors are created in the same way as the device descriptor ...

typedef struct
{
	uint8_t bLength;
	uint8_t bDescriptorType;
	uint16_t wTotalLength;
	uint8_t bNumInterfaces;
	uint8_t bConfigurationValue;
	uint8_t iConfiguration;
	uint8_t bmAttributes;
	uint8_t bMaxPower;
} __attribute__((packed)) USB_ConfigurationDescriptor_t;
typedef struct
{
	uint8_t bLength;
	uint8_t bDescriptorType;
	uint8_t bInterfaceNumber;
	uint8_t bAlternateSetting;
	uint8_t bNumEndpoints;
	uint8_t bInterfaceClass;
	uint8_t bInterfaceSubClass;
	uint8_t bInterfaceProtocol;
	uint8_t iInterface;
} __attribute__((packed)) USB_InterfaceDescriptor_t;
typedef struct
{
	uint8_t bLength;
	uint8_t bDescriptorType;	
        uint8_t bEndpointAddress;
	uint8_t bmAttributes;
	uint16_t wMaxPacketSize;
	uint8_t bInterval;
} __attribute__((packed)) USB_EndpointDescriptor_t;

When requesting the configuration descriptor, in addition to the configuration descriptor, the interface descriptors and endpoint descriptors are also transmitted. Therefore, the individual descriptors are used as members for a new configuration structure, which represents an individual configuration of the device:

typedef struct
{
	USB_ConfigurationDescriptor_t Configuration;
	USB_InterfaceDescriptor_t Interface;
	USB_EndpointDescriptor_t DataINEndpoint;
        USB_EndpointDescriptor_t DataOUTEndpoint;
} USB_Configuration_t;

Then the descriptors are filled with some data:

const USB_Configuration_t PROGMEM ConfigurationDescriptor =
{
	.Configuration =
	{
		.bLength = sizeof(USB_ConfigurationDescriptor_t),
		.bDescriptorType = DESCRIPTOR_TYPE_CONFIGURATION,
		.wTotalLength = sizeof(USB_Configuration_t),
		.bNumInterfaces = 0x01,
		.bConfigurationValue = 0x01,
		.iConfiguration = 0x00,
		.bmAttributes = USB_MASK2CONFIG(USB_CONFIG_SELF_POWERED),
		.bMaxPower = USB_CURRENT_CONSUMPTION(100),
	},
	.Interface =
	{
		.bLength = sizeof(USB_InterfaceDescriptor_t),
		.bDescriptorType = DESCRIPTOR_TYPE_INTERFACE,
		.bInterfaceNumber = 0x00,
		.bAlternateSetting = 0x00,
		.bNumEndpoints = 0x02,
		.bInterfaceClass = USB_CLASS_VENDOR,
		.bInterfaceSubClass = USB_SUBCLASS_NONE,
		.bInterfaceProtocol = USB_PROTOCOL_NONE,
		.iInterface = 0x00,
	},
	.DataINEndpoint =
	{
		.bLength = sizeof(USB_EndpointDescriptor_t),
		.bDescriptorType = DESCRIPTOR_TYPE_ENDPOINT,
		.bEndpointAddress = IN_EP,
		.bmAttributes = USB_ENDPOINT_USAGE_DATA | USB_ENDPOINT_SYNC_NO | USB_ENDPOINT_TRANSFER_INTERRUPT,
		.wMaxPacketSize = EP_SIZE,
		.bInterval = 0x0A,
	},
	.DataOUTEndpoint =
	{
		.bLength = sizeof(USB_EndpointDescriptor_t),
		.bDescriptorType = DESCRIPTOR_TYPE_ENDPOINT,
		.bEndpointAddress = OUT_EP,
		.bmAttributes = USB_ENDPOINT_USAGE_DATA | USB_ENDPOINT_SYNC_NO | USB_ENDPOINT_TRANSFER_INTERRUPT,
		.wMaxPacketSize = EP_SIZE,
		.bInterval = 0x0A,
	}

With these descriptors, too, I had the individual fields filled with macros or corresponding constants.

The descriptors are test descriptors to practice communication between the host and the microcontroller. It is not yet a final mouse descriptor or something else because some fields have to be filled in differently. This information will be added later to update the descriptors.

Finally, a few optional string descriptors were declared:

typedef struct
{
	uint8_t bLength;
	uint8_t bDescriptorType;
	wchar_t bString[];
} __attribute__((packed)) USB_StringDescriptor_t;

String descriptors store a zero-terminated string in UNICODE format. Therefore, an wider character (w_char_t) is used as the data type for the string.

This is 16 bits on an AVR and can therefore ideally store a single UNICODE character.

Unlike the previous descriptors, the string descriptors are only filled with a string. The conversion of a character string into a corresponding descriptor is carried out via macros:

#define WCHAR_TO_STRING_DESCRIPTOR(Array)				{ .bLength = sizeof(USB_StringDescriptor_t) + (sizeof(Array) - 2), .bDescriptorType = DESCRIPTOR_TYPE_STRING, .bString = Array }

const USB_StringDescriptor_t PROGMEM ManufacturerString = WCHAR_TO_STRING_DESCRIPTOR(L"Daniel Kampert");
const USB_StringDescriptor_t PROGMEM ProductString = WCHAR_TO_STRING_DESCRIPTOR(L"AT90USB1287 USB-Example");
const USB_StringDescriptor_t PROGMEM SerialString = WCHAR_TO_STRING_DESCRIPTOR(L"0815");

A special string descriptor is the descriptor with the id 0:

const USB_StringDescriptor_t PROGMEM LANGID = LANG_TO_STRING_DESCRIPTOR(CONV_LANG(LANG_ENGLISH, SUBLANG_ARABIC_SAUDI_ARABIA));

This string returns at least one language id. Using the available language IDs, the application on the host can determine the supported languages ​​and thus read the corresponding descriptors. The macro CONV_LANG converts a primary language and a sub language into the corresponding language ID and transfers this language as an array to the macro LANG_TO_STRING_DESCRIPTOR, which then generates the string descriptor with the id 0.

Last updated