Die vusb-Bibliothek erfinden

Einführung


Nach dem Lesen des Namens kann sich eine logische Frage stellen: Warum sollte man heutzutage die Software-Implementierung von USB mit niedriger Geschwindigkeit untersuchen, wenn es eine Reihe billiger Controller mit einem Hardwaremodul gibt? Tatsache ist, dass das Hardwaremodul, das die Ebene des Austauschs logischer Ebenen verbirgt, das USB-Protokoll in eine Art Magie verwandelt. Um zu spüren, wie diese „Magie“ funktioniert, gibt es nichts Besseres, als sie von Grund auf neu zu reproduzieren.


Zu diesem Zweck werden wir versuchen, ein Gerät so zu gestalten, dass es auf dem ATmega8-Controller als USB-HID-Gerät ausgibt. Im Gegensatz zur weit verbreiteten Literatur werden wir nicht von der Theorie zur Praxis, von der niedrigsten zur höchsten Ebene, von den logischen Spannungen zu den Schlussfolgerungen übergehen und nach jedem Schritt mit der „Erfindung“ desselben Vusb enden, um zu überprüfen, ob der Code wie erwartet funktioniert. Unabhängig davon stelle ich fest, dass ich keine Alternative zu dieser Bibliothek erfinde, sondern deren Quellcode konsistent reproduziere, wobei die ursprüngliche Struktur und die Namen so weit wie möglich erhalten bleiben und erklärt wird, warum dieser oder jener Abschnitt dient. Mein üblicher Stil beim Schreiben von Code unterscheidet sich jedoch vom Stil der Vusb-Autoren. Ich gebe sofort ehrlich zu, dass ich neben altruistischem Interesse (anderen ein schwieriges Thema zu erzählen) auch ein egoistisches Interesse habe - das Thema selbst zu studieren und ein Maximum an subtilen Punkten für mich selbst zu erfassen. Daraus folgt auch, dass ein wichtiger Punkt möglicherweise übersehen wird oder ein Thema nicht vollständig offengelegt wird.


Zum besseren Verständnis des Codes habe ich versucht, die geänderten Abschnitte mit Kommentaren hervorzuheben und sie aus den zuvor beschriebenen Abschnitten zu entfernen. Tatsächlich wird der Quellcode die Hauptinformationsquelle sein, und der Text wird erklären, was und warum getan wurde und welches Ergebnis erwartet wird.


Ich stelle auch fest, dass nur USB mit niedriger Geschwindigkeit berücksichtigt wird, auch ohne zu erwähnen, was mehr Hochgeschwindigkeitsvarianten auszeichnet.


Schritt 0. Eisen und andere Zubereitung


Nehmen wir als Test ein hausgemachtes Debugging-Board auf ATmega8-Basis mit 12-MHz-Quarz. Ich werde das Schema nicht geben, es ist ziemlich normal (siehe die offizielle vusb-Website), das einzige, was erwähnenswert ist, sind die verwendeten Schlussfolgerungen. In meinem Fall entspricht der Ausgang D + PD2, der Ausgang D-PD3 und der Hosenträger hängt an PD4. Grundsätzlich könnte ein Pull-up-Widerstand an die Stromversorgung angeschlossen werden, die manuelle Steuerung scheint jedoch etwas konsistenter mit dem Standard zu sein.


5 V werden über den USB-Anschluss mit Strom versorgt, auf Signalleitungen werden jedoch nicht mehr als 3,6 V erwartet (warum war dies für mich ein Rätsel). Sie müssen also entweder die Leistung des Controllers verringern oder die Zenerdioden auf die Signalleitungen setzen. Ich habe die zweite Option gewählt, aber im Großen und Ganzen spielt es keine Rolle.


Da wir die Implementierung „erfinden“, wäre es schön zu sehen, was in den Gehirnen des Controllers passiert, dh es werden zumindest einige Debugging-Informationen benötigt. In meinem Fall sind dies zwei LEDs an PD6, PD7 und vor allem UART an PD0, PD1, die auf 115200 konfiguriert sind, sodass Sie das Chatter des Controllers über einen normalen Bildschirm oder ein anderes Programm für die Arbeit mit dem COM-Anschluss abhören können:


$ screen /dev/ttyUSB0 115200 

Ein Wireshark mit dem entsprechenden Modul wird sich auch als nützliches Dienstprogramm für das USB-Debugging herausstellen (es beginnt nicht immer sofort, aber die Lösung solcher Probleme befindet sich recht erfolgreich im Internet und ist nicht die Aufgabe dieses Artikels).


