Linux E X P R E S

Facebook

Vývoj jádra XII. - komunikace se zařízením

Smyslem určitého ovladače v jádře obvykle bývá komunikace s nějakých fyzickým zařízením. I když nejde o nic složitého, přesto se vyplatí dát si na některé věci pozor. Kapitola o přístupu k hardwaru právě začíná.


I/O porty a práce s nimi

S většinou zařízení se pracuje jednou ze dvou základních metod: pomocí I/O portů, nebo paměti zařízení. O paměti zařízení a jejím namapování do paměťového prostoru jádra už tu řeč byla (a ještě se k tomu vrátíme), nyní je řada na komunikaci prostřednictvím portů. Připomínám, že porty jsou záležitost hardwarově závislá (jedná se v podstatě o registry) a na některých platformách se s nimi vůbec nesetkáme (veškerá komunikace probíhá přes paměťové operace). Naopak třeba procesory x86 jsou známy využitím adresního prostoru portů a v dobách masivního používání sběrnice ISA to byl hlavní způsob komunikace (kdežto dnes – kdy se používá hlavně sběrnice PCI – je častější paměťový přístup).

I/O porty a jejich vlastnosti se na různých platformách velice liší. Někde je nelze používat vůbec (Cris), někde jsou emulovány pomocí I/O paměti (drtivá většina platforem, např. ARM, SPARC, MIPS, Alpha), někde mají vlastní adresní prostor (x86). Taktéž datový typ pro adresaci portu se liší, většinou je to unsigned long, ale třeba na platformě x86 používají porty z historických důvodů 16-bitový typ unsigned short.

Alokace a uvolňování portů

I když si ohledně fungování portů nemusíme dělat starosti s nějakým přidělováním (komunikační operace fungují vždy), je zcela nezbytné zajistit, aby mohl určité porty používat v dané chvíli pouze jediný ovladač. Proto potřebujeme operace, které na úrovni jádra zajistí vyhrazení portů a po použití jejich uvolnění.

K vyhrazení (alokaci) slouží funkce request_region(). Vrací ukazatel na strukturu resource, což je datová struktura používaná v jádře pro označení obecných prostředků, tedy kromě portů také třeba paměti nebo IRQ. Její položky nás nyní nemusí zajímat, vrácený ukazatel musíme ovšem vždy otestovat. Zde je krátký příklad:

struct resource* rc = request_region(0x420, 4, "mydevice");
if (rc == NULL)
  return -EBUSY;
...
release_region(0x420, 4);

V příkladu se obsadí čtyři porty počínaje adresou 0x420. Pokud se to nepovede (typicky proto, že už si zmíněné porty alokoval někdo jiný), funkce vrátí NULL. Existuje ještě funkce check_region() pro ověření možnosti alokovat porty, ale před jejím použitím musím důrazně varovat, protože udává stav v daném okamžiku a nijak nezaručuje, že při následném volání request_region() bude situace stejná. Alokované porty lze sledovat přes procfs, a sice v souboru /proc/ioports. Zobrazují se podle názvů uvedených při volání funkce request_region().

Operace na portech

V současné době máme dvě možnosti, jak z I/O portů číst data nebo je tam naopak zapisovat. První z cest je starší, je použitelná pouze pro porty a má i některé další nevýhody. Pak je tu cesta novější, sjednocená s přístupem k paměti v zařízení. Ukážeme si obě.

Původní způsob je založen na funkcích inb(), outb(), inw(), outw(), inl() a outl(), resp. insb()/outsb() a dalších. Poslední písmeno v názvu určuje délku registru (8, 16 nebo 32 bitů), funkce s písmenem „s“ jsou řetězcové, umožňující přenést více dat najednou (po jednotkách dané délky). Volba konkrétní funkce se řídí skutečnou délkou registru, je nepřípustné funkce míchat (třeba místo jedné 16bitové použít dvě 8bitové) – i když to leckde bude fungovat, je to dobrý způsob, jak si zadělat na problémy.

Pro případy, že by mohlo zařízení „nestíhat“, existují také zpomalované verze funkcí – inb_p(), outb_p() a další. Často jsou ale tyto funkce jen aliasem, protože vkládání zpomalovacích instrukcí není potřeba.

Novější způsob práce byl zaveden od jádra verze 2.6.9. Vznikl především kvůli přístupu přes I/O paměť, ale lze použít i u portů. Hlavním důvodem tehdy bylo „prasení“ kódu některými vývojáři, usnadněné tím, že na některých architekturách šlo k I/O paměti přistupovat stejně jako k té normální, a také slabou typovou kontrolou původních funkcí.

Nové funkce se jmenují ioread8(), iowrite16() atd., s tím, že big-endian verze mají na konci názvu ještě be (řetězcové funkce pak končí _rep). Těmto funkcím se jako parametr předává ukazatel na I/O paměť, což je void __iomem*. Tento typ není nic jiného než void*, ovšem symbol __iomem značí, že se jedná o speciální paměť (ukazatel se nesmí dereferencovat) a toto lze kontrolovat různými nástroji (např. sparse; kompilátor samotný to obecně nekontroluje).

Většina zařízení pracuje s čísly ve formátu little-endian, s čímž I/O funkce počítají a na big-endian platformě provedou automaticky příslušnou konverzi. Občas se ale najde zařízení používající big-endian. Od verze 2.6.12 jsou v jádře funkce (ioread16be(), ioread32be(), iowrite16be() a iowrite32be()) určené pro tato zařízení. Chovají se přesně opačně, tedy prohazují bajty na platformě little-endian.

