Pisanie OS/Obsługa przerwań i konsola tekstowa
Wstęp
[edytuj]Witamy kolejnej części kursu pt. "Jak zostać Billem Gatesem". Ups, to nie ten tekst :) W poprzedniej części dowiedziałeś się dość dużo o programowaniu OS-ów. Teraz przyszła kolej na trudniejsze rzeczy, czyli praktykę. Napisaliśmy także najprostsze jądro systemu, które tylko wyświetla napis "Hello World !!!" w lewym górnym rogu ekranu. Większość kursów niestety kończyła się na tym, ale mój kurs będzie lepszy i opiszę więcej rzeczy :)
Najpierw zajmiemy się rozszerzeniem naszego kernela o obsługę przerwań. Tym razem nie będzie o obsłudze wyjątków. Będzie o tym w 3 części, ponieważ będzie ona stosunkowo krótka, więc postanowiłem przesunąć ten temat, aby ta część była krótsza.
Dodajemy obsługę przerwań
[edytuj]Jak już wspomniałem w pierwszej części, adresy przerwań są zapisane w IDT. IDT należy załadować instrukcją lidt. Do naszego kodu wstawiamy następujące linijki:
lidt [idt_descr]
zaraz po instrukcji lgdt. Na końcu pliku dopisujemy:
idt_descr:
dw 256*8-1
dd idt
GLOBAL idt
idt:
times 256 dd 0,0
Jak na razie to nasz kod startowy załaduje do rejestru IDTR położenie naszej tablicy. Na razie nie możemy wywołać żadnych przerwań, ponieważ procesor by się zresetował. Teraz wypadałoby stworzyć sobie jakieś procedury do "grzebania" w IDT. Na początek może jakiś plik nagłówkowy (intr.h):
#ifndef __INTR_H
#define __INTR_H
typedef struct desc_struct
{
long a,b;
} desc_table[256];
extern desc_table gdt,idt;
void set_intr_gate(int n,void * addr);
void set_trap_gate(int n,void * addr);
void set_system_gate(int n,void * addr);
#endif
W powyższym pliku mamy zadeklarowany typ desc_struct jako jeden deskryptor oraz desc_table jako tablicę deskryptorów. W kodzie C mamy dostęp do tablic gdt oraz idt, ponieważ w pliku start.asm zadeklarowaliśmy je jako etykiety globalne (czyli można uzyskać adres do nich poza plikiem). Teraz stwórzmy sobie procedury do ustawiania wpisów w IDT. Będzie to plik intr.c:
#include "intr.h"
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__("movw %%dx,%%ax\n\t" \
"movw %0,%%dx\n\t" \
"movl %%eax,%1\n\t" \
"movl %%edx,%2\n\t" \
::"i"((short)(0x8000+(dpl<<13)+(type<<8))), \
"o"(*((char *)(gate_addr))), \
"o"(*(4+(char *)(gate_addr))), \
"d"((char *)(addr)),"a"(0x00080000))
void set_intr_gate(int n,void * addr)
{
_set_gate(&idt[n],14,0,addr);
}
void set_trap_gate(int n,void * addr)
{
_set_gate(&idt[n],15,0,addr);
}
void set_system_gate(int n,void * addr)
{
_set_gate(&idt[n],15,3,addr);
}
Funkcja set_intr_gate ustawia adres do "normalnego" przerwania, czyli bez dostępnego kodu błędu. set_trap_gate ustawia adres do "pułapki", czyli przerwania z kodem błędu (kod błędu po wywołaniu przerwania jest dostępny w [esp]). Funkcja set_system_gate przyda nam się w przyszłości, ponieważ zostanie użyta do ustawienia przerwania, za pomocą którego programy użytkownika będą się komunikować z kernelem (tzw. system calls). I właściwie obsługę przerwań mamy już dodaną.
Jeszcze jedna rzecz do wyjaśnienia: zastosowałem tu tzw. assembler AT&T. Jest on bardzo wygodny w użyciu do zastosowania go w kodzie C. Dla początkującego programisty może się on wydawać dziwny, bo np. w assemblerze Intela to:
mov eax,ebx
mov ebx,[esp+4]
mov ecx,[ebx+edx*4]
inc dword [licznik]
mov edi,licznik
w assemblerze AT&T wygląda następująco:
movl %ebx,%eax
movl 4(%esp),%ebx
movl (%ebx,%edx,4),%ecx
incl licznik
movl $licznik,%edi
Asembler AT&T powinien być opisany w dokumentacji do assemblera as (info as). Plik intr.c kompilujemy poleceniem:
gcc intr.c -c -o intr.o -m32 -O2 -fomit-frame-pointer
Przy następnych kompilacjach plików w C będziemy używać opcji kompilatora w postaci:
gcc -c plik.c -o plik.o -O2 -fomit-frame-pointer -m32 -nostdinc -fno-builtin
Można też sobie napisać skrypt Makefile, który miałby postać:
Uwaga!
|
.SUFFIXES: .asm;
OUTFILE = kernel.bin
OBJS = start.o main.o intr.o
CFLAGS = -O2 -fomit-frame-pointer -nostdinc -fno-builtin -m32
$(OUTFILE): $(OBJS)
ld -Tkernel.ld -o $(OUTFILE) $(OBJS)
.asm.o:
nasm $*.asm -f elf32 -o $*.o
Uwaga!
|
Resztę kompilujemy poleceniami (jeśli nie zrobiliśmy pliku dla make) jak zostało to opisane pod koniec pierwszej części. Potem wszystko linkujemy:
ld -Tkernel.ld -o kernel.bin start.o main.o intr.o
Na razie się nic nie zmieniło, tylko plik wynikowy powiększył się o kilkaset bajtów. Później napiszemy prostą procedurę obsługi przerwania.
Piszemy obsługę ekranu
[edytuj]Pisanie obsługi ekranu jest bardzo proste. Istnieją 2 rodzaje obsługi kart graficznych: Hercules i VGA. Herculesa już dzisaj nikt nie używa. Wspomnę tylko, że procedury obsługi różnią się tylko adresem pamięci karty graficznej i portem do obsługi położenia kursora. Pamięć tekstu karty VGA jest zlokalizowana pod adresem liniowym 0xB8000. Uważam, że w naszym OSie nie warto pisać obsługi karty Hercules, ponieważ każdy dzisiaj ma kartę kompatybilną z VGA. Najpierw napiszmy sobie procedury do robienia operacji na portach. Plik io.h:
#ifndef __IO_H
#define __IO_H
static inline void outportb(unsigned short port,unsigned char val)
{
__asm__("outb %%al,%%dx"::"a"(val),"d"(port));
}
static inline unsigned char inportb(unsigned short port)
{
unsigned char __ret;
__asm__("inb %%dx,%%al":"=a"(__ret):"d"(port));
return __ret;
}
#endif
Te instrukcje posłużą nam do wysyłania danych do portów karty VGA, aby ta zmieniła położenie kursora. Kursor można przesunąć poniższą procedurą:
void przesun_kursor(int x,int y)
{
int temp;
temp=(y*80+x);
outportb(0x3D4+0,14);
outportb(0x3D4+1,temp>>8);
outportb(0x3D4+0,15);
outportb(0x3D4+1,temp);
}
Tutaj przyjmujemy, że ekran tekstowy ma wymiary 80x25. Teraz piszemy właściwy program obsługi ekranu. Plik cons.c:
#include"io.h"
#define __wmemcpy(dst,src,count) \
{ \
int d0,d1,d2; \
__asm__ __volatile__("cld;rep;movsw":"=&S"(d0),"=&D"(d1),"=& c" (d2):"0"(src),"1"(dst),"2"(count):"memory"); \
}
#define __wmemset(dst,val,count) \
{ \
int d0,d1; \
__asm__ __volatile__("cld;rep;stosw":"=&D"(d0),"=&c"(d1):"0" (dst),"a"(val),"1"(count):"memory"); \
}
typedef struct {
unsigned long esc,attrib,csr_x,csr_y,esc1,esc2,esc3;
unsigned short * fb_adr;
} console_t;
static console_t * _curr_vc;
static console_t cons[1];
static unsigned short *_vga_fb_adr;
static unsigned _crtc_io_adr, _vc_width, _vc_height;
static int _num_vcs=1;
static void scroll(console_t *con)
{
unsigned short *fb_adr;
unsigned blank, temp;
blank = 0x20 | ((unsigned)con->attrib << 8);
fb_adr = con->fb_adr;
if(con->csr_y >= _vc_height)
{
temp = con->csr_y - _vc_height + 1;
__wmemcpy(fb_adr, fb_adr + temp * _vc_width,(_vc_height - temp) * _vc_width);
__wmemset(fb_adr + (_vc_height - temp) * _vc_width,blank, _vc_width);
con->csr_y = _vc_height - 1;
}
}
static void set_attrib(console_t *con, unsigned att)
{
static const unsigned ansi_to_vga[] = {
0 , 4, 2, 14, 1, 5, 3, 7
};
unsigned new_att;
new_att = con->attrib;
if(att == 0) new_att &= ~0x08;
else if(att == 1) new_att |= 0x08;
else if(att >= 30 && att <= 37)
{
att = ansi_to_vga[att - 30];
new_att = (new_att & ~0x07) | att;
} else if(att >= 40 && att <= 47)
{
att = ansi_to_vga[att - 40] << 4;
new_att = (new_att & ~0x70) | att;
}
con->attrib = new_att;
}
static void move_csr(void)
{
unsigned temp;
temp = (_curr_vc->csr_y * _vc_width + _curr_vc->csr_x) +
(_curr_vc->fb_adr - _vga_fb_adr);
outportb(_crtc_io_adr + 0, 14);
outportb(_crtc_io_adr + 1, temp >> 8);
outportb(_crtc_io_adr + 0, 15);
outportb(_crtc_io_adr + 1, temp);
}
#define isdigit(c) ((c)>='0' && (c)<='9')
void select_vc(unsigned which_vc)
{
unsigned i;
if(which_vc >= _num_vcs)
return;
_curr_vc = cons + which_vc;
i = _curr_vc->fb_adr - _vga_fb_adr;
outportb(_crtc_io_adr + 0, 12);
outportb(_crtc_io_adr + 1, i >> 8);
outportb(_crtc_io_adr + 0, 13);
outportb(_crtc_io_adr + 1, i);
move_csr();
}
static void putch_help(console_t *con, unsigned c)
{
unsigned short *fb_adr;
unsigned att;
att = (unsigned)con->attrib << 8;
fb_adr = con->fb_adr;
/* maszyna stanów do obsługi sekwencji escape */
if(con->esc == 1)
{
if(c == '[')
{
con->esc++;
con->esc1 = 0;
return;
}
/* inaczej idź do końca: zeruj esc i wypisz c */
} /* ESC[ */
else if(con->esc == 2)
{
if(isdigit(c))
{
con->esc1 = con->esc1 * 10 + c - '0';
return;
} else if(c == ';')
{
con->esc++;
con->esc2 = 0;
return;
}
/* ESC[2J -- wyczyść ekran */
else if(c == 'J')
{
if(con->esc1 == 2)
{
__wmemset(fb_adr, (' ' | att)&0xffff, _vc_height * _vc_width);
con->csr_x = con->csr_y = 0;
}
}
/* ESC[num1m -- ustaw atrybut num1 */
else if(c == 'm')
set_attrib(con, con->esc1);
con->esc = 0;
return;
}
/* ESC[num1; */
else if(con->esc == 3)
{
if(isdigit(c))
{
con->esc2 = con->esc2 * 10 + c - '0';
return;
}
else if(c == ';')
{
con->esc++; /* ESC[num1;num2; */
con->esc3 = 0;
return;
}
else if(c == 'H')
{
if(con->esc2 < _vc_width)
con->csr_x = con->esc2;
if(con->esc1 < _vc_height)
con->csr_y = con->esc1;
}
else if(c == 'm')
{
set_attrib(con, con->esc1);
set_attrib(con, con->esc2);
}
con->esc = 0;
return;
} else if(con->esc == 4)
{
if(isdigit(c))
{
con->esc3 = con->esc3 * 10 + c - '0';
return;
} else if(c == 'm')
{
set_attrib(con, con->esc1);
set_attrib(con, con->esc2);
set_attrib(con, con->esc3);
}
con->esc = 0;
return;
}
con->esc = 0;
if(c == 0x1B)
{
con->esc = 1;
return;
}
if(c == 0x08)
{
if(con->csr_x != 0)
con->csr_x--;
} else if(c == 0x09) con->csr_x = (con->csr_x + 8) & ~(8 - 1);
else if(c == '\r')
con->csr_x = 0;
else if(c == '\n')
{
con->csr_x = 0;
con->csr_y++;
} else if(c >= ' ')
{
unsigned short *where;
where = fb_adr + (con->csr_y * _vc_width + con->csr_x);
*where = (c | att);
con->csr_x++;
}
if(con->csr_x >= _vc_width)
{
con->csr_x = 0;
con->csr_y++;
}
scroll(con);
if(_curr_vc == con)
move_csr();
}
void putch(char c)
{
putch_help(_curr_vc,c);
}
void puts(char * s)
{
for(;s && *s;s++) putch(*s);
}
void init_video(void)
{
_vga_fb_adr=(unsigned short *)0xb8000;
_crtc_io_adr=0x3D4;
_vc_width=80;
_vc_height=25;
cons[0].attrib=1;
cons[0].fb_adr=_vga_fb_adr;
_curr_vc=&cons[0];
puts("\x1B[33;44m\x1B[2J");
}
Teraz mamy gotową procedurę obsługi ekranu. Zastosowana tu implementacja tylko częściowo wykorzystuje standard ANSI. Rozkazy ANSI składają się z ciągów sekwencji ESC. Są one dokładnie takie same jak te używane przez sterownik ANSI.SYS (kto używał DOSa, to powinien go pamiętać). UWAGA: Wszystko w tym rozdziale jest prawdziwe tylko i wyłącznie dla karty VGA i kompatybilnej. Na karcie Hercules inne są tylko adresy pamięci video, porty I/O oraz informacje o kolorze są opisane w inny sposób (Hercules jest kartą monochromatyczną). Na początek wypadałoby opisać, w jaki sposób tekst jest umieszczony w pamięci video.
Każdy znak zapisany jest w postaci jednego słowa (16-bit). Dolna połowa (bity 0-7) jest to po prostu kod znaku ASCII (można także zmieniać znaki zapisane w generatorze znaków karty graficznej, ale o tym kiedy indziej). W górnej połowie słowa (bity 8-15) jest zapisany kolor znaku. W jednobajtowym kolorze znaku zapisane są kolor tła znaku oraz kolor znaku.
Bity koloru znaku:
bity opis ----------------------------- b7 migotanie b6:b4 kolor tła (0-7) b3:b0 kolor znaku (0-15)
Wartości kolorów powinny się już kojarzyć programiście z Turbo Pascalem i stałymi opisującymi kolor ;). Są one dokładnie takie same jak w TP.
- 0 - czarny
- 1 - niebieski
- 2 - zielony
- 3 - cyan
- 4 - czerwony
- 5 - magenta
- 6 - brązowy
- 7 - biały
- 8 - ciemno-szary
- 9 - jasny niebieski
- 10 - jasny zielony
- 11 - jasny cyan
- 12 - różowy
- 13 - jasny magenta
- 14 - żółty
- 15 - jasny biały
Teraz opiszę tutaj kilka sekwencji ANSI escape:
- esc[nA - przesuwa kursor n wierszy w górę
- esc[nB - przesuwa kursor n wierszy w dół
- esc[nC - przesuwa kursor n wierszy w prawo
- esc[nD - przesuwa kursor n wierszy w lewo
- esc[y;xH - przesuwa kursor na pozycję (x,y). Górny lewy róg ekranu to (1,1)
- esc[2J - czyści ekran i umieszcza kursor w (1,1)
- esc[K - czyści linię od pozycji kursora do końca linii
- esc[y;xf - to samo co H
- esc[s - zapamiętuje pozycję kursora na wewnętrznym stosie
- esc[u - odczytuje pozycję kursora zapisaną przez esc[s i przesuwa go tam
- esc[im
- esc[i;jm
- esc[i;j;km - ustawienie atrybutów znaków
- i,j,k - atrybuty:
- 0 - wyłącza wszystkie atrybuty
- 1 - włącza pogrubienie
- 4 - włącza podkreślenie (może nie być obsługiwane)
- 5 - migotanie
- 7 - odwrócone video
- 30 - kolor znaku: czarny
- 31 - kolor znaku: czerwony
- 32 - kolor znaku: zielony
- 33 - kolor znaku: żółty
- 34 - kolor znaku: niebieski
- 35 - kolor znaku: magenta
- 36 - kolor znaku: cyan
- 37 - kolor znaku: biały
- 40 - kolor tła: czarny
- 41 - kolor tła: czerwony
- 42 - kolor tła: zielony
- 43 - kolor tła: zółty
- 44 - kolor tła: niebieski
- 45 - kolor tła: magenta
- 46 - kolor tła: cyan
- 47 - kolor tła: biały
- i,j,k - atrybuty:
Tak więc aby wyczyścić ekran wystarczy wyświetlić sekwencję escape esc[2J (oczywiście esc to znak Escape o kodzie 0x1B, czyli 27. Nie piszemy dosłownie esc !!!). Aby wyczyścić zmienić kolor tła na niebieski, kolor liter na żółty oraz wyczyścić ekran piszemy:
puts("\x1B[33;44m\x1B[2J");
Znaki te nie zostaną nigdy wyświetlone na ekranie, ponieważ analiza kodów ANSI została zrobiona za pomocą tzw. maszyny stanów. Przykładowo dopóki putch_help nie spotka znaku Escape i flaga Escape nie została ustawiona, to znaki są wyświetlane normalnie. Powyższego kodu nie ma za bardzo sensu omawiać. Zrozumieć można go analizując kod źródłowy. Jeśli ktoś by chciał, żeby lepiej wytłumaczyć, proszę pisać komentarze (e-mail na samym początku tekstu).
Teraz zmieniamy main: zostawiamy tylko funkcję start kernel oraz przed puts("Hello ... dajemy init_video();. Wszystko można obejrzeć w załączonym pliku .zip. Jest tam pokazane, jak zmieniać kolor tekstu. Zachęcam do samodzielnych prób.
Nareszcie obsługę ekranu tekstowego mamy napisaną. Tryb graficzny to inna bajka i będzie dużo, dużo później.
Procedura przekierowania przerwań sprzętowych
[edytuj]Wiem, że to było w I części. Małe wyjaśnienie: to co było w I części, może nie zadziałać na nowych komputerach. Prezentuję procedurę, która zadziała na 100%. Jeśli ktoś nie wie, po co przekierowywać przerwania, zachęcam do przeczytania I części dokładniej. Procedurę należy kompilować pod NASM.
Plik irqroute.asm:
[SECTION .text]
[BITS 32]
GLOBAL reroute_irqs
reroute_irqs:
cli
in al,0x21
mov ah,al
in al,0xA1
mov cx,ax
mov al,0x11
out 0x20,al
out 0xEB,al
out 0xA0,al
out 0xEB,al
mov al,0x20
out 0x21,al
out 0xEB,al
add al,0x8
out 0xA1,al
out 0xEB,al
mov al,0x04
out 0x21,al
out 0xEB,al
shr al,1
out 0xA1,al
out 0xEB,al
shr al,1
out 0x21,al
out 0xEB,al
out 0xA1,al
out 0xEB,al
mov ax,cx
out 0xA1,al
mov al,ah
out 0x21,al
mov ecx,0x1000
cld
picl1:
out 0xEB,al
loop picl1
cli
mov al,255
out 0xa1,al
out 0x21,al
ret
Teraz należy wprowadzić odpowiednią poprawkę do start.asm. Ma być tak:
jmp .1
.1:
extern reroute_irqs
call reroute_irqs
push dword 0
push dword 0
push dword 0
push dword L6
EXTERN start_kernel
push dword start_kernel
Piszemy procedurę obsługi przerwania
[edytuj]Pisanie obsługi procedur przerwania jest bardzo proste. Przykładowa procedura:
global irq0
irq0:
push gs
push fs
push es
push ds
pusha
mov ax,0x10
mov ds,ax
mov es,ax
mov al,0x60
outb 0x20,al
call jakas_procedura_obslugi_przerwania_w_c
popa
pop ds
pop es
pop fs
pop gs
iret
To jest cała procedura w assemblerze! Nie ma tutaj żadnych cudów. Najpierw zapisujemy na stos rejestry segmentowe ds, es, fs oraz gs. O cs i ss nie musimy się martwić (patrz część I punkty 5.7 oraz 4.3). Potem zapamiętujemy wszystkie rejestry użytkownika instrukcją pusha. Teraz ustawiamy ds oraz es tak, żeby wskazywały na segmenty danych jądra.
Uwaga!
|
Potem można wywołać jakąś procedurę obsługi przerwania w C. Następnie zdejmujemy ze stosu rejestry użytkownika (popa) oraz rejestry segmentowe i wracamy do programu instrukcją iret.
Wysyłanie EOI do kontrolera przerwań (PIC)
[edytuj]Sposób wysłania EOI zależy od numeru przerwania. Gdy są to przerwania sprzętowe od 0-7 to wysyłamy do portu 0x20 wartość (0x60+numer_przerwania), np. gdy jest to przerwanie 3 to piszemy (np. w asm):
mov al,0x63
out 0x20,al
Kiedy jest to przerwanie o numerze 8 lub większym, to procedura jest trochę inna. Wysyłamy do portu 0xA0 wartość (0x60+(numer_przerwania&7)), a do portu 0x20 wysyłamy wartość 0x62. Np. dla przerwania IDE0, czyli 14 procedura jest następująca:
mov al,0x66
out 0xA0,al
mov al,0x62
out 0x20,al
Blokowanie i odblokowywanie przerwań sprzętowych
[edytuj]Kiedy wywołamy procedurę reroute_irqs (tę z irqroute.asm), to wszystkie przerwania sprzętowe zostaną zablokowane. W rejestrach 0x21 oraz 0xA1 wpisana zostaje tzw. maska przerwań. Maska przerwań to jedno słowo (16 bitów, ponieważ jest 16 przerwań sprzętowych). Maska ta jest zanegowana, czyli gdy chcemy odblokować przerwanie, czyścimy bit maski a gdy chcemy zablokować przerwanie, "zapalamy" go. Dodajemy teraz do intr.c następujący tekst:
static unsigned int cached_irq_mask = 0xffff;
#define __byte(x,y) (((unsigned char *)&(y))[x])
#define cached_21 (__byte(0,cached_irq_mask))
#define cached_A1 (__byte(1,cached_irq_mask))
void disable_irq(unsigned int irq)
{
unsigned int mask = 1 << irq;
cached_irq_mask |= mask;
if (irq & 8)
{
outb(cached_A1,0xA1);
} else {
outb(cached_21,0x21);
}
}
void enable_irq(unsigned int irq)
{
unsigned int mask = ~(1 << irq);
cached_irq_mask &= mask;
if (irq & 8) {
outb(cached_A1,0xA1);
} else {
outb(cached_21,0x21);
}
}
Procedura enable_irq służy do odblokowywania przerwań, a disable_irq do blokowania przerwań. Procedury te są na tyle proste, że nie wymagają tłumaczenia. Jedynie ten fragment:
#define __byte(x,y) (((unsigned char *)&(y))[x])
#define cached_21 (__byte(0,cached_irq_mask))
#define cached_A1 (__byte(1,cached_irq_mask))
może się wydać skomplikowany, ale takie konstrukcje często się stosuje przy pisaniu systemów. __byte(x,y) robi z y tablicę bajtów a x jest indeksem. Kompilator zamieni to na odpowiednie offsety w zmiennej cached_irq_mask, zamiast stosować przesunięcia rejestrów i operację and.
Trochę o klawiaturze
[edytuj]Jak wiadomo istnieją różne typy klawiatur. Są klawiatury na standardowe złącze DIN (które wyszło już z użycia), PS/2 (nadal popularne) oraz USB. Różnią się one jedynie złączem oraz liczbą bajerów, w jakie wyposażył je producent. Programowanie klawiatur jest takie same.
Klawiatura wykorzystuje przerwanie sprzętowe 1. Nieważne, czy to jest klawiatura USB czy na złącze PS/2, przerwanie 1 było, jest i będzie zarezerwowane dla klawiatury. Klawiatura używa także portów 0x60 i 0x64. Port 0x60 służy do przesyłania danych, a 0x64 do przesyłania poleceń.
Klawiatury używają mikroprocesora 8048 (w nowszych modelach może być inny, ale kompatybilny układ). Na płycie głównej jest układ 8042 (w nowszych komputerach jest pewnie inny układ do tego). Kiedy się naciśnie lub zwolni przycisk, klawiatura przesyła 0 lub więcej bajtów. Wtedy generowane jest także przerwanie sprzętowe IRQ1. Dana wysłana przez klawiaturę nazywana jest scan code. Program obsługi przerwania klawiatury musi pobrać te dane z portu 0x60.
Uwaga!
|
Można także używać oczekiwania zamiast przerwań, aby odczytać dane z klawiatury.
Wyróżniamy 3 zestawy kodów wysyłanych przez klawiaturę:
- IBM PC XT
- IBM PC AT
- IBM PS/2
Tylko zestaw 2 jest poprawnie zaimplementowany we wszystkich kontrolerach klawiatury. Pozostałe zestawy mogą zawierać bugi oraz mogą być niepoprawnie zaimplementowane.
Mapa zestawu 2 (przetłumaczona):
Kody "Make" są generowane, gdy klawisz zostanie wciśnięty.
Kody "Break" są generowane, gdy klawisz zostanie zwolniony.
Większość klawiszy:
- jedno bajtowy kod make = nn
- jedno bajtowy kod powtórzenia = nn
- dwu bajtowy kod break = f0nn
"Szare" klawisze (nie ma ich na oryginalnej 84-klawiszowej klawiaturze):
- dwu bajtowy kod make = e0nn
- dwu bajtowy kod powtórzenia = e0nn
- trzy bajtowy kod break = 0ef0nn
"Szare" klawisze są zaznaczone przez [1] i są zależne od NumLock:
Kiedy NumLock na klawiaturze jest włączone:
- czterobajtowy kod make = e012e0nn
- dwubajtowy kod powtórzenia = e0nn
- sześciobajtowy kod break = e0f0nne0f012
___ _______________ _______________ _______________ | | | | | | | | | | | | | | | | | |Esc| |F1 |F2 |F3 |F4 | |F5 |F6 |F7 |F8 | |F9 |F10|F11|F12| | 76| | 05| 06| 04| 0C| | 03| 0B| 83| 0A| | 01| 09| 78| 07| |___| |___|___|___|___| |___|___|___|___| |___|___|___|___| _____________________________________ ___________________________________ | | | | | | | | | | | | | | | | |~ |! |@ |# |$ |% |^ |& |* |( |) |_ |+ || |bksp| |` |1 |2 |3 |4 |5 |6 |7 |8 |9 |0 |- |= |\ | | | 0E| 16| 1E| 26| 25| 2E| 36| 3D| 3E| 46| 45| 4E| 55| 5D| 66| |____|____|____|____|____|____|____|___ |____|____|____|____|____|____|____| | | | | | | | | | | | | | | | |Tab |Q |W |E |R |T |Y |U |I |O |P |{ |} | | | | | | | | | | | | | |[ |] | | | 0D| 15| 1D| 24| 2D| 2C| 35| 3C| 43| 44| 4D| 54| 5B| | |____|____|____|____|____|____|____|___ |____|____|____|____|____| | | | | | | | | | | | | | | | |Caps|A |S |D |F |G |H |J |K |L |: |" | Enter | | | | | | | | | | | |; |' | | | 58| 1C| 1B| 23| 2B| 34| 33| 3B| 42| 4B| 4C| 52| 5A| |____|____|____|____|____|____|____|___ |____|____|____|____|______________| | | | | | | | | | | | | | | L Shift |Z |X |C |V |B |N |M |< |> |? | R Shift | | | | | | | | | |, |. |/ | | | 12| 1A| 22| 21| 2A| 32| 31| 3A| 41| 49| 4A| 59| |_________|____|____|____|____|____|___ |____|____|____|____|______________| | | | | | | | | | |L Ctrl | L win | L Alt | space | R Alt | R win | menu |R Ctrl | | |[1] | | | |[1] |[1] | | | 14| E01F| 11| 29| E011| E027| E02F| E014| |_______|_______|_______|______________ ___|_______|_______|_______|_______|
Dla klawisza PrintScreen/SysRq:
- kod make = e012e07c
- kod powtózenia = e07c
- kod break = e0f07ce0f012
Klawisz Pause/Break nie generuje powtórzeń ani kodu break. Jego kod make to e11477e1f014f077
____ ____ ____ | | | | |Prt |Scrl|Paus| |Scrn|Lock|Brk | | [2]| 7E| [3]| |____|____|____| ____ ____ ____ ____ ____ ____ ____ | | | | | | | | | |Ins |Home|PgUp| |Num |/ |* |- | |[1] |[1] |[1] | |Lock| | | | |E070|E06C|E07D| | 77|E04A| 7C| 7B| |____|____|____| |____|____|____|____| | | | | | | | | | |Del |End |PgDn| |7 |8 |9 | | |[1] |[1] |[1] | |Home|(U) |PgUp| | |E071|E069|E07A| | 6C| 75| 7D| | |____|____|____| |____|____|____| | | | | |+ | |4 |5 |6 | | |(L) | |(R) | | | 6B| 73| 74| 79| ____ |____|____|____|____| | | | | | | | |(U) | |1 |2 |3 | | |[1] | |End |(D) |PgDn| | |E075| | 69| 72| 7A|Ent | ____|____|____ |____|____|____| | | | | | | | | | |(L) |(D) |(R) | |0 |. | | |[1] |[1] |[1] | |Ins |Del | | |E06B|E072|E074| | 70| 71|E05A| |____|____|____| |_________|____|____| kod klawisz kod klawisz kod klawisz kod klawisz --- ------- --- ------- --- ------- --- ------- 01 F9 66 BackSpace 21 C 41 ,< 03 F5 22 X 42 K 69 End 1 04 F3 23 D 43 I 05 F1 24 E 44 O 6B (left) 4 06 F2 25 4$ 45 0) 6C Home 7 07 F12 26 3# 46 9( 70 Ins 0 09 F10 29 Space 49 .> 71 Del . 0A F8 2A V 4A /? 72 (down) 2 0B F6 2B F 4B L 73 5 0C F4 2C T 4C ;: 74 (right) 6 0D Tab 2D R 4D P 75 (up) 8 0E `~ 2E 5% 4E -_ 76 Esc 77 NumLock 11 L Alt 31 N 52 '" 78 F11 12 L Shift 32 B 79 + 33 H 54 [{ 7A PageDown 3 14 L Ctrl 34 G 55 =+ 7B - 15 Q 35 Y 7C * 16 1! 36 6^ 58 CapsLock 7D PageUp 9 59 R Shift 7E ScrollLock 1A Z 3A M 5A Enter 1B S 3B J 5B ]} 83 F7 1C A 3C U 1D W 3D 7& 5D \| 1E 2@ 3E 8* kod klawisz --- ------- E011 R Alt E012E07C Kod make dla PrintScreen E014 R Ctrl E01F L Win E027 R Win E02F Menu E04A / E05A Enter (na klawiaturze numerycznej) E069 End E06B (w lewo) E06C Home E070 Ins E071 Del E072 (w dół) E074 (w prawo) E075 (do góry) E07A PageDown E07C Kod powtórzenia PrintScreen E07D PageUp E0F07CE0F012 Kod break PrintScreen E11477E1F014F077 Pause
Oczywiście powyższej mapy klawiatury nie uczymy się na pamięć! :) Jest ona nam potrzebna tylko przy pisaniu sterownika klawiatury.
Sterownik klawiatury może pełnić również inne funkcje. Służy on także do obsługi portu PS/2 myszy (jeśli jest zamontowany), kontrolowania linii A20 oraz resetowania komputera :P
Przykładowy kod w assemblerzem jak zresetować komputer:
reset:
call kbd
mov al,0xfe
out 0x64,al
kbd0:
jmp short $+2
in al,60h
kbd: jmp short $+2
in al,64h
test al,1
jnz kbd0
test al,2
jnz kbd
ret
Piszemy kod przerwania klawiatury
[edytuj]Do pliku start.asm należy dodać następujący kod:
GLOBAL irq1
irq1:
push gs
push fs
push es
push ds
pusha
mov ax,0x10
mov ds,ax
mov es,ax
mov al,0x61
out 0x20,al
EXTERN do_irq1
call do_irq1
popa
pop ds
pop es
pop fs
pop gs
iret
Już powinniście wiedzieć o co robi ten kod. Jeśli nie, patrz punkt 5. Procedura jest podobna, tylko wywołuje naszą funkcję w C, która ma deklarację:
void do_irq1(void);
Uwaga!
|
My dopiero w procedurze do_irq1() robimy obsługę klawiatury. Procedura w assemblerze inicjalizuje rejestry segmentowe oraz kładzie na stos rejestry użytkownika, czego kompilator C za nas nie może zrobić. Nie ma co się rozpisywać o kodzie w assemblerze.
Bufor danych dla klawiatury
[edytuj]Moglibyśmy stworzyć zmienną, w której zapisany jest kod ostatnio wciśniętego klawisza, ale byłoby to bardzo niedoskonałe, ponieważ gdy naciśniemy kilka klawiszy, a program ich nie zdąży odczytać, to po prostu zostaną "zgubione". Dla tego stosuje się bufory dla klawiatury. My zastosujemy listę cykliczną. Jest to najprostszy sposób implementacji bufora FIFO (First In First Out).
typedef struct
{
/* dane w buforze */
unsigned char *data;
/* rozmiar, początek bufora, koniec bufora */
unsigned size, in_ptr, out_ptr;
} queue_t;
static int inq(queue_t *q, unsigned data)
{
unsigned temp;
temp = q->in_ptr + 1;
if(temp >= q->size) temp = 0;
/* jeśli in_ptr+1==out_ptr to znaczy, że kolejka jest pełna */
if(temp == q->out_ptr) return -1;
q->data[q->in_ptr] = data;
q->in_ptr = temp;
return 0;
}
static int deq(queue_t *q, unsigned char *data)
{
/* gdy out_ptr==in_ptr, znaczy że kolejka jest pusta */
if(q->out_ptr == q->in_ptr) return -1;
*data = q->data[q->out_ptr++];
if(q->out_ptr >= q->size) q->out_ptr = 0;
return 0;
}
static int empty(queue_t *q)
{
return q->out_ptr == q->in_ptr;
}
Funkcja inq służy do wstawiania danych do kolejki, a deq do pobierania danych z kolejki. inq i deq zwracają , gdy kolejka jest odpowiednio pełna lub pusta, lub gdy wstawiono znak poprawnie.
Nie będę tutaj opisywał zasady działania kolejki, ponieważ nie ma tu na to miejsca.
Obsługa przerwania klawiatury
[edytuj]Nareszcie. Teraz napiszemy właściwy kod w C.
Uwaga!
|
#define KBD_QUEUE_SIZE 64
static unsigned char kbd_queue_data[KBD_QUEUE_SIZE];
static queue_t kbd_buf={
kbd_queue_data,
KBD_QUEUE_SIZE,
0,
0
};
static int read_kbd(void)
{
unsigned long timeout;
unsigned stat, data;
for(timeout = 500000L; timeout != 0; timeout--)
{
stat = inportb(0x64);
/* czekaj gdy bufor klawiatury jest pełny */
if(stat & 0x01)
{
data = inportb(0x60);
/* pętla, gdt błąd parzystości, lub koniec czasu oczekiwania */
if((stat & 0xC0) == 0) return data;
}
}
return -1;
}
static void write_kbd(unsigned adr, unsigned data)
{
unsigned long timeout;
unsigned stat;
for(timeout = 500000L; timeout != 0; timeout--)
{
stat = inportb(0x64);
/* czekaj gdy bufor klawiatury nie zrobi się pusty */
if((stat & 0x02) == 0) break;
}
if(timeout == 0)
{
puts("write_kbd: timeout\n");
return;
}
outportb(adr, data);
}
#define KEY_F1 0x80
#define KEY_F2 (KEY_F1 + 1)
#define KEY_F3 (KEY_F2 + 1)
#define KEY_F4 (KEY_F3 + 1)
#define KEY_F5 (KEY_F4 + 1)
#define KEY_F6 (KEY_F5 + 1)
#define KEY_F7 (KEY_F6 + 1)
#define KEY_F8 (KEY_F7 + 1)
#define KEY_F9 (KEY_F8 + 1)
#define KEY_F10 (KEY_F9 + 1)
#define KEY_F11 (KEY_F10 + 1)
#define KEY_F12 (KEY_F11 + 1)
#define KEY_INS 0x90
#define KEY_DEL (KEY_INS + 1)
#define KEY_HOME (KEY_DEL + 1)
#define KEY_END (KEY_HOME + 1)
#define KEY_PGUP (KEY_END + 1)
#define KEY_PGDN (KEY_PGUP + 1)
#define KEY_LFT (KEY_PGDN + 1)
#define KEY_UP (KEY_LFT + 1)
#define KEY_DN (KEY_UP + 1)
#define KEY_RT (KEY_DN + 1)
#define KEY_PRNT (KEY_RT + 1)
#define KEY_PAUSE (KEY_PRNT + 1)
#define KEY_LWIN (KEY_PAUSE + 1)
#define KEY_RWIN (KEY_LWIN + 1)
#define KEY_MENU (KEY_RWIN + 1)
#define KBD_META_ALT 0x0200
#define KBD_META_CTRL 0x0400
#define KBD_META_SHIFT 0x0800
#define KBD_META_ANY (KBD_META_ALT | KBD_META_CTRL | KBD_META_SHIFT)
#define KBD_META_CAPS 0x1000
#define KBD_META_NUM 0x2000
#define KBD_META_SCRL 0x4000
#define RAW1_LEFT_CTRL 0x1D
#define RAW1_LEFT_SHIFT 0x2A
#define RAW1_CAPS_LOCK 0x3A
#define RAW1_LEFT_ALT 0x38
#define RAW1_RIGHT_ALT 0x38
#define RAW1_RIGHT_CTRL 0x1D
#define RAW1_RIGHT_SHIFT 0x36
#define RAW1_SCROLL_LOCK 0x46
#define RAW1_NUM_LOCK 0x45
#define RAW1_DEL 0x53
static int set1_scancode_to_ascii(unsigned code)
{
static const unsigned char map[] =
{
/* 00 */0, 0x1B, '1', '2', '3', '4', '5', '6',
/* 08 */'7', '8', '9', '0', '-', '=', '\b', '\t',
/* 10 */'q', 'w', 'e', 'r', 't', 'y', 'u', 'i',
/* 1Dh to lewy Ctrl */
/* 18 */'o', 'p', '[', ']', '\n', 0, 'a', 's',
/* 20 */'d', 'f', 'g', 'h', 'j', 'k', 'l', ';',
/* 2Ah to lewy Shift */
/* 28 */'''', '`', 0, '\\', 'z', 'x', 'c', 'v',
/* 36h to prawy Shift */
/* 30 */'b', 'n', 'm', ',', '.', '/', 0, 0,
/* 38h to lewy Alt, 3Ah to Caps Lock */
/* 38 */0, ' ', 0, KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5,
/* 45h to Num Lock, 46h to Scroll Lock */
/* 40 */KEY_F6, KEY_F7, KEY_F8, KEY_F9, KEY_F10,0, 0,KEY_HOME,
/* 48 */KEY_UP, KEY_PGUP,'-', KEY_LFT,'5', KEY_RT, '+', KEY_END,
/* 50 */KEY_DN, KEY_PGDN,KEY_INS,KEY_DEL,0, 0, 0, KEY_F11,
/* 58 */KEY_F12
};
static const unsigned char shift_map[] =
{
/* 00 */0, 0x1B, '!', '@', '#', '$', '%', '^',
/* 08 */'&', '*', '(', ')', '_', '+', '\b', '\t',
/* 10 */'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I',
/* 18 */'O', 'P', '{', '}', '\n', 0, 'A', 'S',
/* 20 */'D', 'F', 'G', 'H', 'J', 'K', 'L', ':',
/* 28 */'"', '~', 0, '|', 'Z', 'X', 'C', 'V',
/* 30 */'B', 'N', 'M', '<', '>', '?', 0, 0,
/* 38 */0, ' ', 0, KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5,
/* 40 */KEY_F6, KEY_F7, KEY_F8, KEY_F9, KEY_F10,0, 0, KEY_HOME,
/* 48 */KEY_UP, KEY_PGUP,'-', KEY_LFT,'5', KEY_RT, '+', KEY_END,
/* 50 */KEY_DN, KEY_PGDN,KEY_INS,KEY_DEL,0, 0, 0, KEY_F11,
/* 58 */KEY_F12
};
static unsigned saw_break_code, kbd_status;
unsigned temp;
/* sprawdź czy kod break (np. gdy klawisz został zwolniony) */
if(code >= 0x80)
{
saw_break_code = 1;
code &= 0x7F;
}
/* kod break, które nas na razie interesują
to Ctrl, Shift, Alt */
if(saw_break_code)
{
if(code == RAW1_LEFT_ALT || code == RAW1_RIGHT_ALT)
kbd_status &= ~KBD_META_ALT;
else if(code == RAW1_LEFT_CTRL || code == RAW1_RIGHT_CTRL)
kbd_status &= ~KBD_META_CTRL;
else if(code == RAW1_LEFT_SHIFT || code == RAW1_RIGHT_SHIFT)
kbd_status &= ~KBD_META_SHIFT;
saw_break_code = 0;
return -1;
}
/* jeśli to kod make: sprawdź klawisze "meta"
podobnie jak powyżej */
if(code == RAW1_LEFT_ALT || code == RAW1_RIGHT_ALT)
{
kbd_status |= KBD_META_ALT;
return -1;
}
if(code == RAW1_LEFT_CTRL || code == RAW1_RIGHT_CTRL)
{
kbd_status |= KBD_META_CTRL;
return -1;
}
if(code == RAW1_LEFT_SHIFT || code == RAW1_RIGHT_SHIFT)
{
kbd_status |= KBD_META_SHIFT;
return -1;
}
/* Scroll Lock, Num Lock, i Caps Lock ustawiają diody LED. */
if(code == RAW1_SCROLL_LOCK)
{
kbd_status ^= KBD_META_SCRL;
goto LEDS;
}
if(code == RAW1_NUM_LOCK)
{
kbd_status ^= KBD_META_NUM;
goto LEDS;
}
if(code == RAW1_CAPS_LOCK)
{
kbd_status ^= KBD_META_CAPS;
LEDS:
write_kbd(0x60, 0xED); /* komenda "set LEDS" */
temp = 0;
if(kbd_status & KBD_META_SCRL)
temp |= 1;
if(kbd_status & KBD_META_NUM)
temp |= 2;
if(kbd_status & KBD_META_CAPS)
temp |= 4;
write_kbd(0x60, temp);
return -1;
}
/* brak konwersji, gdy Alt jest naciśnięty */
if(kbd_status & KBD_META_ALT)
return code;
/* konwertuj A-Z[\]^_ na kody sterowania */
if(kbd_status & KBD_META_CTRL)
{
if(code >= sizeof(map) / sizeof(map[0]))
return -1;
temp = map[code];
if(temp >= 'a' && temp <= 'z')
return temp - 'a';
if(temp >= '[' && temp <= '_')
return temp - '[' + 0x1B;
return -1;
}
/* konwertuj kod skanowania na kod ASCII */
if(kbd_status & KBD_META_SHIFT)
{
/* ignoruj niepoprawne kody */
if(code >= sizeof(shift_map) / sizeof(shift_map[0])) return -1;
temp = shift_map[code];
if(temp == 0) return -1;
/* caps lock? */
if(kbd_status & KBD_META_CAPS)
{
if(temp >= 'A' && temp <= 'Z')
temp = map[code];
}
} else {
if(code >= sizeof(map) / sizeof(map[0]))
return -1;
temp = map[code];
if(temp == 0)
return -1;
if(kbd_status & KBD_META_CAPS)
{
if(temp >= 'a' && temp <= 'z')
temp = shift_map[code];
}
}
return temp;
}
void do_irq1(void)
{
int klawisz=inportb(0x60);
klawisz=set1_scancode_to_ascii(klawisz);
if(klawisz>0 && klawisz<256)
{
inq(&kbd_buf,klawisz);
}
}
Uff... Trochę tego było. Kod powinien być łatwy do zrozumienia, ponieważ miejscami jest skomentowany. read_kbd i write_kbd są używane do sterowania klawiaturą. Normalnie nie ma sensu ich używać. set1_scancode_to_ascii konwertuje kod podany przez klawiaturę na kody ASCII. Jak zapewne zauważyliście, klawiatura nie wysyła znaków ASCII tylko kody klawiszy, które następnie trzeba przekształcić na kody ASCII. Jeszcze trzeba napisać procedury, które nam zainicjalizują klawiaturę. Nie ma tak dobrze ;)
extern void irq1(void);
void kbd_init(void)
{
set_intr_gate(0x21,&irq1);
enable_irq(1);
}
Zmodyfikowałem jeszcze main.c i intr.h, ale to można sobie obejrzeć w dołączonym pliku .zip. Sterownik klawiatury jest w pliku kbd.c. Cały kod został przetestowany pod emulatorem PC Bochs.
Proponuję uruchomić teraz naszego kernela i pobawić się klawiaturą ;)
Zakończenie
[edytuj]Najpierw sprawy organizacyjne: Od teraz będziemy używać skryptów Makefile. Jeśli ktoś nie wie co to jest, niech poczyta o programie make, lub kompiluje wszystko ręcznie. Dla "zielonych" ułatwiłem sprawę i dodałem skrypt make i powiem tylko. Dodawanie nowych plików do Makefile jest dziecinnie proste. W naszym przypadku dodajemy pliki na końcu linijki OBJS = ....
Uwaga!
|
Teraz piszemy make i naciskamy Enter. Kompilują nam się tylko te pliki, które zostały zmienione, albo nie były jeszcze skompilowane.
Ćwiczenie
|