Hier wäre es möglich, ein weiteres Kilobyte Text für die Beschreibung des Programmierers, der Makefiles und anderer Dinge auszugeben, aber dies macht kaum Sinn. Ebenso werde ich mich nicht auf Peripherieeinstellungen konzentrieren, die nicht mit USB zusammenhängen. Wenn jemand dies nicht einmal herausfinden kann, ist es zu früh, um in die Eingeweide von Software-USB einzusteigen?


Der Quellcode für alle Schritte ist auf Github verfügbar.


Schritt 1. Akzeptiere mindestens etwas


Laut Dokumentation unterstützt USB mehrere feste Geschwindigkeiten, von denen AVR nur die niedrigste zieht: 1,5 Megabit pro Sekunde. Sie wird durch den Pull-up-Widerstand und die anschließende Kommunikation bestimmt. Für die von uns gewählte Frequenz muss der Widerstand D- mit einer 3,3-V-Stromversorgung verbinden und einen Nennwert von 1,5 kOhm haben. In der Praxis kann er jedoch mit +5 V verbunden werden und der Nennwert kann geringfügig variiert werden. Bei einer Reglerfrequenz von 12 MHz nur 8 Taktzyklen pro Bit. Es ist klar, dass diese Genauigkeit und Geschwindigkeit nur im Assembler erreichbar ist. Daher starten wir die Datei drvasm.S. Dies impliziert auch die Notwendigkeit, einen Interrupt zu verwenden, um den Anfang eines Bytes abzufangen. Ich bin froh, dass das erste über USB übertragene Byte immer dasselbe ist, SYNC. Wenn Sie also zum Anfang kommen, ist es in Ordnung. Infolgedessen vergehen vom Anfang bis zum Ende des Bytes nur 64 Controller-Zyklen (der Spielraum ist sogar noch kleiner), sodass Sie keine anderen Nicht-USB-Interrupts verwenden sollten.


Legen Sie die Konfiguration sofort in einer separaten Datei usbconfig.h ab. Dort werden die für USB verantwortlichen Pins sowie die verwendeten Bits, Konstanten und Register gesetzt.


Theoretische Beilage
Die Übertragung über USB erfolgt in Paketen mit jeweils mehreren Bytes. Das erste Byte ist immer das SYNC-Synchronisationsbyte, gleich 0b10000000, das zweite ist die Bytekennung des PID-Pakets. Die Übertragung jedes Bytes erfolgt unter Verwendung der NRZI-Codierung vom niedrigstwertigen zum höchstwertigen Bit (dies ist nicht ganz richtig, aber in vusb wird diese Subtilität an anderer Stelle ignoriert). Diese Methode besteht darin, dass eine logische Null durch Ändern der logischen Ebene in die entgegengesetzte Richtung übertragen wird und eine logische Einheit durch Nichtänderung übertragen wird. Zusätzlich wird ein Schutz vor der Desynchronisation (die wir nicht verwenden, aber berücksichtigen müssen) der Signalquelle und des Empfängers eingeführt: Wenn die gesendete Sequenz sechs Einheiten in einer Reihe enthält, dh der Zustand der Endgeräte sich sechs aufeinanderfolgende Zeiträume lang nicht ändert, wird der Übertragung eine erzwungene Inversion hinzugefügt, als ob Null wird übertragen. Somit kann die Bytegröße 8 oder 9 Bit betragen.
Es ist auch erwähnenswert, dass die Datenleitungen in USB differentiell sind, dh wenn D + hoch ist, ist D- niedrig (dies wird als K-Zustand bezeichnet) und umgekehrt (J-Zustand). Dies geschieht für eine bessere Störfestigkeit bei hohen Frequenzen. Es stimmt, es gibt eine Ausnahme: Das Signal am Ende des Pakets (es heißt SE0) wird übertragen, indem beide Signalleitungen zur Erde gezogen werden (D + = D- = 0). Es werden zwei weitere Signale übertragen, indem eine niedrige Spannung auf der D + -Leitung und eine hohe Spannung auf der D + -Leitung für unterschiedliche Zeiten gehalten werden. Wenn die Zeit klein ist (eine Bytelänge oder etwas länger), ist dies Leerlauf, eine Pause zwischen Paketen, und wenn sie groß ist, ein Rücksetzsignal.

