Eine noch niedrigere Ebene (avr-vusb)
USB-On-Register: STM32L1 / STM32F1
USB-On-Register: Bulk-Endpunkt am Beispiel von Massenspeicher
USB-On-Register: Isochroner Endpunkt am Beispiel eines Audiogeräts Wir
beschäftigen uns weiterhin mit USB auf STM32L151-Controllern. Wie im vorherigen Teil wird es hier nichts Plattformabhängiges geben, aber es wird USB-abhängig sein. Genauer gesagt werden wir die dritte Art von Endpunkt betrachten - Interrupt. Und wir werden dies am Beispiel eines zusammengesetzten Geräts "Tastatur + Tablet" ( Link zur Quelle ) tun .
Nur für den Fall, ich warne Sie: Dieser Artikel (wie alle anderen auch) ist eher eine Zusammenfassung dessen, was ich beim Verständnis dieses Themas verstanden habe. Viele Dinge sind "magisch" geblieben und ich wäre dankbar, wenn es einen Spezialisten gibt, der sie erklären kann.
Zunächst möchte ich Sie daran erinnern, dass das HID-Protokoll (Human Interface Device) nicht für den Austausch großer Datenmengen vorgesehen ist. Der gesamte Austausch basiert auf zwei Konzepten: einem Ereignis und einem Zustand . Ein Ereignis ist eine einmalige Nachricht, die als Reaktion auf eine externe oder interne Auswirkung auftritt. Beispielsweise hat der Benutzer eine Taste gedrückt oder die Maus bewegt. Oder ich habe auf einer Tastatur NumLock deaktiviert, woraufhin der Host gezwungen wird und der zweite den entsprechenden Befehl zum
Der Zweck eines Interrupt-Punkts ist also der gleiche wie ein Interrupt in einer Steuerung - um ein seltenes Ereignis schnell zu melden. Da USB jedoch hostorientiert ist, hat das Gerät kein Recht, die Übertragung selbst zu starten. Um dies zu umgehen, haben sich die USB-Entwickler eine Krücke ausgedacht: Der Host sendet regelmäßig Anforderungen zum Lesen aller Interrupt-Punkte. Die Häufigkeit der Anforderung wird durch den letzten Parameter im EndpointDescriptor konfiguriert (dies ist Teil des ConfigurationDescriptor). Wir haben das Feld bInterval bereits in den vorherigen Kapiteln gesehen, aber sein Wert wurde ignoriert. Jetzt hat er endlich eine Verwendung gefunden. Der Wert hat eine Größe von 1 Byte und wird in Millisekunden festgelegt, sodass wir in Intervallen von 1 ms bis 2,55 Sekunden abgefragt werden. Bei Geräten mit niedriger Geschwindigkeit beträgt das Mindestintervall 10 ms. Das Vorhandensein einer Krücke mit Unterbrechungspunkten, die für uns abgefragt werden, bedeutetSelbst ohne Austausch verschwenden sie Busbandbreite.
Die logische Schlussfolgerung: Interrupt-Punkte gelten nur für IN-Transaktionen. Insbesondere werden sie verwendet, um Ereignisse von der Tastatur oder Maus zu übertragen, um über Änderungen in den Dienstleitungen des COM-Anschlusses zu benachrichtigen, um den Audiostream und dergleichen zu synchronisieren. Für all dies müssen Sie jedoch andere Arten von Punkten hinzufügen. Um das Beispiel nicht zu komplizieren, beschränken wir uns daher auf die Implementierung des HID-Geräts. Tatsächlich haben wir ein solches Gerät bereits im ersten Teil hergestellt, aber dort wurden zusätzliche Punkte überhaupt nicht verwendet, und die Struktur des HID-Protokolls wurde nicht berücksichtigt.
Konfigurationsdeskriptor
static const uint8_t USB_ConfigDescriptor[] = {
ARRLEN34(
ARRLEN1(
bLENGTH, // bLength: Configuration Descriptor size
USB_DESCR_CONFIG, //bDescriptorType: Configuration
wTOTALLENGTH, //wTotalLength
1, // bNumInterfaces
1, // bConfigurationValue: Configuration value
0, // iConfiguration: Index of string descriptor describing the configuration
0x80, // bmAttributes: bus powered
0x32, // MaxPower 100 mA
)
ARRLEN1(
bLENGTH, //bLength
USB_DESCR_INTERFACE, //bDescriptorType
0, //bInterfaceNumber
0, // bAlternateSetting
2, // bNumEndpoints
HIDCLASS_HID, // bInterfaceClass:
HIDSUBCLASS_BOOT, // bInterfaceSubClass:
HIDPROTOCOL_KEYBOARD, // bInterfaceProtocol:
0x00, // iInterface
)
ARRLEN1(
bLENGTH, //bLength
USB_DESCR_HID, //bDescriptorType
USB_U16(0x0110), //bcdHID
0, //bCountryCode
1, //bNumDescriptors
USB_DESCR_HID_REPORT, //bDescriptorType
USB_U16( sizeof(USB_HIDDescriptor) ), //wDescriptorLength
)
ARRLEN1(
bLENGTH, //bLength
USB_DESCR_ENDPOINT, //bDescriptorType
INTR_NUM, //bEdnpointAddress
USB_ENDP_INTR, //bmAttributes
USB_U16( INTR_SIZE ), //MaxPacketSize
10, //bInterval
)
ARRLEN1(
bLENGTH, //bLength
USB_DESCR_ENDPOINT, //bDescriptorType
INTR_NUM | 0x80, //bEdnpointAddress
USB_ENDP_INTR, //bmAttributes
USB_U16( INTR_SIZE ), //MaxPacketSize
10, //bInterval
)
)
};
Der aufmerksame Leser kann die Beschreibungen der Endpunkte sofort bemerken. Mit der Sekunde ist alles in Ordnung - IN-Punkt (da die Addition mit 0x80) vom Typ Interrupt ist, werden Größe und Intervall angegeben. Aber der erste scheint als OUT deklariert zu sein, aber gleichzeitig zu unterbrechen, was dem widerspricht, was zuvor gesagt wurde. Und auch der gesunde Menschenverstand: Der Host benötigt zu keinem Zeitpunkt Krücken, um etwas auf das Gerät zu übertragen. Auf diese Weise werden jedoch andere Rakes umgangen: Der Endpunkttyp in STM32 wird nicht für einen Punkt festgelegt, sondern nur für das IN / OUT-Paar. Daher funktioniert es nicht, den 0x81st-Punkt auf den Interrupt-Typ, sondern auf den 0x01st zu setzen Steuerung. Dies ist jedoch kein Problem für den Host, es würde wahrscheinlich die gleichen Daten auch im Bulk-Punkt senden ... was ich jedoch nicht überprüfen werde.
HID-Deskriptor
Die Struktur des HID-Deskriptors ist der Konfigurationsdatei "name = value" am ähnlichsten, aber im Gegensatz dazu ist "name" eine numerische Konstante aus der USB-spezifischen Liste, und "value" ist entweder auch eine Konstante oder eine Variable von Größe 0 bis zu 3 Bytes.
Wichtig:Für einige "Namen" wird die Länge des "Werts" in den 2 niedrigstwertigen Bits des Felds "Name" angegeben. Nehmen wir zum Beispiel LOGICAL_MINIMUM (den Mindestwert, den diese Variable im normalen Modus annehmen kann). Der Code für diese Konstante ist 0x14. Wenn es dementsprechend keinen "Wert" gibt (es scheint, dass dies nicht der Fall ist, aber ich werde nicht argumentieren - aus irgendeinem Grund wurde dieser Fall eingegeben), enthält der Deskriptor eine einzelne Zahl 0x14. Wenn der "Wert" 1 (ein Byte) ist, werden 0x15, 0x01 geschrieben. Für einen Zwei-Byte-Wert werden 0x1234, 0x16, 0x34, 0x12 geschrieben - der Wert wird von niedrig nach hoch geschrieben. Nun, vor dem Heap wird die Nummer 0x123456 0x17, 0x56, 0x34, 0x12 sein.
Natürlich bin ich zu faul, um mir all diese numerischen Konstanten zu merken, also werden wir Makros verwenden. Leider habe ich nie einen Weg gefunden, sie dazu zu bringen, die Größe des übergebenen Werts selbst herauszufinden und auf 1, 2, 3 oder 4 Bytes zu erweitern. Deshalb musste ich eine Krücke bauen: Ein Makro ohne Suffix ist für die häufigsten 8-Bit-Werte verantwortlich, mit einem Suffix 16 für 16-Bit-Werte und mit 24 für 24-Bit-Werte. Es wurden auch Makros für "zusammengesetzte" Werte wie den Bereich LOGICAL_MINMAX24 (min, max) geschrieben, die sich auf 4, 6 oder 8 Bytes erweitern.
Wie bei Konfigurationsdateien gibt es Abschnitte, die als Seiten (usage_page) bezeichnet werden und Geräte nach Zweck gruppieren. Zum Beispiel gibt es eine Seite mit grundlegenden Peripheriegeräten wie Tastaturen, Mäusen und nur Tasten, es gibt Joysticks und Gamepads (ich empfehle aufrichtig, sich welche anzuschauen! Es gibt auch Panzer, Raumschiffe, U-Boote und alles andere ) gibt es sogar Displays ... Ich habe keine Ahnung, wo ich nach Software suchen soll, die mit all dem funktioniert.
Innerhalb jeder Seite wird ein bestimmtes Gerät ausgewählt. Bei einer Maus handelt es sich beispielsweise um einen Zeiger und Schaltflächen, bei einem Tablet um einen Stift oder den Finger eines Benutzers (was ?!). Sie bezeichnen auch die Bestandteile des Geräts. Ein Teil des Zeigers sind also seine X- und Y-Koordinaten. Einige Merkmale können zu einer "Sammlung" zusammengefasst werden, aber warum verstehe ich nicht wirklich, warum dies getan wird. In der Dokumentation werden Felder manchmal mit ein paar Buchstaben markiert, die den Zweck des Feldes und die Arbeitsweise damit beschreiben:
| CA. | Sammlung (Antrag) | Serviceinformationen, die keiner Variablen entsprechen |
| CL | Sammlung (logisch) | - / - |
| CP | Sammlung (phisisch) | - / - |
| Dv | Dynamischer Wert | Eingabe- oder Ausgabewert (variabel) |
| MC | Momentane Kontrolle | Statusflag (1-Flag gespannt, 0-gelöscht) |
| OSC | One-Shot-Kontrolle | einmaliges Ereignis. Es wird nur der Übergang 0-> 1 verarbeitet |
Es gibt natürlich auch andere, aber sie werden in meinem Beispiel nicht verwendet. Wenn beispielsweise das X-Feld als DV markiert ist, wird es als Variable mit einer Länge ungleich Null betrachtet und in die Berichtsstruktur aufgenommen. Die MC- oder OSC-Felder sind ebenfalls im Bericht enthalten, jedoch 1 Bit groß.
Ein Bericht (vom Gerät gesendetes oder empfangenes Datenpaket) enthält die Werte aller darin beschriebenen Variablen. Die Beschreibung der Schaltfläche spricht nur von einem belegten Bit, aber für relative Koordinaten (wie viel die Maus beispielsweise bewegt hat) ist mindestens ein Byte erforderlich und für absolute Koordinaten (wie für einen Touchscreen) mindestens 2 Bytes wird gebraucht. Außerdem haben viele Steuerelemente ihre eigenen physischen Einschränkungen. Beispielsweise kann ein ADC desselben Touchscreens eine Auflösung von nur 10 Bit haben, dh Werte von 0 bis 1023 angeben, die der Host auf die Vollbildauflösung skalieren muss. Daher gibt der Deskriptor zusätzlich zum Zweck jedes Felds auch den Bereich seiner zulässigen Werte (LOGICAL_MINMAX) sowie manchmal den Bereich der physikalischen Werte (in Millimetern dort oder in Grad) und die Darstellung in der an Bericht ist obligatorisch.Die Darstellung wird durch zwei Zahlen festgelegt: die Größe einer Variablen (in Bits) und ihre Anzahl. Beispielsweise werden die Koordinaten für das Berühren des Touchscreens in dem von uns erstellten Gerät wie folgt festgelegt:
USAGE( USAGE_X ), // 0x09, 0x30,
USAGE( USAGE_Y ), // 0x09, 0x31,
LOGICAL_MINMAX16( 0, 10000 ), //0x16, 0x00, 0x00, 0x26, 0x10, 0x27,
REPORT_FMT( 16, 2 ), // 0x75, 0x10, 0x95, 0x02,
INPUT_HID( HID_VAR | HID_ABS | HID_DATA), // 0x91, 0x02,
Hier sehen Sie, dass zwei Variablen deklariert sind, die im Bereich von 0 bis 10000 variieren und zwei Abschnitte mit 16 Bit im Bericht belegen.
Das letzte Feld besagt, dass die obigen Variablen vom Host (IN) gelesen werden und erklärt genau, wie. Ich werde seine Flaggen nicht im Detail beschreiben, ich werde nur auf einige eingehen. Das HID_ABS-Flag zeigt an, dass der Wert absolut ist, dh, dass kein Verlauf ihn beeinflusst. Der alternative Wert HID_REL gibt an, dass der Wert ein Offset vom vorherigen ist. Das HID_VAR-Flag gibt an, dass jedes Feld für seine eigene Variable verantwortlich ist. Der alternative Wert HID_ARR besagt, dass nicht die Zustände aller Schaltflächen aus der Liste übertragen werden, sondern nur die Nummern der aktiven Schaltflächen. Dieses Flag gilt nur für Einzelbitfelder. Anstatt 101/102 Zustände aller Tastaturtasten zu übertragen, können Sie sich mit einer Liste gedrückter Tasten auf einige Bytes beschränken. Dann ist der erste Parameter REPORT_FMT für die Größe der Nummer und der zweite für die Nummer verantwortlich.
Da die Größe aller Variablen in Bits festgelegt ist, ist es logisch zu fragen: Was ist mit den Schaltflächen, da ihre Anzahl möglicherweise kein Vielfaches von 8 ist und dies zu Ausrichtungsschwierigkeiten beim Lesen und Schreiben führt. Es wäre möglich, jeder Schaltfläche ein Byte zuzuweisen, aber dann würde sich die Lautstärke des Berichts erheblich erhöhen, was für Hochgeschwindigkeitsprogramme wie Interrupt unangenehm ist. Stattdessen wird versucht, die Schaltflächen näher beieinander zu platzieren, und der verbleibende Platz wird mit Bits mit dem Flag HID_CONST gefüllt.
Jetzt können wir, wenn wir keinen Deskriptor von Grund auf neu schreiben, zumindest versuchen, ihn zu lesen, dh bestimmen, welchen Bits dieses oder jenes Feld entspricht. Es reicht aus, die INPUT_HIDs und die entsprechenden REPORT_FMTs zu zählen. Denken Sie daran, dass ich mir solche Makros ausgedacht habe, die sonst niemand benutzt. In den Deskriptoren anderer Personen müssen Sie nach Eingaben, report_size, report_count oder sogar numerischen Konstanten suchen.
Jetzt können Sie den gesamten Deskriptor mitbringen:
static const uint8_t USB_HIDDescriptor[] = {
//keyboard
USAGE_PAGE( USAGEPAGE_GENERIC ),//0x05, 0x01,
USAGE( USAGE_KEYBOARD ), // 0x09, 0x06,
COLLECTION( COLL_APPLICATION, // 0xA1, 0x01,
REPORT_ID( 1 ), // 0x85, 0x01,
USAGE_PAGE( USAGEPAGE_KEYBOARD ), // 0x05, 0x07,
USAGE_MINMAX(224, 231), //0x19, 0xE0, 0x29, 0xE7,
LOGICAL_MINMAX(0, 1), //0x15, 0x00, 0x25, 0x01,
REPORT_FMT(1, 8), //0x75, 0x01, 0x95, 0x08
INPUT_HID( HID_DATA | HID_VAR | HID_ABS ), // 0x81, 0x02,
//reserved
REPORT_FMT(8, 1), // 0x75, 0x08, 0x95, 0x01,
INPUT_HID(HID_CONST), // 0x81, 0x01,
REPORT_FMT(1, 5), // 0x75, 0x01, 0x95, 0x05,
USAGE_PAGE( USAGEPAGE_LEDS ), // 0x05, 0x08,
USAGE_MINMAX(1, 5), //0x19, 0x01, 0x29, 0x05,
OUTPUT_HID( HID_DATA | HID_VAR | HID_ABS ), // 0x91, 0x02,
// 1
REPORT_FMT(3, 1), // 0x75, 0x03, 0x95, 0x01,
OUTPUT_HID( HID_CONST ), // 0x91, 0x01,
REPORT_FMT(8, 6), // 0x75, 0x08, 0x95, 0x06,
LOGICAL_MINMAX(0, 101), // 0x15, 0x00, 0x25, 0x65,
USAGE_PAGE( USAGEPAGE_KEYBOARD ), // 0x05, 0x07,
USAGE_MINMAX(0, 101), // 0x19, 0x00, 0x29, 0x65,
INPUT_HID( HID_DATA | HID_ARR ), // 0x81, 0x00,
)
//touchscreen
USAGE_PAGE( USAGEPAGE_DIGITIZER ), // 0x05, 0x0D,
USAGE( USAGE_PEN ), // 0x09, 0x02,
COLLECTION( COLL_APPLICATION, // 0xA1, 0x0x01,
REPORT_ID( 2 ), //0x85, 0x02,
USAGE( USAGE_FINGER ), // 0x09, 0x22,
COLLECTION( COLL_PHISICAL, // 0xA1, 0x00,
USAGE( USAGE_TOUCH ), // 0x09, 0x42,
USAGE( USAGE_IN_RANGE ), // 0x09, 0x32,
LOGICAL_MINMAX( 0, 1), // 0x15, 0x00, 0x25, 0x01,
REPORT_FMT( 1, 2 ), // 0x75, 0x01, 0x95, 0x02,
INPUT_HID( HID_VAR | HID_DATA | HID_ABS ), // 0x91, 0x02,
REPORT_FMT( 1, 6 ), // 0x75, 0x01, 0x95, 0x06,
INPUT_HID( HID_CONST ), // 0x81, 0x01,
USAGE_PAGE( USAGEPAGE_GENERIC ), //0x05, 0x01,
USAGE( USAGE_POINTER ), // 0x09, 0x01,
COLLECTION( COLL_PHISICAL, // 0xA1, 0x00,
USAGE( USAGE_X ), // 0x09, 0x30,
USAGE( USAGE_Y ), // 0x09, 0x31,
LOGICAL_MINMAX16( 0, 10000 ), //0x16, 0x00, 0x00, 0x26, 0x10, 0x27,
REPORT_FMT( 16, 2 ), // 0x75, 0x10, 0x95, 0x02,
INPUT_HID( HID_VAR | HID_ABS | HID_DATA), // 0x91, 0x02,
)
)
)
};
Zusätzlich zu den zuvor diskutierten Feldern gibt es auch ein so interessantes Feld wie REPORT_ID. Da unser Gerät, wie aus den Kommentaren hervorgeht, zusammengesetzt ist, muss der Host irgendwie bestimmen, wessen Daten er empfängt. Dafür wird dieses Feld benötigt.
Ein weiteres Feld, auf das ich Sie aufmerksam machen möchte, ist OUTPUT_HID. Wie der Name schon sagt, ist es nicht für den Empfang eines Berichts (IN) verantwortlich, sondern für das Senden (OUT). Es befindet sich im Tastaturbereich und beschreibt die CapsLock-, NumLock-, ScrollLock-Indikatoren sowie zwei exotische Indikatoren - Compose (ein Flag zur Eingabe einiger Zeichen, die keine eigenen Schaltflächen haben, wie á, µ oder) und Kana (Eingabe von Hieroglyphen). . Für dieses Feld haben wir den OUT-Punkt gestartet. In seinem Handler prüfen wir, ob die CapsLock- und NumLock-Anzeigen leuchten müssen: Auf der Platine befinden sich nur zwei Dioden, die verdrahtet sind.
Es gibt ein drittes Feld zum Datenaustausch - FEATURE_HID, das wir im ersten Beispiel verwendet haben. Wenn INPUT und OUTPUT Ereignisse übertragen sollen, ist FEATURE ein Zustand, der gelesen oder geschrieben werden kann. Dies geschieht zwar nicht über dedizierte Endpunkte, sondern über die übliche ep0 mittels entsprechender Anfragen.
Wenn Sie sich den Deskriptor genau ansehen, können Sie die Struktur des Berichts wiederherstellen. Genauer gesagt, zwei Berichte:
struct{
uint8_t report_id; //1
union{
uint8_t modifiers;
struct{
uint8_t lctrl:1; //left control
uint8_t lshift:1;//left shift
uint8_t lalt:1; //left alt
uint8_t lgui:1; //left gui. hyper, winkey
uint8_t rctrl:1; //right control
uint8_t rshift:1;//right shift
uint8_t ralt:1; //right alt
uint8_t rgui:1; //right gui
};
};
uint8_t reserved; //
uint8_t keys[6]; //
}__attribute__((packed)) report_kbd;
struct{
uint8_t report_id; //2
union{
uint8_t buttons;
struct{
uint8_t touch:1; //
uint8_t inrange:1; //
uint8_t reserved:6;// 1
};
};
uint16_t x;
uint16_t y;
}__attribute__((packed)) report_tablet;
Wir werden sie außerdem durch Drücken der Tasten auf der Tafel senden. Da wir nur ein Beispiel für die Implementierung und kein vollständiges Gerät schreiben, werden wir dies auf barbarische Weise tun - indem wir zwei Berichte senden, von denen der erste die Tasten "drückt" und der zweite "loslässt". Außerdem mit einer großen "dummen" Verzögerung zwischen den Nachrichten. Wenn Sie keinen Bericht mit den "freigegebenen" Tasten senden, berücksichtigt das System, dass die Taste noch gedrückt ist, und wiederholt sie. Natürlich gibt es keine Frage der Effizienz, auch der Sicherheit, aber es reicht für den Test. Oh ja, wo ohne einen weiteren Rechen! Die Größe der Struktur muss mit der im Deskriptor beschriebenen Größe übereinstimmen. Andernfalls gibt Windows vor, nicht zu verstehen, was sie von der Struktur erwarten. Wie üblich ignoriert Linux solche Fehler und funktioniert so, als wäre nichts passiert.
Beim Testen stieß ich auf einen lustigen Nebeneffekt: Wenn Sie in Windows 7 auf den "Touchscreen" klicken, wird das Handschriftfenster angezeigt. Ich wusste nichts über diese Funktion.
Wenn Sie ein fertiges Gerät haben
... und ich möchte es von innen betrachten. Zunächst sehen wir natürlich, dass Sie sogar von einem normalen Benutzer, ConfigurationDescriptor, können:
lsusb -v -d <VID:PID>
Für den HID-Deskriptor habe ich keinen besseren Weg gefunden (und nicht gesucht) als von root:
cat /sys/kernel/debug/hid/<address>/rdes
Der Vollständigkeit halber sollte hier hinzugefügt werden, wie ähnliche Dinge in anderen Betriebssystemen betrachtet werden. Aber ich habe keine relevanten Kenntnisse, vielleicht werden sie Ihnen in den Kommentaren sagen. Es ist natürlich wünschenswert, keine Software von Drittanbietern zu installieren.
Fazit
Das ist in der Tat alles, was ich auf HID ausgegraben habe. Der Mindestplan - um zu lernen, wie man vorgefertigte Deskriptoren liest, mehrere Geräte gleichzeitig emuliert und Tablet-Eingaben implementiert - ist abgeschlossen. Nun, gleichzeitig wurde die Philosophie der Unterbrechungspunkte berücksichtigt.
Wie in der schlechten Zeit habe ich eine kleine Dokumentation im Repository hinterlassen, falls die USB-IF-Designer beschließen, die Site erneut zu ruinieren.