Produkty Novinky Články Návody Kontakty

Přístup k vnějšímu zařízení

Na začátku našeho povídání o ovladačích pro systém Linux jsme si vymezili prostor ovladače a definovali jsme si dvě rozhraní, která je třeba naimplementovat, jestliže má být ovladač přístupný z uživatelského prostoru a zároveň jestliže má sám přistupovat na nějaké zařízení. Tato situace je zachycena na obrázku1.8. Rozhraní vůči uživatelskému prostoru je zde označeno jako rozhraní A, zatímco rozhraní pro komunikaci se zařízením je označeno jako rozhraní B. Popisu rozhraní A jsme věnovali celý předchozí text. V této části se proto podíváme jakým způsobem naimplementovat rozhraní B.
obrázek ovladac-a-jeho-rozhrani
Obrázek 1.8 Ovladač a jeho rozhraní II
Avšak než se začneme věnovat samotné implementaci rozhraní B, musíme si něco málo povědět o hardwarové stránce komunikace se zařízením. Pod pojmem zařízení si představíme nějakou periférii počítače (přesněji jeho mikroprocesoru), např. paralelní port nebo obvod reálného času (RTC) apod. Zkrátka vnější zařízení, které je připojeno na adresovoua datovou sběrnici mikroprocesoru a které hodláme ovládat z operačního systému, který na daném mikroprocesoru běží. Pro úplnost je třeba dodat, že častým jevem je připojení periférie k mikroprocesoru přes jinou periférii - např. tiskárna (periférie), připojená přes USB nebo paralelní port (rozšiřující periférie mikroprocesoru - přidává novou sběrnici).
Každé takové zařízení obsahuje (nebo se tak aspoň z pohledu mikroprocesoru musí tvářit) jeden nebo více I/O registrů a případně i vlastní vnitřní paměť. I/O registry slouží pro ovládání a konfiguraci zařízení (ty označujeme jako řídící registry), dále pomocí nich lze vyčíst stav zařízení (stavové registry) a případně pomocí nich přenášíme z/do zařízení i nějaká data (pak jsou to datové registry). Typickým představitelem zařízení, které má pouze I/O registry je paralelní port. Naopak typickým zařízením, které má registry i vnitřní paměť, je obvod reálného času. Do jeho vnitřní paměti, která je zálohována baterií, si můžeme uložit data (budíky, výročí) třeba na rok dopředu a máme tak zajistěno, že zůstanou zachována, i když vypneme počítač.
Přistupovat na zařízení znamená mít možnost vykonávat operace čtení a zápisu do registrů a do vnitřní paměti zařízení. Aby to bylo možné, musí být ovládané zařízení připojeno na datovou a adresovou sběrnici mikroprocesou a jeho I/O registry musí být namapované (rozuměj umístěné) v adresovém prostoru mikroprocesoru. Existují dvě rozdílná řešení jak to zařídit.
První řešení, označované jako přístup přes I/O porty (port-mapped I/O, isolated I/O) předpokládá, že existují dva oddělené adresové prostory. Vnější I/O zařízení (jejich registry a vnitřní paměť) jsou namapovány do adresového prostoru nazývaného I/O adresový prostor. Operační paměť je pak mapována do paměťového adresového prostoru. Toto striktní rozdělení adresového prostoru se projeví i v hardwarové stránce - mikroprocesor má buď rovnou dvě adresové a datové sběrnice: jednu pro běžné paměti a druhou pro vnější I/O zařízení nebo alespoň speciální pin, který rozlišuje jestli jde o přístup do I/O prostoru nebo do operační paměti. A dokonce ovlivňuje i softwarovou stránku věci - pro přístup do I/O adresového prostoru jsou potřeba speciální instrukce (In a OUT).
Připojení vnějších zařízení k mikroprocesoru přes vyhrazený adresový prostor byl používán hlavně v dobách, kdy adresový prostor mikroprocesorů byl malý a proto nebylo účelné vyplýtvat volné adresy na vnější periférie. Známý je především díky architektuře x86 a rozšiřujícím kartám na sběrnici ISA. Novější rozšiřující sběrnice jako PCI, PCI-X a PCI-Express tento způsob přístupu už hardwarově nepodporují.
Druhé řešení nazývanépřístup přes I/O paměť (memory mapped I/O) předpokládá existenci pouze jednoho adresového prostoru, který je sdílen jak operační pamětí, tak vnějšími I/O zařízeními. K I/O registrům a vnitřní paměti zařízení se prostě přistupuje stejně jako k operační paměti. Tedy přes stejnou adresovou a datovou sběrnici a s pomocí stejných instrukcí. Tento způsob umožňuje jednodušší zápojení mikroprocesoru, menší množinu instrukcí (to má význam hlavně u RISCových CPU) a snazší optimalizaci zdrojového kódu při překladu, takže je logické, že je oblíben hlavně u mikroprocesorů a mikrokontrolérů určených pro konstrukci embedded zařízení.
Zajímavostí je, že řada platforem umožňuje přístup k vnějším zařízením přes I/O porty i přesto, že nemají oddělený adresový prostor pro I/O a pro operační paměť. Stejně tak Linux podporuje na všech platformách, na kterých běží, přístup přes I/O porty. Mírně se tak stírají rozdíly mezi platformami a je jednoduší přenést ovladače z jedné platformy na druhou.
V dalším textu se podíváme, jaké funkce a makra nám linuxové jádro 2.6 nabízí pro realizaci jednotlivých variant přístupu k vnějšímu zařízení.

