USB-Tutorial mit STM32: Unterschied zwischen den Versionen

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche
(Anfang des USB-Tutorials, bis zum Kapitel "Der USB-Puffer")
 
K (Link zu Blue Pill Board auf uc.net-Artikel gesetzt)
Zeile 45: Zeile 45:
* Das '''OTG_FS'''-Modul unterstützt OTG und kann somit auch als Host agieren und ist komplizierter zu programmieren.
* Das '''OTG_FS'''-Modul unterstützt OTG und kann somit auch als Host agieren und ist komplizierter zu programmieren.
* Das '''OTG_HS'''-Modul unterstützt zusätzlich USB High Speed (480 MBit/Sec).
* Das '''OTG_HS'''-Modul unterstützt zusätzlich USB High Speed (480 MBit/Sec).
Hier wird ein Controller mit der einfachsten Variante gewählt, der [http://www.st.com/en/microcontrollers/stm32f103rb.html STM32F103RB]. Dieser ist beispielsweise auf dem [https://www.olimex.com/Products/Duino/STM32/OLIMEXINO-STM32/open-source-hardware Olimexino-STM32] zu finden, im Folgenden wird dieses als Grundlage für die Beispiele genutzt. Der auf den  günstigen [http://wiki.stm32duino.com/index.php?title=Blue_Pill Blue Pill]-Boards zu findende [http://www.st.com/en/microcontrollers/stm32f103c8.html STM32F103C8] kann ebenfalls genutzt werden - dazu müssen im Linkerscript die Speichergröße und im Code die genutzten Pins angepasst werden.
Hier wird ein Controller mit der einfachsten Variante gewählt, der [http://www.st.com/en/microcontrollers/stm32f103rb.html STM32F103RB]. Dieser ist beispielsweise auf dem [https://www.olimex.com/Products/Duino/STM32/OLIMEXINO-STM32/open-source-hardware Olimexino-STM32] zu finden, im Folgenden wird dieses als Grundlage für die Beispiele genutzt. Der auf den  günstigen [[STM32F103C8T6_STM32_Billig_Board|Blue Pill]]-Boards zu findende [http://www.st.com/en/microcontrollers/stm32f103c8.html STM32F103C8] kann ebenfalls genutzt werden - dazu müssen im Linkerscript die Speichergröße und im Code die genutzten Pins angepasst werden.


[[Datei:Olimexino_STM32_USB.png|right|thumb|USB-Beschaltung des Olimexino-STM32]]Bei der Entwicklung eigener Platinen kann der Schaltplan das Olimexino als Inspiration genutzt werden. Die Pins PA11 und PA12 des Controllers müssen mit D- bzw. D+ der USB-Buchse verbunden werden. Die einzig erforderliche zusätzliche Komponente ist der 1,5kΩ-Widerstand von der USB-Datenleitung D+ nach +3,0V - +3,6V. An diesem Widerstand erkennt der Host das angeschlossene Gerät. Wird der Widerstand schaltbar ausgeführt, z.B. über einen PNP-Transistor, kann das Gerät die Verbindung trennen, aber noch eingeschaltet bleiben. Das ist zum Testen praktisch - startet man eine neue Debugging Session, wird vor dem Start des Hauptprogramms der Widerstand zunächst abgeschaltet, sodass der Host das zuvor ggf. fehlerhaft erkannte Gerät vergisst, und dann das neu gestartete Programm erneut ansteuert sobald der Widerstand aktiviert ist. Bei der Verwendung von USB ist außerdem ein Quarz Pflicht, damit die Frequenz exakt gehalten werden kann.
[[Datei:Olimexino_STM32_USB.png|right|thumb|USB-Beschaltung des Olimexino-STM32]]Bei der Entwicklung eigener Platinen kann der Schaltplan das Olimexino als Inspiration genutzt werden. Die Pins PA11 und PA12 des Controllers müssen mit D- bzw. D+ der USB-Buchse verbunden werden. Die einzig erforderliche zusätzliche Komponente ist der 1,5kΩ-Widerstand von der USB-Datenleitung D+ nach +3,0V - +3,6V. An diesem Widerstand erkennt der Host das angeschlossene Gerät. Wird der Widerstand schaltbar ausgeführt, z.B. über einen PNP-Transistor, kann das Gerät die Verbindung trennen, aber noch eingeschaltet bleiben. Das ist zum Testen praktisch - startet man eine neue Debugging Session, wird vor dem Start des Hauptprogramms der Widerstand zunächst abgeschaltet, sodass der Host das zuvor ggf. fehlerhaft erkannte Gerät vergisst, und dann das neu gestartete Programm erneut ansteuert sobald der Widerstand aktiviert ist. Bei der Verwendung von USB ist außerdem ein Quarz Pflicht, damit die Frequenz exakt gehalten werden kann.

Version vom 21. November 2017, 17:59 Uhr

Dieser Artikel ist noch unvollständig und wird noch erweitert!

Die USB-Schnittstelle ist mittlerweile im Consumer-Bereich allgegenwärtig, während aber im Hobby- und auch Industriebereich noch die serielle Schnittstelle (RS232/UART) sehr verbreitet ist. Der Grund dafür dürfte hauptsächlich in der komplizierteren Implementierung von USB liegen, dafür ist USB aber insbesondere für den Anwender deutlich einfacher einzusetzen - ein gut umgesetztes USB-Gerät kann nach dem Anschließen ohne jegliche Konfiguration oder Installation direkt genutzt werden. Da mittlerweile viele direkt USB-fähige Mikrocontroller auch für den Hobby-Entwickler verfügbar sind, ist es an der Zeit sich von der seriellen Schnittstelle zu verabschieden.

Dafür soll in diesem Artikel ein Tutorial zur Entwicklung eines eigenen einfachen USB-Geräts gezeigt werden, um dies auch für einfache Hobby-Projekte zugänglich zu machen. Als Mikrocontroller wird der STM32F103RB genutzt, welcher native Unterstützung für USB FullSpeed Devices bietet. Um das Verständnis für die Hardware zu fördern und die komplexe und eher undurchsichtige USB-Bibliothek des Herstellers selbst zu vermeiden, erfolgt der Hardware-Zugriff direkt über die Peripherie-Register. An externem Code wird lediglich die Header-Datei mit den Registerdefinitionen, sowie der obligatorische Startup-Code und Linkerscript verwendet. Somit richtet sich dieses Tutorial an Leser, die USB nutzen und dabei auch verstehen möchten, was genau in der Software passiert. Für komplexe USB-Geräte oder eine schnelle Implementierung sei auf die im STM32CubeF1 enthaltene Bibliothek verwiesen. Es wird Grundlagenwissen über die Programmierung der STM32-Controller und über die C++-Programmierung vorausgesetzt.

Zunächst wird ein "USB Hello-World" entwickelt, welches dem PC die Steuerung von LEDs ermöglicht sowie in der Art eines "Loopbacks" empfangene Daten byteweise umdreht und zurücksendet. Dieses Device gehört keiner Standard-Klasse an, sondern wird von einer eigenen Anwendung gesteuert. Es wird eine Möglichkeit gezeigt, wie dies auch unter aktuellen Windows-Versionen ohne manuelle Treiber-Installation oder sonstige Konfiguration gelingt. Als zweites Beispiel erfolgt die Implementierung eines 3-fach-Adapters von USB auf die serielle Schnittstelle (VCP, Virtual COM Port) auf Basis der Standard-Klasse CDC-ACM, was somit ebenfalls ohne Treiber-Installation funktioniert.

Einleitung

USB-Grundlagen

Im Folgenden werden knapp die relevanten Basics der USB-Schnittstelle aufgezählt:

  • USB wurde ursprünglich für die Verbindung von PCs mit diversen Peripheriegeräten entwickelt. Es gibt mehrere Versionen (1.0, 1.1, 2.0, 3.0, 3.1) die insbesondere unterschiedliche Geschwindigkeiten ermöglichen. Im Folgenden wird nur auf USB 2.0 im FullSpeed Modus eingegangen, was Geschwindigkeiten bis 12 MBit/Sec ermöglicht. Dies ist noch relativ einfach auf Mikrocontrollern umzusetzen.
  • USB-Geräte sind immer entweder ein "Host" oder ein "Device". "USB On-the-Go" (OTG)-Geräte können zwischen den beiden Rollen umschalten. Die (logische) Kommunikation erfolgt immer zwischen einem Host und einem Device; Hosts und Devices können untereinander jeweils nicht kommunizieren. An einem Host können aber mehrere Devices angeschlossen werden. Die meisten PCs haben mehrere USB-Host-Controller (allein schon um die unterschiedlichen Standards zu unterstützten), die wiederum meist jeweils mehrere USB-Ports versorgen.
  • In der USB-Spezifikation fest vorgesehen sind USB-Hubs, mit denen mehrere Devices an einem Anschluss des Hosts betrieben werden können. Mit USB-Hubs kann eine Baumstruktur aufgebaut werden, an deren Wurzel der Host steht, deren Blätter die Devices sind und die inneren Knoten die Hubs. Es sind keine Kreise möglich. Es handelt sich also um eine Stern-Topologie mit mehreren Ebenen (max. 5). Jeder Host kann max. 127 Geräte nutzen.
  • Jedes Device "sieht" nur den Host - Hubs und andere Devices sind transparent bzw. "unsichtbar". Daher wird im Folgenden von einer direkten Kommunikation Host<->Device ausgegangen.
  • Es sind diverse Standard-Klassen mit vorgegebenen Protokollen definiert, denen Geräte entsprechen können um mit denen bei Betriebssystemen mitgelieferten Standard-Treibern zu funktionieren. Geräte können sich aber auch als "herstellerspezifisch" anmelden und funktionieren dann nur mit eigenen Treibern.
  • Jedes USB-Gerät wird über zwei fest einprogrammierte 16bit-Zahlen, die Vendor ID (VID) und Product ID (PID) identifiziert. Zur Vermeidung von Überschneidungen wird die VID vom USB Implementers Forum verwaltet und vergeben; eine eigene zu erhalten kostet derzeit einmalig 5000$ oder 4000$ jährlich. Dies ist für Hobby-Entwickler wenig realistisch, weshalb bei solchen Projekten oft die vorhandene VID eines Herstellers "geborgt" oder eine Fantasie-Zahl wie z.B. "0xDEAD" genutzt wird, die vermutlich nie vergeben wird. Eine andere Möglichkeit bietet pid.codes. Die PID wird vom Hersteller nach Belieben vergeben.
  • Zur Kommunikation mit dem Device ist auf Host-Seite ein Treiber nötig. Bei den Standard-Klassen sind diese bei den Betriebssystemen mitgeliefert. Die Treiberentwicklung ist ein großer Aufwand, insbesondere unter Windows ist die erforderliche Signierung eine Hürde. Stattdessen kann von Anwendungen aus auch direkt auf die Geräte zugegriffen werden:
    • Der Linux-Kernel stellt dafür via udev die Geräte-Dateien in /dev/bus/usb zur Verfügung, auf die von Anwendungen zugegriffen werden kann
    • Unter Windows kann für ein Gerät der "WinUSB"-Treiber geladen werden, über dessen API Anwendungen mit Geräten kommunizieren können
    • Für beide Varianten bietet libusb einen Wrapper, welche den plattformunabhängigen einfachen Zugriff auf Geräte ermöglicht. Dies wird auch im Beispiel gezeigt.
    • Es kann so aber nur eine Anwendung gleichzeitig auf das Gerät zugreifen; diese kann notfalls die Zugriffe weiterleiten.

Vergleich mit serieller Schnittstelle

Vorteile USB

  • Höhere Geschwindigkeit (hier: 12MBit/Sec)
  • Einfache Nutzung für Endanwender ("Plug and Play"), keine Konfiguration von Baudrate/Portnummer nötig
  • Anwendungen können anhand VID/PID direkt das korrekte Gerät finden, es muss nicht wie bei der seriellen Schnittstelle der richtige Port ausgewählt werden
  • Stromversorgung der Geräte möglich
  • Je nach Controller geringerer Hardware-Aufwand als RS-232 (wg. Pegelwandler)
  • Standard-Treiber für typische Anwendungen im Betriebssystem verfügbar
  • Das USB-Protokoll teilt den Datenstrom explizit in Pakete ein, im Gerät ist direkt klar wo ein Paket anfängt und wo es endet, während man bei der seriellen Schnittstelle ein Protokoll benötigt, um Paketanfänge zu erkennen (z.B. an Pausen)
  • USB erkennt automatisch das Verbinden/Trennen der Gegenstelle, es ist keine manuelle Erkennung per "Ping" o.ä. nötig

Vorteile Serielle Schnittstelle

  • Deutlich einfacher in der Implementierung
  • Praktisch jeder Controller bietet UART-Module zur Unterstützung der seriellen Schnittstelle
  • Controller können auch problemlos untereinander direkt kommunizieren (USB-Host Implementierung ist aufwendig)
  • Gar keine Treiber-Installation nötig
  • Keine VID/PID nötig
  • Kommunikation reißt nicht ab, wenn Controller längere Zeit nicht antwortet, während USB kurze Timeouts bei der Enumeration hat (Anhalten des Controllers z.B. zum Debuggen während der Enumeration führt zu sofortiger Abmeldung des Geräts)

Hardware & Beschaltung

Wie die Controller selbst unterscheiden sich auch die USB-Peripheriemodule. Die STM32 sind mit drei verschiedenen USB-Peripherien verfügbar:

  • Das einfach nur USB genannte Modul der kleineren Controller unterstützt nur den Device-Modus und nur FullSpeed.
  • Das OTG_FS-Modul unterstützt OTG und kann somit auch als Host agieren und ist komplizierter zu programmieren.
  • Das OTG_HS-Modul unterstützt zusätzlich USB High Speed (480 MBit/Sec).

Hier wird ein Controller mit der einfachsten Variante gewählt, der STM32F103RB. Dieser ist beispielsweise auf dem Olimexino-STM32 zu finden, im Folgenden wird dieses als Grundlage für die Beispiele genutzt. Der auf den günstigen Blue Pill-Boards zu findende STM32F103C8 kann ebenfalls genutzt werden - dazu müssen im Linkerscript die Speichergröße und im Code die genutzten Pins angepasst werden.

USB-Beschaltung des Olimexino-STM32

Bei der Entwicklung eigener Platinen kann der Schaltplan das Olimexino als Inspiration genutzt werden. Die Pins PA11 und PA12 des Controllers müssen mit D- bzw. D+ der USB-Buchse verbunden werden. Die einzig erforderliche zusätzliche Komponente ist der 1,5kΩ-Widerstand von der USB-Datenleitung D+ nach +3,0V - +3,6V. An diesem Widerstand erkennt der Host das angeschlossene Gerät. Wird der Widerstand schaltbar ausgeführt, z.B. über einen PNP-Transistor, kann das Gerät die Verbindung trennen, aber noch eingeschaltet bleiben. Das ist zum Testen praktisch - startet man eine neue Debugging Session, wird vor dem Start des Hauptprogramms der Widerstand zunächst abgeschaltet, sodass der Host das zuvor ggf. fehlerhaft erkannte Gerät vergisst, und dann das neu gestartete Programm erneut ansteuert sobald der Widerstand aktiviert ist. Bei der Verwendung von USB ist außerdem ein Quarz Pflicht, damit die Frequenz exakt gehalten werden kann.

Beispielprojekt

Das Ergebnis dieses Tutorials ist als Beispielprogramm über GitHub verfügbar. Die drei hier gezeigten Varianten sind dort als einzelne Branches ausgeführt. Das Projekt ist als eclipse-cdt Projekt unter Nutzung des GNU MCU Eclipse-Plugins eingerichtet. Es funktioniert mit der GCC-Distribution GNU Arm Embedded Toolchain in der Version 6.3.1 (Juni 2017). Das Projekt funktioniert mit dem SEGGER J-Link, kann aber auch für andere Debugger angepasst werden (entsprechende Launch Configurations hinzufügen). Das Projekt kann als Ausgangspunkt für eigene Modifikationen genutzt werden, oder als leere Vorlage durch Löschen der Dateien im "src"-Ordner und Neuschreiben des Codes nach diesem Tutorial. Auf GitHub sind auch die Kompilate als Binärdateien zum direkten Flashen verfügbar.

Debugging

Zur Fehlersuche ist dringend die Verwendung eines Debuggers empfohlen, wie z.B. des ST-Link oder des SEGGER J-Link. Die Verwendung von Linux ist sinnvoll, weil der Linux-Kernel relativ hilfreiche Fehlermeldungen bei sich falsch verhaltenden Geräten ausgibt. Diese sind im Kernel-Log zu finden und bspw. über den "dmesg"-Befehl abzurufen. Zusätzlich ist Wireshark's Fähigkeit, USB-Traffic anzuzeigen, sehr hilfreich. Da dabei auch die Daten aller anderen angeschlossenen Geräte mit angezeigt werden, ist es sinnvoll, zum Testen ein Notebook zu nutzen, an dem nur das eigene Gerät hängt. Bei einem Desktop-PC kann man herausfinden, welche USB-Ports zu welchem USB-Host-Controller gehören, und einen Controller ausschließlich für das eigene Gerät reservieren.

Literatur

Dieses Tutorial ist im Endeffekt eine übersichtlichere Zusammenstellung vorhandener Informationen. Viele weitere Details finden sich in den entsprechenden Dokumenten:

Hello-World per USB

Als erstes wird ein einfaches Testprogramm ohne großen Nutzen geschrieben, welches die Grundfunktionalität verdeutlicht. Die erste Herausforderung besteht darin, dass sich das Gerät korrekt am Host anmelden soll - ist das geschafft, können eigene Funktionen implementiert werden. Wir starten zunächst mit einem leeren Projekt, in dem die grundlegende Umgebung bereits eingerichtet ist (Linker-Script, Startup-Code) und der Haupt-Takt auf 72 MHz konfiguriert ist (Quarz und PLL eingeschaltet). Im Beispielprojekt kann hier der Branch "minimal" genutzt werden.

Aktivierung der Peripherie

Das Einschalten des USB-Peripheriemoduls ist noch recht einfach. Zunächst müssen die Peripherietakte und der Pin für den 1,5kΩ-Widerstand konfiguriert werden. Die USB-Pins sind im Port A, aber der muss nicht aktiviert werden, USB funktioniert auch so.

void init () {
	// Aktiviere USB-Takt
	RCC->APB1ENR |= RCC_APB1ENR_USBEN_Msk;
	RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
	// Konfiguriere Pin für 1.5kOhm-Widerstand, und schalte Pin auf high, s.d. Widerstand aus ist
	GPIOC->CRH = 0x44474444;
	GPIOC->BSRR = GPIO_BSRR_BS12;
	// Schalte USB Interrupt ein
	NVIC_EnableIRQ (USB_LP_CAN1_RX0_IRQn);
}

Um dann tatsächlich eine Verbindung zu initiieren, muss laut Controller-Manual eine bestimmte Sequenz beachtet werden. Das standardmäßig aktive Bit "PDWN" im "CNTR" Register wird ausgeschaltet, so dass der Transceiver aktiviert wird. Danach müssen wir 1µs warten (tSTARTUP). Das wird hier mit einer einfachen Schleife realisiert, welche mindestens 72 Takte braucht. Dann kann auch das "FRES" -Bit abgeschaltet werden - danach ist die Peripherie sofort bereit. Es müssen lediglich noch die Interrupts konfiguriert werden. Wir aktivieren nur die Interrupts "CTR" (Transfer abgeschlossen) und "RESET" (Host setzt Gerät zurück - passiert normalerweise beim Verbinden). Beide Interrupt-Quellen lösen im Interrupt-Controller den selben Interrupt aus, den wir auch aktivieren. Zuletzt signalisieren wir dem Host durch Einschalten des 1,5kΩ-Widerstands, dass ein Gerät vorhanden ist:

static void delay () {
	for (int i = 0; i < 72; ++i) __NOP ();
}
void connect () {
	// Schalte Transceiver ein, lasse Logik aus
	USB->CNTR = USB_CNTR_FRES;
	// Warte auf Hochfahren der analogen Schaltung (tSTARTUP)
	delay ();
	// Schalte USB ein, aktiviere Interrupts
	USB->CNTR = USB_CNTR_CTRM | USB_CNTR_RESETM;
	// Lösche alle Interrupts außer Reset-Interrupt
	USB->ISTR = USB_ISTR_RESET_Msk;
	NVIC_ClearPendingIRQ (USB_LP_CAN1_RX0_IRQn);
	// Schalte 1.5kOhm-Widerstand ein, s.d. Host das Gerät erkennt.
	GPIOC->BSRR = GPIO_BSRR_BR12;
}

Die beiden gezeigten Funktionen rufen wir von der main() aus auf, und starten dann eine Endlosschleifen. Der Linux-Kernel erkennt das Vorhandensein des Geräts, und beschwert sich prompt darüber, dass das Gerät nicht auf Anfragen antwortet, wie im Syslog zu erkennen ist:

[20661.625605] usb 2-2: new full-speed USB device number 17 using xhci_hcd
[20661.737623] usb 2-2: device descriptor read/64, error -71

Hier beginnt der schwierige Teil. Wir müssen die Anfragen empfangen und verarbeiten. Doch dazu ist einiges an Vorarbeit nötig.

Der USB-Puffer

Die USB-Peripherie des genutzten Controllers kann einzelne Pakete senden und empfangen, und benachrichtigt die Software per Interrupt über dessen Vervollständigung. Dafür wird aber nicht wie bei anderen Peripheriemodulen DMA eingesetzt, sondern die Peripherie hat ihren eigenen Pufferspeicher. Dieser ist 512 Byte groß und wird auch für das CAN-Modul genutzt - daher können USB und CAN nicht gleichzeitig verwendet werden. Auf diesen Puffer können wir per Software direkt zugreifen, die Hardware simuliert hier einen Dual-Port-RAM. Die Struktur des Puffers ist nicht vorgegeben - wir müssen der Hardware mitteilen, was wo gespeichert werden soll.

Schematische Darstellung der Struktur des USB-Pufferspeichers

Der Zugriff auf den Puffer von der Software-Seite ist etwas unintuitiv: Die Hardware sieht den Puffer als eine Folge von 512 Bytes, beginnend ab der Adresse 0. Der Prozessorkern und damit die Software sieht den Puffer als Folge von 256 16bit-Worten, zwischen denen jeweils eine 16bit-Lücke ist, an der nichts gespeichert werden kann. Somit erscheint der Puffer der Software als 1024 Bytes groß, wobei die Hälfte Lücken sind. Aus Software-Sicht beginnt der Puffer ab Adresse 0x40006000. Im Bild ist dies grafisch dargestellt. Beim Schreiben bzw. Lesen von Paket-Daten im Pufferspeicher muss dies berücksichtigt werden.

Die Einteilung des Pufferspeichers in Bereiche für die einzelnen Pakete muss durch die Software vorgenommen werden. Da man sich hier schnell verrechnen kann, überlassen wir diese Aufgabe einem Programm, das genau darauf optimiert ist: Dem Linker. Dazu legen wir im Linker-Script (STM32F103RB.ld) innerhalb des "MEMORY"-Blocks einen weiteren Speicherbereich für den USB-Pufferspeicher an:

MEMORY {
	FLASH		: ORIGIN = 0x8000000,	LENGTH = 128K
	SRAM		: ORIGIN = 0x20000000,	LENGTH =  20K
	USBBUF		: ORIGIN = 0x40006000,	LENGTH = 1024
}

Dazu geben wir Größe und Adresse aus Sicht des Prozessors (daher 1024 Bytes) an. Dann übernehmen wir alle Daten in der Eingabe-Section ".usbbuf" in diesen Speicher, indem wir folgendes innerhalb der "SECTIONS"-Anweisung ablegen:

	.UsbBuffer (NOLOAD) : {
		UsbBufBegin = .;
		*(.usbbuf)
		*(.usbbuf*)
	} > USBBUF

Das NOLOAD sorgt dafür, dass dort abgelegte Daten nicht automatisch initialisiert werden. Jetzt können wir im Code globale Variablen anlegen und speziell markieren, sodass Compiler & Linker sie in diesem Speicherbereich ablegen, d.h. ihnen eine Adresse im gewünschten Bereich zuweisen, die beim Zugriff aus dem Code heraus angewendet wird. Da dies etwas umständlich ist, definieren wir dafür ein Makro. Das geschieht funktioniert dann so:

#define USB_MEM __attribute__((section(".usbbuf")))
uint16_t myBufferData USB_MEM;

Zugriffe auf myBufferData werden dann auf den USB-Pufferspeicher umgeleitet. Um ganze Pakete ablegen zu können, möchten wir Arrays verwenden, bei denen flexibel die Größe geändert werden kann. Dabei muss aber für den Software-Zugriff die Lücke nach jedem 16bit-Wort beachtet werden. Daher definieren wir eine Klasse "UsbMem":

class UsbMem {
	public:
		uint16_t data;
	private:
		char padding [2];
};

Sie ist 4 Bytes groß, aber nur die ersten 2 Bytes sind als 16bit-Wort zugänglich. Legen wir davon jetzt ein Array im Pufferspeicher an, können wir da unsere Paketdaten hineinschreiben. Dabei ist es aber etwas lästig, dass die Größe jetzt in 16bit-Wörtern statt wie üblich in Bytes angegeben werden muss. Daher definieren wir eine template-Klasse namens "UsbAlloc", welcher die gewünschte Größe in Bytes übergeben wird, und die dann ein Array der korrekten Größe enthält. Wie wir später sehen werden, unterliegt die Größe weiteren Einschränkungen, damit der Puffer genutzt werden kann. Diese überprüfen wir mit static_assert, um das Einstellen einer ungültigen Größe zu vermeiden. Den []-Operator überladen wir, um den Zugriff zu vereinfachen:

template <size_t N>
struct UsbAlloc {
	static_assert (((N <= 62) && (N%2 == 0)) || ((N <= 512) && (N % 32 == 0)), "Invalid reception buffer size requested");
	static constexpr size_t size = N;

	/// Das eigentliche Daten-Array
	UsbMem data [N/2];
	/// Bietet Zugriff auf 16bit-Word "i".
	usb_always_inline uint16_t& operator [] (size_t i) {
		return data [i].data;
	}
};

Die Buffer Descriptor Table

Die EPnR-Register

Konfiguration von Transfers

Unser erstes USB-Paket

USB-Deskriptoren

Das Protokoll von Control Endpoints

Eigene Requests

Eigener Bulk-Endpoint

Eigener Anwendung für PC-Seite

Virtueller COM-Port