Foto von Dr. S. Salewski

Homepage von Stefan Salewski

Freie USB-Firmware für Mikrocontroller

Fast jeder moderne PC ist heute mit USB-Buchsen ausgestattet, die für den Anschluss von Peripheriegeräten verwendet werden. Auch für den Anschluss diverser Mikrocontroller-Schaltungen, von einfachen Experimentierplatinen bis zu professionellen Geräten für Mess-, Steuerungs- und Regelungsaufgaben, bietet sich der USB an. Gegenüber dem traditionellen seriellen RS-232 (UART) Anschluss oder dem parallelen Druckerport zeichnet sich der USB insbesondere durch die einfachere Verkabelung, höhere Datenübertragungsraten und die integrierte Spannungsversorgung aus. Im Gegensatz zu den erwähnten herkömmlichen Schnittstellen sind für den USB feste Protokolle und Übertragungsmodi definiert, die bei seiner Nutzung streng einzuhalten sind.

Beschäftigt man sich etwas näher mit den Grundlagen des USB, so wird man zunächst durch dessen Komplexität abgeschreckt. (Die frei zugängliche USB-Spezifikation (erhältlich vom USB Implementers Forum) ist rund 650 Seiten stark.) Dies führt oft dazu, dass entweder weiterhin veraltete Schnittstellen verwendet werden, oder dass die Komplexität des USB durch aufgeblähte Bibliotheken kaschiert wird. Solche Bibliotheken (oder Beispielanwendungen) werden z.B. von den Herstellern der USB-Chips oder zusammen mit Experimentierplatinen angeboten -- oft aber nur als Binärdateien (DLL) für ein ganz bestimmtes Betriebssystem, oder mit strengen Copyright-Auflagen, die meistens eine Weitergabe der Software einschränken. Eine Freigabe der entwickelten Software einschließlich sämtlicher Quelltexte unter der GPL oder ähnlicher freier Lizenzen ist dann in der Regel nicht gestattet, womit eine Weiterentwicklung oder Anpassung an andere Controller oder Betriebssysteme durch andere Entwickler praktisch unmöglich ist. Leider wird auch für einige kommerziell vertriebene USB-Geräte die konkrete Realisierung der Firmware-Schnittstellen nicht angegeben, so dass diese Geräte dann nur mit den spezifischen Treibern der Hersteller auf einer ganz bestimmten Betriebssystem-Version verwendet werden können.

Seit einiger Zeit gibt es daher verschiedene Projekte, die sich mit der Entwicklung freier, quelloffener USB-Firmware (auch als USB Device Stack bezeichnet) für verschiedene Controller beschäftigen. Beispielsweise das usbn2mc-Projekt von B. Sauter, das den USB-Bridge-Chip USBN9604 der Firma National-Semiconductor einsetzt, um beliebige Mikrocontroller um USB-Funktionalität zu erweitern. Für dieses Projekt existieren bereits viele interessante Anwendungen in Verbindung mit AVR Controllern. Interessant erscheint auch das Konzept des Porus-Projekts, das mit Hilfe eines Script-gesteuerten Code-Generators und an die Anwendung anzupassenden Konfigurationsdateien weitgehend automatisch Firmware-Code erzeugt.

Mit dem LUFA-Projekt (ehemals MyUSB) von Dean Camera gibt es für die AVR-Controller mit USB-Schnittstelle (AT90USB...) jetzt eine weitere freie USB-Software. Diese ist wesentlich umfangreicher als meine Firmware, u.a. werden von ihr auch Protokolle wie HID, die kleineren AT90USB162 und der Betrieb unter Windows unterstützt. Leider existierte das Projekt im Jahre 2006, als ich meine Firmware schrieb, noch nicht. Sonst hätte ich mir viel Arbeit sparen können. Das soll nun nicht heißen, dass meine Firmware nun überflüssig ist, aber Sie sollten sich auf jeden Fall das Projekt von Dean Camera ansehen (Ich selbst bin noch nicht dazu gekommen, und ich werde wohl eh weiterhin bevorzugt meine eigene Firmware verwenden). Hier finden Sie die Projektseite:

Freie (GPL) USB-Firmware für Atmels AT90USB Mikrocontroller

Der Controller AT90USB ist in der Version 1287 seit Sommer 2006 in größeren Stückzahlen verfügbar -- zusätzlich existieren noch die Untertypen 1286 (ohne Host-Funktionalität) und die Typen 646 und 647 mit halbierter Speichergröße. Ich habe bisher ausschließlich den Typ 1287 im Device-Modus verwendet, die Firmware sollte jedoch zu den anderen Typen kompatibel sein. Allerdings wird es mit den Typen 646 und 647 wahrscheinlich mit dem RAM sehr eng, wenn man mit Debugging-Code kompiliert, da die String-Konstanten in den RAM kopiert werden. Man kann das umgehen, wenn man den Code etwas umschreibt... (Seit einiger Zeit sind nun auch die kleineren Typen AT90USB162 und AT90USB82 verfügbar -- mit diesem habe ich mich bisher nicht beschäftigt, die Firmware müsste für diese Typen vermutlich etwas angepasst werden.)

