Generic HID auf Arduino Micro

Wie implementiert man ein Sensor-Interface auf einem Arduino Micro, vergleichbar Arduino Leonardo, basierend auf ATmega32U4?

Die üblichen Lösungen benutzen hierfür eine (nämlich die von dem Arduino-Firmware-Framework „frei Haus“ gebotene) USB-CDC-Schnittstelle, also ein COM-Port.

Der Hauptnachteil davon ist, dass man dafür auf Fremdcomputern einen Treiber installieren muss. Also immer schön einen USB-Speicherstick mitnehmen oder auf Internet hoffen, um den Treiber parat zu haben.

Weiterhin nachteilig ist, dass man sich irgendwie um die Paketisierung der Messdaten kümmern muss. Oftmals werden die Daten als kommaseparierte Textzeilen geliefert, die das Empfängerprogramm wieder in Binärdaten umwandeln muss (zum Verrechnen u. ä.) - das ist rechentechnischer Unsinn. Klar, man kann auch Binärdaten übertragen, muss sich aber trotzdem um die Datensatzgrenzen kümmern.

Drittens kann nur ein Client-Programm die serielle Schnittstelle abfragen, andere Programme können die COM-Schnittstelle nicht mehr öffnen, sie ist exklusiv. HID hingegen kann mehrere Clients bedienen.

Die optimale Implementierung ist via Generic HID. (Praktikable Einführung.) Aber auch ein HID-Joystick lässt sich brauchbar dafür verwenden. HID-Tastaturen und -Mäuse sind hingegen indiskutabel.

Im Beispiel geht es um die Implementierung eines taktilen Kraftsensor-Arrays.

Die Arduino-Firmwarebibliothek

Die Unterstützung für Generic HID oder HID-Joysticks seitens Arduino ist (offiziell) nicht vorhanden.

Deshalb gibt es hier eine Anleitung, die Arduino-Bibliotheken (der auf dem Windows-PC installierten Java-Software) zu patchen: Add-USB-Game-Controller-to-Arduino-LeonardoMicro. Damit kann man dann einen Joystick implementieren.

Das kommt dem Kraftsensor schon so nahe, dass man es dabei schon belassen kann und statt der unter Windows umständlichen HID-Schnittstelle die viel einfachere Joystick-Schnittstelle verwenden kann. API-Funktionen:

Da kommen genau die Zahlenwerte an, die man in der Firmware reinsteckt. Und man kann in der Systemsteuerung — ganz ohne eine Zeile Windows-Kode zu schreiben — die Funktion prüfen! Siehe animierter Screenshot rechts.

Da die üblichen Joysticks (und sicherlich auch der o.a. Patch) 8-Bit-Positionswerte liefern, muss man per Modifikation des HID-Report-Descriptors und damit des Report-Aufbaus auf 16 Bit aufblasen. Wenn alles zusammen passt, hat Windows damit kein Problem. Hierzu muss man jedoch das Konzept von HID-Deskriptoren komplett verstanden haben. Genauer, wie aus den Angaben im Deskriptor die Datenbits im Report verteilt werden.

Der übermäßige Gebrauch von CDC kommt zustande, weil das Arduino-Firmware-Framework immer eine serielle Schnittstelle via USB-CDC kreiert (für Debug­ausgaben gedacht) und dafür bereits 3 der wenigen USB-Pipes verbrät. (Letzteres stört beim ATmega32U4 nicht so sehr.)

Die Grenzen des Joystick-Interfaces von Windows sind bei 6+1 analogen Achsen erreicht. Die siebente Achse (dwPOV) ist möglicherweise keine Kopie von den Daten des HID-Interfaces.

Urlader, doppelt gemoppelt

Zusammenfassung 2020: Urlader (neudeutsch Bootloader) gibt es einige:

Irrtum

Pardon, der durchgestrichene Text ist falsch und bezieht sich womöglich auf die denkbare Situation beim Vorgängermodell AT90USB162:

Hinzu kommt noch das Urlader-Wirrwarr: Während der ATmega32U4 einen eingebauten und unlöschbaren USB-Urlader beinhaltet, werden die Arduinos mit einem sekundären Arduino-Urlader ausgeliefert, der 4 KByte von den 32 KByte Flash abknapst.

Der Werks-Urlader muss mit diesem klobigen 60-MByte-Java-Programm, das nur unter Windows läuft, namens „Flip“, bedient werden. Da hat m. W. noch niemand für Ersatz gesorgt. Gestartet wird der Urlader durch Betätigen von RESET mit Low-Pegel am Pin HWB. Dummerweise ist das Pin HWB an den Arduinos nicht herausgeführt. Um diesen Urlader zu verwenden, muss man also ein kleines Drähtchen anlöten und gemeinsam mit RESET an Masse legen. Ohne die Verbindung wird die Firmware (oder ein weiterer Urlader) unverzüglich gestartet.

Richtigstellung

