Kampis Elektroecke

Implementierung der Standarddeskriptoren

Da der Kontrollendpunkt erfolgreich initialisiert wurde, soll es in diesem Teil um den Aufbau und die Definition der Standard Deskriptoren gehen. USB-Geräte werden ihren Aufgaben entsprechend in Klassen unterteilt (wie z. B. HID, Audio oder Printer) und für jede Kategorie gibt es eigene Treiber und zusätzliche Deskriptoren, sodass sich jedes USB-Gerät durch zwei Sätze Deskriptoren identifizieren kann:

  • Die Standard Deskriptoren
  • Die klassenspezifischen Deskriptoren

Dieser Teil des Tutorials befasst sich nur mit den Standard Deskriptoren, sprich um den Geräte-, den Konfigurations-, den Interface und den Endpunktdeskriptor, sodass der Host ein grobes Bild von dem angeschlossenen USB-Gerät bekommt. Die klassenspezifischen Deskriptoren folgen in einem späteren Teil und erlauben dem Host das grobe Bild des angeschlossenen USB-Gerätes abzurunden und schlussendlich den passenden Treiber für das Gerät zu laden. Doch dafür müssen wir erst einmal verstehen, wie ein Deskriptor aufgebaut ist und wie wir mit dem Host kommunizieren können.

Jedes USB-fähige Gerät muss über mindestens einen Geräte- und über mindestens einen kompletten Konfigurations-Deskriptor verfügen. Der Konfigurations-Deskriptor unterteilt sich zudem noch in den Konfigurations-Deskriptor selbst, mindestens einen Interface-Deskriptor und mindestens einen Endpunkt-Deskriptor. Die Software muss also die folgenden Deskriptoren enthalten:

  • Geräte-Deskriptor
  • Konfigurations-Deskriptor
  • Interface-Deskriptor
  • Endpunkt-Deskriptor

Die Deskriptoren haben alle einen festen Aufbau, der in Kapitel 9.6 der USB-Spezifikation beschrieben wird. Schauen wir uns mal nacheinander an, wie die einzelnen Deskriptoren aufgebaut sind…

Der Geräte-Deskriptor:

Der Geräte-Deskriptor ist der immer der erste Deskriptor, der vom Host angefragt wird, sobald das USB-Gerät mit dem Bus verbunden wird. Er hat die Aufgabe dem Host allgemeine Informationen über das angeschlossene Gerät zu liefern. Jedes USB-fähige Gerät besitzt einen Kontrollendpunkt, dessen Größe im Geräte-Deskriptor beschrieben ist. Der Host muss diese Größe frühzeitig genug erfahren, damit die Kommunikation mit dem Gerät fehlerfrei ablaufen kann. Jedes USB-fähige Gerät darf nur einen Geräte-Deskriptor besitzen, der folgendermaßen aufgebaut ist:

Offset Feld Größe Beschreibung
0 bLength 1 Größe des Deskriptors in Bytes
1 bDescriptorType 1 DEVICE Deskriptor (Feld = 1)
2 bcdUSB 2 Verwendete USB Version
4 bDeviceClass 1 Durch das USB-IF vergebener Klassencode
5 bDeviceSubClass 1 Durch das USB-IF vergebener Subklassencode
6 bDeviceProtocol 1 Durch das USB-IF vergebener Protokolcode
7 bMaxPacketSize0 1 Maximale Paketgröße für Endpunkt 0.

Muss entweder 8, 16, 32 oder 64 sein

8 idVendor 2 Durch das USB-IF vergebene Vendor-ID
10 idProduct 2 Durch den Hersteller vergebene Produkt-ID
12 bcdDevice 2 Releasenummer des Gerätes
14 iManufacturer 1 Index des Stringdeskriptors, der den Hersteller beschreibt
15 iProduct 1 Index des Stringdeskriptors, der das Produkt beschreibt
16 iSerialNumber 1 Index des Stringdeskriptors, der die Seriennummer beschreibt
17 bNumConfigurations 1 Anzahl der Gerätekonfigurationen

Der Geräte-Deskriptor soll durch eine Struktur beschrieben werden:

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;

Hinweis:

Durch den Zusatz __attribute__((packed)) wird der Compiler angewiesen nur den tatsächlich benötigten Speicher zu reservieren und keine Paddingbytes einzufügen. Auf diese Weise können die von Host gesendeten Daten in den Speicher Array kopiert und in eine entsprechende Strukturvariable gecastet werden.