Přístup k zařízení přes I/O porty

Předtím než můžeme začít používat určitý I/O port, případně rozsah I/O portů, musíme k nim získat výhradní přístup, to znamená, že musíme požádat jádro o rezervaci vybraného I/O portu či rozsahu portů. Obvyklým místem, kde se tak děje, je inicializační funkce ovladače. Pro rezervaci I/O portu slouží funkce request_region():
struct resource * request_region (unsigned long start, unsigned long n,
                                                                        const char *name);
kde parametr start je fyzická adresa portu, n je počet portů, které chceme od adresy start rezervovat, a name je řetězec (obvykle jméno ovladače), které se objeví v souboru /proc/ioports. Návratová hodnota různá od NULL říká, že se rezervace portu podařila a že jej můžeme začít používat. Hodnota NULL znamená, že máme smůlu - port vlastní už někdo jiný. Pak je zvykem ukončit inicializační funkci s chybou -EBUSY. Samotná návratová hodnata se už dále v kódu ovladače nepoužívá, při přístupu na port se stále používá jeho fyzická adresa.
Uvolnění rezervovaného portu zařídí funkce release_region() s prototypem uvedeným níže. Deklarace obou funkcí jsou v hlavičkovém souboru <linux/ioport.h>. Hodnoty parametrů start a len musí být stejné jako při rezervaci I/O portu.
void release_region(unsigned long start, unsigned long n);
Ke čtení/zápisu hodnot z/do I/O registrům vnějšího zařízení se používají následující funkce rozdělené podle šířky I/O registru (8 - 32bitů). Jejich deklarace jsou v hlavičkovém souboru <asm/io.h>:
// cteni z registru
unsigned inb(unsigned port); // 8bitovy registr
unsigned inw(unsigned port); // 16bitovy registr
unsigned inl(unsigned port); // 32bitovy registr
​
// zapis do registru
void outb(unsigned char byte, unsigned port); // 8bitovy registr
void outw(unsigned short word, unsigned port); // 16bitovy registr
void outl(unsigned long word, unsigned port); // 32bitovy registr
Neúplný typ unsigned podle typu architektury představuje buď unsigned int nebo unsigned long. Díky jeho použití jsou uvedené prototypy funkcí přenositelné mezi platformami. Parametr port je fyzická adresa portu, např. 0x378 (paralelní port).
Obecně není možné dva 8bitové registry vyčíst jako jeden 16bitový apod. Vždy je nutné použít správnou funkci s ohledem na šířku I/O registru.
Pro přenos více než jedné hodnoty do/z I/O registru se s výhodou používají funkce insb(), outsb() atd., kterým se říká řetězcové operace - přenáší řetězec hodnot.