Funkce lze přímo použít pro I/O paměť, pro porty se ale musí provést nejdřív namapování funkcí ioport_map(). Tato funkce vrátí ukazatel, s nímž lze (pomocí výše uvedených funkcí) pracovat jako s I/O pamětí. Po použití se mapování zruší zavoláním ioport_unmap(). Následující příklad ukazuje použití obou metod:

u16 val = 0xff00;

outw(val, 0x420);

void __iomem* ptr = ioport_map(0x420, 4);
iowrite16(val, ptr);
ioport_unmap(ptr);

V obou případech se zapisuje tatáž hodnota na stejný port. Snad je každému jasné, že namapování se provede jen na začátku (např. při načtení modulu) a ruší se až konci (při uvolnění modulu).

I/O paměť

Již o ní kdysi byla řeč. Někdy v 6. kapitole jsem ji ukazoval v souvislosti s funkcí ioremap() a také jsem upozornil, že se nesmí používat jako normální paměť, ale pouze pomocí funkcí readb() apod. Nyní se k tomu vrátíme – na příkladu si ukážeme, jak by se používal novější přístup:

u32 val;
void __iomem* ptr = ...

val = readl(ptr + 8);

val = ioread32(ptr + 8);

Není to žádná věda, prostě se jen použije jinak pojmenovaná funkce. Příslušný ukazatel (ptr) pochází z funkce ioremap().

Alokace oblasti I/O paměti

Podobně jako u portů i v případě I/O paměti musíme zajistit, abychom měli příslušnou oblast pouze pro sebe. A dělá se to úplně stejně, jako jsme to dělali u portů. Používá se funkce request_mem_region(), resp. pro uvolnění release_mem_region(). Opět krátký příklad:

struct resource* rc = request_mem_region(0xb4000000, 0x00010000, "mydevice");
if (rc == NULL)
  return -EBUSY;
...
release_mem_region(0xb4000000, 0x00010000);

Alokaci musíme provést ještě dřív, než zavoláme ioremap(). Aktuální přidělení úseků lze sledovat přes /proc/iomem.

Bariéry

Následující část se týká obou popisovaných druhů komunikace – jak portů, tak paměti. Za normálních okolností se kompilátor snaží některé operace optimalizovat. Prostě přehází pořadí instrukcí, aby se běh celkově zrychlil. Jenže toto může mít zásadní dopady na fungování I/O komunikace, protože by se mohlo klidně stát, že by nějaké čtení proběhlo dříve než zápis s vedlejším efektem, který má vliv na přečtenou hodnotu. Proto je takovému chování nutno zabránit. Kromě kompilátoru se mohou takto chovat i procesory nebo řadiče sběrnic, takže i tento problém se musí vyřešit.

První problém řeší makro barrier(). Zajišťuje, že všechny operace, které se provádějí na základě instrukcí umístěných před bariérou, budou úplně dokončeny a za bariérou se bude pokračovat, jako kdyby neexistovaly. Tedy, že hodnoty v registrech se nesmějí použít a následné přístupy do paměti se musí skutečně provést.

Zmíněné makro ovšem nijak nezasahuje do případného přerovnání instrukcí na hardwarové úrovni. Tam se musí použít jiná bariérová makra – rmb(), wmb() a mb(). První zajistí správnou posloupnost čtení, druhé zápisů a třetí obojího. Implementace je samozřejmě silně hardwarově závislá a v některých případech (kdy k přehození instrukcí dojít nemůže) dokonce ani nemusí do řazení instrukcí nijak zasahovat. Vždy ale vkládají kompilační bariéru (barrier()).

Existuje ještě několik dalších bariér. Jedna z nich je read_barrier_depends(), což je zvláštní verze rmb(), zasahující pouze takové operace, které závisejí na jiných. Použití se doporučuje jen v případě, že autor dobře ví, proč to dělá (obecně je lépe používat rmb()). Dále je tu ještě skupina bariér, které jsou určeny pro SMP jádra. Pokud se jádro kompiluje pro víceprocesorové systémy, chovají se jako své normální protějšky. Na jednoprocesorovém jádře degenerují v kompilační bariéru. Mají názvy začínající smp_ (např. smp_wmb()). Ještě je tu jedna speciální bariéra – mmiowb(), která zajišťuje pouze dodržení pořadí zápisů, ovšem nezaručuje dokončení zápisu před bariérou.

val1 = ioread16(ptr);
val2 = ioread16(ptr + 2);
rmb();
val3 = ioread16(ptr);
if (val1 == val3) {
...
}

Příklad ukazuje použití hardwarové bariéry pro čtení. Víme předem, že druhé čtení (z vyšší adresy) vyvolá vedlejší efekt, který má dopady na hodnotu čtenou z nižší adresy. Musíme proto za toto čtení vložit bariéru. Tak je zajištěno, že další čtení z nižší adresy proběhne zaručeně až po tom předchozím.

Všichni víme, že popisovaný druh komunikace se zařízením je pomalý, a při větších nárocích se proto používá přímý přístup do paměti – DMA. Příště se podíváme na to, jak si připravit paměť, ovládat řadič DMA a co dělat po přijetí dat. Dále se pak budeme věnovat další trochu podobné věci, a sice přímému přístupu z jádra do paměti procesu.

Diskuze (0) Nahoru