Der AT90USB1287 gehört zur Familie der AVRs und ist im Prinzip ein um USB-Funktionalität erweiterter ATmega128, also ein 8-Bit-Prozessor mit 128 kByte Programmspeicher (Flash), 8 kByte Arbeitsspeicher (RAM) und bis zu 16 MHz Taktfrequenz. Bedingt durch seine Architektur werden die meisten Instruktionen in nur einem Taktzyklus ausgeführt, so dass er für viele Anwendungen schnell genug ist. Er beherrscht zwar nur USB Full- und Low-Speed (kein High-Speed), aber auch dies ist mit einer Datenübertragungsrate von bis zu 1 MByte/s für viele Anwendungen ausreichend. Wie die übrigen AVRs hat auch der AT90USB eine Vielzahl von zusätzlichen Funktionen auf dem Chip integriert -- dazu zählen USART, 10-Bit-ADC, verschiedene Timer und ein Komparator. Der Chip kann per ISP (In-System-Programmer) bis zu 100.000 mal neu programmiert werden -- alternativ kann die Programmierung auch einfach über die USB-Schnittstelle mit Hilfe des standardmäßig vorhandenen USB-Bootloaders erfolgen. Mit dem Compiler avr-gcc, dem Programmier-Werkzeug avr-dude und dem Tool dfu-programmer zum Firmware-Upload via USB und vielen weiteren Hilfsprogrammen sind unter GNU/Linux alle nötigen Werkzeuge in freier, quelloffener Form vorhanden. (Für Windows gibt es ähnliche Tools, und zusätzlich die von Atmel kostenlos bereitgestellte Programmieroberfläche AVR-Studio.)

Der AT90USB1287 ist nur im 64-poligen TQFP- oder QFN-Gehäuse verfügbar, was den Einsatz im Hobby-Bereich mit Steckbrettern oder Lochrasterplatinen nahezu unmöglich macht. Das TQFP-Gehäuse lässt sich mit etwas Geschick und geeignetem Lötwerkzeug allerdings ganz gut von Hand einlöten, sofern man eine passende Platine zur Verfügung hat.

Mit der als STK525 bezeichneten Experimentierplatine und einer USBKEY genannten Miniaturplatine gibt es von Atmel derzeit zwei "Starterkits", die auch elektronisch und löttechnisch unerfahrenen Personen den Zugang zu diesem Controller ermöglichen.

Ich habe mir eine eigene Platine für den AT90USB angefertigt -- den Schaltplan und das Platinenlayout finden Sie hier:

Aufbau und Struktur dieser USB-Firmware

Die Firmware und die Beispielanwendung sind vollständig in der Programmiersprache C programmiert. Dies verbessert gegenüber Assembler deutlich die Übersichtlichkeit und erleichtert damit Veränderungen oder Erweiterungen und eine eventuelle Portierung auf andere Controller. Der vom Compiler avr-gcc erzeugte Code ist recht gut, so dass eine Portierung nach Assembler allenfalls für die zeitkritischen Code-Segmente sinnvoll wäre. (In der Regel wird dies jeweils das Füllen oder Auslesen des USB-Pufferspeichers (FIFO) sein.) Sofern man die Beispielanwendung mit deaktivierten Debugging-Meldungen compiliert, belegt sie übrigens lediglich gut 4 kByte Programmspeicher im AT90USB. Die Firmware unterstützt den gesamten Enumeration-Prozess und die Verarbeitung von Standard-Device-Requests. Klassenspezifische Requests, der Übergang in den Schlafmodus mit dem zugehörigen Wiedererwachen und das Aufwecken des Hosts (PC) durch das Device (Remote-Wakeup) sind noch nicht implementiert. Host-Funktionalität (USB OTG) wird (noch) nicht unterstützt.

Das PC-seitige Anwendungsprogramm ist ebenfalls in C geschrieben, die Kommunikation mit dem Device erfolgt mit Funktionen der Bibliothek LibUSB. Tests der Firmware wurden bisher ausschließlich unter Linux (Gentoo für AMD64) durchgeführt -- da die Firmware entsprechend dem USB-Standard programmiert ist, sollte die Kommunikation mit dem PC auch dann gelingen, wenn auf ihm ein anderes Betriebssystem oder eine andere Programmiersprache eingesetzt wird. (Die Firmware lässt sich mit avr-gcc in der Version 3.4.6 oder 4.1.2 compilieren; es ist nicht völlig auszuschließen, dass für andere Compiler-Versionen kleine Änderungen nötig sind.)

Da Treiber-Programmierung unter den Microsoft-Betriebssystemen nicht gerade einfach und meist nur mit speziellen Softwarepaketen möglich ist, wird dort oft die HID-Geräteklasse für die Kommunikation mit USB-Geräten verwendet. Beim Einsatz dieser Geräteklasse sind keine speziellen Treiber erforderlich, allerdings ist die Kommunikation eventuell auf Low-Speed beschränkt. Derzeit unterstützt die Firmware noch keine speziellen Geräteklassen wie HID, eine Erweiterung um diese Funktionalität ist jedoch möglich. Allerdings existiert auch für die Microsoft-Betriebssysteme eine spezielle Version der LibUSB und kann als Alternative zur HID-Geräteklasse eingesetzt werden.

Die Dateien im Überblick

Nachfolgend sind die Quelltextdateien der Firmware einschließlich der Beispielanwendung in alphabetischer Reihenfolge aufgelistet.

