Softwarová implementace USB do uC AVR třídy HID

Ondřej Buš, Pavel Paták
xbuson00stud.feec.vutbr.cz, xpatak03stud.feec.vutbr.cz

Obsah:

  1. Úvod
  2. Realizace
  3. Závěr
  4. Literatura

Úvod

U řady elektronických zařízení je běžně požadována komunikace tohoto zařízení s počítačem přes sběrnici USB. Často užívané řešení spočívá v připojení převodníku, typicky FT232, který převádí sběrnici procesoru UART na standard USB. V počítači se pak využívá virtuálního COM portu.

Cílem tohoto projektu bylo vyzkoušet a popsat řešení jinou metodou. Na straně mikrokontroléru nebude použit žádný převodník, celá komunikace přes USB bude řešena softwarově přímo samotným mikrokontrolérem. Na straně počítače bude napsán obslužný program, který bude se zařízením komunikovat s využitím knihoven pro práci s USB třídy HID, nebude tedy použit virtuální COM port. Data, která bude možné posílat oběma směry budou moct být obecného charakteru.

Realizace

Třída HID

Zařízení připojovaná pomocí rozhraní USB jsou dělena do tříd, přičemž komunikace v každé třídě je standardizována. Proto pokud operační systém počítače rozezná zařízení jako příslušníka určité třídy, není již potřeba instalovat žádné ovladače a zařízení může s počítačem přímo komunikovat.

Mezi zařízení třídy HID, neboli Human Interface Devices, patří typicky klávesnice, myš a joystick. Třídu lze ale využít i pro posílání obecných dat s s obecným zařízením. Pro ovládání je ale samozřejmě nutné mít v počítači spuštěnu příslušnou řídící aplikaci. HID není určený pro přenosy velkých objemů dat, spíše pro nepříliš časté odeslání nebo přijetí několika bytů dat. Využití této třidy je tedy především v aplikacích, kde se pomocí počítačového programu ovládá cílové zařízení, například USB generátor apod.

Strana mikrokontroléru

Jak hardwarové zapojení, tak i ovladač USB použitý v mikrokontroléru, vychází z projektu V-USB [1]. Na uvedených stránkách je také uvedeno množství různých ukázkových realizací. Základní vlastnosti V-USB jsou následující:

Zapojení

Schema připojení sběrnice USB k mikrokontroléru vyžaduje minimum externích součástek a je zobrazeno na obrázku 1. V našem projektu jsme použili mikrokontrolér ATmega8, většina pinů je vyvedena na připojovací lišty, aby bylo možné zapojení použít jako univerzální testovací přípravek.

Logo Ăşstavu
Obrázek 1: Zapojení přípravku

Pro samotnou komunikaci s USB jsou potřebné pouze rezistory R1, R2, R3 a zenerovy diody DZ1 a DZ2 a samozřejmě USB konektor. Na stránkách V-USB jsou uvedeny i některá další verze zapojení, všechny však zachovávají přibližně stejný počet součástek.

USB ovladač pro mikrokontrolér

V balíku vusb-20100715.zip, který lze stáhnou v sekci download v [1] jsou potřebné ovladače uloženy ve složce usbdrv. Do našeho projektu si z této složky nakopírujem:

a podle toho, jaký zamýšlíme použít krystal pro oscilátor mikrokontrélu, vybereme soubor usbdrvasmXX.inc. Pro krystal 16 MHz je to:

Je nutné podotknout, že ovladač vyžaduje opravdu krystal zvolené hodnoty, ne keramický rezonátor ani interní oscilátor. Autoři zdůvodňují tento požadavek tím, že v kritických pasážích musí být přesně zachováno časování v přesnosti na strojové cykly procesoru, z těchto důvodů je také část programu psána v assembleru.

V AVR studiu založíme nový projekt a všechny výše uvedené soubory .c, .h a .s do něho vložíme (Add Existing Source Files a Add Existing Header Files). V nastavení AVR studia je nutné definovat taktovací kmitočet. Druhá možnost je doplnit do souboru usbdrvasm.s pod uvedenou výzvu definici taktovacího kmitočtu v kHz:

