Hlavní navigace

Programujeme OS: řídíme textový režim VGA

28. 7. 2009
Doba čtení: 6 minut

Sdílet

V minulém díle našeho seriálu o programování operačního systému jsme si ukázali, jak se dá vytvořit základ k jádru operačního systému - dnes si objasníme již nakousnuté VGA pro textový režim a také některé nejasnosti, jež se objevily v diskuzi článku ohledně zavaděče GRUB a linkovacího skriptu.

Naše jádro je nyní schopno vypsat text „Ahoj svete !“, současná implementace funkce print () bohužel ale nestačí, pokud chceme, aby se náš text nacházel na jiné pozici, pokud ho budeme chtít obarvit, nebo třeba vypisovat na obrazovce litanie – pomůže nám s tím již zmiňované VGA.

Co je to vlastně VGA?

Je to počítačový standard definující techniku zobrazení – standard, dle kterého se řídí všechny grafické karty pro architekturu x86, jednoduše řečeno nám říká, jak „něco“ zobrazit na obrazovce pomocí grafického adaptéru z dnešního pohledu nejprimitivnějším způsobem.

A jak to funguje?

Každé zobrazovací grafické zařízení (grafická karta) operuje s blokem paměti, do kterého přistupuje jak operační systém (ten zpravidla do bloku zapisuje), tak samotné zařízení (čte z bloku data). My musíme jakožto programátoři obstarat zápis – proto je nutné pochopit, v jakém tvaru se mají nacházet data a kde vůbec …

Ještě před samotným vysvětlováním si musíme objasnit, že se bavíme o textovém režimu – to proto, že existuje ještě tzv. grafický a je mezi nimi značný rozdíl. Vycházejme tedy z názvu, textový je schopen vypisovat pouze text, přesněji znaky – každý znak se skládá z bodů (pixelů), ty jsou uloženy v zásobnících hardwaru.

Jednotlivé znaky mají přiřazené svoje číslo dle dalšího standardu ASCII. Identifikační číslo znaku je definováno 8 bity, což je 1 byte. Pro nás to znamená, že řetězec „Ahoj svete !“ zabral v paměti 12 bytů. Pro naše účely je tedy vhodnější textový režim, který je automaticky přednastavený. Naneštěstí pro větší zmatek existují i samotné submódy, tj. několik pro textový a několik pro grafický režim. Všechny se od sebe liší především rozlišením – schopností vykreslovat větší či menší počet znaků na obrazovce, ale také možností použít různou škálu barev.

Jednotlivé režimy se dají nastavit pomocí tzv. přerušení BIOSu a nebo pomocí specifické sekvence dat zapsané na vyhrazenou adresu v paměti. Přednastavený textový režim má parametry 80×25 znaků, kde každému lze přiřadit jednu z 16 barev, 8 možných barev jako pozadí a jako poslední zajímavou „vymoženost“ – blikání. Barvy změnit nemůžeme, přesto věřím, že si s nimi většina vystačí; Každá má vlastní číslo a to následovně:0: černá, 1: modrá, 2: zelená, 3: azurová, 4: červená, 5: fialová, 6: hnědá, 7: světle šedá, 8: tmavě šedá, 9: světle modrá, 10: světle zelená, 11: světle azurová, 12: světle červená, 13: světle fialová, 14: žlutá, 15: bílá. Nyní si definujeme jak vypadá struktura pro zápis jednoho znaku:

Grafický režim je takový, kde můžeme vykreslovat kterýkoliv bod na obrazovce dle své libosti. V podstatě se jedná jen o nastavení barvy na určitém místě v paměti. Pokud bychom chtěli vykreslovat znaky, musíme si je sami všechny definovat, pixel po pixelu a dále napsat funkce schopné je vykreslovat.

/* Struktura jedné datové buňky dle VGA standardu (16bitů) */
typedef struct {
    unsigned char c;        /* znak (8bitů) */
    unsigned font_color : 4;    /* barva znaku (4bity) */
    unsigned bg_color : 3;      /* barva pozadí (3bity) */
    unsigned blink : 1;     /* blikáme ? (1bit) */
} __attribute__ ((__packed__)) vga_cell_t;

Vidíme, že má dohromady 2 byty (16 bitů) – barva znaku je definovaná čtyřmi bity, což je celkem 16 kombinací (0 až 15), barva pozadí pouze 3 bity což dává 8 kombinací (dle vztahu 2^n, kde n je počet bitů). Zajisté jste si všimli podivného atributu struktury __attribute__ ((__packed__)) , ten je velmi důležitý, neboť říká kompilátoru, aby nevytvářel tzv. boundary alignment. Jsou to pro programátora neviditelné úseky paměti mezi proměnnými, které mají různou velikost. Často se tvoří při definici počtu bitů proměnných. Pokud přidáme tento atribut, je vše přesně tak, jak je vidno ze struktury – v budoucnu tento poznatek využijeme také.

