Podstawy programowania na przerwaniach
ŁF
Co to jest przerwanie? Najogólniej mówiąc jest to procedura obsługi
jakiegoś zdarzenia - sprzętowego, np.: ruchu myszą, wciśnięcia klawisza,
zegara RTC, bądź programowego: sprawdzenia czy klawisz został naciśnięty,
włączenia jakiegoś trybu graficznego, odczyt/zapis z dysku itp.
Zasadnicza różnica polega na tym, czy przerwania zażądał sprzęt, czy wywołało
je oprogramowanie.
My zajmiemy się oczywiście tym drugim przypadkiem (jak ktoś chce zobaczyć
przykład działania przerwań sprzętowych, niech walnie młotkiem w napęd
CD i spróbuje coś odczytać; uwaga: nie polecam tej metody).
Przerwań jest 256, ich adresy są umieszczone w pamięci pod adresem 0:0
w tablicy o rozmiarze 1024 bajtów (1 kB). każdy adres to cztery bajty:
dwa na segment i dwa na offest adresu - czyli innymi słowy typ pointer.
Przerwanie wywołuje się pod assemblerem instrukcją int z zapodanym numerem,
wszelakie parametry przekazuje się w rejestrach (umieszczając w ax odpowiedni
numer wywołuje się jedną z funkcji danego prerwania); często zachodzi potrzeba
użycia rejestru ds, wtedy wrzuca się go na stos, a po wykonaniu przerwania
zdejmuje (instrukcje push i pop).
OK, to teraz konkrety. Na początek bardzo ogólny spis przerwań. Zainteresowanych
odsyłam do pliku int.tph [nie znam nowego adresu pliku, więc usunąłem linka - Vogel], gdzie znajduje
się spis praktycznie wszystkich przerwań i ich funkcji. Literka "h" po liczbie
oznacza zapis heksadecymalny, czyli po polskiemu szesnastkowy ;-) (ostatnia cyfra+
przedostatnia16+przed_przedostatnia256 itp, gdzi A=10, F=15).
# przerwania | Opis |
00h | Błąd dzielenia przez zero |
10h | BIOS: karta graficzna: ah=0: ustawianie rozdzielczości i trybu (np.: al=03h - tryb tekstowy 80x25,16 kolorów; al=13h - tryb graficzny 320x200, 256 kolorów); ax=4F02h: ustawianie rozdzielczości SVGA (np.: bx=101h - 640x480x8; bx=103h - 800x600x8; bx=10Fh - 320x200x24) |
13h | BIOS: twardy dysk - bezpośredni odczyt, zapis i formatowanie sektorów |
16h | BIOS: klawiatura: ax=00h - czeka na naciśnięcie klawisza, zwraca w ax jego kod i czyści bufor (readkey); ax=01h - zwraca w ax kod naciśniętego klawisza (keypressed); ax=10h - jak ax=0, tylko zwraca kod rozszerzony (wraz z F11, F12 itp); ax=11h - jak ax=1, tylko dla kodu rozszerzonego. |
19h | reset systemu; pod windowsem natomiast błyskawicznie zamyka program |
1Ah | BIOS: czas systemowy - pobieranie i ustawianie |
1Bh | Przerwanie wywoływane przez naciśnięcie Ctrl+Break |
1Ch | Przerwanie wywoływane co 1/18 sekundy |
21h |
..itp itd.
Małe wyjaśnienie na dzień dobry: moduł dos udostępnia coś takiego jak typ
TRegisters oraz procedury Intr i MsDos; są one jednak tragicznie powolne w
porównaniu z gołym assemblerem, ponadto dorzucają dodatkowy kod do pliku exe,
czyli w zasadzie niepotrzebnie zwiększają jego rozmiar.
Jeśli jednak przeraża Cię assembler, możesz posługiwać się typem TRegisters,
w sumie chodzi o to, żeby coś zrobić i żeby ewentualnie było to szybkie,
nie odwrotnie. Po co komu szybki program, który nic nie robi, bo jego autor
zrezygnował z połowy jego funkcji na rzecz szybkości?
Więc zacznijmy od czegoś miłego i łatwego z cyklu int2Fh:
function DOS7 : boolean; assembler;
asm
push SI {na stos te rejestry, które muszą być takie same po wyjściu}
push DS {z procedury, a jest szansa, że przerwanie je zmieni}
mov ax, 4A33h {wybranie odpowiedniej funkcji przerwania}
int 2Fh {wywołanie przerwania}
pop ds {zdjęcie ze stosu (uwaga! kolejność zdejmowania odwrotna}
pop si {do kolejności wsadzania}
{wynik w al; al=0 - DOS >= 7.0 zainstalowany, al=1 - DOS >= 7.0 nie zainstalowany}
{ cmp ax,0 pierwsza metoda: ax=0?
jz @@1 tak? skacz do @@1
mov ax,0 nie? ax:=0
jmp @@2 skacz do @@2
@@1:
mov ax,1 ax:=1
@@2:}
not al {druga metoda: negacja al - z prawdy robi fałsz i odwrotnie}
end;
W wyniku funkcja zwróci true jeśli masz DOSa w wersji conajmniej 7.0, co oznacza,
że masz zainstalowanego minimum Win95. Jak zauważyłeś, funkcja ma w nagłówku dyrektywe assembler - oznacza to, że w nim jest cała zrobiona i jako taka nie potrzebuje
kilku dodatków niezbędnych zwykłym procedurom. Dzięki temu wywołuje się szybciej.
Jeśli funkcja zwraca coś o wielkości bajta lub worda, to jest to wartość umieszczona w rejestrze ax.
To skoro potrafimy sprawdzić wersję DOSu, to pora na wersję Windowsa. Z pomocą
znowu przychodzi multipleks 2Fh. Przy okazji można sprawdzić, czy program dosowy
został uruchomiony z Windowsem działającym w tle (uwaga, we właściwościach skrótu
do dosowego pliku exe można ustawić wyłączenie tej funkcji -
właściwości->program->zaawansowane->zapobiegaj wykrywaniu Windows... )
function WinVer : word; {= 0 dos, < 0 windoza} assembler; { 400h->4.0 = Win95, 401h -> 4.1 - Win98 itp} asm mov ax,160Ah int 2Fh cmp ax,0 jz @@1 xor ax,ax {szybsze ax=0} jmp @@2 @@1: mov ax,bx {jeśli ax=0, to wersja windows w bx; bh - 4, bl - podwersja} @@2: end;
Proste, prawda? BTW: Multipleks 2Fh do społu z 21h obsługują 99% wszelkich
odwołań systemowych (tzn. takich, gdzie poprzez przerwanie wywoływana jest
funkcja - coś jak wywoływanie funkcji po indeksie z bibliotek dll - ale to
tak nawiasem mówiąc ;)).
Teraz kolej na procesor (niechcący przerwanie 15h, ale to wyjątek który potwierdzi regułę):
Function CPUVer : byte; assembler; asm mov ah,0C9h mov al,10h int 15h mov al,ch end; {03h 80386DX or clone 04h 80486 05h Pentium 23h 80386SX or clone 33h Intel i376 43h 80386SL or clone A3h IBM 386SLC A4h IBM 486SLC}
Aha - ta funkcja oszukuje, tzn. na procesorach powyżej 486 nie zwraca prawidłowych wartości.
Sprawdźmy jeszcze, co tam u myszy słychać - zwierzę to siedzi na przerwaniu 33h,
niezależnie od fizycznego podłączenia myszy (tzn. jest to przerwanie sterownika,
a nie samej myszy).
function MouseVersion : word; assembler;
asm
mov ax,0024h
int 33h
cmp ax,$FFFF
jz @@1
mov ax,bx
jmp @@2
@@1:
mov ax,0
@@2:
end;
Zero - mysz nie podłączona, każdy inny wynik zwraca wersję sterownika.
Dotatkowo można pokusić się o doklejenie kodu, który będzie wykorzystywać
poniższe informacje:
CH = typ: 1=bus, 2=serial, 3=InPort, 4=PS/2, 5=HP;
CL = przerwanie: 0=PS/2, 2=IRQ2, 3=IRQ3, ..., 7=IRQ7;
albo zrobienie oddzielnych funkcji.
To może teraz konsekwentnie więcej o myszy? Jedziemy!
Najpierw pobieranie i zmienianie ustawień tego zwierzaczka.
procedure GetMouseSenst(var h,v,d : word); assembler;
asm
mov ax,001Bh
int 33h
les di,h
mov [es:di],bx
les di,v
mov [es:di],cx
les di,d
mov [es:di],dx
end;
Pierwszy parametr zwraca przyspieszenie wskaźnika w poziomie, drugi w pionie,
a trzeci odstęp czasu w milisekundach który musi upłynąć, aby dwa kliknięcia
nie były uważane za podwójne kliknięcie. Procedura jest trochę połatana (trzy
instrukcje les), ale działa znakomicie.
Teraz przydałoby się ustawić jakieś zmiany. Oto Wielce Skomplikowana I Jakże
Trudna Procedura Do Robienia Zmian.
procedure SetMouseSenst(h,v,d : word); assembler;
asm
mov ax,001Ah
mov bx,h
mov cx,v
mov dx,d
int 33h
end;
To tyle o myszy. W następnym odcinku pomęczymy przewanie 21h (tzw. DOS multiplex).
Następny odcinek ;-)
Oto jest w DOSie taka funkcja o bardzo długiej nazwie, służąca do odczytywania
danych z tablicy partycji dysków. Aż prosi się o to, żeby z niej skorzystać - więc
skorzystamy. Najpierw definicja typu:
PDPBRec = ^DPBRec; DPBRec = record num : byte; {drive number (00h = A:, 01h = B:, etc} unit_nun : byte; {unit number within device driver} bytes_per_sector : word; {bytes per sector} highsec : byte; {highest sector number within a cluster} reserved1 : byte; {shift count to convert clusters into sectors} reserv_sectors : word; {number of reserved sectors at beginning of drive} num_of_FATs : byte; {number of FATs} root_entries : word; {number of root directory entries} first_user_sector : word; {number of first sector containing user data} highest_cluster : word; {highest cluster number (number of data clusters + 1) 16-bit FAT if greater than 0FF6h, else 12-bit FAT} sectors_per_FAT : byte; {number of sectors per FAT} first_dir_sector : word; {sector number of first directory sector} device_driver : pointer; {address of device driver header (see #0987)} media_byte : byte; {media ID byte (see #0703)} access : byte; {00h if disk accessed, FFh if not} next_DPB : PDPBRec; {pointer to next DPB} writing_clust : word; {cluster at which to start search for free space when writing, usually the last cluster allocated} free_clusters : word; {number of free clusters on drive, FFFFh = unknown} end;
Jak widzisz jest tu mnóstwo ciekawych rzeczy, co prawda dotyczących fatu 16,
ale dobre i to na start. Numery tabel, podane w komentarzach, odnoszą się do
pliku pomocy int.tph.
Zapewne przyda się procedura, która wypełni ten rekord odpowiednimi danymi?...
I oto jest; zwraca true dla pomyślnie wykonanej operacji.
function GetDPB(num : byte;var data : PDPBRec) : boolean; assembler;
asm
mov ah,32h
mov dl,num
push ds
int 21h
cmp al,0
jne @@1
les di,data
mov [es:di+2],ds
mov [es:di],bx
@@1:
pop ds
not al
end;
To może teraz coś z innej bajki (wracamy do przerwania 2Fh) - pobieranie
i ustawianie strony kodowej (oczywiście DOSowej):
procedure SET_ACTIVE_CODE_PAGE(page : word); assembler; asm mov ax,0AD01h mov bx,page int 2Fh end; function GET_ACTIVE_CODE_PAGE : word; assembler; asm mov ax,0AD02h int 2Fh mov ax,bx end;
I znowu inna bajka: parkowanie dysków (tzn. ustawianie głowic w miejscach,
gdzie nie uszkodzą dysku przy jego potrząsaniu).
function ParkDisk(num : byte) : boolean;{80h = first, 81h = second hard disk} assembler; asm mov ah,0Dh mov dl,num int 13h jnc @@1 xor ax,ax jmp @@2 @@1: mov ax,1 @@2: end;
Te i dużo innych funkcji znajduje się w module int2f, który możesz ściągnąć
tutaj. Plik pomocy Turbo Pascala, na
podstawie którego powstał ninijeszy artykuł, jest tu.
Z tych przerwań które zostały, ważnym i dość często używanym, choć przeważnie
nie bezpośrednio, jest 10h. Rzut oka na tabelę wyżej i już wiesz, że chodzi o
kartę graficzną. Dokładnie. Więc teraz szybki kurs programowania w trybie 13h
(rozdzielczość 320x200, 256 kolorów) - tzw. tryb X.
Najpierw włączanie i wyłączanie trybu graficznego: zapodajesz w rejestrze al numer
trybu, ustawiasz ah = 0 i wywołujesz przerwanie 10h. Czyli:
procedure SetVGAMode(mode : byte); assembler;
asm
mov al, mode
xor ah,ah
int 10h
end;
Przerwanie 10h lubi jednak czasem psuć kilka potrzebnych rejestrów,
więc do powyższej procedury jest potrzebna mała modyfikacja:
procedure Int10h; assembler; asm PUSH DS PUSH SS PUSH BP PUSH SP INT 10H POP SP POP BP POP SS POP DS end; procedure SetVGAMode(mode : byte); assembler; asm mov al, mode xor ah,ah call Int10h end;
I już nic się nie popsuje.
Więc teraz kombinujesz co tam podać jako parametr? Więc dwie podstawowe wartości:
03h to tryb tekstowy 80x25x16 kolorów, 13h (19d) to już wspomniany tryb graficzny X:
320x200x8bpp. Jak się bazgrze po ekranie w takim trybie?
Otóż 320*200 = 64000 bajtów. Jeden segment - w sam raz na możliwości TP.
Pamięć trybu graficznego jest umieszczona pod adresem 0A000h:0. Na tej podstawie
możesz sobie zrobić procedurki putpixel i getpixel:
procedure PutPixel(x,y,color : byte); begin mem[$A000:word(y)*320+word(x)] := color end; function GetPixel(x,y,color : byte) : byte; begin GetPixel := mem[$A000:word(y)*320+word(x)] end;
albo w wersji assemblerowej:
procedure PutPixel(x,y,color : byte); assembler; asm xor ah,ah xor bh,bh mov al,y mov bx,320 mul bx mov bl,x add bx,ax mov ax,0A000h mov es,ax mov al, color mov [es:bx], al end; function GetPixel(x,y : byte) : byte; assembler; asm xor ah,ah xor bh,bh mov al,y mov bx,320 mul bx mov bl,x add bx,ax mov ax,0A000h mov es,ax mov al,[es:bx] end;
W sumie prosta sprawa. Ale można tu robić takie sztuczki, że głowa mała.
Zapraszam do ściągnięcia biblioteki xvga,
w której jest gigantyczna ilość procedur do obróbki grafiki trójwymiarowej
wyświetlanej właśnie w trybie X.
Z przerwania 10h można wyciągnąć znacznie więcej niż tylko tryb 13h. Spójrz
na tą tabelkę. Sporo tego jest... Ale nie
wszsytko jest do osiągnięcia na Twojej karcie graficznej; jeśli chcesz się
przekonać co konkretnie możesz uzyskać u siebie, ściągnij ten programik.
Co jeszcze można wyczarować na przerwaniach? Ano - może być obsługa klawiatury.
Wreszcie nie trzeba będzie korzystać z modułu crt... Popatrzmy: klawiatura wisi
na przerwaniu 16h, można z tego skorzystać i zrobić własne readkey i keypressed.
Oto nadchodzi:
var ScanCode : byte; function ReadKey : char; assembler; asm MOV AL,ScanCode MOV ScanCode,0 OR AL,AL JNE @@3 mov ah,10h INT 16H CMP AL,0E0h JZ @@1 JMP @@2 @@1: MOV AL,0 @@2: OR AL,AL JNE @@3 MOV ScanCode,AH OR AH,AH JNE @@3 MOV AL,'C'-64 @@3: end; function KeyPressed : boolean; assembler; asm CMP ScanCode,0 JNE @@1 mov ah,11h INT 16H XOR AL,AL JE @@2 @@1: MOV AL,1 @@2: end;
Obie funkcje są całkowicie kompatybilne z tymi z modułu crt, zwracają przy tym
wszystkie rozszerzone kody - F11, F12, Alt+(Spacja, kursory, Enter) itp.
A jak zrobić własną funkcję obsługującą przerwania? Więc po pierwsze: da się.
Po drugie: robisz zupełnie normalną procedurę (jedna rzecz jest wymagana: musi ona
być jak najszybsza, żeby nie blokowała komputera kiedy będzie zbyt często wywoływana,
lub musi byc zabezpieczona przed powielaniem samej siebie, tzn. wywołaniem przerwania
zanim skończy się obsługa wywołania poprzedniego). Więc tworzysz normalną procedurę,
jako parametry podajesz (Flags, CS, IP, AX, BX, CX, DX, SI, DI, DS, ES, BP: Word)
a nastepnie w nagłówku dodajesz słowo interrupt. Ustawiasz ją zamiast jakiegoś
przerwania procedurą SetIntVec z modułu dos. Ale uwaga! Musisz zapamiętać adres
procedury, która wcześniej obsługiwała dane przerwanie, i przy zakończeniu programu
przywrócić ją; inaczej wywołanie przerwania będzie trafiało w miejsce, w którym kiedyś
była Twoja procedura, a teraz mogą być śmiecie. Jak się zapewne domyślasz do pobrania
adresu kodu obsługującego przerwanie służy procedura GetIntVec.
Ponadto jeśli parametry nie należą do dziedziny obsługiwanej przez Twoją procedurę,
należy również wywołać jej poprzednika (którego to wcześniej na szczęście zachowałeś).
W ten sposób tworzy się łańcuszek procedur, każda sprawdza, czy np.: w ax jest
podany numer ich funkcji, a jeśli nie, to żądanie przerwania podają dalej.
Tak właśnie działają opisywane wyżej multipleksy 21h i 2Fh.
Jak wykorzystać powyższe informacje? Otóż możesz na przykład zablokować kombinację
klawiszy Ctrl-Break, która zamyka każdy standardowy program zrobiony pod TP.
Wystarczy oprogramować przewanie 1Bh. Prosta sprawa.
uses dos, crt; var OldProc : pointer; {albo procedure} procedure nic; interrupt; {można zignorować nagłówek z rejestrami} begin end; begin GetIntVec($1B,OldProc); SetIntVec($1B,@nic); repeat {tu możesz sobie naciskać Ctrl-Break ile tylko chcesz, o ile program nie działa z TP w tle; jeśli chcesz wyjść z tej pętli, to naciśnij np. spację} until keypressed; {pętla nie robiąca w zasadzie nic} SetIntVec($1B,OldProc); end.
W ten sam spoób możesz oprogramować przerwanie 1Ch, wywoływane 18.2 raza
na sekundę (65536 razy na godzinę), podklejając pod nie zegarek, a następnie
możesz zakończyć program rezydentnie. Co to takiego? Najprościej: kończysz program,
a on zostaje w pamięci i może sobie działać dalej, sterowany właśnie przerwaniami.
Taka pseudowielozadaniowość. Bez wywłaszczania ~;-)
Służy do tego procedura Keep, analogiczna do Halt. Tylko musisz uważać, żeby
Twój program załadowany rezydentnie nie zajmował całej dostępnej pamięci,
bo inaczej przerąbane - DOS się wykrzacza i o ile nie działasz pod Windozą
(a pewnie nie działasz, bo programy rezydentne pod Windą to totalna porażka,
po prostu nie mają sensu) system się wiesza. Pamięć przydzielasz dyrektywą
kompilatora $M albo w oknie Options->Memory sizes... Przydzielasz jej tak
mało, żeby tylko program się nie wywalał przy włączonej opcji Stack checking
(Options->Compiler). przy okazji zyskasz na tym, że program będzie o dwa grosze
szybszy (mniejszy stos do zarządzania).