Přístup k zařízení přes I/O paměť

I/O paměť se musí, stejně jako I/O porty, nejprve rezervovat, resp. získat k ní výhradní přístup. V okamžiku, kdy už nepotřebujeme s I/O paměté pracovat je nutné ji uvolnit. K těmto účelům jsou v hlavičkovém souboru <linux/ioport.h> definovány funkce:
struct resource * request_mem_region(unsigned long start, unsigned long len,
                                                                                char * name);
void release_mem_region(unsigned long start, unsigned long len);
kde parametr start je fyzická adresa začátku I/O paměti, kterou si hodláme rezervovat, parametr len je pak velikost bloku I/O paměti a name je řetězec, který se objeví v souboru /proc/iomem (nejčastěji jméno ovladače).
Návratová hodnota rúzná od NULL při rezervaci I/O paměti indikuje úspěch, v opačném případě už I/O paměť používá někdo jiný.
Rezervováním bloku I/O paměti ovšem naše práce nekončí. Rezervovanou I/O pamět musíme namapovat do adresového prostoru jádra. Přímý přístup nelze použít s ohledem na to, že i linuxové jádro může na dané platformě používat virtuální adresový prostor a/nebo stránkování paměti. K bezpečnému namapování I/O paměti do prostoru jádra, a naopak k uvolnění bloku I/O paměti z adresového prostoru jádra, slouží funkce definované v <asm/io.h>:
void __iomem * ioremap(unsigned long phys_addr, unsigned long size);
void __iomem * ioremap_nocache(unsigned long phys_addr, unsigned long size);
​
void iounmap(void __iomem * addr);
kde parametr phys_addr funkce ioremap() udává fyzickou adresu začátku bloku I/O paměti a parametr size jeho velikost. Jde o stejné hodnoty jako v případě parametrů start a len použitých při rezervaci I/O paměti. Návratová hodnota je adresa začátku bloku I/O paměti (proto je označena tokenem __iomem) převedená do adresového prostoru jádra. Nicméně i přesto nemůžeme na namapovaný blok I/O paměti přistupovat z ovladače přímou derefencí získaného ukazatele, ale pouze a jen prostřednictvím funkcí/maker popsaných dále v textu.
Poznámka: Funkce ioremap_nocache() namapuje I/O paměť do adresového prostoru jádra tak, aby přístup na I/O paměť nebyl cachovaný. Na naprosté většině platforem nemá smysl ji používat - je totiž shodná s funkcí ioremp().
Při uvolňování I/O paměti z adresového prostoru jádra vkládáme jako parametr funkce iounmap() adresu získanou funkcí ioremap().
Níže uvádíme seznam funkcí pro přístup k rezervovanému a namapovanému bloku I/O paměti. Tyto funkce jsou definované v hlavičkovém souboru <asm/io.h>. Jejich parametr addr je adresa z adresového prostoru jádra, tj. adresa začátku bloku I/O paměti získaná funkcí ioremap() + ofset. Uvedené funkce umoňují čtení/zápis jedné 8 - 32bitové hodnoty, případně více hodnot na stejnou adresu (funkce s příponou _rep).
// cteni z I/O pameti
unsigned int ioread8(void __iomem * addr);
unsigned int ioread16(void __iomem * addr);
unsigned int ioread32(void __iomem * addr);
​
// zapis do I/O pameti
void iowrite8(u8 value, void __iomem * addr);
void iowrite16(u16 value, void __iomem * addr);
void iowrite32(u32 value, void __iomem * addr);
​
// opakovane cteni z jedne adresy v I/O pameti do buferu 
void ioread8_rep(void __iomem * addr, void * buf, unsigned long count);
void ioread16_rep(void __iomem * addr, void * buf, unsigned long count);
void ioread32_rep(void __iomem * addr, void * buf, unsigned long count);
​
// opakovany zapis hodnot z buferu na jednu adresu v I/O pameti
void iowrite8_rep(void __iomem * addr, const void * buf, unsigned long count);
void iowrite16_rep(void __iomem * addr, const void * buf, unsigned long count);
void iowrite32_rep(void __iomem * addr, const void * buf, unsigned long count);
Pro kopírování dat mezi I/O pamětí a operační pamětí slouží níže uvedené funkce memcpy(). Tyto funkce se chovají podobně jako jejich protějšky z knihovny libc. Jejich prototypy nalezneme v souboru <asm/io.h>.
// nastavení obsahu I/O pameti na definovanou hodnotu
void memset_io(void __iomem * addr, unsigned char val, size_t count);
​
// kopirovani dat mezi operacni a I/O pameti
void memcpy_fromio(void * dst, const void __iomem * src, size_t count);
void memcpy_toio(void * __iomem dsr, const void * src, size_t count);
Ve zdrojovém kódu mnoha ovladačů se pro přístup k I/O paměti používají místo funkcí ioread() a iowrite() funkce readb(), writeb() atd. Tyto funkce sice budou stále fungovat - ovladač půjde přeložit, ale jejich použití v nově psaných ovladačích se již nedoporučuje.