com_def.h      Von Device- und Host-Software gemeinsam genutzte Konstanten 
daq_dev.c      Timer-gesteuerte Datenaufzeichnung mit dem internen ADC (Port ADC0)
daq_dev.h
defines.h      Projekt-spezifische Konstanten
macros.h       Einige nützliche Makros
ringbuffer.c   Einfacher Ringspeicher (FIFO) für die Zwischenspeicherung der Messdaten 
ringbuffer.h
SUDAP.c        Anwendung auf PC: Shell-Programm nimmt Parameter entgegen und gibt Messwerte aus
SUDD.c         Anwendung auf Geräteseite: Ein Signal wird digitalisiert und an den PC gesendet
usart_debug.c  Ausgabe von Statusinformationen über die serielle Schnittstelle
usart_debug.h	
usart_drv.c    Funktionen um über den seriellen Port Text oder Hexadezimalzahlen auszugeben
usart_drv.h
usb_api.c      Anwendungsspezifische Teile des USB-Protokolls
usb_api.h      
usb_drv.c      Elementare, Controller-spezifische USB-Funktionen
usb_drv.h      Makros um USB-Register zu setzen oder abzufragen
usb_isr.c      USB-spezifische Interrupt-Service-Routinen
usb_requests.c USB (standard) Requests, Enumeration
usb_requests.h
usb_spec.c     USB Datenstrukturen (Descriptors) und Konstanten
usb_spec.h	

Die Gesamtgröße aller Quelltexte beträgt derzeit ca. 130 kByte. Damit ist diese Firmware noch recht übersichtlich. Die Dateien SUDAP.c und SUDD.c bilden zusammen mit daq_dev.c und daq_dev.h die Beipielanwendung. In den Dateien usb_api.c und usb_api.h sind die anwendungsspezifischen Teile des USB zusammengefasst, diese müssen für andere Anwendungen in der Regel modifiziert werden.

Die Dateien sind im sowohl einzeln als auch als verfügbar. Für den Fall, dass Sie sämtliche Quelltexte ausdrucken möchten (ca. 60 Seiten), ist eine vorformatierte vorhanden. Sofern auf Ihrem Rechner das Programm enscript installiert ist, können Sie sich alternativ auch die Script-Datei gen_postscript.txt an Ihre Vorlieben anpassen und dann mit ihr die PostScript-Datei erzeugen. Alternativ gibt es die Quelltexte auch als vorformatierte die mit pdflatex und dem listings-Paket erstellt wurde.

Grundlagen des USB

Der USB ist durch seine Konzeption deutlich leistungsfähiger, aber auch komplizierter als die traditionellen seriellen oder parallelen Computerschnittstellen. So gibt es verschiedene Übertragungsmodi, z.T. mit automatischer Fehlerkorrektur, und verschiedene Datenkanäle (Endpoints) mit konfigurierbaren Pufferspeichern (FIFO), und es können bei Einsatz entsprechender Verteiler (Hubs) bis zu 127 Geräte an einen PC angeschlossen werden. Will man selbst entwickelte USB-Geräte optimal einsetzen, so benötigt man daher einige Grundkenntnisse dieses Busses. Nun mag es den einen oder anderen geben, der mit seinem selbst gebauten USB-Gerät vielleicht nur einige Signalleitungen periodisch abfragen oder ein- und ausschalten möchte, um beispielsweise seine Modelleisenbahn zu steuern. Dies könnte etwa durch zwei Funktionen GetPort() und SetPort() geschehen -- außer deren Namen und deren Parametern wären dann zunächst keine weiteren Kenntnisse erforderlich. Tatsächlich ist in meiner Beispielanwendung eine ähnliche Funktion zum Setzen eines Ausgangsports vorhanden. Aber dies ist nur eine von vielen möglichen Anwendungen, und ich kann unmöglich für alle denkbaren Fälle konkrete Beispiele angeben. Sofern Sie sich jedoch ein wenig mit den Grundbegriffen des USB auskennen, können Sie mit meiner Firmware relativ einfach an Ihre konkreten Anforderungen angepasste Anwendungen erstellen. Für eine erfolgreiche Nutzung der Firmware sollte Ihnen zumindest die Bedeutung der USB-Grundbegriffe wie Host, Device, Enumeration, Descriptor, Configuration, Interface und Endpoint vertraut sein.

Eine recht verständliche Einführung wird in den ersten zwei Kapiteln (ca. 100 Seiten) des Buches gegeben. Vielleicht haben Sie die Möglichkeit dieses Buch irgendwo für einige Tage auszuleihen. Frei zugängliche, aber deutlich knappere Darstellungen der USB-Grundlagen finden Sie u.a. bei Von der Homepage des USB-Konsortiums können Sie bei Bedarf auch die komplette, 650-seitige USB-Spezifikation als PDF-Dokument und andere Informationen bezüglich USB erhalten.

Selbstverständlich sollten Sie sich auch das Datenblatt des AT90USB von Atmels Homepage besorgen und sich Kapitel 21 (USB Controller) und Kapitel 22 (USB Device Operating modes) ansehen, insbesondere die Abschnitte, die das Lesen von Daten aus dem FIFO-Speicher bzw. das Schreiben der Daten in diesen Speicher und die zugehörigen Interruptquellen beschreiben.