;----------------------------------------------------------------------------
; Now include the clock rate specific code
;----------------------------------------------------------------------------
 
#define USB_CFG_CLOCK_KHZ 16000
 

To zajistí, že se vloží správný .inc soubor. Dále je nutné nastavit správně údaje v souboru usbconfig.h. Na ukázku uvádíme několik parametrů jako jsou piny, které jsou určeny především tím, kde má daný mikrokontrolér vyvedeno externí přerušení, a vendor ID a device ID, který si uživatel nastaví. Pomocí těchto dvou čísel aplikace v počítači identifikuje naše zařízení. Při komerční výrobě se o vlastní vendor ID musí zažádat. ID, která lze použít zdarma, jsou uvedena v souboru USB-IDs-for-free.txt.

#define USB_CFG_IOPORTNAME      D	//nastaveni portu, kde bude pripojeno USB
#define USB_CFG_DMINUS_BIT      4	//nastaveni pinu
#define USB_CFG_DPLUS_BIT       2
#define USB_CFG_MAX_BUS_POWER           80 //max proudovy odber v mA, pro zajimavost
#define  USB_CFG_VENDOR_ID       0xc0, 0x16	//pro rozeznani naseho zarizeni
#define  USB_CFG_DEVICE_ID 		0xdf, 0x05 //pro rozeznani naseho zarizeni 

#define USB_CFG_VENDOR_NAME     'U', 'R', 'E', 'L'
#define USB_CFG_VENDOR_NAME_LEN 4   // delka vendor name

#define USB_CFG_DEVICE_NAME     'M', 'M', 'I', 'A', '_', 'H', 'I', 'D',
 
#define USB_CFG_DEVICE_NAME_LEN 8  // delka device name
#define USB_CFG_INTERFACE_CLASS     3	//trida HID
#define USB_CFG_HID_REPORT_DESCRIPTOR_LENGTH   38 //delka deskriptoru

Ve složce examples je uvedeno několik příkladů hlavního programu. Pro nás je zajímavý především příklad nazvaný hid-data, který popisuje program chovající se jako "fleška". Z počítače se pošlou do zařízení data, která jsou uložena do EEPROM. Zpětně je lze přečíst. Z tohoto programu lze vypozorovat strukturu, kterou budem do používat i my. Funkce, které jsme použili pak vychází z [8].

První věc, kterou zařízení pošle počítači po svém připojení, je tzv. deskriptor. Je to soubor údajů, které dané zařízení popisují a říkají počítači, co má od zařízení očekávat. Deskriptor je uveden přímo v hlavním programu jako globální proměnná. Jeho délka musí odpovídat délce zapsané v souboru usbconfig.h, jak bylo uvedeno výše. Použili jsme následující deskriptor:

PROGMEM char usbHidReportDescriptor[38] = {    /* USB report descriptor */
	0x06, 0x00, 0xff,              // USAGE_PAGE (Generic Desktop) 
    0x09, 0x01,                    // USAGE (Vendor Usage 1) 
    0xa1, 0x01,                    // COLLECTION (Application) 
    0x19, 0x01,                    //   USAGE_MINIMUM (0), minimalni pocet bajtu, ktere lze prenest
    0x29, 0x03,                    //   USAGE_MAXIMUM (3), maximalni pocet bajtu, ktere lze prenest
    0x15, 0x00,                    //   LOGICAL_MINIMUM (0), min. hodnota bajtu
    0x26, 0xff, 0x00,              //   LOGICAL_MAXIMUM (255), max hodnota bajtu 
    0x75, 0x08,                    //   REPORT_SIZE (8) 
    0x95, 0x03,                    //   REPORT_COUNT (3), pocet bajtu, co se naraz posle
 
    0x81, 0x02,                    //   INPUT (Data,Var,Abs) 
    0x19, 0x01,                    //   USAGE_MINIMUM (0) 
    0x29, 0x03,                    //   USAGE_MAXIMUM (3) 
    0x15, 0x00,                    //   LOGICAL_MINIMUM (0) 
    0x26, 0xff, 0x00,              //   LOGICAL_MAXIMUM (255)
    0x75, 0x08,                    //   REPORT_SIZE (8) 
    0x95, 0x03,                    //   REPORT_COUNT (3)
    0x91, 0x02,                    //   OUTPUT (Data,Var,Abs) 
    0xc0                           // END_COLLECTION
};