I/O porty namapované jako I/O paměť

Kvůli snažší přenostitelnosti kódu ovladače a také kvůli celkovém příklonu k používání I/O pamětí při přístupu k zařízení byla v jádře 2.6 uvedena novinka - možnost namapovat I/O porty do jádra jako I/O paměť a dále s nimi pracovat jako s regulérní I/O pamětí.
První krok při mapování I/O portu na I/O paměť je stejný jako u přístupu přes I/O porty - je třeba si požadovaný rozsah portů zarezervovat pomocí funkce request_region(). Zarezervovaný I/O port si posléze pomocí funkce ioport_map() namapujeme jako I/O paměť do adresového prostoru jádra. Pro uvolnění I/O portu z adresního prostoru jádra pak použijeme funkci ioport_unmap(). Prototypy obou funkcí jsou definované v <asm/io.h>:
void __iomem *ioport_map(unsigned long port, unsigned int nr);
void ioport_unmap(void __iomem * addr);
kde port je fyzická adresa portu, např. 0x378 (paralelní port) a nr je počet portů, které chceme namapovat na I/O paměť. Návratová hodnota je typu ukazatel na I/O paměť, proto je označen tokenem __iomem. Tento ukazatel už můžeme přímo použít jako adresu ve všech funkcích pro přístup k I/O paměti, které jsme popsali výše.
V okamžiku, kdy už nehodláme dále používat I/O port nejprve zrušíme jeho namapování na I/O paměť a zrušíme ejho rezervaci uvolníme s pomocí funkce release_region().

Ukázka implementace v ovladači dskel