Die Beispielanwendung

Die Beispielanwendung ist ein einfaches Programm zur Aufzeichnung eines (analogen) elektrischen Signals, zusätzlich kann Port B des AT90USB auf definierte Ausgangspegel gesetzt werden. (So dass etwa eine Leuchtdiode, die (mit Vorwiderstand) an einem der 8 Pins von Port B angeschlossen ist, programmgesteuert ein- oder ausgeschaltet werden kann.) Um das Programm möglichst kurz und übersichtlich zu halten, wird nur ein Kanal des internen ADC verwendet (ADC0), wobei die interne Spannungsreferenz von 2,56 V des AT90USB genutzt wird. (Die zu messende Spannung an Pin ADC0 sollte daher im Bereich von 0 bis 2,56 V liegen.)

Die Anwendung besteht im wesentlichen aus den Dateien SUDAP.c (Simple USB Data-Acquisition Program), SUDD.c (Simple USB Data-acquisition Device) und daq_dev.c. SUPAP.c ist das PC-Programm, das in der Linux-Shell verwendet wird. Es nimmt einige Parameter entgegen, startet die Datenaufzeichnung und gibt die Messwerte in der Shell aus. SUDD.c ist Firmware-seitig im Prinzip das Hauptprogramm, da es die main()-Funktion enthält. Tatsächlich hat diese Funktion in dieser Beispielanwendung jedoch keine wirkliche Aufgabe, sie schaltet lediglich Pin A0 in rascher Folge zwischen 0 und 5 V um, so dass eine angeschlossene LED blinkt. Die eigentliche Datenaufzeichnung geschieht periodisch über einen Timer-Interrupt -- die zugehörige Funktion befindet sich in der Datei daq_dev.c. Sofern Sie eine funktionsfähige Platine mit einem AT90USB zur Verfügung haben, können Sie diese Beispielanwendung sofort ausprobieren. Benutzen Sie vorzugsweise einen 16 MHz-Quarz, anderenfalls muss die Datei defines.h angepasst werden und es kann nicht die höchste Datenaufzeichnungsrate verwendet werden. (Bei Problemen sollten Sie die Fuse-Bytes des AT90USB1287 überprüfen, insbesondere das für die Takterzeugung relevante "Fuse Low Byte" (Table 6-3, 6-4 und 29-5 im Datenblatt). Dieses war bei meinen Chips auf "01011110" (Binär) bzw. "5E" (Hexadezimal) voreingestellt. Damit funktionierte der AT90USB1287 bei mir durchaus, allerdings ist dies laut Datenblatt eine undefinierte Einstellung. Ich habe daher dieses Byte für meinen 16 MHz Quarz auf "01111111" bzw. "7F" umprogrammiert. Verwenden Sie für die ersten Tests am besten ein nicht zu langes USB-Kabel -- mit einem sehr günstigen 5 Meter langen Kabel funktionieren bei mir dfu-programmer und mein Testprogramm auch problemlos.) Vorgabemäßig sendet die Firmware eine Reihe von Textausgaben über die serielle Schnittstelle, wobei dies natürlich voraussetzt, dass ihre Experimentierplatine mit einem Pegelwandler (Max232) bestückt und mit einem geeigneten Verbindungskabel mit der seriellen Schnittstelle Ihres Rechners verbunden ist. Ist dies der Fall, so öffnen Sie am besten ein weiteres Shell-Fenster und starten dort das Programm minicom (eventuell auch ein anderes Terminalprogramm) mit den Parametern 8N1 bei 9600 Baud.

Verbinden Sie nun Ihre Testplatine per USB-Kabel mit Ihrem Computer.

Gehen Sie in das Verzeichnis mit den Quelltexten und tippen Sie nacheinander folgende Anweisungen ein:

make
make dfu
gcc -l usb -o sudap SUDAP.c

Die zweite Zeile überträgt die Firmware mit Hilfe des Programms dfu-programmer per USB auf den Chip und startet die Anwendung. Eventuell erscheinen jetzt einige Textausgaben im Terminalfenster, gleichzeitig blinkt eine an Port A0 angeschlossen Leuchtdiode und zeigt damit die Betriebsbereitschaft an. Sollte das Kommando make dfu eine Fehlermeldung ausgeben, so ist entweder die Software dfu-programmer nicht ordnungsgemäß installiert, oder der Bootloader des AT90USB ist nicht aktiv (Evtl. benötigen Sie root-Rechte (SuperUser) für dfu-programmer?). Im letzteren Fall sollten Sie sich informieren, wie bei Ihrer Platine der Bootloader aktiviert werden kann. (Bei STK525, USBKEY und meiner Platine wird dazu Pin E2 (HWB) während des Resets auf Masse gelegt und solange auf diesem Potenzial gehalten, bis der Reset beendet ist. Evtl. muss der Pin bei der aktuellen Version von Atmels Bootloader auf Masse-Potenzial gehalten werden, bis die Programmierung per USB-Bootloader abgeschlossen ist?) Aber selbstverständlich können Sie statt des USB Bootloaders auch ein beliebiges Programmierwerkzeug einsetzen, um die Firmware auf den Chip zu transferieren. Allerdings überschreibt Programmierung per ISP den USB-Bootloader!