Nyní přišel čas na varování – některé emulátory nebo virtualizační nástroje nedodržují tak základní standard jako je VGA na 100 %, týká se to hlavně zmíněné barvy pozadí, kdy je chybně definováno, že se jedná o 4bitovou proměnnou, tzn. blikání zde není podporováno (nesplnila by se podmínka celkové velikosti 16 bitů), naopak je zde více barev. Lze si tento problém snadno ověřit mezi reálným PC a příkladně Qemu (zde je implementace chybná).

Nyní už víme, jak vypadá jedna buňka, zapisující se do celého pole video paměti. Takovýchto buněk musí být v našem případě 80×25, pro informaci náš paměťový blok se rovná 4000 bytům, tj. 2000 za sebou jdoucích struktur vga_cell_t. Pořád tady píšu o nějakém bloku, teď konkrétněji .. jedná se o paměťový blok, který se nachází v naprosté většině případů na adrese 0×b8000 (hexadecimální), je namapován prostřednictvím BIOSu na RAM a mezi nimi probíhají naše data.

Poznámka: U starších nebo exotičtějších desek se můžeme setkat s jinou adresou, v tomto případě bychom na obrazovce neviděli vůbec nic. Naštěstí existuje způsob, jak lze zjistit tu správnou – to už ale není pro nás tak podstatné.

Implementace

Využijeme jednoduché matematiky a spočítáme si, na jaké adrese bychom zapisovali např. znak ‚Z‘, pokud by to mělo být na 3. řádku a 5. sloupci. Zde je vzor pro výpočet spolu s typováním na strukturu vga_cell_t definující jednu buňku.

vga_cell_t *cell = (vga_cell_t *) vga.vid_mem + (vga.cursor_y * VGA_RES_X) + vga.cursor_x;

Výpočet vypadá následovně: adresa = 0×b8000 + [(3–1) * 80] + (5–1)

Z výpočtu je tedy zjevné, že musíme od bodů souřadnic odečíst 1, protože se začíná od 0. Na tuto adresu zapíšeme 1 byte – znak a hned za něj přidáme atributy. Toto celé realizuje funkce put_char () nacházející se v ukázkovém kódu (odkaz na konci článku).

Nalezneme tam i několik dalších funkcí, ty už ale obstarávají vyšší záležitosti (výpis řetězců, apod). Nebyla zde zmíněna ještě jedna věc – tou je hardwarový kurzor, není ničím jiným, než to, co známe jako blikající podtržítko. V kódu je obsažena funkce, pro přesun kurzoru. Pokud se na ní podíváte blíže, uvidíte také outb () starající se o zápis 1 bytu na zvolený port (adresu). Tato funkce se nachází v souboru utils.c napsaném v jazyce C, ale využívá možnost použití inline assembleru (ten má ale přednastavenou odlišnou syntaxi od překladače NASM zvanou AT&T).Tímto vás nechci dezorientovat, ale ukázat, že to jde udělat i jinak – cílem tohoto seriálu není vytvořit dokonalé jádro (popř. operační systém), ale ukázat možné cesty vývoje.

Odpovědi na některé otázky z minulé diskuze

GRUB je zavaděč, který svým způsobem ulehčuje práci vývojářům jádra, proto je docela vhodný i pro nás – před spuštěním samotného jádra přepne režim procesoru na tzv. chráněný režim, což dovoluje použít např. stránkování, virtuální paměť nebo bezpečnější multitasking.

Přesněji GRUB nastaví automaticky GDT tabulku, na jednu stranu je to užitečné, na druhou nebezpečné… Další dotaz byl na téma linkovacích skriptů – proč je tam nějaké code = .; _code = .; __code = .; nebo *(.data) a *(.rodata)  – ld má v oblibě přidávat mezi klasické sekce ELFích binárek i další, pro náš účel je to to nevhodné a právě takto napsaný linkovací skript sjednotí daný typ sekce v jeden (nakonec jsou tedy opravdu jen 4) a nastolí pořádek a jednoduchost.

CS24_early

Na závěr

Dnes zde byla především teorie – ale jak se říká – „bez teorie není praxe“ – to platí i obráceně. Po dnešku už bychom měli být schopni vypisovat téměř cokoliv a myslím, že se to na začátek docela hodí – bez toho, že si uděláte výpisy to jednoduše někdy nejde. Archív s ukázkovým zdrojovým kódem se dá stáhnout na ZeXOS.org.

Příští díl se bude zabývat chráněným módem, nastavením GDT a IDT – o přerušení nouze nebude.

Byl pro vás článek přínosný?

Autor článku

Autor je studentem VŠB FEI a pracuje na několika projektech jako např. operační systém ZeX/OS nebo hra Tuxánci.