Níže uvádíme části kódu, které by bylo nutné vložit do zdrojového kódu našeho modulu dskel, kdybychom chtěli používat paralelní port, přesněji zpřístupnit jeho kontrolní, stavové a datové registry. Využili jsme vlastnosti linuxového jádra 2.6 a I/O porty, které zpřístupňují registry paralelního portu, jsme na platformě x86 namapovali do jádra jako I/O paměť.
Na výpisu 30 je uvedeno potřebné rozšíření hlavičky zdrojového kódu modulu.
1/* ######## prechozi include ######## */
2#include <linux/ioport.h>
3#include <asm/io.h>
45// ——————————————————————
6#define DEV_NAME              "dskel"
7#define DEV_COUNT             1
89#define PAR_PORT_BASE 0x378
10#define PAR_PORT_SIZE        3
1112//——————————————————————--
13enum
14{
15     /* ######## prechozi chyby ######## */
16     Failed_Region,
17     Failed_IoMap,
18};
1920//——————————————————————--
21struct dskel_t
22{
23     /* ######## predchozi clenove struktury ######## */
24     // zacatek bloku registru paralelniho portu namapovaneho jako I/O pamet
25     void __iomem *          port_base;
26};
2728static struct dskel_t dskel = 
29{
30     /* ######## predchozi clenove struktury ######## */
31     .port_base = NULL,      
32}
33
Výpis zdrojového kódu ovladače - hlavička (doplněk kvůli I/O portům)
Na výpisu 30 je doplnění kódu inicializační funkce o rezervaci rozsahu I/O portů, kde jsou namapovány registry paralelního portu, a mapování I/O portu do I/O paměti.
1struct resource * registr_ptr;
23/* ######## prechozi kod funkce dskel_init() ######## */
45// rezervujeme si paralelni port, resp. jeho I/O port 
6registr_ptr = request_region(PAR_PORT_BASE, PAR_PORT_SIZE, DEV_NAME); 
7if ( registr_ptr == NULL )
8{
9      pr_err("dskel_init(): parallel port already in use!\n");
10     cleanup(Failed_ReqRegion);      
11     result = -EBUSY;
12     goto End;
13}
1415// premapujeme port jako I/O pamet   
16dskel.port_base = ioport_map(PAR_PORT_BASE, PAR_PORT_SIZE);  
17if ( dskel.port_base == NULL )
18{
19     cleanup(Failed_IoMap);
20     result = -ENOMEM;
21     goto End;
22}
2324/* ######## pridani ovladace do jadra ######## */
Výpis zdrojového kódu ovladače - funkce init() (doplněk kvůli I/O portům)
A konečně na výpisu 30 je vypsán kompletní kód úklidové funkce cleanup(), kde přibylo zrušení mapování I/O portu a zrušení jeho rezervace pro náš modul.
1static void cleanup(int error)
2{
3      switch ( error )
4      {
5              // kompletni uklid pri exitu
6              case ModuleExit:
7                      // zrusime adresar modulu /sys
8                      device_destroy(dskel.sys_class_ptr, dskel.dev_num);
910             case Failed_SysDev:
11                     // zrusime tridu
12                     class_destroy(dskel.sys_class_ptr);
1314             case Failed_SysClass:
15                     // vyjmeme modul z jadra
16                     // cdev_del() se vola jen po uspesnem cdev_add()
17                     cdev_del(dskel.cdev_ptr);
1819             case Failed_CdevAdd:
20                     // zrusime mapovani portu               
21                     ioport_unmap(dskel.port_base);
2223             case Failed_IoMap:
24                     // zrusime rezervaci I/O portu          
25                     release_region(PAR_PORT_BASE, PAR_PORT_SIZE);
26
                                                27             case Failed_ReqRegion:
28                     // pozn.: neexistuje opak fce cdev_alloc(), uvolneni pameti resi
29                     // jadro samo
30             case Failed_CdevAlloc:
31                     // uvolnime nami zabrany major-minor par
32                     unregister_chrdev_region(dskel.dev_num, DEV_COUNT);
3334             case Failed_ChrdevRegion:
35                     // jeste nic se nealokovalo, takze neni co vracet
36                     break;
3738             default:
39                     break;
40     }
41}
Výpis zdrojového kódu ovladače - funkce cleanup() (úprava kvůli I/O portům)

Paměťové bariéry