Ein originaler ATmega32U4 wird mit einem eingebauten und per Fuse-Einstellung nicht auslesbaren (also kopiergeschützten) USB-Urlader ausgeliefert. Dieser zwackt 4 KByte von den 32 KByte Flash ab. Dieser Werks-Urlader soll mit dem klobigen 60-MByte-Java-Programm namens „Flip“ bedient werden, das: Als Alternative existiert die Open-Source dfu-programmer. Allerdings funktionierte jene Software bei mir bei einem AT90USB162 nicht. Mit einem ATmega16U4 hingegen super, das olle Atmel Flip kann ich gleich wieder löschen.

Der Arduino-Urlader wurde bei der Auslieferung des Arduinos mittels SPI-Interface auf den ATmega32U4 in dieselben 4 KByte einprogrammiert und ersetzt damit den Werks-Urlader.

Das hier war richtig

Der Arduino-Urlader ist da handlicher. Er kann auch vom Kommandozeilenprogramm „avrdude“ bedient werden und ist kompatibel zu den anderen Arduinos. „avrdude“ ist das Arbeitspferd unter der Haube der Arduino-Java-Software. In der Originalversion wartet der Urlader 10 Sekunden nach Controller-Reset auf ein Kommando vom PC und startet erst dann die eigentliche Firmware. Das ist der Hauptnachteil, wenn es dafür kein reserviertes Pin am Mikro­controller gibt. Im Netz geistern Urlader herum, die genau dieses Manko mildern (kürzere Zeit) oder vermeiden (meist ein Pin verbraten).

Folglich gilt, selbst wenn man mittels o.a. Patch die Sensordaten per Joystick-Achsen ausgibt und Windows dafür keinen Fremd­treiber anfordert, bleibt beim Anstecken eines solchen Gerätes an einen Fremd­computer ein Frage­zeichen im Geräte­manager stehen: Für die ungenutzte serielle Schnitt­stelle. Nicht schlimm, aber unschön.

Will man das vermeiden, hilft nur ein weiterer, tiefgründiger Patch der Arduino-Firmware­bibliothek.

Beide Urlader erfordern die Installation eines Treibers; keiner von beiden geht über die („treiberlose“) HID-Geräteklasse. Hier hat MSP430 die Nase vorn.

Weg vom Arduino(-Framework)

Besser, man trennt sich von der Arduino-Java-Software. Ich benutze winavr. Auch wenn die aktuelle Version aufs Jahr 2010 datiert. Im Prinzip ist das Arduino-Framework selbst über ein abgespecktes winavr gestülpt.

Um den Urlader loszuwerden, benutzt man am besten den Draht an HWB und füllt dann den Controller. Oder benutzt das SPI-Interface (2x3-Pfostenstecker) und einen geeigneten Programmier­adapter (etwa ein Parallelport oder mein USB2LPT). Aber wenn die Hochlauf­zeit okay ist, kann der zweite Urlader auch drauf­bleiben.

Der erste Vorteil von winavr ist, dass man nichts unsinniges lernt: Ein C-Programm beginnt nun mal mit main()!

Als USB-Framework benutzt man in diesem Fall LUFA. (Auch die Arduino-USB-Implementation basiert auf LUFA.) Oder gar keins, denn auch in LUFA muss man sich einarbeiten. Vieles wird nämlich ziemlich direkt von der ATmega32U4-Hardware erledigt. Schließlich bietet auch Atmel ein eigenes Framework für Generic HID an, welches auf LUFA basiert. Sofern Flash-Speicher reichlich da ist, spricht nichts gegen den Einsatz von LUFA, abgesehen vom Einarbeitungsaufwand. LUFA ist auch für die 32-Bit-Controller von Atmel verwendbar.

Die aktuelle Entwicklungsumgebung von Atmel basiert auf Microsoft Visual Studio und bietet gegenüber winavr über Quelltext-Verfolgung und -Vervollständigung (IntelliSense) und eine (immerhin!!) funktionierende Kontexthilfe, sofern Internet vorhanden ist. Ein meilenweiter Fortschritt (auch) gegenüber Arduino, auf Kosten eines riesigen Downloads und dass es nicht mehr unter Linux und XP läuft. Sowas gab's schon vor 20 Jahren und nannte sich Borland Pascal: Auch da hat die kontextsensitive Hilfe funktioniert. Sogar ohne Netz.

Allerdings habe ich es nicht geschafft, die irreführende Rechtschreibprüfung abzuschalten. Außerdem ist der Compile-Vorgang schwerfällig und langsam, und zum effektiven Arbeiten und Navigieren fehlen Tastenkombinationen. Die Anbindung von dfu-programmer habe ich auch noch nicht hinbekommen. Vom Debuggen ganz zu schweigen. So bleibe ich beim alten „Programmer's Notepad“ und dem handgemachten makefile.

Generic HID

Für mehr als sechs Analogwerte ist das Windows-Joystick-Interface am Ende. Auf der Firmware-Seite wäre zu ändern:

Auf der Windows-Seite ist deutlich mehr Holz zu hacken. Hier einige Kodeschnipsel für die Dinge, die zu tun sind.

