Kampis Elektroecke

Anmeldung beim Host – Teil 1

Im letzten Teil des USB-Tutorials haben wir gelernt, wie die unterschiedlichen Deskriptoren für ein beispielhaftes USB-Gerät aussehen können und wie diese in per Software implementiert werden. Am Ende dieses Teils wird der Mikrocontroller in der Lage sein sich beim Host anzumelden, sodass wir uns im nächsten Teil darum kümmern können mit dem Mikrocontroller über USB zu kommunizieren.

Für die spätere Applikation sind bestimmte Ereignisse, die während der Verwendung des USB auftreten können (wie z. B. eine erfolgreiche Enumeration oder bei einem Fehler), wichtig. Daher soll als im ersten Schritt eine Struktur definiert werden, die die Callbacks für bestimmte Events bereithält:

Diese Callbacks werden nun im Hauptprogramm deklariert und die Struktur erstellt:

Als nächstes wird die USB_Init-Funktion angepasst werden um die Callbacks zu speichern:

Und dann wird der USB-Interrupt angepasst um den Error-Callback aufzurufen, wenn die Konfiguration des Kontrollendpunktes fehlschlägt:


Hinweis:

Die Variable __USBEvents ist durch den Zusatz extern für andere Source-Files verfügbar, sodass die Callbacks an jeder Stelle im Treibercode verwendet werden können.


Beim USB wird jede Kommunikation Host vom initiiert, sprich USB-Devices sind nicht in der Lage selbstständig Daten zu senden. Der USB-Host muss somit die einzelnen Busteilnehmer periodisch nach neuen Daten abfragen (dieses Verfahren wird auch Pollen genannt) oder Interrupts verwenden (nicht in diesem Beispiel verwendet). Die Abfrage soll in der main-Funktion der Applikation erfolgen:

Die Konfiguration, bzw. die Parametrierung des USB-Devices durch den Host erfolgt mit Hilfe von sogenannten Standard Request, die über SETUP-Pakete versendet werden. Über diese Requests kann der Host bestimmte Informationen (wie z. B. die Deskriptoren) vom Device abfragen oder bestimmte Konfigurationen am Device vornehmen.

Die Funktion USB_Poll überprüft mit Hilfe des Zustandsautomaten, ob der Mikrocontroller noch mit dem USB verbunden ist.

Wenn der Mikrocontroller mit dem Bus verbunden ist, wird der zur Zeit aktive Endpunkt aus dem UENUM-Register ausgelesen und gespeichert, auf den Kontrollendpunkt gewechselt und überprüft ob ein SETUP-Paket empfangen wurde, indem das RXSTPI-Bit im UEINTX-Register des Endpunktes abgefragt wird. Wenn kein SETUP-Paket empfangen wurde, wechselt die Software wieder zurück auf den vorherigen Endpunkt und verlässt die Funktion.

Andernfalls springt die Software in die Funktion USBDevice_ControlRequest, in der das empfangene SETUP-Paket eingelesen und weiterverarbeitet wird. In dieser Funktion werden die empfangenen Requests ausgewertet. Der Einfachheit halber beschränke ich die Anzahl der ausgewerteten Requests in diesem einfachen Beispiel auf die mindestens benötigten Requests:

  • SET_ADDRESS – Wird benötigt um die Adresse, die der Host während der Enumeration vergibt, zu setzen
  • GET_DESCRIPTOR – Wird benötigt um dem Host die unterschiedlichen Deskriptoren zu senden
  • SET_CONFIGURATION – Wird benötigt um das Ende der Enumeration zu erkennen

Die komplette Kommunikation findet über den IN- und den OUT-Kontrollendpunkt statt. Es werden somit Daten gelesen (z. B. die Adresse, die der Host zum Device gesendet hat) und Daten geschrieben (z. B. die Deskriptoren, die das Device zum Host sendet). Der Ablauf eines Schreib- bzw. Lesevorgangs ist im Datenblatt des USB Devicecontrollers beschrieben.

Zu Beginn eines jeden Controlrequests wird das RXSTPI-Bit durch den USB-Controller gesetzt. Dies signalisiert der CPU, dass ein neues SETUP-Paket eingetroffen ist und ausgelesen werden kann:

Dazu wird ein Zeiger auf die statische Variable __ControlRequest erstellt und anschließend werden die einzelnen Datenbytes aus dem Kontrollendpunkt ausgelesen und in den Speicher der Variable __ControlRequest geschrieben.


Hinweis:

Damit die Struktur korrekt befüllt wird, muss sie mit dem Attribut __attribute__((packed)) deklariert werden. Andernfalls fügt der Compiler u. U. Paddingbytes ein, wodurch bei der Iteration über den Speicherbereich mittels Zeiger auf die Struktur die Daten nicht korrekt abgespeichert werden.


Nachdem die Daten gelesen wurden muss das RXSTPI-Bit gelöscht werden:

Das Löschen des RXSTPI-Bits signalisiert das Ende der SETUP-Stage und es wird zur nächsten Stage gewechselt. Je nach Art des Requests ist die nächste Stage die DATA– oder die STATUS-Stage. Die Art des Requests lässt sich über das Feld bRequest der __ControlRequest-Struktur ermitteln und über eine Switch-Anweisung abfragen:

