Bezpośredni dostęp do ekranu

Jeśli myślicie, że odpowiednie funkcje przerwań 10h i 21h są jedynym sposobem na to, aby napisać coś na ekranie, to ten kurs pokaże Wam, jak bardzo się mylicie.

Na ekran w trybie tekstowym składa się 80x25 = 2000 znaków. Nie oznacza to jednak 2000 bajtów, gdyż każdy znak zaopatrzony jest w pewną wartość (1 bajt) mówiącą o jego wyglądzie. Łącznie jest więc 2000 słów (word, 16 bitów = 2 bajty), czyli 4000 bajtów. Mało, w porównaniu z wielkością 1 segmentu (64kB). Te 4000 bajtów żyje sobie w pewnym segmencie pamięci - 0B800h (kolorowe karty graficzne) lub 0B000h (mono).

Struktura tego bloku nie jest skomplikowana i wygląda następująco:

b800:0000 - znak 1, w lewym górnym rogu
b800:0001 - atrybut znaku 1
b800:0002 - znak 2, znajdujący się o 1 pozycję w prawo od znaku 1
b800:0003 - atrybut znaku 2
i tak dalej

Czym zaś jest atrybut?
Jest to bajt mówiący o kolorze danego znaku i kolorze tła dla tego znaku. Bity w tym bajcie oznaczają:
3-0 - kolor znaku (16 możliwości)
6-4 - kolor tła (8 możliwości)
7 - miganie znaku (jeśli nie działa, to oznacza, że mamy 16 kolorów tła zamiast 8)

Jeszcze tylko wystarczy omówić kolory odpowiadające poszczególnym bitom i możemy coś pisać.
Oto te kolory:
Czarny - 0, niebieski - 1, zielony - 2, błękitny - 3, czerwony - 4, różowy - 5, brązowy - 6, jasnoszary (ten standardowy) - 7, ciemnoszary - 8, jasnoniebieski - 9, jasnozielony - 10, jasnobłękitny - 11, jasnoczerwony - 12, jasnoróżowy - 13, żółty - 14, biały - 15.

To powinno mówić samo za siebie: chcemy biały znak na czarnym tle? Odpowiedni bajt = 0fh.
A może żółty znak na niebieskim tle? Bajt = 1eh.

Poniżej zamieszczam także programik, który szybko napisałem w celu przetestowania teorii tu przedstawionej (składnia NASM):


(przeskocz przykładowy program)
	; nasm -O999 -o test.com -f bin test.asm

	org 100h

		mov ax, 0b800h
		mov bx, cs
		mov es, ax		; es = 0b800 = segment pamięci ekranu
		mov ds, bx		; ds = cs
		xor di, di		; pozycja docelowa = di = 0
		mov si, tekst		; skąd brać bajty
		mov cx, dlugosc		; ile bajtów brać

		rep movsb		; przesuń CX bajtów z DS:SI do ES:DI

		xor ah, ah
		int 16h

		mov ax, 4c00h
		int 21h

	tekst	db "T",1,"e",2,"k",3,"s",4,"t",5
		db " ",6,"w",7,"i",8,"e",9,"l",10,"o",11,"k",12,"o",13
		db "l",14,"o",15,"r",16,"o",27h,"w",38h,"y",49h

	dlugosc equ $-tekst

Zastosowałem w nim stałą typu equ, aby nie zmieniać CX po każdorazowej nawet najdrobniejszej zmianie tekstu.