Dále se program skládá z několika významných funkcí. Jsou to:

Začněme od konce. Funkce usbPoll se musí volat stále dokola, nejvíce po 50 ms. Interval, v kterém bude operační systém čekat volání této funkce, se nastavuje v usbconfig.h. Funkce musí být volána v kratších intervalech, než zde nastavíme, jinak bude docházet k odpojování zařízení. Periodické volání funkce jsme vyřešili jejím umístěním do přerušení od časovače.

Funkce usbFunctionSetup je volána vždy, když počítač zahajuje komunikaci. Rozhoduje se zde, zda se zavolá funkce usbFunctionWrite.

Zápis dat ve směru počítač -> zařízení obstarává funkce usbFunctionWrite.

uchar   usbFunctionWrite(uchar *data, uchar len)
{
 	uchar i;
    if(len > bytesRemaining)                
    	len = bytesRemaining;               // pokud je poslano vic bajtu, nez muzem prijmout
    bytesRemaining -= len;
    for(i = 0; i < len; i++)
        prijata_data[currentPosition++] = data[i];
    return bytesRemaining == 0;             // vrati 1, pokud jsou prijata vsechna data
}

Tato funkce je volána vždy, když počítač pošle data. Jejími vstupními parametry jsou ukazatel na pole, kam se mají data uložit a počet bajtů, která se posílají. My přijatá data ukládáme do pole prijata_data.

V ukázkovém programu hid-data se pro čtení dat ze zařízení, tedy komunikaci směrem zařízení -> počítač, používá funkce usbFunctionRead, která vypadá podobně jako funkce pro čtení. S využitím této funkce byly však problémy. Proto bylo zvoleno jiné řešení spočívající ve využití funkce usbSetInterrupt. Funkci usbFunctionRead pak můžeme v souboru usbconfig.h deaktivovat.

#define USB_CFG_IMPLEMENT_FN_READ       0
Volání funkce je umístěno do hlavní smyčky ve funkci main.

if(usbInterruptIsReady()){                // je volno na sbernici?    		
  uchar *p = odeslana_data;
  uchar len = sizeof(odeslana_data);      		
    if(len > 0)                           // jsou-li data, posleme je
       usbSetInterrupt(p, len);
		}

Nejdříve se zjistí, zda je data možné poslat, zda už byla dokončena předchozí komunikace. Data k odeslání jsou uložena v poli odeslana_data. Zjistíme jeho délku, kterou uložíme do len, a spolu s ukazatelem na pole odeslana_data ji předáme funkci usbSetInterrupt. Tato funkce bude volána periodicky. Pokud si však počítač data nevyžádá, nebudou tyto data přijata (a pravděpodobně ani odeslána). Nepodařilo se vyřešit, aby byla funkce volána jen tehdy, požaduje-li počítač data. Nabízí se umístění funkce do obsluhy přerušení přetečení časovače. Takový program však nepracuje, nejspíš proto, že pro svoji funkci potřebuje obsluhu externího přerušení.

Teoretická vložka: USB komunikuje prostřednictvím tzv. endpointů. Můžeme si je představit jako logické konce sběrnice. Data můžeme po USB odeslat, pokud je ale budeme čekat na jiném endpointu, než na který byla odeslána, nic nepřijmeme. Komunikace počítač -> zařízení pracuje pomocí řídícího endpointu 0, opačná komunikace pomocí interrupt endpointu. Zájemce o bližší rozbor problematiky USB odkazujeme na odbornou literaturu. Pokud chce čtenář číst něco čtivějšího, doporučujeme například bakalářskou práci [5], v které jsme našli spousty užitečných poznatků.

Popsaný program opravdu lze zkompilovat a pokud se při připojení zařízení k počítači objeví bublina, že bylo rozpoznáno zařízení třídy HID a že ovladače jsou nainstalovány, je všechno v pořádku. Pokud se neobjeví, všechno v pořádku není.