Die Übertragung erfolgt also auf einem Differentialpaar, wobei der exotische Fall von SE0 nicht berücksichtigt wird, aber wir werden ihn noch nicht berücksichtigen. Um den Status des USB-Busses zu bestimmen, benötigen wir nur eine Leitung, D + oder D-. Im Großen und Ganzen gibt es keinen Unterschied, welchen man wählen soll, aber für die Bestimmtheit sei D-.


Der Beginn des Pakets kann durch Empfangen des SYNC-Bytes nach einem langen Leerlauf bestimmt werden. Der Leerlaufzustand entspricht log.1 auf der D-Leitung (es ist auch der J-Zustand), und das SYNC-Byte ist 0b100000, aber es wird vom niedrigstwertigen Bit zum höchstwertigen übertragen, außerdem wird es in NRZI codiert, dh jede Null bedeutet Signalumkehrung und ein Mittel das gleiche Niveau halten. Die Folge der Zustände D- ist also wie folgt:


ByteLeerlaufSYNCPID
USB1..100000001????????
D-1..101010100????????

Der Anfang des Pakets ist bei einer fallenden Flanke am einfachsten zu erkennen, und wir werden einen Interrupt darauf konfigurieren. Was aber, wenn der Controller zu Beginn des Empfangs beschäftigt ist und den Interrupt nicht sofort eingeben kann? Um zu vermeiden, dass in einer solchen Situation die Anzahl der Titel verloren geht, verwenden wir das SYNC-Byte für den vorgesehenen Zweck. Es besteht ausschließlich aus Fronten an den Grenzen von Bits, so dass wir auf eine von ihnen warten können, dann auf eine weitere halbe Bit, und direkt in die Mitte der nächsten gelangen können. Es ist jedoch keine gute Idee, auf eine „einige“ Front zu warten, da wir nicht nur in die Mitte des Stücks geraten müssen, sondern auch wissen müssen, welches Stück wir in die Partitur bekommen haben. Und dafür ist auch SYNC geeignet: Es hat am Ende zwei Nullbits hintereinander (es sind K-Zustände). Hier werden wir sie fangen. In der Datei drvasm.S wird also ein Code vom Interrupt-Eintrag zu foundK angezeigt. Aufgrund der Zeit für die Überprüfung des Status des Ports, für einen bedingungslosen Übergang usw. erreichen wir die Markierung nicht am Anfang des Bits, sondern nur in der Mitte. Es ist jedoch sinnlos, dasselbe Bit zu überprüfen, da wir dessen Bedeutung bereits kennen. Daher warten wir auf 8 Taktzyklen (bisher leeres Nop'ami) und überprüfen das nächste Bit. Wenn es auch Null ist, haben wir das Ende von SYNC gefunden und können mit dem Empfang signifikanter Bits fortfahren.


Tatsächlich ist der gesamte weitere Code zum Lesen von zwei weiteren Bytes mit anschließender Ausgabe an UART vorgesehen. Nun, warten Sie auf den Status von SE0, um nicht versehentlich in das nächste Paket zu gelangen.


Jetzt können Sie den resultierenden Code kompilieren und sehen, welche Bytes unser Gerät akzeptiert. Persönlich habe ich die folgende Reihenfolge:


 4E 55 00 00 4E 55 00 00 4E 55 00 00 4E 55 00 00 4E 55 00 00 

Denken Sie daran, dass wir Rohdaten ohne inkrementelle Nullen und NRZI-Decodierung ausgeben. Versuchen wir, manuell zu dekodieren, beginnend mit dem niedrigen Bit:


4E
NRZI010011100 (vorheriges Bit)
Byte00101101
2D

55
NRZI010101010 (vorheriges Bit)
Byte00000000
00

Es ist nicht sinnvoll, Nullen zu dekodieren, da 16 identische Werte in einer Zeile nicht in einem Paket enthalten sein können.


Auf diese Weise konnten wir Firmware schreiben, die die ersten beiden Bytes des Pakets akzeptiert, allerdings bisher ohne Dekodierung.


Schritt 2. Demoversion von NRZI


Um nicht manuell neu zu codieren, können Sie dies dem Controller selbst anvertrauen: Die XOR-Operation macht genau das, was Sie benötigen, obwohl das Ergebnis invertiert ist. Fügen Sie danach eine weitere Inversion hinzu:


 mov temp, shift lsl shift eor temp, shift com temp rcall uart_hex 

Das Ergebnis ist durchaus zu erwarten:


 2D 00 FF FF 2D 00 FF FF 2D 00 FF FF 2D 00 FF FF 2D 00 FF FF 

Schritt 3. Entfernen Sie den Byte-Empfangszyklus


Machen wir noch einen kleinen Schritt und erweitern den Zyklus des Empfangs des ersten Bytes in einem linearen Code. Es stellt sich also heraus, dass viele Nops nur auf den Start des nächsten Bits warten müssen. Anstelle einiger von ihnen können Sie den NRZI-Decoder verwenden, andere werden später nützlich sein.


Das Ergebnis der vorherigen Option ist nicht anders.


Schritt 4. Lesen Sie in den Puffer


Das Lesen in separaten Registern ist natürlich schnell und schön, aber wenn zu viele Daten vorhanden sind, ist es besser, einen Puffereintrag zu verwenden, der sich irgendwo im RAM befindet. Dazu deklarieren wir im Main ein Array von ausreichender Größe und schreiben im Interrupt dort.
Theoretische Beilage


Die Paketstruktur in USB ist standardisiert und besteht aus den folgenden Teilen: SYNC-Byte, PID + CHECK-Byte (2 Felder mit jeweils 4 Bit), Datenfeld (manchmal 11 Bit, häufiger jedoch eine beliebige Anzahl von 8-Bit-Bytes) und eine CRC-Prüfsumme von entweder 5 ( für ein 11-Bit-Datenfeld) oder 16 (für den Rest) Bits. Schließlich beträgt das Ende der Paketanzeige (EOP) zwei Pausenbits, dies sind jedoch keine Daten mehr.


Bevor Sie mit dem Array arbeiten können, müssen Sie noch die Register konfigurieren und nop freigeben, bevor das erste Bit nicht ausreicht. Daher müssen Sie das Lesen der ersten beiden Bits in den linearen Abschnitt des Codes einfügen, zwischen dessen Befehlen wir den Initialisierungscode einfügen, und dann in die Mitte des Lesezyklus zum rxbit2-Label springen. Apropos Puffergröße. Laut Dokumentation können nicht mehr als 8 Datenbytes in einem Paket übertragen werden. Wir addieren die Service-Bytes PID und CRC16, wir erhalten eine Puffergröße von 11 Bytes. SYNC-Byte und EOP-Status werden nicht geschrieben. Wir können das Intervall der Anforderungen vom Host nicht steuern, möchten sie aber auch nicht verlieren. Daher nehmen wir einen doppelten Spielraum für das Lesen. Im Moment werden wir nicht den gesamten Puffer verwenden, aber um in Zukunft nicht zurückzukehren, ist es besser, das erforderliche Volume sofort zuzuweisen.


Schritt 5. Menschlich mit dem Puffer arbeiten


Anstatt die ersten Bytes des Arrays direkt zu lesen, schreiben wir einen Code, der genau so viele Bytes liest, wie tatsächlich in das Array geschrieben wurden. Fügen Sie gleichzeitig ein Trennzeichen zwischen den Paketen hinzu.
Jetzt sieht die Ausgabe folgendermaßen aus:


 >03 2D 00 10 >01 FF >03 2D 00 10 >01 FF >03 2D 00 10 >01 FF >03 2D 00 10 >01 FF >03 2D 00 10 >01 FF 

Schritt 6. Hinzufügen eines Additivs Nulladditiv


Schließlich ist es Zeit, den Bitstream zum Standard zu lesen. Das letzte Element, ohne das wir erfolgreich auskommen konnten, war eine falsche Null, die alle sechs aufeinander folgenden Einheiten hinzugefügt wurde. Da der Byteempfang für den linearen Körper der Schleife bereitgestellt wird, müssen Sie nach jedem Bit an allen acht Stellen überprüfen. Betrachten Sie die ersten beiden Bits als Beispiel:


 unstuff0: ;1 (  breq) andi x3, ~(1<<0) ;1 [15]  0-  .     mov x1, x2 ;1 [16]      () in x2, USBIN ;1 [17] <-- 1-   .     ori shift, (1<<0) ;1 [18]  0-   .1      rjmp didUnstuff0 ;2 [20] ;<---//---> rxLoop: eor shift, x3 ;1 [0] in x1, USBIN ;1 [1] st y+, shift ;2 [3] ldi x3, 0xFF ;1 [4] nop ;1 [5] eor x2, x1 ;1 [6] bst x2, USBMINUS ;1 [7]     0-   shift bld shift, 0 ;1 [8] in x2, USBIN ;1 [9] <--  1- (, ) andi x2, USBMASK ;1 [10] breq se0 ;1 [11] andi shift, 0xF9 ;1 [12] didUnstuff0: breq unstuff0 ;1 [13] eor x1, x2 ;1 [14]; bst x1, USBMINUS ;1 [15]     1-   shift bld shift, 1 ;1 [16] rxbit2: in x1, USBIN ;1 [17] <--  2-  (, ) andi shift, 0xF3 ;1 [18] breq unstuff1 ;1 [19] didUnstuff1: 

Zur Vereinfachung der Navigation werden die Adressen der beschriebenen Befehle durch die Beschriftungen auf der rechten Seite gezählt. Bitte beachten Sie, dass sie zum Zählen der Taktzyklen des Controllers eingeführt wurden, sodass sie nicht in Ordnung sind. Das nächste Byte wird auf dem rxLoop-Label gelesen, das vorherige Byte wird invertiert und in den Puffer [0, 3] geschrieben. Als nächstes wird auf dem Etikett [1] der Status der D-Zeile gelesen, gemäß XOR mit dem zuvor akzeptierten Zustand, wir dekodieren NRZI (ich erinnere mich, dass gewöhnliches XOR seine Inversion hinzufügt, um zu korrigieren, welches wir in das mit den Einheiten 0xFF initialisierte Maskenregister x3 eingeben) und schreiben auf 0- i-tes Bit des Schieberegisters [7,8]. Dann beginnt der Spaß - wir prüfen, ob das empfangene Bit das sechste unverändert war. Das mit D- empfangene konstante Bit entspricht dem Schreiben von Null (nicht Eins! Wir werden am Ende zu Eins wechseln, XOR) in das Register. Daher müssen Sie überprüfen, ob die Bits 0, 7, 6, 5, 4, 3 Nullen sind. Die verbleibenden zwei Bits spielen keine Rolle, sie blieben vom vorherigen Byte und wurden früher überprüft. Um sie loszuwerden, schneiden wir das Register durch die Maske [12] ab, wobei alle für uns interessanten Bits auf 1: 0b11111001 = 0xF9 gesetzt sind. Wenn sich nach dem Anwenden der Maske herausstellt, dass alle Bits Nullen sind, ist die Situation des Hinzufügens eines Bits behoben und es gibt einen Übergang zum Label unstuff0. Dort wird ein weiteres Bit [17] gelesen, anstatt das, was zuvor im Intervall zwischen anderen Operationen von einem Überschuss [9] gelesen wurde. Wir tauschen auch die Register der aktuellen und vorherigen Werte x1, x2 aus. Tatsache ist, dass auf jedem Bit der Wert in einem Register gelesen wird und dann XOR mit einem anderen, wonach die Register ausgetauscht werden. Dementsprechend muss beim Lesen des inkrementellen Registers auch diese Operation ausgeführt werden. Das Interessanteste ist jedoch, dass wir in das Schichtdatenregister nicht die Null schreiben, die wir ehrlich erhalten haben, sondern die Einheit, die der Host zu übertragen versucht hat [18]. Dies liegt an der Tatsache, dass beim Empfang der nächsten Bits auch der Wert Null berücksichtigt werden muss. Wenn wir Null aufgezeichnet haben, konnte die Maskenprüfung nicht feststellen, dass das zusätzliche Bit bereits berücksichtigt wurde. Somit werden im Schieberegister alle Bits invertiert (relativ zu den vom Host übertragenen) und die Null nicht. Um ein solches Durcheinander im Puffer zu verhindern, führen wir eine umgekehrte Inversion gemäß XOR nicht mit 0xFF [0] durch, sondern mit 0xFE, dh einem Register, in dem das entsprechende Bit auf 0 zurückgesetzt wird und dementsprechend nicht zur Inversion führt. Dazu am Sample [15] das Nullbit zurücksetzen.


Eine ähnliche Situation tritt bei den Bits 1 bis 5 auf. Angenommen, das 1. Bit entspricht der Prüfung 1, 0, 7, 6, 5, 4, während die Bits 2, 3 ignoriert werden. Dies entspricht der Maske 0xF3.
Die Verarbeitung von 6 und 7 Bits ist jedoch unterschiedlich:


 didUnstuff5: andi shift, 0x3F ;1 [45]   5-0 breq unstuff5 ;1 [46] ;<---//---> bld shift, 6 ;1 [52] didUnstuff6: cpi shift, 0x02 ;1 [53]   6-1 brlo unstuff6 ;1 [54] ;<---//---> bld shift, 7 ;1 [60] didUnstuff7: cpi shift, 0x04 ;1 [61]   7-2 brsh rxLoop ;3 [63] unstuff7: 

Die Maske für das 6. Bit ist die Nummer 0b01111110 (0x7E), Sie können sie jedoch nicht dem Schieberegister überlagern, da das 0. Bit zurückgesetzt wird, das in das Array geschrieben werden muss. Außerdem wurde beim Countdown [45] bereits eine Maske überlagert, die 7 Bits zurücksetzte. Daher ist es notwendig, das zusätzliche Bit zu verarbeiten, wenn die Bits 1-6 gleich Null sind und das 0. Bit keine Rolle spielt. Das heißt, der Wert des Registers sollte 0 oder 1 sein, was durch Vergleichen von "weniger als 2" perfekt überprüft wird [53, 54].


Das gleiche Prinzip wurde für das 7. Bit verwendet: Anstatt die 0xFC-Maske anzuwenden, wird eine Prüfung auf „weniger als 4“ durchgeführt [61, 63].


Schritt 7. Sortieren Sie die Pakete


Da wir ein Paket mit dem ersten Byte (PID) gleich 0x2D (SETUP) empfangen können, werden wir versuchen, das empfangene zu sortieren. Übrigens, warum habe ich das Paket 0x2D SETUP aufgerufen, wenn es ACK zu sein scheint? Tatsache ist, dass die USB-Übertragung vom niedrigstwertigen zum höchstwertigen Bit in jedem Feld und nicht in Byte ausgeführt wird, während wir Byte für Byte akzeptieren. Das erste signifikante Feld, PID, nimmt nur 4 Bits ein, gefolgt von 4 weiteren CHECK-Bits, die eine bitweise Inversion des PID-Feldes darstellen. Somit ist das erste empfangene Byte nicht PID + CHECK, sondern CHECK + PID. Es gibt jedoch keinen großen Unterschied, da alle Werte im Voraus bekannt sind und es einfach ist, die Knabbereien stellenweise neu anzuordnen. Wir werden sofort die wichtigsten Codes, die für uns nützlich sein können, in die Datei usbconfig.h schreiben.


Wir haben noch nicht begonnen, den PID-Verarbeitungscode hinzuzufügen. Beachten Sie, dass er schnell sein sollte (dh im Assembler), aber eine Ausrichtung durch Uhren ist nicht erforderlich, da wir das Paket bereits akzeptiert haben. Daher wird dieser Abschnitt anschließend in die Datei asmcommon.inc übertragen, die Assembler-Code enthält, der nicht an die Häufigkeit gebunden ist. Markieren Sie in der Zwischenzeit einfach den Kommentar.
Fahren wir nun mit dem Sortieren der empfangenen Pakete fort.


Theoretische Beilage
Datenpakete auf dem USB-Bus werden zu Transaktionen zusammengefasst. Jede Transaktion beginnt mit dem Senden eines speziellen Markierungspakets durch den Host, das Informationen darüber enthält, was der Host mit dem Gerät tun möchte: konfigurieren (SETUP), Daten senden (OUT) oder empfangen (IN). Nachdem das Markierungspaket gesendet wurde, folgt eine Pause von zwei Bits. Darauf folgt ein Datenpaket (DATA0 oder DATA1), das je nach Markierungspaket sowohl vom Host als auch vom Gerät gesendet werden kann. Als nächstes eine weitere Pause von zwei Bits Länge und die Antwort ist HANDSHAKE, ein Bestätigungspaket (ACK, NAK, STALL, wir werden sie ein anderes Mal betrachten).
SETUPDATA0Handschlag
Host-> GerätPauseHost-> GerätPauseGerät-> Host

OUTDATA0 / DATA1Handschlag
Host-> GerätPauseHost-> GerätPauseGerät-> Host

INDATA0 / DATA1Handschlag
Host-> GerätPauseGerät-> HostPauseHost-> Gerät


Da der Austausch auf denselben Leitungen erfolgt, müssen der Host und das Gerät ständig zwischen Senden und Empfangen wechseln. Offensichtlich ist die Zwei-Bit-Verzögerung genau für diesen Zweck vorgesehen und so eingestellt, dass sie nicht mit dem Push-Push beginnen, während gleichzeitig versucht wird, einige Daten auf den Bus zu übertragen.

Wir kennen also alle Arten von Paketen, die für den Austausch benötigt werden. Wir fügen eine Überprüfung des empfangenen PID-Bytes hinzu, um die Übereinstimmung mit jedem zu überprüfen. Derzeit kann das Gerät noch nicht einmal primitive Pakete wie ACK auf den Bus schreiben, was bedeutet, dass es dem Host nicht mitteilen kann, um was es sich handelt. Befehle wie IN sind daher nicht zu erwarten. Wir werden also nur den Empfang der Befehle SETUP und OUT überprüfen, für die wir die Aufnahme der entsprechenden LEDs in die entsprechenden Zweige anzeigen.


Darüber hinaus lohnt es sich, Protokolle über den Interrupt hinaus irgendwo in der Hauptsache zu senden.


Wir flashen das Gerät mit dem, was nach diesen Änderungen passiert ist, und beobachten die folgende Reihenfolge der empfangenen Bytes:


 2D|80|06|00|01|00|00|40|00 C3|80|06|00|01|00|00|40|00 2D|80|06|00|01|00|00|40|00 C3|80|06|00|01|00|00|40|00 

Und außerdem - beide brennenden LEDs. Also haben wir SETUP und OUT erwischt.


Schritt 8. Lesen Sie die Adresse auf dem Umschlag


Theoretische Beilage
Markierungspakete (SETUP, IN, OUT) dienen nicht nur dazu, dem Gerät zu zeigen, was sie von ihm wollen, sondern auch ein bestimmtes Gerät auf dem Bus und einen bestimmten Endpunkt darin zu adressieren. Endpunkte werden benötigt, um eine bestimmte Unterfunktion eines Geräts funktional hervorzuheben. Sie können in Abrufhäufigkeit, Wechselkurs und anderen Parametern variieren. Wenn das Gerät ein USB-COM-Adapter zu sein scheint, besteht seine Hauptaufgabe darin, Daten vom Bus zu empfangen und an den Port (erster Endpunkt) zu übertragen und Daten vom Port zu empfangen und an den Bus zu senden (zweiter). In Bezug auf die Bedeutung sind diese Punkte für einen großen Fluss unstrukturierter Daten gedacht. Abgesehen davon muss das Gerät von Zeit zu Zeit den Status der Steuerleitungen (alle Arten von RTS, DTR und anderen) und die Austauscheinstellungen (Geschwindigkeit, Parität) mit dem Host austauschen. Und hier werden keine großen Datenmengen erwartet. Darüber hinaus ist es praktisch, wenn Serviceinformationen nicht mit Daten gemischt werden. Es stellt sich also heraus, dass es praktisch ist, mindestens 3 Endpunkte für den USB-COM-Adapter zu verwenden. In der Praxis geschieht dies natürlich auf unterschiedliche Weise ...
Eine ebenso interessante Frage ist, warum dem Gerät seine Adresse gesendet wird, da Sie ansonsten nichts in diesen bestimmten Port stecken können. Dies geschieht, um die Entwicklung von USB-Hubs zu vereinfachen. Sie können ziemlich "dumm" sein und einfach Signale vom Host an alle Geräte senden, ohne sich um das Sortieren kümmern zu müssen. Und das Gerät selbst wird es herausfinden, das Paket verarbeiten oder es ignorieren.
Daher sind sowohl die Geräteadresse als auch die Endpunktadresse in den Markierungspaketen enthalten. Die Struktur solcher Pakete ist unten angegeben:
das Feld
das FeldSYNCaddrEndpunktCRCEop
USB-Bits0-7012345601230123401
empfangene Bits0123456701234567


, - ( - PID = SETUP OUT) (IN) , .

, (-) (Handshake) :


  • : , , NAK
  • -: SETUP OUT, , IN — ,
  • . , , ,

« — » . PID', , . «PID» . usbCurrentTok. PID' (DATA0, DATA1) , . , ? : , ( 0 usbCurrentTok ), , . ( SE0) , - , D+, D- . , SYNC, . , , . «» , . .


, . x3, (, , , ).


, USB , , . , , , CRC ( ). , [21]. 0- . , [26]. , CRC, .


9.


, , « », ACK. NAK', ( cnt — ). USB , , SYNC PID. Y, cnt ( ). , — ACK. x3 — 1 , . x3 ( r20) 20.


( SETUP, ), ACK' , , , . , .


, D+, D- ( ), — . XOR , , , , - .


, , , , . , , , . . vusb : txBitloop 2 ([00], [08]). 3 , 6 . , . 1 3 : 171. ( 171, 11 , ), — , . cnt=4:


4 — 171 = -167 = ( ) 89 (+ )
89 — 171 = -82 = ( ) 174 (+ )
174 — 171 = 3. ,
, .


, 3 , 1. 6 , , x4. D+, D- , . .
:


 2D|80|06|00|01|00|00|40|00 69|00|10|00|01|00|00|40|00 

C3 . , , UART . , , IN , . , .


10. NAK


NAK , . , . , - .


, . , , - , . usbRxBuf, . , — , USB_BUFSIZE. usbInputBufOffset, . .


NAK handleData , [22]. (usbRxLen), - . ( — ), usbRxLen, , — usbRxToken, SETUP OUT - . : , , ACK .


. , , - , -, . ? , , , , - .


,


 2D|80|06|00|01|00|00|40|00 

, NAK`, , .


11.


, , . — . , , , , , . . . , USB, usbPoll. — , . — . SETUP , PID CRC, SETUP 5- , 16-. 3 «» . «» PID usbRxToken, CRC , , . usbProcessRx, , .


, , — , SE0. , USB .


. SETUP, . . SETUP usbRequest_t 8 . : ( USB-) , - . , . .
, , , .


12. SETUP'


, , . . usbDriverSetup, . , . , ( , , ) . , : ACK NAK, .


13.


, SETUP + DATAx, DATAx 8 . IN DATAx, . , . , ACK NAK. , . — usbTxBuf, , usbTxLen . low-speed USB 8 ( PID, CRC), usbTxLen 11. PID, , . , 16, , 0x0F, . PID , . IN, , (handshake , ).


:
SETUP + DATAx, ACK NAK . , , usbPoll, , ( PID=DATA1 ( DATA0 DATA1 , , DATA1). CRC . , , - . — 4 . , 3 , 4. , SYNC . « IN NAK?» NAK. , , DATA1 .


, — USBRQ_SET_ADDRESS ( , ). . (drvsdm.S, make SE0). , , , DATA1 , , . , , , , , . , , .


14.


, . , USBRQ_GET_DESCRIPTOR USBRQ_SET_ADDRESS, , . usbDriverDescriptor, . , USBRQ_GET_DESCRIPTOR. , , :


USBDESCR_DEVICE — : USB (1.1 ), , , . .
USBDESCR_CONFIG — , , . .
USBDESCR_STRING — , .
, , USBDESCR_DEVICE, , .


15.


. -, . , - - , , HID, , . Vendor ID Product ID, USB, . , vusb .


, , - . , , , (, ) usbMsgPtr, — len, usbMsgLen. ( ) 18 , 8. , , 3 . - , STALL.


usbDeviceRead. , memcpy_P, , , .


, , , . , , .


, , .



PID' DATA0 DATA1 . PID' , , - .

, DATA0 / DATA1 ( ), , , 3 , . XOR PID', . , , XOR' . PID DATA1, XOR PID , XOR DATA0 .


, , USBDESCR_CONFIG.


16. - !


USBDESCR_CONFIG USBDESCR_DEVICE. ( , ) . , - USB-, , D+, D-.


, : , , . , ( , ). , UTF-16, . USB UTF-8 .


vusb , lsusb . VID, PID , . , VID, PID, — .


, , ( ). SETUP: , , . 0, , — . , , , .
.


17. (HID)



HID — human interface device, , , . HID , . , , , , , . «» . HID ( low-speed 800 ), .

HID , USBDESCR_HID_REPORT. vusb, . , usbDriverSetup ( ) usbFunctionSetup ( ). , SETUP, OUT. , , , usbFunctionWrite.


, usbDeviceRead usbFunctionRead, . , , usbFunctionSetup ( , ) USB_FLG_USE_USER_RW, usbDriverSetup .


— — usbFunctionWrite usbFunctionRead. . — , .


usbDriverSetup.


18.


, , . HID, , , ( udev - ). , , . , , , .
UPD: ramzes2 , HIDAPI


.


19. vusb


vusb , .


drvasm.S - usbdrvasm.S asmcommon.inc, -, , usbdrvasm12.inc — usbdrvasm20.inc.


main.c main.c ( ) usbdrv.c ( vusb)
usbconfig.h ( ), , , usbconfig.h.


Fazit


vusb, , , . , , . . , , , USB-HID. , , , vusb, , , , .



https://www.obdev.at/products/vusb/index.html ( vusb)
http://microsin.net/programming/arm-working-with-usb/usb-in-a-nutshell-part1.html
.. USB:
https://radiohlam.ru/tag/usb/
http://we.easyelectronics.ru/electro-and-pc/usb-dlya-avr-chast-1-vvodnaya.html
http://usb.fober.net/cat/teoriya/


PS - (, ) ,

Source: https://habr.com/ru/post/de460815/


All Articles