Příklad toho, co není dobře dokumentováno, ale může být užitečné - gadgetfs.
Za dlouhých zimních večerů jsem chtěl připojit k ARM LPC1343 po SPI rozhraní SDHC kartu (kromě jiného), přičemž ARM by se měl chovat jako USB mass-storage zařízení. Karta je dnes poměrně levná a mít ve svém zařízení data takhle jednoduše přístupná se mi docela líbilo. Na webu lze najít skoro vše potřebné, zdálo se tedy, že stačí to pospojovat dohromady a bude po problému. Ale nic není tak jednoduché, jak se zdá na první pohled. Čtení z paměťové karty není problém, ten nastane při zápisu. PC začne valit po USB data a karta nestíhá zapisovat. Co s tím ? A tak nezbude než začít ladit. A hledat vhodné nástroje. Problém jsem nakonec vyřešil, nástroje jsem našel a chtěl bych se tedy podělit o zkušenost, protože vyzkoumat jak to vlastně funguje zabralo dost práce. Třeba jí někdo využije.
Když člověk vyvíjí zařízení s mikroprocesorem, které by mělo komunikovat po USB, většinou brzy narazí na problém. Na webu se sice najde spousta docela dobře napsaných příkladů, nicméně USB je dost složité, chybička se může vloudit, nehledě na to, že příklady jsou někdy hodně zjednodušené (byť i funkční). A protože dnes obsahují USB řadič i velmi levné mikrokontroléry je zřejmě na čase opustit osvědčená řešení specializovaných firem (jako je např. FTDI) a pustit se do práce. Ona je to vlastně práce dvojí -
A obojí není zase taková legrace. USB rozhraní má za sebou léta neustálého vývoje a podobně jako např. Ethernet (a ostatně veškerá technika) se rozrostlo do podoby, kdy je už takřka nemožné postihnout do detailu veškeré dění. Ono to ani není nutné, prostě některé věci budeme brát jako dané a zkusíme pro jejich realizaci použít nějaký již hotový nástroj. Trochu v tom samosebou budeme plavat, protože těch nástrojů je spousta a není tedy snadné vybrat ty správné. Pro bod 2. - ovladač je situace o něco jednodušší. Existují pevně definované tzv. třídy zařízení (class), pro které je ovladač již součástí operačního systému. Je to ovšem velice relativní - množství práce co ušetříme na ovladači na druhé straně (firmware) zase více než přibude, protože je pak nutné přesně se držet standardu. A protože tuto situaci vývojáři brzy pochopili, vznikla knihovna libusb. Má docela slušné API, které je prakticky stejné na platformách Windows i Linux a hlavně je docela slušně dokumentované, včetně příkladů. Pokud budeme tedy potřebovat jen "rouru na data", což je obvyklý případ, je toto docela dobrá volba. Na straně firmware je situace jiná. Každý mikrokontroler má řadič USB trochu jiný. Při procházení webu lze zjistit, že snad nejvíce je používán (tedy alespoň co se týká NXP ARM) USB stack vyvinutý pro ARM lidmi z fy. Keil. Má vrstevnatou strukturu, kterou velmi striktně dodržuje. Ve spodní vrstvě usbhw.c je vše, co je potřeba pro spolupráci s konkrétním hardware. To je vlastně to jediné, co se mění od jednoho mikrořadiče k druhému. Je dobré vědět, zda jsou funkce vyšších vrstev volány z přerušení od události na USB portu nebo ve smyčce. Většina příkladů volá obsluhu událostí z přerušení, což je samosebou lepší, není to však pevně dané pravidlo. Zde je to právě z přerušení. Sice to na první pohled vypadá složitě, ale tuto spodní vrstvu lze považovat pro daný hardware za danou a není třeba se v ní příliš vrtat. Jediné, co je třeba o USB vědět je, že používá pro data jakési roury, v literatuře nazývané endpoint, což evokuje představu, že jde o zakončení této roury. Jenže roura má dva konce a zde je stejné označení a hlavně stejná adresa pro konec ve firmware i v hostiteli. Vše je nahlíženo z hlediska hostitele, protože hostitel je nadřízen všem zařízením (master), ostatní jsou v podřízeném postavení (slave), každý přenos vždy zahajuje hostitel. Roury jsou jednosměrné, můžeme jich vytvořit více. Adresy si volíme sami, specifikace říká, že adresa endpointu IN má nastaven bit 0x80. Každý endpoint má jinou adresu. Trochu matoucí je adresace endpointů u řadičů NXP ARM, kde se používá pár endpointů, jeden IN a druhý OUT a které mají defakto stejnou adresu - liší se jen tím bitem 0x80 (tedy příznakem IN). Nicméně i to funguje. A aby se to nepletlo, každé zařízení musí mít endpoint s adresou 0, který je na rozdíl od ostatních obousměrný (tedy používá i adresu 0x80 jako IN). Po tom probíhají řídící informace. Obsluha tohoto endpointu je víceméně standardní (jsou samosebou výjimky) a někdy je součásti spodní vrstvy firmware. Zde v usbcore.c. Náročnější částí USB je potom popisovač (descriptor) funkce celého zařízení, což je vlastně hierarchický systém jednotlivých deskriptorů, končící u jednotlivých endpointů. Hierarchie Device->Configuration->Interface->Endpoint je popsána ve specifikaci a vzhledem k množství doplňujících informací není zrovna jednoduchá. Je ale dobré ji pochopit, protože hostitel se baví se zařízením právě pomocí těchto deskriptorů. Tolik lehký úvod a můžeme se pustit do práce.
Takže začneme s tím, že přeložíme nějaký příklad z webu pro daný kontroler, nasypeme do něj firmware, připojíme k USB - a většinou se něco děje. Pokud se děje to, co chceme, pak netřeba nic podnikat, jenže asi budeme něco přidávat či modifikovat a pak je třeba zjistit, zda jsou data správná či nikoli. Takže prvním nástrojem, který budeme potřebovat bude asi "čuchač" (sniffer), který ukáže data na sběrnici USB. Oblíbený je wireshark, já osobně dělám na Linuxu, pro ethernet jej používám, ale plugin pro USB má docela problémy. A než nespolehlivý nástroj, to raději žádný. Existuje však řešení - usbmon. Tím lze dokonce proniknout hlouběji do podstaty problému. Základem je modul Linuxového jádra usbmon. Měl by jít aktivovat takto:
#!/bin/bash su mount -t debugfs none_debugs /sys/kernel/debug # pokud není zakompilováno přímo v jádře modprobe usbmon exit
V některých distribucích (Ubuntu) je již aktivován. Metoda vytvoří v adresáři /dev zařízení usbmonx, kde x je číslo sběrnice (co to vlastně je, není podstatné), které zjistíme příkazem lsusb. Viz manuál lsusb. A z tohoto zařízení lze číst pomocí ioctl, co se na sběrnici děje. Formát dat je dokumentován (kernel/Documentation) a jednoduchý prográmek s filtrací dat je možné spáchat za pár hodin práce. Vlastně se zde s výhodou dá použít kód, který zpracovává data v mikrokontroleru (alespoň jeho podstatná část), přičemž lze přidat ladící výpisy. Pro ilustraci kódu jednoduchý čuchač - usbmon. Další možnost je číst USB data přímo ze souboru /sys/kernel/debug/usb/usbmon/n*. Je to také popsáno v dokumentaci.
Konečně se dostáváme k jádru tohoto projektu. V Linuxovém kernelu je hluboce zakopána část nazvaná gadget. Tento název nic neříká o tom, k čemu že by to mohlo být dobré. Ani v dokumentaci se toho moc nedozvíme. A pokud se dozvíme, pak je to ovladač USB hardware v embedded zařízení, která se mají chovat jako device, tedy věc, která by se problému mikrokontroleru bez Linuxu jakoby netýkala. Nicméně použít se dá a to dokonce s velkou výhodou. Na PC obvykle nemáme nic, co by se podobalo device-side řadiči, který máme v mikrokontroleru. Existuje však modul dummy_hcd, který tento řadič dokáže v PC korektně emulovat. Sice k tomu nejdou připojit dráty, ale jde k tomu připojit další software - jaderný modul a ten dokáže emulovat třeba mass-storage nebo cdc class. To si lze vyzkoušet - zavedeme do jádra tento modul, potom modul g_file_storage s parametrem file=soubor.img, kde soubor.img je buď obraz reálného filesystému nebo to může být i prázdný soubor (několik MB velký). A ejhle - v systému se objeví další disk /dev/sdx - a s tím je možné pracovat jako s normálním harddiskem. Lze ho rozdělit fdiskem, vytvořit na něm filesystém a nakopírovat na něj soubory. Všechna data jsou pak schována v tom soubor.img. To sice vypadá jen jako zajímavá hračka, ale ten gadget poskytuje i driver pro vývoj device-side ovladačů v user-space prostoru. Tedy jako obyčejný program. Protože když se koukneme na zdrojáky toho g_file_storage asi se z toho moc nedozvíme. To vše je velmi provázané s Linuxovým jádrem a pak vyvíjet software pro kernel je trochu o ústa a hlavně nepohodlné. V praxi jsem totiž zjistil, že pokud píšu firmware v C a mám něco složitějsího, pak je lépe si to napsat jako normální program, odladit a to pak portovat na mikroprocesor. Většinou to zabere mnohem míň času, než to ladit přímo v mikroprocesoru. A je neuvěřitelné, že to jde i pro to USB. Použijeme tedy modul pro ladění v user-space (gadgetfs.ko):
#!/bin/bash # Příkazy je nutné provést jako superuživatel : su mkdir /dev/gadget # modprobe dummy_hcd -> toto není nutné, provede se de fakto následujícím příkazem, # ilustruje však použití řadiče HCD, zde je to testovací, tedy emulovaný řadič. # práva je možné nastavit zde, je to tak lepší modprobe gadgetfs default_perm=0666 # filesystem je nutné namountovat (což je sice logické, nikoli však dobře dokumentované) mount -t gadgetfs none /dev/gadget # ukončení su - vlastní testovací software může běžet pod běžným uživatelem, exit # oprávnění je už nastaveno
Pozn. Distribuční verze gadgetfs je kompilována jako DUALSPEED. Pokud emulujeme full-speed zařízení, vznikne problém. Proto je nutné modul kompilovat s parametrem CONFIG_USB_GADGET_DUALSPEED=n. Pozor v menuconfig jádra pro to není volba, protože je to vlastně vlastnost hardware (tedy dummy_hcd a ten je skutečně DUALSPEED), musí se to udělat ručně editací .config. Protože je to trochu zmatené, možná bude lepší modul gadgetfs.ko zkompilovat extra ze zdrojáků, které jsou přiloženy k tomuto příkladu v adresáři ./usr/gadgetfs. V nich je tato vlastnost již postižena. V kernel 3.x.x (a možná i dřív) je asi jinak pojatý scheduler (plánovač), nutno aplikovat 3.1.1.patch.
Tohle tedy nikde moc popsané není, vypadá to složitě a hlavně není úplně jasné k čemu to vlastně je. A přitom je to nasnadě: Jak bylo uvedeno výše, vrstva firmware, která se stará přímo o hardware je poměrně dobře oddělena od všeho ostatního. Je tedy možné napsat (a poměrně jednoduše) něco podobného pro tento gadget místo pro mikrokontroler. A to se bude chovat stejně. Takže vyšší vrstvy firmware je možné začlenit přímo do tohoto programu a ladit je přímo v PC. Zdrojový text těchto vyšších vrstev v C by měl být bez problémů přenositelný. Tedy když si dáme pozor na šířku slova a Indiány - v těch USB protokolech jsou struktury, které jsou na tom dost závislé. Můžeme si libovolně přidávat ladící výpisy, což je u mikrokontroleru poměrně problematické a hlavně odpadne to otravné flashování po každé změně a připojování konektorů. A přitom v PC můžeme data normálně pozorovat pomocí usbmon, testovat přenosy pomocí libusb nebo sledovat chování standardních driverů (třeba právě u tohoto mass-storage, který byl náhodně zvolen jako příklad). Bližší popis funkce emulátoru je popsána v dokumentaci k usbem.c. Pro překlad firmware je potřeba gnu toolset pro ARM (gcc, binutils), pro překlad software jen standardní nástroje z Linuxu. Nástroj pro flašování LPC1343 po USB, které působí v Linuxu problémy jsem přidal do tohoto projektu do adresáře ./usr/loader. Funguje bez problémů.
Mass storage class byl zvolen jako demo. Vyzkoušeno to bylo i nad kompozitním zařízením podle AN11018, které obsahuje kromě tohoto MSC ještě CDC class, tedy virtuální COM port. Tahle legrace má 5 aktivních endpointů (kromě EP0) a běží to také. V zdrojovém kódu emulátoru přitom nebylo nutné dělat žádné změny. Takže k experimentům je cesta otevřená.
Jak software, tak firmware se chová v Linuxu stejně, je to prostě malý USB disk. Od sebe se liší vlastně jen v jednom souboru - ve fw je usbhw.c, v sw usbem.c. Ostatní (vyjma inicializace chipu, pochopitelně) je stejné. Takhle pak vypadá výpis lsusb -v (deskriptory):
Bus 006 Device 034: ID 1fc9:0003 Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 2.00 bDeviceClass 0 (Defined at Interface level) bDeviceSubClass 0 bDeviceProtocol 0 bMaxPacketSize0 64 idVendor 0x1fc9 idProduct 0x0003 bcdDevice 1.00 iManufacturer 1 NXP SEMICOND iProduct 2 NXP LPC13xx Memory iSerial 3 0001A0000000 bNumConfigurations 1 Configuration Descriptor: bLength 9 bDescriptorType 2 wTotalLength 32 bNumInterfaces 1 bConfigurationValue 1 iConfiguration 0 bmAttributes 0x80 (Bus Powered) MaxPower 100mA Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 0 bAlternateSetting 0 bNumEndpoints 2 bInterfaceClass 8 Mass Storage bInterfaceSubClass 6 SCSI bInterfaceProtocol 80 Bulk (Zip) iInterface 4 Memory Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x82 EP 2 IN bmAttributes 2 Transfer Type Bulk Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 0 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x02 EP 2 OUT bmAttributes 2 Transfer Type Bulk Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 0 Device Status: 0x0000 (Bus Powered)
Vidíte, že jsem u původního příkladu ze stránek NXP celkem nic neměnil. Dmesg pak generuje:
[196629.744438] scsi60 : SCSI emulation for USB Mass Storage devices [196629.744657] usb-storage: device found at 34 [196629.744661] usb-storage: waiting for device to settle before scanning [196634.760092] usb-storage: device scan complete [196634.790123] scsi 60:0:0:0: Direct-Access Keil LPC134x Disk 1.0 PQ: 0 ANSI: 0 CCS [196634.793195] sd 60:0:0:0: Attached scsi generic sg3 type 0 [196634.842577] sd 60:0:0:0: [sdc] 12 512-byte logical blocks: (6.14 kB/6.00 KiB) [196634.872571] sd 60:0:0:0: [sdc] Write Protect is on [196634.872579] sd 60:0:0:0: [sdc] Mode Sense: 03 00 00 00 [196634.872583] sd 60:0:0:0: [sdc] Assuming drive cache: write through [196635.022564] sd 60:0:0:0: [sdc] Assuming drive cache: write through [196635.022575] sdc: [196635.572571] sd 60:0:0:0: [sdc] Assuming drive cache: write through [196635.572582] sd 60:0:0:0: [sdc] Attached SCSI removable disk
GNU GPL
Jsou zabaleny zde. Interní odkazy na této stránce nejsou funkční. Dokumentace není uložena na serveru celá. Pokud si stáhnete zdrojáky, rozbalíte a pomocí doxygen vygenerujete celou dokumentaci, bude to fungovat.