Jak widać, wpisywanie każdorazowo znaku z jego argumentem niekoniecznie sprawia przyjemność. Na szczęście z pomocą przychodzi nam BIOS, ale nie funkcja 0e przerwania 10h, lecz funkcja 13h tegoż przerwania (opis wycięty z Ralf Brown's Intterrupt List):


(przeskocz opis int 10h, ah=13h)
	INT 10 - VIDEO - WRITE STRING (AT and later,EGA)
	        AH = 13h
	        AL = write mode
	           bit 0: update cursor after writing
	           bit 1: string contains alternating characters
	           	 and attributes
	           bits 2-7: reserved (0)
	        BH = page number
	        BL = attribute if string contains only characters
	        CX = number of characters in string
	        DH,DL = row,column at which to start writing
	        ES:BP -> string to write

I krótki przykładzik zastosowania (fragment kodu dla TASMa):


(przeskocz przykład zastosowania int 10h, ah=13h)
	mov cx,cs
	mov ax,1301h			; funkcja pisania ciągu znaków
	mov es,cx			; es = cs
	mov bx,j_czer			; atrybut (kolor)
	mov cx,info1_dl			; długość ciągu
	mov bp,offset info1		; adres ciągu
	mov dx,(11 shl 8) or (40 - (info1_dl shr 1))	;wiersz+kolumna
	int 10h				; piszemy napis

	info1		db	"Informacja"
	info1_dl	equ	$ - info1

Najwięcej wątpliwości może wzbudzać linia kodu, która zapisuje wartość do DX (wiersz i kolumnę ekranu). Do DH idzie oczywiście 11 (bo do DX idzie b=11 shl 8, czyli 0b00h). Napis (info1_dl shr 1) dzieli długość tekstu na 2, po czym tę wartość odejmujemy od 40. Po co?
Jak wiemy, ekran ma 80 znaków szerokości. A tutaj od 40 odejmujemy połowę długości tekstu, który chcemy wyświetlić. Uzyskamy więc w taki sposób efekt wyśrodkowania tekstu na ekranie. I to wszystko.

No dobrze, a co jeśli nie chcemy używać przerwań a i tak chcemy mieć tekst w wyznaczonej przez nas pozycji?
Trzeba wyliczyć odległość naszego miejsca od lewego górnego rogu ekranu. Jak nietrudno zgadnąć, wyraża się ona wzorem (gdy znamy współrzędne przed kompilacją):
wiersz*80 + kolumna
i to tę wartość umieszczamy w DI i wykonujemy rep movsb.
Gdy zaś współrzędne mogą się zmieniać lub zależą od użytkownika, to użyjemy następującej sztuczki (kolumna i wiersz to 2 zmienne po 16 bitów):


(przeskocz obliczanie adresu w pamięci ze współrzędnych)
	mov ax, [wiersz]
	mov bx, ax		; BX = AX
	shl ax, 6		; AX = AX*64
	shl bx, 4		; BX = BX*16 = AX*16
	add ax, bx		; AX = AX*64 + AX*16 = AX*80
	add ax, [kolumna]	; AX = 80*wiersz + kolumna

	mov di, ax
	shl di, 1	; DI mnożymy przez 2, bo są 2 bajty na pozycję

i też uzyskamy prawidłowy wynik. Odradzam stosowanie instrukcji (I)MUL, gdyż jest dość powolna.

Zajmiemy się teraz czymś jeszcze ciekawszym: rysowanie ramek na ekranie. Oto programik, który na ekranie narysuje 2 wypełnione prostokąty (jeden będzie wypełniony kolorem czarnym). Korzysta on z procedury, która napisałem specjalnie w tym celu. Oto ten programik:


(przeskocz program rysujący okienka z ramką)
; Rysowanie okienek z ramką
;
; Autor: Bogdan D.
;
; nasm -O999 -o ramki.com -f bin ramki.asm


org 100h

; ramki podwójne:

	mov	ah, 7
	xor	bx, bx
	xor	cx, cx
	mov	dx, 9
	mov	bp, 9
	call	rysuj_okienko

	mov	ah, 42h
	mov	bx, 10
	mov	cx, 10
	mov	dx, 20
	mov	bp, 16
	call	rysuj_okienko

	xor	ah, ah
	int	16h

	mov	ax, 4c00h
	int	21h

rysuj_okienko:

; wejście:
;
;  AH = atrybut znaku (kolor)
;  BX = kolumna lewego górnego rogu
;  CX = wiersz lewego górnego rogu
;  DX = kolumna prawego dolnego rogu
;  BP = wiersz prawego dolnego rogu
;
; wyjście:
;  nic


r_p	equ	0bah		; prawa boczna
r_pg	equ	0bbh		; prawa górna (narożnik)
r_pd	equ	0bch		; prawa dolna

r_g	equ	0cdh		; górna
r_d	equ	r_g		; dolna

r_l	equ	r_p		; lewa boczna
r_lg	equ	0c9h		; lewa górna
r_ld	equ	0c8h		; lewa dolna

spacja	equ	20h


	push	di
	push	si
	push	es
	push	ax

	mov	di, cx
	mov	si, cx
	shl	di, 6
	shl	si, 4
	add	di, si		; DI = DI*80 = numer pierwszego wiersza * 80


	mov	si, 0b800h
	mov	es, si		; ES = segment ekranu

	mov	si, di
	add	di, bx		; DI = pozycja początku
	add	si, dx		; SI = pozycja końca

	shl	di, 1		; 2 bajty/element
	shl	si, 1

	mov	al, r_lg
	mov	[es:di], ax	; rysujemy lewy górny narożnik

	add	di, 2

	mov	al, r_g		; będziemy rysować górny brzeg

.rysuj_gore:

	cmp	di, si		; dopóki DI < pozycja końcowa
	jae	.koniec_gora

	mov	[es:di], ax
	add	di, 2
	jmp	short .rysuj_gore

.koniec_gora:
	mov	al, r_pg
	mov	[es:di], ax	; rysujemy prawy górny narożnik


.wnetrze:
	shr	di, 1

	add	di, 80		; kolejny wiersz
	sub	di, dx		; początek wiersza

	push	di

	mov	di, bp
	mov	si, bp
	shl	di, 6
	shl	si, 4
	add	si, di		; SI = SI*80 = numer ostatniego wiersza * 80

	pop	di

	cmp	di, si		; czy skończyliśmy?
	je	.koniec_wnetrze

	mov	si, di
	add	di, bx		; DI = pozycja początku
	add	si, dx		; SI = pozycja końca

	shl	di, 1		; 2 bajty / element
	shl	si, 1

	mov	al, r_l
	mov	[es:di], ax	; rysujemy lewy brzeg
	add	di, 2

	mov	al, spacja	; wnętrze okienka wypełniamy spacjami
.rysuj_srodek:

	cmp	di, si		; dopóki DI < pozycja końcowa
	jae	.koniec_srodek

	mov	[es:di], ax
	add	di, 2
	jmp	short .rysuj_srodek

.koniec_srodek:

	mov	al, r_p
	mov	[es:di], ax	; rysujemy prawy brzeg

	jmp	short .wnetrze

.koniec_wnetrze:


	mov	di, bp
	mov	si, bp
	shl	di, 6
	shl	si, 4
	add	di, si		; DI = DI*80


	mov	si, di
	add	di, bx		; DI = pozycja początku w ostatnim wierszu
	add	si, dx		; SI = pozycja końca w ostatnim wierszu

	shl	di, 1		; 2 bajty / element
	shl	si, 1

	mov	al, r_ld
	mov	[es:di], ax	; rysujemy lewy dolny narożnik

	add	di, 2

	mov	al, r_d		; będziemy rysować dolny brzeg

.rysuj_dol:

	cmp	di, si		; dopóki DI < pozycja końcowa
	jae	.koniec_dol

	mov	[es:di], ax
	add	di, 2
	jmp	short .rysuj_dol

.koniec_dol:
	mov	al, r_pd
	mov	[es:di], ax	; rysujemy prawy dolny narożnik


	pop	ax
	pop	es
	pop	si
	pop	di

	ret

Program nie jest skomplikowany, a komentarze powinny rozwiać wszystkie wątpliwości. Nie będę więc szczegółowo omawiał, co każda linijka robi, skupię się jednak na kilku sprawach:

Jak widać, ręczne manipulowanie ekranem wcale nie musi być trudne, a jest wprost idealnym rozwiązaniem, jeśli zależy nam na szybkości i nie chcemy używać powolnych przerwań.

Miłego eksperymentowania!



Spis treści off-line (Alt+1)
Spis treści on-line (Alt+2)
Ułatwienia dla niepełnosprawnych (Alt+0)