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).

# przerwaniaOpis
00hBłąd dzielenia przez zero
10hBIOS: 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)
13hBIOS: twardy dysk - bezpośredni odczyt, zapis i formatowanie sektorów
16hBIOS: 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.
19hreset systemu; pod windowsem natomiast błyskawicznie zamyka program
1AhBIOS: czas systemowy - pobieranie i ustawianie
1BhPrzerwanie wywoływane przez naciśnięcie Ctrl+Break
1ChPrzerwanie wywoływane co 1/18 sekundy
21h
DOS: przerywanie działania programu, procedury związane z obsługą systemu plików (odczyt, zapis, zmiana nazw, zakładanie i kasowanie plików i katalogów itp.), wypisywanie łańcuchów znaków, czas, ładowanie programów rezydentnych, obsługa pamięci niskiej, obsługa sieci, długie nazwy plików i katalogów...</td></tr>23h</td>Przerwanie wywoływane przez naciśnięcie Ctrl+C</td></tr>25h</td>DOS: odczyt z partycji - bez podziału na Cylinder/Head/Sector</td></tr>26h</td>zapis j.w.</td></tr>28h</td>DOS: wektor wywoływany gdy DOS się nudzi (nie jest wykonywany żaden program poza COMMAND.COM)</td></tr>2Fh</td>DOS Multiplex - zestaw bardzo wielu funkcji robiących niemal wszystko (nie licząc wiązania krawata i robienia jajecznicy). Właśnie to przerwanie będzie tutaj opisywane</td></tr>31h</td>DPMI: obługa trybu chronionego</td></tr>33h</td>Mysz: pokazywanie/ukrywanie, pobieranie zdarzeń, położenia itp.</td></tr>4Bh</td>DMA: operacje na pamięci bez udziału procesora</td></tr>67h</td>Obsługa pamięci EMS (więcej w tym artykule)</td></tr>...</td>...</td></tr></table>

..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).

0 komentarzy