Při přístupu modulu na zařízení je třeba věnovat pozornost ještě několika dalším ”maličkostem”. Jednak je to použití vyrovnávací paměti na různé úrovni - přímo v zařízení, v procesoru nebo třeba i díky kompilátoru, který často přenášená data uloží do registru CPU místo do zařízení.
A pak je to také skutečnost, že operace čtení/zápisu mohou být díky optimalizacím aplikovaných kompilátorem přeskupené. Jinými slovy kompilátor nebo mechanismy procesoru zajišťující souběžné vykonávání více instrukcí naráz mohou způsobit, že sekvence operací čtení/zápisu proběhne jinak, než programátor zamýšlel. Toto se běžně děje při přístupu k operační paměti, kde s tím nebývá problém, protože důležitý je jen výsledek. Tam jsme za tuto optimalizaci dokonce rádi.
Ovšem při ovládání zařízení vzniká díky optimalizaci přístupu k zařízení zásadní problém - představte si, že do zařízení nejdříve musíme zapsat příkaz, vyčíst si stav zařízení, zapsat další příkaz a zase si vyčíst stav. Kompilátor si tuto sekvenci vyloží tak, že pro výsledek tohoto sledu operací je vlastně důležité provést pouze poslední zápis, tj. zapsat druhý příkaz. Takže první operaci zápisu z výsledného kódu odstraní. Druhé čtení stavu zařízení vyhodnotí také jako zbytečné, protože čteme ze stejného místa a výsledek ukládáme do stejné proměnné. A tak odstraní i operaci druhého čtení a do proměnné uloží stav získaný prvním čtením stavu ze zařízení, který si pro tyto účely podržel ve registru CPU. Zábavné, že? Ano, ale jen do doby než musíte najít chybu, která je důsledkem takovéto optimalizace.
Zatímco problém s vyrovnávací pamětí řešit nemusíme - inicializační kód v jádře Linuxu zajistí její vypnutí - o optimalizace ohledně pořadí operací přístupu k zařízení a o optimalizace při čtení hodnot z registrů CPU místo ze zařízení, se už postarat musíme. Slouží k tomu tzv. paměťové bariéry, resp. hardwarové paměťové bariéry.
Rozeznáváme tři druhy hardwarových paměťových bariér, resp. funkcí definovaných v hlavičkovém souboru <asm/system.h>, které bariéry do kódu vkládají:
  • rmb() - zajistí, že všechny operace čtení uvedené před bariérou budou vykonány (v libovolném pořadí daném optimalizací) PŘED operací čtení následující ZA bariérou.
  • wmb() - funguje stejně jako rmb(), ale pro operace zápisu.
  • mb() - funguje pro oba typy operací - čtení i zápis. Tzn., že všechny operace uvedené před bariérou jsou provedeny před operacemi následujícími za bariérou.
Použití bariér ovšem snižuje výkon, takže je třeba je používat s rozmyslem.
Nejlepší ukázkou použití bariér je příklad - vraťme se k našemu příkladu - zapíšeme do zařízení příkaz, pak si vyčteme jeho stav, zapíšeme druhý příkaz a znovu si vyčteme stav:
iowrite8(cmd1, parPortAddr);
mb();
status = ioread8(parPortAddr + 1);
mb();
iowrite8(cmd2, parPortAddr);
mb();
status = ioread8(parPortAddr + 1);
Vidíme, že nejprve jsme do kontrolního registru paralelního portu zapsali příkaz cmd1, pak jsme vložili bariéru, abychom zajistili, že vyčtení stavu ze stavového registru paralelního portu bude skutečně následovat až po zapsání příkazu cmd1. Další příkaz zas díky druhé bariéře zapíšeme do zařízení skutečně až tehdy, když jsme si vyčetli stav paralelního portu. A vyčtení stavu zařízení po zápisu druhého příkazu je díky bariéře také zajištěno. A hlavně jsou vyrušeny optimalizace kompilátoru, takže stav čteme skutečně ze zařízení a nikoliv z registru CPU.