Zu Anfang sendet der USB-Host einen GET_DESCRIPTOR-Request an das angeschlossene Device, damit der Host die unterschiedlichen Deskriptoren einlesen kann um u. a. Informationen über die Größe des Kontrollendpunktes zu erhalten.


Hinweis:

Es ist sinnvoll das Feld bmRequestType des Request auszuwerten, dazusätzlich zu den Standardrequests noch klassenspezifische Requests verwendet werden können, wodurch die unterschiedlichen USB-Klassen die Funktion eines Requests mit klassenspezifischen Funktionen erweitern (z. B. die HID-Klasse).


Im nächsten Schritt wird das Feld bmRequestType ausgewertet werden, welches bei einem Standardrequest GET_DESCRIPTOR-Request mit dem 100000002 gefüllt ist. Aus diesem Wert ergeben sich die folgenden Informationen:

  • Transferrichtung: Device to Host (also ein IN-Transfer)
  • Art des Request: Standard
  • Empfänger: Device

Und für die anderen Felder des Request sind die folgende Funktionen festgelegt:

Anfragefeld Beschreibung
wValue Art und Index des Deskriptors
wIndex Null oder Sprach-ID (bei einem String-Deskriptor)
wLength Größe des Deskriptors
Data Der Deskriptor

Jetzt wird noch eine Funktion benötigt um, basierend auf den Parametern wValue und wIndex, den entsprechenden Deskriptor aus dem Programmspeicher zu laden. Im zweiten Schritt müssen wLength Datenbytes des Deskriptors an den Host übermittelt werden. Die Funktion zum Laden der Deskriptoren kann z. B. folgendermaßen aussehen:

Die Funktion USB_GetDescriptor erwartet als Übergabeparameter die Werte von wValue und wIndex aus dem Controlrequest des Hostes, sowie einen Zeiger auf einen Speicherbereich für die Länge des Deskriptors und gibt einen Zeiger auf den Speicherbereich des Deskriptors zurück. Dazu wird zuerst die Art und der Index des Deskriptors aus dem Parameter wValue extrahiert und anschließend wird mittels Switch-Anweisung die Adresse und die Größe des Deskriptors ermittelt.


Achtung:

Bei dem Geräte- und dem Konfigurationsdeskriptor kann die Länge des Deskriptors über die sizeof()-Funktion und den Aufbau der Deskriptorstruktur ermittelt werden. Bei dem String-Deskriptor funktioniert dies nicht, sizeof() bei einem Array nur die Größe des Zeigers auf das erste Element zurückgibt. Daher muss in diesem Fall die Größe des Deskriptors mittels pgm_read_byte aus dem Feld bLength des Deskriptors im Programmspeicher gelesen werden.


Die erstellte Funktion wird bei der Bearbeitung des GET_DESCRIPTOR-Request aufgerufen und die empfangenen Parameter für wValue und wIndex übergeben:

Jetzt muss der Deskriptor noch an den Host übermittelt werden. Hierbei muss beachtet werden, dass der Host über den Parameter wLength vorgibt, wie viele Datenbytes er vom Deskriptor einlesen möchte. Die Funktion zum Senden der Daten sieht folgendermaßen aus:

Zu Beginn überprüft die Funktion ob die zu übertragende Nachricht länger ist als die Anzahl der Datenbytes, die der Host angefragt hat. In der nachfolgenden While-Schleife werden die Daten anschließend übertragen. Dazu wird als erstes der Status des Kontrollendpunktes überprüft:

Wenn sich der Zustand des USB-Devices ändert, oder der Host ein neues SETUP– oder Daten-Paket in den OUT-Endpunkt geschrieben hat, bricht die Übertragung ab und es wird ein entsprechender Fehler zurückgegeben.

Anschließend wird die Datenübertragung entsprechend des gezeigten Ablaufdiagramms umgesetzt. Zuerst wird über das TXINI-Bit geprüft ob der IN-Endpunkt bereit eine neue IN-Transaktion erhalten, sprich ob die DATA-Stage, begonnen hat.

Wurde eine entsprechende IN-Transaktion erkannt, wird der Endpunkt mit Daten gefüllt. Sobald der Endpunkt voll ist, wird eine Übertragung in Richtung Host initiiert und der Endpunkt geleert und auf das Ende der Übertragung gewartet:

Sobald sämtliche Daten übertragen wurden beginnt die STATUS-Stage. Das erste Kommando der STATUS-Stage wird vom USB-Controller immer mit einem NAK quittiert. Das Datenblatt gibt vier Schritte vor, die in diesem Fall bearbeitet werden müssen:

  • Transmit Ready setzen
  • Auf Transmit Complete oder Receive Complete warten
  • Wenn Receive Complete: Flag löschen und beenden
  • Wenn Transmit Complete: Weitermachen

Dieses Vorgehen muss nun programmtechnisch umgesetzt werden, wobei auf die Auswertung vom Transmit Complete in diesem Fall verzichtet werden kann:

Während die Software auf den Receive Complete wartet, wird permanent der Status des Endpunktes überprüft und ggf. ein Fehler zurückgegeben. Die fertige Funktion kann nun in den Abarbeitung des GET_DESCRIPTOR-Request eingebaut werden:

Damit wird der erste Request erfolgreich abgearbeitet. Die beiden anderen benötigten Requests folgen dann im nächsten Teil.

Das komplette Projekt könnt ihr bei GitLab downloaden.

Zurück

Last Updated on

Schreibe einen Kommentar

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