Strana počítače - ovládací aplikace

Ovládací aplikace pro počítač byla vytvořena hned ve dvou vývojových prostředích. To nebylo dáno pílí autorů ale tím, že se objevovalo nepřeberné množství problémů, a proto jsme zkoušeli různá vývojová prostředí.

C++ Builder

Jako základ pro vytvoření aplikace v C++ Builderu 6 jsme použili vzor ze cvičení v předmětu MPOA. Abychom mohli přistupovat k USB, je nutné si stáhnout knihovny z volně dostupného projektu Jedi, který je ke stažení na [7]. Postup pro instalaci je následující:

Po spuštění C++ Buildru se v záložkách funkcí objeví JvHidDeviceController, který se vloží do projektu jeho přetažením do formuláře form. Pokud se pokusíme takový program zkompilovat, hlásí kompilátor chybu. Pro její odstranění je potřeba zakomentovat v souboru Program Files\CBuilder6\include\vcl\setupApi.h následující řádky začínající na slovo static:

//-- var, const, procedure ---------------------------------------------------
static const Shortint SPFILENOTIFY_STARTREGISTRATION = 0x19;
static const Shortint SPFILENOTIFY_ENDREGISTRATION = 0x20;

JvHidDeviceController volá sám různé funkce, které jsou potřeba pro obsluhu komunikace. Při každé změně na sběrnici USB, tedy připojení nebo odpojení jakéhokoli zařízení, je volána funkce HIDCtlDeviceChange. V této funkci je volána funkce HIDCtlEnumerate. Tu se testuje, zda se jedná o naše zařízení, které je definováno pomocí PID a VID. Pokud je zařízení rozpoznáno, je s ním zahájena komunikace. Ve funkci HIDCtlEnumerate je podstatné toto:

if ((HidDev->Attributes.VendorID == 0x16c0) && (HidDev->Attributes.ProductID == 0x05DF))
	{
         ...
 
	  	  //checkout
	  HIDCtl->CheckOutByIndex(ActiveDevice, Idx);
	}

Pro správnou funkci je ještě nutné doplnit do hlavičkového souboru main.h tento řádek:

TJvHidDevice *ActiveDevice;

Pro odeslání dat do zařízení je klíčový následující fragment zdrojového kódu:

buff[0] = 0;         // ReportID = 0
  buff[1] = StrToInt(Edit1->Text);
  buff[2] = StrToInt(Edit8->Text);
  buff[3] = StrToInt(Edit9->Text);
  ActiveDevice->WriteFile(buff, 4 , Written);0

Je důležité si uvědomit, že i když odesíláme 3 bajty (jak bylo definováno v deskriptoru), musí mít buffer velikost 4. Nultá pozice pak musí být nulová, čímž definujeme využití řídícího endpointu. Buffer nesmí být ani větší, ani menší, zkrátka musí mít velikost 4 bajty. Zavolání funkce WriteFile dojde k odeslání dat.

Pro čtení dat ze zařízení exustují dvě možnosti. Buď číst data pořád, nebo na vyžádání.

void __fastcall TForm1::OnDeviceData(TJvHidDevice *HidDev,
      BYTE ReportID, const Pointer Data, WORD Size)
{
  //tato funkce prijima data
  //cekame ReportID = 0
  //Size = 3 (byte)
  unsigned char rep[3];
 
  rep[0]=((unsigned char *) Data)[0];
  rep[1]=((unsigned char *) Data)[1];
  rep[2]=((unsigned char *) Data)[2];
}

První ukázka je část kódu zajišťující neustále čtení dat ze zařízení. Proměnná data je ukazatel na místo, kde jsou přijatá data uložena. My je následně ukládáme do pole rep.

Druhá možnost, tedy příjem na vyžádání, je realizován voláním jednoduché funkce, například po stisku tlačítka:

ActiveDevice->ReadFile(buff, 4, Read);

Přijatá data jsou uložena do proměnné buff. Povšimněme si, že v tomto případě musí mít buff velikost 4 prvky, přičemž naše data jsou uložena až od 1. prvku. buff[0] je report ID, které pro nás nemá význam [5].

