USB HID kommunikáció

A fejezet tartalma:

Az USB HID eszközosztály

Az USB eszközök funkció szerint elkülönülnek egymástól, ennek megfelelően az USB 2.0 specifikációja különféle osztályokba sorolja az eszközöket. Ilyen osztályok pl. a hangeszköz (Audio), képeszköz (Image), kommunikációs eszköz (Communication), Nyomtató, Tárolóeszköz (Mass Storage), stb. Egy speciális osztály a HID (Human Interface Device). A HID eszközök eredetileg olyan periféria csoportot jelentenek, amelyek segítségével a felhasználó képes vezérelni a számítógépet, illetve a számítógép megjelenítheti a saját kimeneteit. Ilyen eszközök pl. a billentyűzet, egér, játékvezérlő, különböző kijelzők. Ezek a hardverek nagyon elterjedtek, ezért az USB szabványban előre definiált alosztályok szerepelnek a fent említett eszközök számára.

A HID kapcsolat főbb jellemzői:
Mivel a legtöbb operációs rendszer alapértelmezetten támogatja a HID eszközöket, így nincs szükség speciális meghajtó programra. További jó tulajdonság a HID kapcsolat robusztussága: az eszköz le- és felcsatlakozásához sem kell leállítani, vagy újraindítani a futó alkalmazást.

Az általános (generic) HID eszközök alosztálya

Amikor egy USB eszközt csatlakoztatunk a számítógéphez, akkor egy "bemutatkozó párbeszéd" kezdődik, melynek során a gazdagép (ami esetünkben a PC) "kikérdezi" az USB eszközt (adatsebesség, eszközosztály, gyártó, VID/PID azonosítók stb.). Ezeket az információkat az eszköz firmware ún. eszközleíró táblába tárolja. A HID eszközök emellett még egy jelentésleíró táblát is tartalmaznak, amelyek a HID eszköz által küldött adatcsomagokról (az úgynevezett jelentésekről) nyújtanak tájékoztatást. A szabványos HID eszközöknél az ún.  HID report descriptor (HID jelentés leíró tábla) megmondja, hogy az átvitt adatcsomag egyes bájtjai (vagy bitjei) mit jelentenek.

A HID eszközosztály azonban nem korlátozódik kizárólag ilyen előre definiált, szabványos eszközökre, hanem teret nyújt az egyedileg készített eszközöknek is. A HID eszközosztály legáltalánosabb alosztálya az Általános HID Eszköz (Generic HID Device), amelynél nincs kötött protokoll. Ezeknél az eszközöknél a HID jelentésleíró tábla  csak az átvitt adatok mennyiségét mondja meg, az adatok értelmezése azonban a mikrovezérlőbe töltött firmware-re és a PC-n futó alkalmazásra van bízva. Ebben az esetben tehát csak az átvitel mikéntje szabványos, az adatok felhasználása pedig gyártóspecifikus.

Az USBHID objektumosztály

Az USBHID objektumosztály a fentebb említett "Általános HID Eszközt" (Generic HID Device) valósítja meg. Az USBHID osztály tagfüggvényeivel üzeneteket küldhetünk és kaphatunk az USB buszon. Saját protokollt dolgozhatunk ki és eszerint kommunikálhatunk a mikrovezérlő és a számítógép között.

A mikrovezérlő oldalon ehhez a kommunikációs típushoz az USBHID objektumosztályt kell használnunk. Az mbed API alapértelmezetten nem tartalmazza az USB kommunikációt kezelő programkönyvtárakat. Projektjeinkhez - eszközosztálytól függetlenül - az USBDevice programkönyvtárat kell importálnunk.

Az USBHID objektumosztály legfontosabb tagfüggvényeit az alábbi táblázatban foglaltuk össze.

Függvénynév
Rövid leírás
USBHID (uint8_t output_report_length=64, uint8_t input_report_length=64, uint16_t vendor_id=0x1234, uint16_t product_id=0x0006, uint16_t product_release=0x0001, bool connect=true)
Konstruktor függvény. Példányosítja és inicializálja az USBHID objektumosztályt, megadja a kimenő és bejövő adatblokk méretét, a VID/PID azonosítót, s a connect paraméter értékétől függően kapcsolódik (vagy nem)
read(HID_REPORT* report)
Egy adatblokk fogadása blokkoló várakozással
readNB(HID_REPORT* report)
Egy adatblokk fogadása nem blokkoló módon
send(HID_REPORT* report)
Egy adatblokk küldése blokkoló várakozással
sendNB(HID_REPORT* report) Egy adatblokk küldése nem blokkoló módon

Megjegyzések:

Mintapélda: USB HID kommunikáció

Az alábbi programot a Cypress AN82072 alkalmazási mintapéldájához igazítva készítettük el, hogy annak a letölthető ZIP csomagjában található grafikus PC alkalmazását használni tudjuk. Ennek megfelelően mindkét irányba 8 bájtos csomagokat küldünk, s az adatok értelmezése és felhasználása az alábbiak szerint történik:


1. ábra: Az adatcsomagok értelmezése és felhasználása

Az Input/Output irány mindig a gazdagép szempontjából értendő. Az Input Report tehát a  mikrovezérlő által küldött 8 bájtos csomagot jelenti, melynek első bájtja egy nyomógombhoz rendelt bemenet állapotát jelzi (0: lenyomva, 1: felengedve), a következő négy bájt pedig egy analóg bemenet jelének ADC-vel konvertált eredményét jelenti (a Cypress mikrovezérlőknél van 20 bites ADC is, ezért nem volt elég egy kétbájtos adatküldés az ADC-hez). A bájtsorrend úgynevezett Big Endian, azaz a legmagasabb helyiértékű bitek állnak elől.

Az Output Report (a gazdagép által kiküldött üzenetcsomag) pedig vezérlési funkciót lát el:  az elsó bájt egy LED-et  vezérel (0: ki, 1: be), a második bájt pedig egy másik LED fényerősségét vezérli (1 - 100 közötti értéket küldhetünk ki, ami a PWM jel százalékos kitöltését adja meg).

Esetünkben a D3 (belső felhúzásra kapcsolt) bemenet figyeli a nyomógomb állapotát, s az A0 bemenetre kapcsolt feszültséget mérjük meg. A kimeneti vezérlésnél LED1 (a piros LED) lesz a ki-be kapcsolható LED, s LED2 (a zöld LED) lesz az, melynek fényerősségét folyamatosan szabályozzuk (PWM).

Végeredményben tehát az egyszerű mintapéldán keresztül megmutatjuk, hogy az USB HID kapcsolat segítségével hogyan végezhetünk analóg és digitális adatgyűjtést/adatbeolvasást, és hogyan létesíthetünk digitális és (kvázi) analóg vezérlést.

Hardver követelmények:
1. lista: A 13_USBHID_demo/main.cpp program listája
#include "mbed.h"
#include "USBHID.h"

//We declare a USBHID device. By default input and output reports are 8 bytes long.
USBHID hid(8, 8);

HID_REPORT send_report; //This report will contain data to be sent
HID_REPORT recv_report; //This report will contain data received

DigitalOut LED_1(LED1);
PwmOut LED_2(LED2);
DigitalIn SW1(D3,PullUp);;
AnalogIn adc(A0);

int main(void) {
send_report.length = 8;
LED_1 = 1;
LED_2.period_ms(20);

while (1) {
uint16_t raw = adc.read_u16(); //Read ADC (A0 chan)
for (int i = 0; i < send_report.length; i++) //Fill the report
send_report.data[i] = 0x00;
send_report.data[0] = SW1.read();
send_report.data[3] = (raw>>8);
send_report.data[4] = (raw & 0xff);
hid.send(&send_report); //Send the report

if(hid.readNB(&recv_report)) { //try to read a msg
LED_1 = !recv_report.data[0];
LED_2.write(1.0 - recv_report.data[1]*0.01f);
}
}
}

A program elején be kell csatolnunk az USBHID.h  fejléc állományát. Ehhez az új projekt létrehozása után importálnunk kell az USBDevice programkönyvtárat. Ezután példányosítanunk kell az USBHID objektumosztályt. Programunkban csak az ki- és bemenő üzenetcsomag méreténél tértünk el az alapértelmezett értékektől. Az üzenetcsomagok tárolásához deklarálnunk kell egy-egy HID_REPORT típusú tömböt is (a ki- és bemeneti buffer elnevezése itt a mikrovezérlő szempontjából értendő!).

A program végtelen ciklusában az ADC mérés után automatikusan összeállítjuk és kiküldjük a gazdagépnek szánt csomagot. A nem használt bájtokat nullával töltjük fel.

A LED-ek vezérlése a bejövő üzenetcsomagok első két bájtja szerint történik. Ha az első bájt nullától különböző, akkor kigyújtjuk a piros LED-et. A második bájtot úgy konvertáljuk, hogy 100 esetén kapjuk a maximális kitöltést. Mindkét LED vezérlésénél invertálnunk kell a jelet a közös anódú LED-ek fordított logikája miatt.

Megjegyzések:
A program kipróbálásához az AN82072 alkalmazási mintapélda letöltött ZIP állományából a Host Application/Windows Application mappából a Generic HID UI.exe és a CyUSB.dll állományokra van szükségünk.
  1. A program futtatásakor be kell állítanunk a VID és PID azonosítókat és rá kell kattintanunk a Set gombra. Ezután a zöld Connected felirat jelenik meg, ha a 13_USBHID_demo programmal felprogramozott kártya csatlakoztatva van.
  2. A piros LED-et a LED felirat melleti jelölőnégyzet kiválasztásával kapcsolható be és ki.
  3. A zöld LED fényereje a PWM Duty Cyle címke mellett állítható be 1 és 100 közötti értékre, de ennek kiküldése csak az Update gomb kattintásakor történik meg.
  4. A bejövő értékek (az ADC konverzió 16 bites eredménye és a D3 digitális bemenet állapota) a vezérlőelemek alatt látható. Az ON kiírás a digitális bemenet magas szintjét, az OFF pedig az alacsony szintjét jelenti. Mivel a nyomógombunk a GND felé húz, ezért a fordított logika miatt a felirat a nyomógomb állapotát fordítva jelzi ki.

2. ábra: A Cypress AN82072 alkalmazási mintapéldából "kölcsönvett" program futtatása

HID alkalmazások és tesztprogramok

Az alábbiakban bemutatunk néhány lehetőséget, amellyel gyorsan és egyszerűen kipróbálhatjuk mbed USB HID mikrovezérlő programjainkat, vagy akár saját igényeinkhez igazított PC alkalmazásokat is készíthetünk - ingyen, s természetesen jogszerűen. Jan Axelson HID Page című honlapjának Tools szekciója számos további lehetőséget is felsorol, ám azok közül nem mindegyik vált be a fenti mintaprogramunkkal kipróbálva.

SimpleHidWrite

A SimpleHidWrite alkalmazás egyszerű segédeszköz a HID eszközök kipróbálásához vagy felderítéséhez. Minimális HID és USB ismeretek kellenek a program megértéséhez. A grafikus alkalmazás ablakának felső részében található lista a pillanatnyilag elérhető HID eszközöket sorolja fel. A kattintással kiválasztott eszközt ismételten lekérdezi a program, s a beérkezett üzeneteket kilistázza a program ablakának középső részen látható  szövegdobozban. A szövegdoboz alatti részben a kimenő üzeneteket állíthatjuk össze (a beírt számok hexadecimálisan lesznek értelmezve, az üresen hagyott mezők automatikusan nullával töltődnek fel), illetve a nyomógombok segítségével az alábbi tevékenységek indíthatók:
Az alsó sorban levő gombok csak akkor aktívak, ha ki van választva egy eszköz és arra vonatkozóan (elvileg) használható az adott gomb. Az aktív állapottól függetlenül a fenti 13_USBHID_demo mintapélda esetén ezek közül csak a Write (adatcsomag küldés) használható!
A fenti 13_USBHID_demo mintapélda esetében az USB HID kommunikáció kipróbálását az alábbi lépésekben végezhetjük el:
  1. Töltsük le és bontsuk ki SimpleHIDWrite3.zip csomagot!
  2. Indítsuk el a SimpeHidWrite.exe alkalmazást!
  3. A megnyíló ablak felső részében kattintsunk a felismert HID eszközünk sorára (a HID DEVICE (Serial=0123456789) eszközt keressük)! A listázó ablakban ekkor elkezdenek pörögni a bejövő üzenetek. Figyeljünk rá, hogy az RD (olvasást) jelzést követő első szám még nem az üzenetcsomagunk része, hanem a jelentés alapértelmezett azonosítója (00)! Az ez követő bájt jelzi a nyomógombunk állapotát, a következő négy bájt pedig az ADC konverzió eredményét.
  4. Állítsunk össze egy adatcsomagot! A Report ID kötelezően 0, az alatta levő 8 bájt pedig a kiküldendő adatok. Csak a nullától különböző mezőket kell kitölteni! Például: 01 00 00 00 00 00 00 00 bekapcsolja a piros LED-et és leoltja a zöld LED-et. A 0064 00 00 00 00 00 00 csomag lekapcsolja a piros LED-et és teljes fényerőn (100 %-os kitöltés) kigyújtja a zöld LED-et. 
  5. Küldjük ki az adatcsomagot a Write gombra kattintva!
  6. Mentsük el egy napló állományba a beérkezett és kiküldött csomagokat (Save As... gomb), majd tanulmányozzuk az adatokat! A kiküldött adatokat a WD kezdetű sorok tartalmazzák.

3. ábra: A SimpleHidWrite program használata

HIDAPI - testgui

Alan Ott multiplatformos programkönyvtára Linux, Mac OS X és Windows alatt is kezelni tudja az USB, illetve Bluetooth HID eszközöket. A programkönyvtár C/C++ alkalmazásainkból meghívható, így egyszerűen készíthetünk USB HID eszközt kezelő saját alkalmazásokat. A HIDAPI programcsomag egy grafikus felületű tesztprogramot is tartalmaz, amellyel egyszerűen kipróbálhatjuk és vizsgálhatjuk HID eszközeinket. A programcsomag legegyszerűbben a Letöltések oldalról tölthető le. Nekünk csak a legfrissebb hidapi-0.7.0.zip lesz szükségünk (a hidapi-externals.zip csomag csupán a Windowsos testgui.exe újrafordításához vagy módosításához kellhet).

A kibontott csomagból először a testgui.exe alkalmazást keressük meg és próbáljuk ki!
  1. A program elindításakor megnyíló ablak felső részén keressük meg az 1234:0006 - mbed.org eszközt!
  2. Ha a program elindítása után csatlakoztattuk a kártyánkat, akkor a Re-Scan devices gombra kattintva frissítsük az elérhető eszközök listáját!
  3. Válasszuk ki az 1234:0006 - mbed.org eszközt és kattintsunk a Connect gombra! Az ablak alsó részén, az Input szövegdobozban ekkor megjelennek a beérkező üzenetcsomagok (ezek mérete esetünkben 8 bájt, tehát a jelentés azonosítószáma (Report ID) nincs megjelenítve.
  4. A Output rovatban állítsunk össze egy üzenetet, és küldjük ki a Send Output Report gombra kattintva!
Megjegyzések:
  1. Az Output rovatba a 8 db adatbájt elé be kell írnunk az alapértelmezett jelentés azonosítót (0) is, tehát szigorúan mindig 9 bájtot kell beírnunk, s az első bájt kötelezően nulla legyen! Minden más esetben hibajelzést kapunk.
  2. Az általunk beírt adatokat alapértelmezetten decimálisnak veszi a program. Hexadecimális bájtokat 0x prefix-szel adhatunk meg. A beérkezett adatokat mindig hexadecimálisan jeleníti meg a program.

Példa adatok:

4. ábra: A HIDAPI testgui.exe alkalmazás futtatása


HIDAPI - hidtest

Alan Ott multiplatformos programkönyvtárának legfontosabb része a hidapi.dll, amelyet saját C/C++ programjainkból meghívhatjuk. A hidapi csomag windows mappájában található egy Visual C++ 2008 Express Edition-nal lefordítható projekt (a hidapi.sln állományt nyissuk meg!), amely a hidapi.dll mellett egy konzolos (nem grafikus felületű) mintaalkalmazást is lefordít (hidtest.exe). Ennek forráskódját némileg módosítottuk. A módosítások lényege:
  1. Módosítottuk a Vid/Pid azonosítót (0x1234, 0x0001), és az üzenetcsomagok méretét (9 byte a Report ID-vel együtt).
  2. Összesen 20 adatcsomagot küldünk ki és fogadunk (minden küldés után 500 ms várakozást iktatunk be).
  3. A kiküldött adatokat úgy állítjuk elő, hogy a piros LED csak kétszer villan fel, a zöld LED fényereje pedig fokozatosan növekszik 5 %-os lépésekben.
  4. A kiküldött és a beérkező adatcsomagokat egymás mellé kiíratjuk.

2. lista: A módosított hidtest.c program listája
#include <stdio.h>
#include <wchar.h>
#include <string.h>
#include <stdlib.h>
#include "hidapi.h"

// Headers needed for sleeping.
#ifdef _WIN32
#include <windows.h>
#else
#include <unistd.h>
#endif

int main(int argc, char* argv[])
{
int res;
unsigned char buf[9];// 1 extra byte for the report ID
#define MAX_STR 255
wchar_t wstr[MAX_STR];
hid_device *handle;
int i;

#ifdef WIN32
UNREFERENCED_PARAMETER(argc);
UNREFERENCED_PARAMETER(argv);
#endif

struct hid_device_info *devs, *cur_dev;

devs = hid_enumerate(0x0, 0x0);
cur_dev = devs;
while (cur_dev) {
printf("Device Found\n type: %04hx %04hx\n path: %s
\n serial_number: %ls", cur_dev->vendor_id, cur_dev->product_id,
 cur_dev->path, cur_dev->serial_number);
printf("\n");
printf(" Manufacturer: %ls\n", cur_dev->manufacturer_string);
printf(" Product: %ls\n", cur_dev->product_string);
printf(" Release: %hx\n", cur_dev->release_number);
printf(" Interface: %d\n", cur_dev->interface_number);
printf("\n");
cur_dev = cur_dev->next;
}
hid_free_enumeration(devs);

// Set up the command buffer.
memset(buf,0x00,sizeof(buf));
buf[0] = 0x01;
buf[1] = 0x01;
buf[2] = 0x01;

// Open the device using the VID, PID,
// and optionally the Serial number.
////handle = hid_open(0x4d8, 0x3f, L"12345");
handle = hid_open(0x1234, 0x6, NULL);
if (!handle) {
printf("unable to open device\n");
return 1;
}

// Read the Manufacturer String
wstr[0] = 0x0000;
res = hid_get_manufacturer_string(handle, wstr, MAX_STR);
if (res < 0)
printf("Unable to read manufacturer string\n");
printf("Manufacturer String: %ls\n", wstr);

// Read the Product String
wstr[0] = 0x0000;
res = hid_get_product_string(handle, wstr, MAX_STR);
if (res < 0)
printf("Unable to read product string\n");
printf("Product String: %ls\n", wstr);

// Read the Serial Number String
wstr[0] = 0x0000;
res = hid_get_serial_number_string(handle, wstr, MAX_STR);
if (res < 0)
printf("Unable to read serial number string\n");
printf("Serial Number String: (%d) %ls", wstr[0], wstr);
printf("\n");

// Read Indexed String 1
wstr[0] = 0x0000;
res = hid_get_indexed_string(handle, 1, wstr, MAX_STR);
if (res < 0)
printf("Unable to read indexed string 1\n");
printf("Indexed String 1: %ls\n", wstr);

// Set the hid_read() function to be non-blocking.
hid_set_nonblocking(handle, 1);

// send and receive 20 reports
for (int r = 0; r < 20; r++) {
// send some dummy data
buf[0] = 0; //Report ID
buf[1] = ((r%10)==8); //Only for 8 and 18
buf[2] = 5*r; //0,5,10,15...
buf[3] = 0;
buf[4] = 0;

res = hid_write(handle, buf, sizeof(buf));
if (res < 0) {
printf("Unable to write()\n");
printf("Error: %ls\n", hid_error(handle));
} else {
printf("Data sent: ");
// Print out the returned buffer.
for (i = 0; i < res; i++) printf("%02hhx ", buf[i]);
}

// Read requested state. hid_read() has been set to be
// non-blocking by the call to hid_set_nonblocking() above.
// This loop demonstrates the non-blocking nature of hid_read().
res = 0;
while (res == 0) {
res = hid_read(handle, buf, sizeof(buf));
if (res == 0)
printf("waiting...\n");
if (res < 0)
printf("Unable to read()\n");
#ifdef WIN32
Sleep(500);
#else
usleep(500*1000);
#endif
}

printf(" Data read: ");
// Print out the returned buffer.
for (i = 0; i < res; i++) printf("%02hhx ", buf[i]);
printf("\n");
}//end 20 reports

//close HID device
hid_close(handle);

/* Free static HIDAPI objects. */
hid_exit();

#ifdef WIN32
system("pause");
#endif

return 0;
}
A program futási eredménye az alábbi ábrán látható.


5. ábra: A módosított hidtest program futtatása

Megjegyzés: A testgui.exe programnál említett kettősség itt is megfigyelhető: A kimenő adatcsomagnál a Report ID is megjelenik, beolvasásnál viszont a HIDAPI "lenyeli" a Report ID-t, a bejövő csomagoknál csak a nyolc adatbájtot kapjuk meg!