Jetzt können Sie mit der Firmware von der Shell aus kommunizieren. Geben Sie nacheinander folgende Anweisungen ein:

./sudap --help
./sudap -p 11111111
./sudap -t 500us -s 100

Die zweite Zeile schaltet alle 8 Ausgänge von Port B auf High (5 V), die dritte Zeile liest von Pin ADC0 100 Spannungswerte im Abstand von 100 us ein und gibt die Messwerte in der Konsole aus. Die Ausgabe erfolgt ohne Umrechnung, d.h. Sie erhalten je nach Eingangsspannung Werte zwischen Null und 1023. (Legen Sie für den Test eine Signalquelle an Pin ADC0 an, z.B. den Schleiferausgang eines Potenziometer.)

Beachten Sie, dass diese Beispielanwendung die Funktion des USB, nicht aber die des ADC und des Timers demonstrieren soll. Die korrekte Funktion von ADC und Timer und die Güte der Messwerte wurde von mir bisher nicht näher untersucht, zudem ist der ADC bei der kleinsten Abtastperiode (20 us) schon deutlich überfordert.

Falls jemand diese Anwendung in der Praxis einsetzten will, so sollte er die Ergebnisse genau überprüfen. Wahrscheinlich wird derjenige dann eh Erweiterungen durchführen, beispielsweise die Selektion unterschiedlicher Referenzspannungen oder Wechsel zwischen unterschiedlichen Kanälen des ADC ermöglichen.

Bei dieser Anwendung fallen die Daten jeweils als Einzelwerte in äquidistanten Zeitintervallen an und müssen entweder unmittelbar per USB an den Host übertragen oder im Controller zwischengespeichert werden. Um etwaige Latenzzeiten des USB (bis zu einigen Millisekunden auf meinem Testrechner) zu überbrücken, werden die Daten daher in einem Ringspeicher im Controller zwischengespeichert. Bei der minimalen Abtastzeit von 20 us ist der Controller durch die Verwaltung dieses Zwischenspeichers schon recht stark ausgelastet.

Werden große Datensätze bei hoher Abtastfrequenz aufgezeichnet und in der Shell ausgegeben, so kann es zu Engpässen in der Datenübertragung kommen -- die folgenden Messwerte werden dann durch den Wert 65535 als ungültig gekennzeichnet und es wird eine Fehlermeldung ausgegeben. Ob dieser Fehler eintritt wird von Ihrem PC mitbestimmt -- bei mir trat er nur gelegentlich bei direkter Bildschirmausgabe auf, nicht hingegen, wenn ich die Ausgabe in eine Datei umleite. (Eine höhere Datenübertragungsrate bei gleichzeitig geringerer Auslastung des Controllers kann prinzipiell erreicht werden, wenn die Daten lediglich von einem großen Pufferspeicher gelesen oder in diesen geschrieben werden müssen, beispielsweise wenn externer RAM, ein externer ADC mit eigenem Pufferspeicher oder ein FPGA angeschlossen werden.)

Die internen Abläufe dieser Anwendung werden im nächsten Abschnitt näher erklärt.

Nutzung der Firmware und Anpassung an andere Anwendungen

Bei der Kommunikation über USB gibt es im wesentlichen drei grundlegende Phasen: Die Enumeration, bei der das Device dem Host seine Eigenschaften mitteilt, die Auswahl einer Konfiguration durch den Host, und letztendlich die Übertragungen der Nutzdaten zwischen Host und Device.

Je nachdem welches Gerät an den USB angeschlossen wird, unterscheiden sich diese drei Phasen -- dementsprechend muss die Firmware für jedes Gerät angepasst werden.

Bei der Enumeration werden so genannte Deskriptoren (Descriptors) vom Device an den Host übertragen, um ihm die Eigenheiten und Fähigkeiten des angeschlossenen Gerätes zu übermitteln. Dazu müssen diese Deskriptoren zunächst vom Device bereitgestellt werden. Nahe liegend ist es, diese Deskriptoren im Speicher des Controllers anzulegen. Dabei ist allerdings zu beachten, dass verschiedene Typen von Deskriptoren existieren, die miteinander verknüpft sind. Diese internen Abhängigkeiten können in der einfachsten Form durch die Reihenfolge der Ablage im Speicher, oder etwa in Form von verketteten Listen ausgedrückt werden. Denkbar wäre auch die Nutzung mehrdimensionaler Felder, wobei der Zugriff über verschiedene Indizes gesteuert wird. Aufgrund der Struktur und der variablen Anzahl der Deskriptoren wären bei der Speicherung in Feldern in der Regel einige Speicherplätze ungenutzt. Dies impliziert unnötigen Speicherverbrauch, so dass man diese Lösung in der Praxis nicht einsetzen wird. Die direkte Ablage im Speicher, bei der über die bekannten, festen Adressen auf die Deskriptoren zugegriffen wird, ist relativ kompakt, aber wenig flexibel und etwas unübersichtlich und damit fehleranfällig. Zur Vereinfachung kann man die Strukturen Script-gesteuert generieren und dann im Speicher ablegen -- in diese Richtung geht das Porus-Projekt.

Die Verwaltung der Deskriptoren durch verkettete Listen hat den Vorteil, dass Strukturen über Funktionsaufrufe dynamisch angelegt werden können. Mit der Anweisung ep3 = NewEndpoint(config1, interface2, altsetting1, ...); könnte etwa ein weiterer Endpoint für das zweite Interface der ersten Konfiguration angelegt werden. Ein ähnliches Vorgehen verwendet das usbn2mc-Projekt von B. Sauter. Konzeptionell erscheint diese Lösung als sehr flexibel und anwenderfreundlich. Nachteilig ist aber die geringe Transparenz und der Mehraufwand (Overhead) durch die Funktionsaufrufe, die vielen zur Verkettung nötigen Zeiger (Pointer) und die nötige dynamische Speicherverwaltung (malloc()). Zudem benötigen die Funktionen sehr viele Parameter, wenn alle denkbaren Anwendungsfälle abgedeckt werden sollen. Damit wird dann aber die einfache Anwendbarkeit wieder relativiert.

Ich habe für meine Firmware (vorerst) einen Ansatz gewählt, bei dem die einzelnen Deskriptoren über Indizes adressiert werden. Um möglichst wenig Speicherplatz in Anspruch zu nehmen, werden jedoch keine Felder zur Ablage verwendet, sondern jeder einzelne Deskriptor wird bei Bedarf jeweils von Funktionen, die mit entsprechenden Parametern aufgerufen werden, zur Verfügung gestellt. Dieser Ansatz funktioniert recht gut. Er bedingt allerdings, dass diese Funktionen für jedes neue Gerät etwas modifiziert werden. Sofern der Programmierer einige Grundkenntnisse über den USB besitzt und die Bedeutung der Deskriptoren kennt, ist diese Anpassung nicht schwierig.

Anpassung der Funktionen zur Bereitstellung der Deskriptoren

Nach der USB-Spezifikation muss für jedes Gerät ein Device-Descriptor vorhanden sein. Weiterhin muss für jedes Gerät mindestens ein Configuration-Descriptor existieren, zu dem wiederum je mindestens ein Interface-Descriptor gehört, wobei jedes Interface im Prinzip mehrfach mit verschiedenen "Alternate Settings" existieren darf. Jedem Interface zugeordnet sind dann ein oder mehrere Endpoints, welche die eigentlichen Datenkanäle bilden. Einfache Anwendungen wie die Beispielanwendung haben nur eine Konfiguration mit einem einzigen Interface, dem dann einige Endpoints zugeordnet werden.

Diese Festlegungen werden in den Dateien usb_api.h und usb_api.c getroffen:

#define USB_NumConfigurations 1
const uint8_t USB_Interfaces[USB_MaxConfigurations] = {1, 0, 0, 0};
const uint8_t USB_MaxPower_2mA[USB_MaxConfigurations] = {50, 0, 0, 0};
const uint8_t USB_AltSettings[USB_MaxConfigurations][USB_MaxInterfaces] =
  {{1, 0, 0, 0}, // number of alt. settings of interfaces of first configuration 
    {0, 0, 0, 0}, // number of alt. settings of interfaces of second configuration 
    {0, 0, 0, 0},
    {0, 0, 0, 0}};
const uint8_t USB_Endpoints[USB_MaxConfigurations][USB_MaxInterfaces] =
  {{3, 0, 0, 0}, // number of endpoints of interfaces of first configuration 
    {0, 0, 0, 0}, // number of endpoints of interfaces of second configuration 
    {0, 0, 0, 0},
    {0, 0, 0, 0}};

USB_NumConfigurations definiert zunächst die Anzahl von Konfigurationen, das Feld USB_Interfaces[USB_MaxConfigurations] gibt an, wie viele Interfaces zu jeder Konfiguration gehören. Die zweidimensionalen Matrizen USB_AltSettings[USB_MaxConfigurations][USB_MaxInterfaces] und USB_Endpoints[USB_MaxConfigurations][USB_MaxInterfaces] geben an, wie viele "Alternate Settings" bzw. Endpoints zu jedem Tupel (Configuration, Interface) gehören. Zusätzlich existiert noch das Feld USB_MaxPower_2mA[USB_MaxConfigurations], das den Strombedarf der einzelnen Konfigurationen angibt. Vorgesehen sind bis zu vier verschiedene Konfigurationen (USB_MaxConfigurations) mit je bis zu vier Interfaces (USB_MaxInterfaces) mit jeweils unbegrenzt vielen "Alternate Settings". Dies kann aber auch angepasst werden. Beim AT90USB können maximal 6 Daten-Endpoints genutzt werden.

Zur Bereitstellung der Deskriptoren existieren in den Dateien usb_api.h und usb_api.c folgende Funktionen:

void UsbGetDeviceDescriptor(USB_DeviceDescriptor *d);
bool UsbGetConfigurationDescriptor(USB_ConfigurationDescriptor *c, uint8_t confIndex);
bool UsbGetInterfaceDescriptor(USB_InterfaceDescriptor *i, uint8_t confIndex,
  uint8_t intIndex, uint8_t altSetting);
bool UsbGetEndpointDescriptor(USB_EndpointDescriptor *e, uint8_t confIndex,
  uint8_t intIndex, uint8_t altSetting, uint8_t endIndex);
void UsbGetStringDescriptor(char s[], uint8_t index);

Nach einem Blick in den Quellcode wird Ihre Funktion sofort klar: Falls der abgefragte Deskriptor für das betreffende Gerät nicht existiert wird false zurückgegeben, anderenfalls wird die Deskriptorstruktur mit den zugehörigen Daten aufgefüllt.

Anpassung der Funktionen zur Auswahl von Konfiguration und Interface

Hat der Host sämtliche Deskriptoren abgefragt, so kann er eine Konfiguration aktivieren und eventuell ein Interface mit einem speziellen "Alternate Setting" auswählen.

Für diesen Prozess existieren in der Datei usb_api.c die Funktionen

bool UsbDevSetConfiguration(uint8_t c);
bool UsbDevSetInterface(uint8_t conf, uint8_t inf, uint8_t as);

Auch diese beiden Funktionen sind relativ einfach aufgebaut und können dementsprechend leicht angepasst werden. UsbDevSetInterface() konfiguriert die von der Firmware unterstützten Endpoints, oder gibt unmittelbar false zurück, sofern das betreffende "Alternate Setting" des Interfaces in der gewählten Konfiguration nicht existiert. UsbDevSetConfiguration() ruft UsbDevSetInterface() für alle Interfaces der Konfiguration mit dem Defaultwert as == 0 auf. Die Konfiguration der Endpoints erfolgt dabei mit Hilfe der Funktion

bool UsbDevEP_Setup(uint8_t num, uint8_t type, uint16_t size, uint8_t banks, uint8_t dir);
      

aus der Datei usb_drv.c. Der erste Parameter num bezeichnet die Nummer des zu konfigurierenden Endpoints, type kann die Werte UsbEP_TypeIso, UsbEP_TypeBulk oder UsbEP_TypeInterrupt annehmen. Für size sind Zweierpotenzen zwischen 8 und 64 Byte erlaubt, bei UsbEP_TypeIso bis zu 512 Byte. Der Parameter banks entscheidet ob ein oder zwei (dual bank, ping-pong-mode) Pufferspeicher verwendet werden. Für dir können die Werte UsbEP_DirOut oder UsbEP_DirIn eingesetzt werden. Beachten Sie, dass für alle genutzten FIFO-Speicher laut Datenblatt insgesamt maximal 832 Byte zur Verfügung stehen.

Damit ist im Prinzip die Enumeration und Konfiguration des Gerätes abgeschlossen, jetzt können Nutzdaten über die Endpoints übertragen werden.

Übertragung von Nutzdaten über die Endpoints

Der AT90USB unterstützt bis zu 6 Daten-Endpoints, über die Nutzdaten vom Host zum Device (Out-Endpoint) oder vom Device zum Host (In-Endpoint) transferiert werden können. Prinzipiell können die Daten von der Firmware entweder kontinuierlich oder Interrupt-gesteuert in die Pufferspeicher (FIFO) geschrieben oder aus diesen ausgelesen werden. Bei der Interrupt-gesteuerten Kommunikation muss die gewünschte Interruptquelle zunächst ausgewählt bzw. freigegeben werden. Bevor Daten in den FIFO-Speicher geschrieben werden können, muss in der Regel zunächst geprüft werden, ob der FIFO bereit ist und ob noch genügend Platz vorhanden ist. Ist der FIFO dann gefüllt, muss er explizit freigegeben bzw. abgeschickt werden. Bei Out-Endpoints (die Firmware liest den FIFO aus) ist es ähnlich: Es muss geprüft werden, ob und wie viele Daten vom FIFO verfügbar sind, dann werden die Daten ausgelesen und am Schluss wird ein Flag gesetzt, das anzeigt, dass alle Daten gelesen wurden und der Host den FIFO-Speicher mit neuen Daten füllen darf.

Wie man das Auffüllen oder Auslesen des FIFO-Speichers am besten gestaltet hängt von der konkreten Anwendung ab, und ich habe noch nicht alle Möglichkeiten ausprobiert. Daher werde ich zum Abschluss dieses Abschnitts lediglich die drei Methoden erläutern, die in meiner Beispielanwendung benutzt werden. Es ist dann an Ihnen, für Ihre Anwendung mit Hilfe des Datenblattes (insbesondere Abschnitt 22.14, 22.15 und 22.18) und einiger Experimente einen optimalen Kommunikationsprozess zu gestalten.

In meiner Beispielanwendung werden die Messdaten kontinuierlich (Timer-gesteuert) in den FIFO-Speicher eines IN-Endpoints geschrieben. Das sieht dann in etwa so aus:

ISR(TIMER0_COMPA_vect)
{
  UsbDevSelectEndpoint(2);
  if (UsbDevTransmitterReady())
  {
    switch (UsbDevGetByteCountLow())
    {
      default:
        w = RB_Read();
        UsbDevWriteByte(LSB(w));
        UsbDevWriteByte(MSB(w));
        break;
      case 64:
        UsbDevClearTransmitterReady();
        UsbDevSendInData();
    }
  }
}

Zunächst muss der entsprechende Endpoint selektiert werden, dann wird abgefragt, ob er bereit ist neue Daten aufzunehmen (es wäre möglich, dass dieser FIFO soeben an den Host gesendet wird, dann muss gewartet werden). Dann wird der Füllstand überprüft. Ist er kleiner als 64 (FIFO-Größe dieses Endpoints), so können mit UsbDevWriteByte() Bytes eingeschrieben werden, anderenfalls ist er voll und muss mit UsbDevSendInData() abgeschickt werden, wobei laut Datenblatt zuvor UsbDevClearTransmitterReady() aufgerufen werden muss. Diese Funktionen sind übrigens lediglich Makros, die ein einzelnes Bit in einem USB-Register setzen und in der Datei usb_drv.h definiert sind.

Kurz erwähnen sollte ich noch, wie eine Datenaufzeichnung gestartet wird. Dazu wird in der Beispielanwendung ein Vendor-Request benutzt, man hätte aber genauso gut einen beliebigen Out-Endpoint verwenden können: Das PC-Programm führt den Funktionsaufruf

usb_control_msg(handle, USB_VendorRequestCode, UC_ADC_Read,
        (int) timeres, (int) samples, NULL, 0, TimeOut);
      

(Funktion aus der Bibliothek LibUSB) mit der speziellen Kennung USB_VendorRequestCode aus, worauf hin dann von der Firmware die Funktion

void UsbDevProcessVendorRequest(USB_DeviceRequest *req)
      

aufgerufen wird. Dabei wird der 8 Byte große Device-Request verwendet, um ein Kommando, die Anzahl der zu lesenden Daten und die Zeitauflösung zu übermitteln.

Die anderen beiden Endpoints werden Interrupt-gesteuert gefüllt bzw. ausgelesen. Dazu müssen zunächst die entsprechenden Interruptquellen aktiviert werden -- dies geschieht direkt bei der Konfiguration der Endpoints in der Funktion UsbDevSetInterface():

if (UsbDevEP_Setup(1, UsbEP_TypeBulk, EP1_FIFO_Size, 1, UsbEP_DirIn))
        UsbDevEnableNAK_IN_Int(); // trigger interrupt when host got a NAK as a result of a read request
      else
        Debug("Setup of EP1 failed!");
      if (UsbDevEP_Setup(2, UsbEP_TypeBulk, EP2_FIFO_Size, 2, UsbEP_DirIn));
      else
        Debug("Setup of EP2 failed!");
      if (UsbDevEP_Setup(3, UsbEP_TypeBulk, EP3_FIFO_Size, 1, UsbEP_DirOut))
        UsbDevEnableReceivedOUT_DATA_Int(); // trigger interrupt when out data is available
      else
        Debug("Setup of EP3 failed!");
      

Endpoint 1 wird für die Statusabfrage des Gerätes verwendet. Der Aufruf von UsbDevEnableNAK_IN_Int(); sorgt dafür, dass immer dann, wenn der Host vergeblich versucht, aus dem leeren FIFO zu lesen, ein Interrupt ausgelöst wird, so dass die Firmware dann ein aktuelles Statusbyte in den FIFO schreiben kann, das beim nächsten Leseversuch für den Host zur Verfügung steht. Über Endpoint 3 kann ein einzelnes Byte vom Host an das Device gesendet werden, das dann von der Firmware verwendet wird, um Port B zu setzen. UsbDevEnableReceivedOUT_DATA_Int() sorgt dafür, dass ein Interrupt generiert wird, wenn in den FIFO dieses Endpoints vom Host Daten geschrieben wurden.

Um auf die Endpoint-Interrupts angemessen reagieren zu können, werden in der Datei usb_api.h einige Makros der Form

#define UsbDevEP1IntAction() UsbDevFillEP1FIFO() 
      

definiert, die von der in der Datei usb_isr.c definierten Funktion ISR(USB_COM_vect) aufgerufen werden. Die Definition dieser aufzurufenden Funktionen erfolgt in der Datei usb_api.c:

void
UsbDevFillEP1FIFO(void)
{
  if UsbDevTransmitterReady()
  {
    UsbDevClearTransmitterReady();
    UsbDevClearNAK_ResponseInBit();
    UsbDevWriteByte(DAQ_Result);
    UsbDevSendInData();
  }
}

void
UsbDevReadEP3FIFO(void)
{
  if (UsbDevHasReceivedOUT_Data())
  {
    UsbDevClearHasReceivedOUT_Data();
    if (UsbDevReadAllowed())
    {
      DDRB = 0xFF;
      PORTB = UsbDevReadByte();
      UsbDevClearFifoControllBit(); // maybe we should use an alias for this macro
    }
  }
}

Die Funktion UsbDevFillEP1FIFO() überprüft zunächst mit UsbDevTransmitterReady() ob der FIFO-Speicher bereit für die Datenaufnahme ist, löscht dann mit UsbDevClearTransmitterReady() das FIFO-Flag und mit UsbDevClearNAK_ResponseInBit() das entsprechende Interruptflag, um dann ein Status-Byte in den FIFO zu schreiben und diesen zum Versand freizugeben.

Die Funktion UsbDevReadEP3FIFO(void) überprüft zunächst, ob Daten zum Lesen zur Verfügung stehen und löscht dann eben dieses Flag. Mit UsbDevReadAllowed() wird nochmals überprüft, ob mindestens ein Byte im FIFO zum Lesen bereit steht. Daraufhin wird dieses Byte gelesen und in das Register für Port B geschrieben. Durch den Aufruf von UsbDevClearFifoControllBit() wird das Ende der Leseoperation angezeigt und der FIFO für eine erneute Befüllung durch den Host freigegeben.