static HANDLE OpenDevInterface(const GUID *guid, int idx, DWORD access, DWORD share) {
 HANDLE ret=0;
 HDEVINFO devs=SetupDiGetClassDevs(guid,0,0,DIGCF_PRESENT|DIGCF_DEVICEINTERFACE);
 if (devs!=INVALID_HANDLE_VALUE) {
  SP_DEVICE_INTERFACE_DATA devinterface;
  devinterface.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
  if (SetupDiEnumDeviceInterfaces(devs,NULL,guid,idx,&devinterface)) {
   struct{
    SP_DEVICE_INTERFACE_DETAIL_DATA id;
    TCHAR space[MAX_PATH];
   }id;
   id.id.cbSize=sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
   if (SetupDiGetDeviceInterfaceDetail(devs,&devinterface,&id.id,sizeof(id),NULL,NULL)) {
    ret=CreateFile(id.id.DevicePath,access,share,NULL,OPEN_EXISTING,FILE_FLAG_OVERLAPPED,NULL);
    if (ret==INVALID_HANDLE_VALUE) ret=0;
   }
  }
  SetupDiDestroyDeviceInfoList(devs);
 }
 return ret;
}

typedef struct{
 HANDLE hDev;		// Gerätetreiber-Kanal
 PHIDP_PREPARSED_DATA pd;//HID-...
 HIDP_CAPS caps;	// HID-...
}THid;

static bool HidOpen(THid*Hid) {
 GUID hidGuid;
 HidD_GetHidGuid(&hidGuid);
 Hid->hDev=OpenDevInterface(&hidGuid,Config.iUsbHid,GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ|FILE_SHARE_WRITE);
 if (Hid->hDev) {
  if (HidD_GetPreparsedData(Hid->hDev,&Hid->pd)) {
   HidP_GetCaps(Hid->pd,&Hid->caps);
   return true;
  }
  CloseHandle(Hid->hDev);
  Hid->hDev=0;
 }
 return false;
}

static void HidClose(THid*Hid) {
 if (Hid->pd) {
  HidD_FreePreparsedData(Hid->pd);
  Hid->pd=0;
 }
 if (Hid->hDev) {
  CloseHandle(Hid->hDev);
  Hid->hDev=0;
 }
}

static int GetMaxReport(const HIDP_CAPS*caps) {
 int ret=caps->InputReportByteLength;
 if (ret<caps->OutputReportByteLength) ret=caps->OutputReportByteLength;
 if (ret<caps->FeatureReportByteLength) ret=caps->FeatureReportByteLength;
 return ret;
}

// ersetzt HidD_GetInputReport(), mit TimeOut bei FILE_FLAG_OVERLAPPED
static bool GetInputReport(HANDLE hDev, void*buf, int len, OVERLAPPED*o) {
 DWORD br;
 zeroOverlapped(o);
 return ReadFile(hDev,buf,len,&br,o)
 || GetLastError()==ERROR_IO_PENDING
 && !WaitForSingleObject(o->hEvent,100)
 && GetOverlappedResult(hDev,o,&br,FALSE)
 && br;
}

static DWORD CALLBACK HidThread(LPVOID p) {
 PUCHAR report;		// HID-Report-Puffer
 int len=gHid.caps.InputReportByteLength;
 report=LocalAlloc(LPTR,len<<1);
 if (report) {
  for (;;) {
   HidP_InitializeReportForID(HidP_Input,4,gHid.pd,report,len);
   if (!GetInputReport(hCom,report,len,&o)) break;
   if (*(DWORD*)(report+len+2)!=*(DWORD*)(report+2)) {
    *(DWORD*)(report+len+2)=*(DWORD*)(report+2);
    if (PropWnd) {
     HWND w=PropSheet_IndexToHwnd(PropWnd,0);
     if (w==PropSheet_GetCurrentPageHwnd(PropWnd)) {
      //SendMessage(w,WM_FUNKRECV,14,(LPARAM)report);
      FillHidRep4(w,&gHid,report);
     }
    }
   }
  }
  CancelIo(hCom);
  LocalFree(report);
 }
 return 5;
}

Siehe auch

Wie man am letzten Beispiel sieht, nimmt LUFA nicht wirklich viel Arbeit ab: Nur ein Drittel des Quelltextes von usb.c erspart man sich mit LUFA. IMHO ist der Einarbeitungsaufwand in LUFA höher als das letztgenannte Beispiel zu benutzen und anzupassen.

Zum Teufel mit LUFA

Ein k.-o.-Kriterium stellte sich sogleich heraus, dass LUFA im HID-Modus keinen OUT-Endpoint unterstützt. Das ist gewissermaßen HID mit angezogener Handbremse, wenn massiv Daten auszugeben sind.

Außerdem bietet LUFA keine MSP430-Unterstützung. (Ist der Entwickler von Atmel gekauft worden?) Damit wäre ein Umzug quasi sinnlos.

Die USB-Implementierung in den AVR-Mikrocontrollern erscheint mir derart ähnlich untereinander, dass mir der Abstraktions-Overhead in Verbindung mit der völlig unzureichenden und unhandlichen Dokumentation viel zu groß angesetzt ist. Musste man bspw. Big-Endian-Architekturen einplanen? IMHO sind diese längst ausgestorben, und das bissel SETUPDAT hätte man auch endian-unabhängig in 8 Bytes aufteilen können.