Visual Studio

Pro přístup k USB ve Visual Studiu se používají funkce z knihovny DDK. Knihovna je dosti objemná, proto existuje i její chudší verze MinGW. Tyto knihovny se nainstalují a dále se s nimi pracuje. Naše zkušenost však s nimi je jen špatná. Různé verze DDK, MinGW, Visual Studia a operačních systémů hlásily vždy jiné chyby při kompilaci. Proto jsme od použití těchto knihoven upustili.

Využili jsme kostru programu, která je součástí [5], původem od Microchipu, která umí komunikovat s USB aniž by bylo zapotřebí instalovat jakékoli knihovny. Je pouze nutné mít na počítači instalováno Microsoft .NET Framework 2.0, což je ale téměř běžná výbava.

Všechny pro nás zajímavé funkce jsou uloženy v souboru Form1.h. Nejdříve je nutné definovat naše VID a PID, aby aplikace rozeznala naše zařízení. To se provede hned pod includováním knihoven, formát zápisu je popsán v komentářích.

#define MY_DEVICE_ID  "Vid_16c0&Pid_05DF"

Dále následuje velká spousta řádků kódu, který nijak neupravujeme. Automaticky generovaný kód při vytváření okna aplikace začíná řádkem:

#pragma region Windows Form Designer generated code

Funkce

private: System::Void FormUpdateTimer_Tick(..)

je volána vždy, když dojde ke změne týkající se našeho programu. Tedy připojení nebo odpojení našeho zařízení, zatržení nebo odtržení boxu v aplikaci atd. Proto zde doplníme příkazy, které budou tyto události obsluhovat. My zde v případě zatržení boxu nastavíme proměnné led1 a led2 na ptřičné hodnoty, které vyhodnotí procesor. Přijatá data vypisujeme do TextBoxů.

led1 = 0;
		led2 = 0;
		if (LED1->Checked) led1 = 15;
		if (LED2->Checked) led2 = 77;
 
		// P&#345;ijatá data
		this->textBox1->Text = prijem[0].ToString();
		this->textBox2->Text = prijem[1].ToString();
		this->textBox3->Text = prijem[2].ToString();

Odeslání a příjem dat do/ze zařízení je prováděno v nekonečné smyčce. Pro odeslání i příjem musíme mít opět pole větší o jeden prvek:

if(AttachedState == TRUE)	//Nepokouset se cist/zapisovat, dokud není zarizeni pripojeno a pripraveno
	{
		OUTBuffer[0] = 0;		// Prvni byte je "Report ID" a neprenasi se pres USB, vzdy nastavit 0
		OUTBuffer[1] = led1;	// Zapsat pozadovany stav LED diod
		OUTBuffer[2] = led2;	// Zapsat pozadovany stav LED diod
		OUTBuffer[3] = 0;	// Zapsat pozadovany stav LED diod
 
		WriteFile(WriteHandleToUSBDevice, &OUTBuffer, 4, &BytesWritten, 0); 
 
		INBuffer[0] = 0;	//INBuffer[0] je Report ID, nestarame se o naj
		ReadFile(ReadHandleToUSBDevice, &INBuffer, 4, &BytesRead, 0); 
		prijem[0] = INBuffer[1];
		prijem[1] = INBuffer[2];
		prijem[2] = INBuffer[3];
	}

Porovnání použitých vývovojových prostředí

Aplikace v C++ Builderu je pro programátora jednoznačně přehlednější a pracuje se s ní lépe. Vše potřebné zajišťuje JvHidDeviceController, programátor jen doplní do funkcí svoje příkazy. Bohužel vývoj prostředí Buildru je podle všeho již zastaven a s tímto vývojovým prostředím se budeme pravděpodobně setkávat stále méně.

Program napsaný ve Visual Studiu je méně přehledný. Je to především proto, že v kódu je velké množství příkazů a funkcí oblsuhujících komunikaci USB, které jsou součástí převzaté šablony. Je pak na programátorovi, jak dokáže udělat kód přehledný, aby se vlastní funkce neztratily ve funkcích předlohy.

Ukázka realizovaného pokusu