Jetzt kann eine entsprechende Variable für den Geräte-Deskriptor erstellt werden:

const USB_DeviceDescriptor_t PROGMEM DeviceDescriptor;

Mit Hilfe des PROGMEM Attributes wird der Deskriptor beim Programmstart nicht durch den Startup-Code vom Flash in den SRAM kopiert. Als Konsequenz daraus müssen die Daten direkt aus dem Programmspeicher gelesen werden, wodurch andere Assemblerinstruktionen verwendet werden müssen.


Hinweis:

Das Attribut PROGMEM ist für die Funktion des USB-Treibers nicht wichtig. Vielmehr stellt es eine Optimierung dar, die dafür sorgt, dass große Datenstrukturen nicht in den SRAM kopiert werden. Optimalerweise werden große Datenstrukturen (wie z. B. Bitmaps für Displays) oder konstante Strings über PROGMEM deklariert um SRAM zu sparen und die Startzeit vom Programm zu verkürzen.


Der initialisierte Geräte-Deskriptor muss nun noch mit Werten gefüttert werden:

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
};

Die einzelnen Konstanten, Definitionen und Makros der Standard-Deskriptoren habe ich in eine entsprechende Include-Datei eingetragen, sodass die eingetragenen Werte leichter zu verstehen sind. Da es sich bei diesem Deskriptor noch nicht um eine komplette Maus, sondern um einen Beispieldeskriptor handelt, wird als Device Class die Klasse Vendor, also Herstellerspezifisch, eingetragen. Die Vendor– und die Product-ID kann, wenn es sich nicht um ein kommerzielles Produkt handelt, frei gewählt werden. Man sollte allerdings aufpassen das nicht andere Geräte bereits die selben IDs verwenden, da es sonst ggf. zu Problemen mit den Treibern kommen kann.

Die Erstellung der anderen Deskriptoren erfolgt analog zum Geräte-Deskriptor…

Der vollständige Konfigurations-Deskriptor:

Für die übrigen Deskriptoren werden ebenfalls entsprechende Strukturen angelegt:

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;

Bei der Anfrage nach dem Konfigurations-Deskriptor werden, neben dem Konfigurations-Deskriptor, auch die Interface- und die Endpunkt-Deskriptoren übertragen. Daher werden die einzelnen Deskriptoren als Member für eine neue Konfigurationsstruktur genutzt, welche eine einzelne Konfiguration des Gerätes darstellt:

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

Anschließend werden die Deskriptoren mit Inhalt gefüllt:

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,
	}
};

Auch bei diesen Deskriptoren habe ich die einzelnen Felder durch Makros, bzw. entsprechende Konstanten füllen lassen.


Optionale String-Deskriptoren:

Als letztes wurden noch ein paar optionale String-Deskriptoren deklariert:

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

String-Deskriptoren speichern eine Null-terminierte Zeichenkette im UNICODE-Format. Daher wird als Datentyp für den String ein Wider Character (w_char_t) genutzt. Dieser ist auf einem AVR 16 Bit groß und kann damit ideal ein einzelnes UNICODE-Zeichen speichern.

Anders als die vorangegangenen Deskriptoren werden die String-Deskriptoren lediglich mit einer Zeichenkette gefüllt. Die Umwandlung einer Zeichenkette in einen entsprechenden Deskriptor wird über Makros ausgeführt:

#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");

Ein besonderer String-Deskriptor ist der Deskriptor mit der ID 0:

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

Dieser String gibt mindestens eine Language-ID zurück. Über die vorhandenen Language-IDs kann die Anwendung auf dem Host die unterstützen Sprachen ermitteln und so die entsprechenden Deskriptoren auslesen. Das Makro CONV_LANG wandelt eine Primärsprache und eine Sekundärsprache in die entsprechende Sprach-ID um und übergibt diese Sprache als Array an das Makro LANG_TO_STRING_DESCRIPTOR, welches dann den String-Deskriptor mit der ID 0 erzeugt.

Damit sind die Deskriptoren vollständig. Im nächsten Teil schauen uns an wie der Host mit dem USB-Device kommuniziert. Dazu müssen wir diese sogenannten Standard-Requests auswerten und beantworten. Wenn alles klappt, meldet sich der Mikrocontroller am Ende des nächsten Teils bereits erfolgreich beim Host, sodass wir mit ihm kommunizieren können.

Zurück

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert