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 hozzáadni a projekthez. Ehhez a Makefileban ezt írjuk:
USER_LIBS := $(GCC4MBED_DIR)/external/mbed-os/features/unsupported/USBDevice

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 (1: lenyomva, 0: 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"

USBHID hid(8, 8);
HID_REPORT send_report;  // Buffer for transmission to Host (MCU -> PC)
HID_REPORT recv_report;  // Buffer for data received from Host (PC -> MCU)

// Hardware Pin Configurations for FRDM-KL25Z Board
DigitalOut LED_1(LED1);            // Onboard Red LED
PwmOut     LED_2(LED2);            // Onboard Green LED
DigitalIn  SW1(D3, PullUp);        // Push button with internal pull-up
AnalogIn   adc(A0);                // Analog Input on channel A0

int main(void)
{
    // Set the report length explicitly (8 bytes payload)
    send_report.length = 8;
    LED_1 = 1;                     // Turn OFF LED_1 initially (Active-LOW hardware driver)
    LED_2.period_ms(20);           // Configure PWM base period to 20ms (50 Hz frame frequency)

    while (1) {
        uint16_t raw = adc.read_u16();                // Read 16-bit ADC data from pin A0
        for (uint8_t i = 0; i < send_report.length; i++) {
            send_report.data[i] = 0x00;
        }

        send_report.data[0] = !SW1.read();            // Button state: 0: released, 1: pressed
        send_report.data[3] = (uint8_t)(raw >> 8);    // Most Significant Byte of ADC
        send_report.data[4] = (uint8_t)(raw & 0xFF);  // Least Significant Byte of ADC
        hid.send(&send_report);                       // Transmit the report (Blocking Call)
        if (hid.readNB(&recv_report)) {               // Non-blocking poll on USB OUT Endpoint.
            LED_1 = !recv_report.data[0];             // Digital LED control (1: On, 0: off)
            LED_2.write(1.0f - (recv_report.data[1] * 0.01f)); // PWM LED Control (0 to 100 %)
        }
    }
}
        

A 13_USBHID_demo projekt Makefile állománya:


        PROJECT         := 13_USBHID_demo
        DEVICES         := KL25Z
        GCC4MBED_DIR    := ../..
        GCC4MBED_TYPE   := Release
        USER_LIBS := $(GCC4MBED_DIR)/external/mbed-os/features/unsupported/USBDevice
        MBED_OS_ENABLE  := 0
        NEWLIB_NANO     := 1
        NO_FLOAT_SCANF  := 1
        NO_FLOAT_PRINTF := 1

        include $(GCC4MBED_DIR)/build/gcc4mbed.mk
A program elején be kell csatolnunk az USBHID.h  fejléc állományát. 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, tehát csak ezeket a paramétereket kell átadni a konstruktor függvénynek. 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

Saját GUI alakalmazás készítése a 13_USBHID_demo projekthez

A tananyag eredeti megírása után 6 évvel elérkezettnek láttuk az időt, hogy a mikrokontrolleres oldal mellé egy teljesen saját, nyílt forráskódú PC-s célszoftvert is letegyünk az asztalra. A választás a Python nyelvre esett: az USB HID csomagok alacsony szintű kezelését a pywinusb könyvtárra bíztuk, míg a grafikus kezelőfelületet (GUI) a Qt-alapú PySide6 segítségével építettük fel.

Hogy a PySide6 elrendezési képességeit és rugalmasságát élesben is próbára tegyük, a felület vizuális elrendezését szándékosan a Cypress tesztprogram mintájára alakítottuk ki. Így a felhasználó pontosan ugyanazokat a vezérlőelemeket (gomb állapot, PWM csúszka, ADC kijelzés) találja meg, de már egy modern, platformfüggetlen Python környezetben.

Az előzményekről és a technikai részletekről a 2022. őszi előadásokban esett szó: A 2022-ben közzétett HIDTest_app program azonban még két rejtett hibát hordozott:

A problémákat csak 2026-ban sikerült végleg felszámolni egy szálbiztos Qt Signal architektúrával. A letölthető h2022_05.zip melléklet már ezt a kijavított HIDTest_app projektet tartalmazza.

A HIDTest_app alkalmazás futtatásához az alábbi szoftverkörnyezet szükséges: Python 3.10 (vagy újabb 64 bites kiadás) pywinusb 0.4.2, valamint PySide6 6.3.2


3. ábra: A saját GUI alkalmazás futtatása

Általános (generikus) tesztprogramok a HID kapcsolatok vizsgálatára

Az alábbiakban bemutatunk néhány további lehetőséget, amellyel gyorsan és egyszerűen kipróbálhatjuk mbed USB HID mikrovezérlő programjainkat. Ezek az úgynevezett generikus (általános) HID tesztprogramok nem ismerik az alkalmazásspecifikus protokollt, csupán „fatengelyes”, nyers, bájtszintű hozzáférést biztosítanak bármely USB HID eszközhöz, ami felbecsülhetetlen segítség, ha a firmware legelső lépéseit kell ellenőrizni és nincs kéznél egy előre megírt grafikus kezelői felület.

A legelterjedtebb általános teszteszközök:

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!