Pro vyzkoušení komunikace byl vytvořen popsaný program pro mikrokontrolér a ovládací aplikace pro počítač. Programem v počítači lze ovládat rozsvěcení dvou diod na přípravku, a naopak v aplikaci v počítači se vypíšou hodnoty, které pošle přes USB přípravek. Tyto hodnoty jsou pevně uloženy v jeho paměti. Na obrázku 2 je ukázána aplikace vytvořená v C++ Buildru. V okně OnDeviceData jsou hodnoty, které čte program ze zařízení opakovaně, v okýncích pod ním by se zobrazily přijatá (stejná) po stisknutí tlačítka Cist.

Logo Ăşstavu
Obrázek 2: Program vytvořený v C++ Buildru
Logo Ăşstavu
Obrázek 3: Program vytvořený ve Visual Studiu

Na obrázku 4 je nakonec uvedena fotografie přípravku. Povšiměte si profesionálně přidané LED diody.

Logo Ăşstavu
Obrázek 3: Realizovaný přípravek

Závěr

V projektu bylo odzkoušeno a realizováno propojení počítače a zařízení s mikrokontrélerem řady AVR od firmy Atmel přes sběrnici USB. Cílem nebylo vytvořit vlastní ovladač USB, ale především komunikaci zprovoznit, proto byly využity různé hotové projekty, jejichž vhodnou kombinací bylo dosaženo kýženého výsledku.

Pokus bude dodržen postup, uvedený na této stránce, nemělo by zprovoznění komunikace činit problémy. Funkce byla úspěšně vyzkoušena na operančích systémech Windows XP, Windows Vista a 64 bitových Windows 7. Velkou výhodu spatřujeme především ve využití třídy HID, kdy není třeba na počítači nic instalovat a zařízení funguje takřka hned po připojení. Dalším plusem tohoto řešení je bezesporu jednoduchost a nízká cena, kdy na straně zařízení není kromě několika pasivních součástek nic připojovat. Záporem je samozřejmě ta skutečnost, že procesor část svého výkonu spotřebuje na obsluhu USB, USB potřebuje externí přerušení a je nutné pravidelně volat funkci usbPoll. Je třeba si také uvědomit, že třída HID není určena pro přenosy velkých objemů dat, v našem případě přenášíme naráz jen 3 B. To však naprosto postačuje pro aplikace, kde něco nastavujeme, něco ovládáme, nebo kdy procesor například sbírá data a průběžně je posílá počítači.

Veškeré zdrojové kódy, schéma a návrh desky plošného spoje, je možné stáhnout zde.

Literatura

[1] OBJECTIVE DEVELOPMENT Virtual USB port for AVR microcontrollers, dostupné na WWW: http://www.obdev.at/products/vusb/index.html
[2] INTEL CORP HID Descriptor Tool, dostupné na WWW: http://www.usb.org/developers/hidpage/dt2_4.zip
[3] PAVLÍČEK P. Regulovatelný zdroj napájený a řízený pomocí USB, Fakulta elektrotechniky a komunikačních technologií VUT, 2010
[4] MICROSOFT. Windows Driver Kit Version 7.1.0 , dostupné na WWW: http://www.microsoft.com/downloads/en/details.aspx?displaylang=en&FamilyID=36a2630f-5d56-43b5-b996-7633f2ec14ff
[5] OLIVÍK L. Rotační ovladač k počítači, bakalářská práce, Fakulta elektrotechniky a komunikačních technologií VUT, 2010, dostupné z WWW: https://www.vutbr.cz/studium/zaverecne-prace?action=detail&zp_id=30991&fid=&rok=&typ=&jazyk=&text=oliv%C3%ADk&hl_klic_slova=0&hl_abstrakt=0&hl_nazev=0&hl_autor=1&str=1
[6] OBJECTIVE DEVELOPMENT. Automator , dostupné na WWW:http://www.obdev.at/products/vusb/automator.html
[7] PROJECT JEDI PORTAL, dostupné na WWW: http://www.delphi-jedi.org/
[8] A Firmware-Only USB Driver for the AVR, dostupné na WWW: http://vusb.wikidot.com/driver-api