Na ile sposobów można rozumieć liczbę? Niezliczenie wiele! Może to być wysokość
dziwęku, numer buta, dzień tygodnia wielkość gwiazdy... Co tylko zapragniesz! Niestety, im więcej znaczeń, tym większy
kłopot. Im więcej języków, tym trudniej się porozumieć.
ASCII
Mówiliśmy już, że jednym z możliwych sposobów interpretacji liczby zawartrej w
jednym bajcie pamięci jest znak pisarski, taki jak cyfra, litera, kreska, kropka, itd. W takim ujęciu mówimy o
liczbach, że stanowią kody znaków, a prościej: ich numery. Trzeba pamiętać, że odbierany przez nas efekt wizualny, np.
kształt litery "X", jest szczególnym sposobem odwzorowania na ekranie liczby 120 przez procesor obrazu. Znaczna część
cywilizowanego świata podporządkowała się umowie, że liczba 65 oznacza znak "A", liczba 66 - "B", itd. Cyfra 0 ma numer
porządkowy 48, cyfra 1 - 49 itd. Ten standard chyba najszerzej rozpowszechniony nazywa się ascii. Jest tych znaków 128
(kody od 0 do 127) lecz te od 0 do 31 są "niewidzialne", ponieważ służą nie do wyświetlania, lecz do wymuszania
pewnych działań, jak ruch kursora na ekranie lub ustawienie zgęszczonego druku w drukarce. Jeżeli posługujemy się
asemblerem, to zbędne jest pamiętanie kodów poszczególnych znaków. Aby wziąść do akumulatora kod litery "A", piszemy:
lda #'A'
a QA wygeneruje identyczny rozkaz maszynowy jak w przypadku:
lda #65
Symbol 'A' jest więc traktowany tak, jak liczba. Może być stosowany wszędzie, gdzie asembler spodziewa się wartości
liczbowej. Poniższa procedura służy do zamiany małych liter na duże, innym znakom nie czyni krzywdy. Domniema się, że
znak został umieszczony w akumulatorze, tam też pojawia się odpowiedź.
upper cmp #'a'
bcc ok mniejsze od a
cmp #'z'+1
bcs ok wieksze niż z
sec
sbc #'a'-'A'
ok rts
Proszę zauważyć, że asembler sam oblicza wyrażenia 'a', 'z'+1, 'a'-'A'. W rozkazach programu maszynowego zostaną użyte
odpowiednio liczby: 97,123, 32, lecz o tym nie potrzeba nam wiedzieć. Zapamiętajmy: budowa wyrażeń w języku asemblera nie
ma wpływu na złożoność programu wynikowego. Nie oszczędzajmy więc kosztem czytelności! Otrzymałem ostatnio list, w
którym Czytelnik pyta, czy programista powinien mieć w głowie tablicę ASCII. Zapewne czytał przewrotny tekst o
"Prawdziwych programistach" To jakieś nieporozumienie. Kochani, programista powinien mieć w głowie rozum!
Dla umieszczania w programie całych ciągów znaków służy rozkaz DTA typu C:
dta c'Tajemnice ATARI'
Ponieważ 128 znaków nie wyczerpuje wszystkich możliwości bajtu, istnieje wiele odmian "poszerzonego ASCII" lub "ASCII
8-bitowego". To poszerzenie polega na dodaniu 128 dowolnych znaków. Tak, to nie żart. Znaki 0 ... 127 mają obowiązek być
zawsze takie same, lecz dla kodów od 128 do 255 nie ma jednolitego standartu. To jest bardzo wygodne. Rosjanie
umieszczają tu cyrylicę, Polacy swoje ą i ę, Niemcy umlauty i basy, zwykle też zmieszczą się jakieś znaczki
semigraficzne, symbole matematyczne itp.
Przy przenoszeniu danych między komputerami różnych systemów zawsze trzymamy się bezpiecznego 7-bitowego zestawu. Na nim
też oparte są wszystkie języki programowania. Idylla. Jaka szkoda, że nasze ATARI nie używa ASCII.
ATASCII
To bodajże największa (prócz nie standardowego złącza wy/we) krzywda wyrządzona temu
komputerowi przez jego twórców. Duża niezgodność "ATari ASCII" z ASCII powoduje, że nie jest możliwe bezpośrednie
przenoszenie danych między naszym ulubieńcem i resztą świata. Trudności napotyka też podłączenie typowych urządzeń
zewnętrznych, np. drukarek, modemów, itp. Niektórzy mówią o 256-znakowym zestawie ATASCII, lecz nie wierzcie im. Mamy do
czynienia ze 128 różnymi znakami, a najstarszy bit daje tylko informację o kolorze - te same znaki mogą być wyświetlane w
inny sposób. Na dodatek w niektórych trybach liczba znaków jest redukowana do 64, by tym kosztem uzyskać "aż" dwa bity
definjujące kolor znaku.
Zestaw znaków ANTIC'a
Jakby tych atrakcji było mało, procesor obrazu w naszym komputerze posługuje się
zgoła odrębnym zestawem znaków. Muszę przyznać, że go rozumiem. W niektórych trybach każą mu wyświetlać znaki w czterech
kolorach. Kod znaku ma zatem tylko 6-bitów (dwa przeznaczono na numer koloru). A przecież znaki 0 ... 63 możliwe do
wyrażenia tym sposobem nie obejmują liter! Dlatego dokonano przetasowania. Kody Antica 0 ... 63 odpowiadają znakom 32
... 95 zestawu ATASCII. Znaki ATASCII 0 ... 31 mają teraz kody 64 ... 95, tylko znaki 96 ... 127 pozostały na swoim
miejscu.
Ten pokraczny "standard" znaków bywa w literaturze zwany peek-poke, ekranowy, internal (wewnętrzny) lub iocode.
Początkujący programista nie zdaje sobie sprawy z jego istnienia. Pisze:
PRINT "ATARI"
i na ekranie widzi:
ATARI
To systemowe procedury wyświetlania tekstów dokonują po cichu konwersji naszego napisu. Ale próba bezpośredniego
wysłania znaku na ekran zawodzi.
ekran equ $58
ldy #2
lda #'A'
sta (ekran),y
a na ekranie pojawia się coś całkiem innego. Aby otrzymać znak "A" trzeba wysłać, zgodnie z nabytą wiedzą, znak o
kodzie o 32 mniejszym, czyli
lda #'A'-32
Można definiować całe napisy zgodne z zestawem Antica za pomocą DTA typu D, np.
dta d'Tajemnice ATARI'
Ten tekst (odmiennie niż dta c) można przenieśc bezpośrednio na ekran. Wielu programistów stosuje tę sztukę dla
przyspieszenia i skrócenia programu, odpada bowiem konieczność konwersji. Ja jednak nie jestem zwolennikiem tej
metody, gdyż w praktyce niejednokrotnie trzeba odwoływać się do systemu, na przykład podając nazwę programu (koniecznie
ATASCI!) lub pobierając listę plików poleceniem DIR. Operowanie na zestawie znaków ATASCII daje wymierne korzyści. Więc
jednak konwersja. Procedura zmieniająca znak w akumulatorze z kodu ATASCII na ANTIC (7-bitowy) mogłaby wyglądać tak:
konwer1 cmp #32
bcc dod64
cmp #96
bcc ode32
rts
dod64 clc
adc #64
rts
ode32 sec
sbc #32
rts
Nie uwzględnia ona jednak bitu koloru (musi on być w tym wypadku zerem). Po odpowiednim usprawnieniu otrzymujemy taki
oto podprogram (krótki i szybki, lecz o ileż mniej zrozumiały!).
konwer2 asl @
php
cmp #192
bcs ok
sbc #63
bcs ok
adc #192
ok plp
ror @
rts
Niekiedy okazuje się, że taka konwersja, przy wypełnianiu znakami całego ekranu, jest zbyt czasochłonna. Można się
wówczas posłóżyć 256 bajtową tablicą, którą wstępnie wypełniamy przy pomocy poniższej procedury:
ldx #0
wyp txa
jsr konwer2
sta tab,x
inx
bne wyp
Tablicę należy zadeklarować jako:
tab org *+256
Odtąd można się już posługiwać taką błyskawiczną procedurą:
konwer3 tax
lda tab,x
rts
Nawiasem mówiąc, czasem i to jest zbyt wolne. Nie pozostaje nic innego, jak zmusić ANTIC, by wyświetlał dane
bezpośrednio w kodach ATASCII. Jak? Wiem ale nie powiem. A może ktoś z Was sam zgadnie?
Kody klawiatury
Klawiatura ma swoją własną interpretację znaków i przynajmniej w tym jednym wypadku
jest to w pełni usprawiedliwione. Ponieważ elektrycznie zbudowana jest ona na zasadzie matrycy krzyżujących się
przewodów, naturalnie sygnał od naciśniętego klawisza niesie informację o wierszu i kolumnie na przecięciu których
znajduje się naciśnięty klawisz. Klawisze leżące blisko siebie mają podobne kody. Ich kolejność nie jest więc
alfabetyczna, lecz, rzec można topograficzna. Nie ma żadnej, dającej się ująć w ramy algorytmu zależności między tymi
kodami, a zestawem ATASCII. Jedynym ratunkiem jest tabela konwersji, której adres znajduje się we współczesnych
komputerach w słowie $79.
UWAGA: W tym miejscu autor odsyła nas do lektury mapy pamięci także publikowanej w "Tajemnicach ATARI".
Oczywiście wszyscy ci czytelnicy, którzy posiadają w swoich zbiorach czasopismo "Tajemnice ATARI" mogą sobie i cały kurs
programowania tamże wyczytać, my publikujemy ten materiał dla ludzi którzy nie mieli okazji zdobyć tego legendarnego dla
nas już czasopisma. Jak tam wyżej kolega Wiśniewski wspominał, wektor zawarty w słowie $79 wskazuje na tablicę
znajdującą się w pamięci ROM komputera. W tablicy tej umieszczono kolejno:
- Znaki ATASCII odpowiadające kodom klawiszy (jest ich 64).
- Znaki ATASCII odpowiadające kodom klawiszy wciśniętych wraz z SHIFT.
- Znaki ATASCII odpowiadające kodom klawiszy wciśniętych wraz z CONTROL.
I co z tego? Poniżej zamieszczamy procedurę oczekiwania na naciśnięcie klawisza, napisaną w asemblerze, która wydaje się
być przydatną w bardzo wielu programach:
ktab equ $79
ch equ $2fc
getk lda ch odczytaj ch
cmp #255 czy jest znak?
beq getk nie: czekaj
ldy #255 daj znać, że klawisz odebrany...
sty ch
tay kod klawisza jako
lda (ktab),y index do tablicy
rts w ROM'ie.
W akumulatorze znajduje się kod ATASCII, który odpowiada wciśniętemu klawiszowi, pobrany za pomocą tablicy, o której
cały czas się rozpisujemy. Wielokrotnie zdarza się, że dla pisanego programu nie ma znaczenia to, czy podczas naciskania
klawisza był wciśnięty klawisz SHIFT lub CONTROL. W tym przypadku wystarczy przed rozkazem tay wpisać and #%00111111
(ignoruj SHIFT i CONTROL).
Warto tylko podkreślić, że korzystanie z systemowego wejścia (przez kanał otwarty dla urządzenia "K:") uwalnia nas od
zawiłych problemów: otrzymujemy bowiem gotowe znaki w standardzie ATASCII.
Znak bez znaku
Ponieważ każdy znak można utożsamiać z jego jednobajtowym kodem i z drugiej strony
każdy bajt zawiera kod jakiegoś znaku, istnieje tendencja do zamiennego stosowania nazw "znak" i "bajt". Zwłaszcza w
językach wysokiego poziomu, gdzie rzadziej operuje się na bajtach, terminem "znak" określa się liczbę jednobajtową, na
każdą inną liczbę mówiąc poprostu "liczba".
To prowadzi do zabawnego paradoksu: liczby, które określamy mianem znaków, mogą mieć znak (plus, minus), lub go nie mieć.
Nie jest to oczywiście cecha liczb lecz naszego subiektywnego na nie spojrzenia. Pisałem już o tym: jeśli myślimy, że
bajt zawiera liczbę od 0 do 255, to jest ona zawsze nieujemna, ale jeśli umówić się, że ustawiony bit 7 oznacza liczbę
ujemną to mamy do czynienia z zakresem -128 ... 127. Jaka to różnica dla komputera? Żadna. Procesor 6502 wykonuje operacje
artmetyczne w obu trybach na raz! Weźmy liczby 30 i 140. Ich suma powinna dać 170. W dwójkowych rejestrach procesora
odbędzie się to tak:
00011110
+10001100
=10101010
Dla tych, którzy chcą to sprawdzić przypominam technikę dodawania na papierze. Sumujemy odpowiednie cyfry poczynając od
końca:
0 + 0 = 0
1 + 0 = 1
1 + 1 = 0 przenosimy 1
1 + 1 + 1 = 1 przenosimy 1
1 + 0 + 1 = 0 przenosimy 1
0 + 0 + 1 = 1
0 + 0 = 0
0 + 1 = 1
Wynik, liczba %10101010, to rzeczywiście 170. Jeśli jednak uświadomimy sobie, że liczba 140 jest w rozumieniu ze znakiem
równa -116, to mamy do czynienia z działaniem 30+(-116)=-86. Nikogo chyba nie zdziwi, że dwójkowo ten wynik zapisuje się
%10101010. Również porównywanie takich liczb odbywa się zwykle bez kłopotów, gdyż
lda #30
cmp #170
spowoduje skasowanie znacznika C (jako, że 30<170) i jednocześnie skasowanie znacznika N (ponieważ 30>86). Dla liczb
bez znaku, by skoczyć "gdy mniejsze" stosujemy więc rozkaz BCC, a dla liczb ze znakiem BMI. Nasz procesor jest
przygotowany na obydwie ewentualności. Trzeba jednak pamiętać, że porównywanie ze znakiem daje niepoprawny wynik w
sytuacjach, w których odejmowanie tych samych liczb ustawia znacznik V. Dotyczy to porównywania liczb o różnych znakach,
gdy suma ich wartości bezwzględnych przekracza 127. Z tego powodu porównania ze znakiem mają bardzo ograniczone
zastosowanie (głównie do niewielkich liczb).
Nietrudno w literaturze natknąć się na przepis własnoręcznego wykonywania liczb ujemnych. Np. -5 robimy tak: bierzemy
liczbę 5 (%00000101),zamieniamy wszystkie bity na przeciwne (%11111010) i dodajemy jedynkę (%11111011). To właśnie
jest -5. Ta metoda wydaje mi się jednak zbyt skomplikowana, przez co trudna do zapamiętania. Ja rozumuję tak: -5 to
0-5, czyli:
(1)00000000
- 00000101
= 11111011
Jedynka w nawiasie oznacza dodatkowy bit, z którego można zaczerpnąć "pożyczkę" w procesorze 6502 jest to znacznik C. Ta
metoda ma dodatkową zaletę: można zwalić całą robotę na komputer. Rozkaz
lda #%100000000-5
umieści w akumulatorze liczbę -5. Sposób zawiłego ręcznego przekształcania liczb ma więc tylko znaczenie ciekawostkowe.
Nie ma potrzeby się go uczyć.
BCD
Kolejną stosowaną w komputerze ATARI konwencją dotyczącą liczb, to tryb dziesiętny.
Tym się różni od pozostałych, że nie wszystkie z 256 kombinacji bitów są dozwolone. W tym ujęciu pojedyńczym elementem
jest połówka bajtu (cztery bity), która wyraża jedną cyfrę z zakresu 0 ... 9. A zatem bajt zawiera liczbę dziesiętną
(zapisaną dwójkowo) z zakresu 0 ... 99. Ten temat jest jednak na tyle obszerny, że poświęcę mu więcej miejsca w którymś z
następnych odcinków.
Bardzo ciekawe dane
Prawdziwi programiści pogardzają liczbami BCD. Wszak komputery myślą dwójkowo, czyż to
nie wystarczający argument? Kod BCD został stworzony specjalnie dla maminsynków, którzy nie mają dość rozumu, żeby się
nauczyć myśleć jak komputer...
A teraz poważnie: BCD, czyli liczby dziesiętne kodowane dwójkowo (Binary Coded
Decimal), stanowią pomost, ogniwo pośrednie pomiędzy wewnętrzną reprezentacją liczby w komputerze, a postacią, w jakiej
zwykli śmiertelnicy wolą oglądać liczby: ciągiem cyfr dzieśiętnych. Liczba wyświetlona na ekranie lub wydrukowana na
papierze składa się z cyfr, będących znakami z zestawu ASCII. Dla sterownika obrazu lub drukarki nie ma przytym
rozróżnienia między literkami, cyframi i innymi znaczkami dziwaczkami. Każdy jest po prostu jakimś zestawem ośmiu bitów
czyli bajtem. Wyobraźmy sobie jednak, że wyświetlamy (drukujemy) TYLKO liczby dodatnie. Przyjrzyjmy się kodom potrzebnych
znaków:
'0' = %00110000
'1' = %00110001
'2' = %00110010
'3' = %00110011
'4' = %00110100
'5' = %00110101
'6' = %00110110
'7' = %00110111
'8' = %00111000
'9' = %00111001
Wyraźnie widać, że cztery starsze bity są zawsze takie same. A zatem w przypadku przechowywania w pamięci danych tego
typu połowa informacji jest zupełnie zbędna. Do zidentyfikowania znaku o którym wiemy, że jest cyfrą dziesiętną,
wystarczą cztery bity. Więc może by tak umieścić po dwie cyfry w każdym bajcie? Wtedy np.
'17' = %00010111
'46' = %01000110
'92' = %10010010
itd...
Na zapisanie liczby 1993 potrzeba tylko dwóch bajtów:
'1993' = %0001100110010011
Taką koncepcję zapisu liczb dziesiętnych nazywamy właśnie BCD. W tym miejscu warto uczynić dwa spostrzeżenia.
- Nie wszystkie możliwe kombinacje czterech bitów są wykorzystane (dozwolone). Wskutek tego "gęstoś zapisu" liczb w
pamięci jest w tym systemie mniejsza niż przy naturalnym dla komputera, dwójkowym sposobie. Jeden bajt BCD może
przechowć liczbę od 0 do 99 (a dwójkowo 0-255).
- Wartość liczby odczytanej z bajtu będzie różna w zależności od sposobu interpretacji. Przykład: %01000110 oznacza
binarne 70, a tylko 46 w BCD; z koleji 64 zapisuje się dwójkowo jako %01000000, a w BCD jako %01100100.
Po co tyle zamętu?
Dane w formacie BCD są bardzo łatwe do prezentacji wizualnej (zamiany na cyfry ASCII). Wystarczy podzielić bajt na
połówki, do każdej doczepić cztery bity wzięte np. z cyfry '0' i gotowe. Natomiast dane dwójkowe są trudne do prezentacji
w formie dziesiętnej i najczęściej przekształca się je w celu wyświetlenia właśnie do formatu BCD, co nie jest łatwe i
zajmuje masę czasu. Dlatego, o ile inne względy nie przemawiają przeciwko temu, wygodnie jest przechowywać liczby w
postaci BCD. Prosta procedura wyświetlająca młodszą cyfrę BCD może wyglądać tak:
savmsc equ 88
disp_1 and #%00001111
ora #'0'-32
sta (savmsc),y
dey
rts
Aby nie zmieniać algorytmu dodatkowymi zabiegami, wyświetlamy cyfrę w pobliżu początku pamięci obrazu (wektor savmsc).
Odjęcie liczby 32 od kodu '0' jest najprostszym sposobem zamiany z ASCII na kod Antic'a. Przed wywołaniem procedury
należy oczywiście umieścić stosowną liczbę w akumulatorze i zainicjować rejestr Y. Zmniejszenie wartości tego rejestru
po wyświetleniu cyfry ma taki skutek, że kolejne wywołania procedury disp_1 będą umieszczać następne cyfry na lewo od
poprzednich. Wyświetlenie całego bajtu, czyli dwóch cyfr BCD:
disp_2 pha
jsr disp_1
pla
lsr @
lsr @
lsr @
lsr @
jsr disp_1
rts
Pierwsze wywołanie disp_1 wyświetla młodszą cyfrę, ale niszczy przy tym starszą, stąd konieczność przechowania bajtu na
stosie. Przesunięcie bitów o cztery pozycje w prawo sprawi, że za drugim razem starsza cyfra znajdzie się na miejscu
młodszej.
Zwolennicy upraszczania zauważą zapewne ,że można zamienić sekwencję:
JSR DISP_1
RTS
na równoważny jej rozkaz
JMP DISP_1
Rozkaz ten z koleji można całkowicie pominąć, jeśli umieścić procedurę disp_2 bezpośrednio przed disp_1. Komu wydaje się
jednak zbyt skomplikowane, niech lepiej zaniecha tych uproszczeń.
Aby zadelarować dane typu BCD (assemblery na ogół nie oferują dla nich osobnego formatu), trzeba posłużyć się prostą
sztuczką. Należy zastosować tryb szesnastkowy z tym zastrzeżeniem, że używa się tylko cyfr od '0' do '9'. A więc liczbę
1956 zapiszemy jako:
year dta a($1956)
wyświetlenie takiej liczby jest łatwe i przyjemne:
len equ 2
disp_y ldx #0
ldy #5
dsp lda year,x
jsr disp_2
inx
cpx #len
bcc dsp
rts
Jak łatwo zauważyć, nie nakłada się tu żadnego ograniczenia na długość liczby może ona zajmować dowolnie wiele kolejnych
bajtów. W naszym przypadku są dwa, odlicza je rejestr X.
Artmetyka BCD
Liczby dwójkowe podlegają ścisłym prawom, w myśl których:
%00001001
+%00000001
=%00001010
czyli szesnastkowo $09+$01=$0A (dziesiętnie 9+1=10). W przypadku cyfr BCD dla osiągnięcia poprawnego wyniku chciałoby
się żeby:
%00001001
+%00000001
=%00010000
A więc szesnastkowo $9+$1=$10 (dziesiętnie 9+1=16). Aby zmusić nasz procesor do dodawania w tak niezwykły sposób, należy
ustawić znacznik 'D'. Spróbuj zasemblować i wykonać:
sed
clc
lda #%1001
adc #%0001
cld
brk
i zaobserwuj wynik. Pod wpływem znacznika 'D' dodawanie przebiega zgodnie z regułami BCD, przepełnienie ponad 9 powoduje
przeniesienie jedynki z młodszej połówki bajtu do starszej, ze starszej zaś do znacznika C. Pamiętaj, że znacznik 'D' ma
wpływ wyłącznie na rozkazy ADC i SBC, zaś inne (jak INC, DEC) mu nie ulegają. Istnieje zdrowy zwyczaj włączania trybu
dziesiętnego TYLKO na czas wykonywania operacji na liczbach BCD, natychmiast potem należy go wyłączyć (rozkazem CLD),
aby zapobiec nieporozumieniom w czasie przyszłych rachunków.
Konwersja liczby dwójkowej na BCD
Często, gdy zachodzi potrzeba wyświetlenia liczby, stajemy przed koniecznością jej
zamiany z postaci binarnej na BCD. Typowym przykładem jest kod błędu, zwracany przez procedury we/wy w rejestrze Y.
byte equ $cb
word equ $cc
sty byte
jsr convr
ldy #5
lda word
jsr disp_2
lda word+1
jsr disp_1
Przepisujemy kod błędu do komórki byte procedura konwersji umieszcza kod w słowie word. Ponieważ liczba z zakresu 0-255
jest co najwyżej trzycyfrowa, wywołuje się najpierw wyświetlenie dwóch cyfr, a z drugiego bajtu jednej. Procedura
konwersji może mieć taką postać:
convr lda #0
sta word
sta word+1
ldx #8
sed
cv1 asl byte
lda word
adc word
sta word
rol word+1
dex
bne cv1
cld
rts
Zastosowana tu metoda polega na "przelewaniu" bitów z liczby binarnej do liczby w formacie BCD. Wychodzące w wyniku ASL
(ten rozkaz działa zawsze binarnie) bity chwyta się za pomocą znacznika C do tworzonego słowa word, gdzie kolejne
mnożenia przez 2 (za pomocą dodawania w trybie BCD) nadadzą im właściwą wartość. Przykładowo, bit 7 z bajtu byte wejdzie
w pierwszym obiegu pętli jako 1 (lub 0) do słowa word. W wyniku pozostałych siedmiu mnożeń będzie pomnożony przez
128, czyli osiągnie swą wagę z bajtu byte. Każdy następny bit dostąpi odpowiednio mniej mnożeń, aż do bitu nr.0, który
wejdzie na najniższą pozycję i tam już pozostanie.
Inną metodą konwersji może być "przelewanie jednostek". Algorytm ten polega na
zmniejszaniu liczby binarnej (np. przez DEC) z równoczesnym zwiększaniem liczby BCD (rozkazem ADC #1 w trybie
dziesiętnym) aż do osiągnięcia przez tę pierwszą wartość zera. Ta metoda prosta i łatwa do zrozumienia, jest jednak
znacznie mniej efektywna od tej jaką pokazuje procedura CONVR. O trybie dziesiętnym należy wiedzieć i pamiętać nawet
wtedy gdy go nie używamy. Szczególnie w procedurach obsługi przerwań warto mieć na uwadze to, że mogą być one wywoływane
w dowolnej chwili. Także w takiej kiedy jest ustawiony znacznik D. Dlatego każda szanująca się obsługa przerwania
zaczyna się od sakramentalnego CLD.
PRZERWANIA
W niektórych dziedzinach przerywanie uważane bywa za niezdrowe. Dla procesora
wszakże jest to zwykła rzecz. Przerwania są wręcz niezbędne. To w komputerze coś, jak rytm serca.
Przerwanie w rozumieniu informatycznym oznacza pewne zdarzenie, nierzadko losowe,
które wymaga od procesora natychmiastowego zainteresowania. Klasyczne przerwanie objawia się sygnałem elektrycznym
podanym na nóżkę procesora. Procesor 6502 ma dwie takie nóżki zwane IRQ i NMI. Różnica między nimi polega na tym, że
można programowo wyłączyć wrażliwość procesora na sygnał IRQ, podczas gdy wejście NMI pozostaje zawsze aktywne. Żródłami
sygnałów przerwań, są w komputerze ATARI układy pomocnicze: POKEY (IRQ) i ANTIC (NMI). Nadchodzące od nich sygnały mówią
o takich zdarzeniach, jak naciśnięcie klawisza, rozpoczęcie pionowego powrotu plamki kreślącej obraz, itd. To system
nerwowy procesora, jedyny jego kontakt z realnym światem.
Fakt istnienia (pojawiania się) przerwań mąci idyllę programisty, nie jest on bowiem
w stanie przewidzieć, w którym momencie wykonywania jego programu nadejdzie sygnał, skupiający na sobie całą uwagę
procesora. Przerwanie jest zawsze ważniejsze od jakiegoś głupiego programu! Większość przerwań wywoływane jest przez
zdarzenia wymagające natychmiastwej reakcji. Procesor PRZERYWA więc wykonywanie programu użytkownika (naszego
programu!), zamiast nigo realizuje tak zwaną procedurę obsługi przerwania. Po zakończeniu tej procedury powraca do
wykonania przerwanego programu, jakgdyby nic się nie zdarzyło. Aby przerwanie nie miało wpływu na wykonanie naszego
programu, procedura jego obsługi jest zobowiązana do pozostawienia procesora DOKŁADNIE w takim stanie, w jakim go
zastała. Sprzyja temu "odruchowy" mechanizm reakcji na sygnał przerwania: procesor umieszcza na stosie rejestr
znaczników oraz adres kolejnego rozkazu, który miał być wykonany w chwili wystąpienia przerwania. Specjalny rozkaz
powrotu z przerwania, którym musi się kończyć każda procedura obsługi, RTI powoduje odtworzenie adresu programu i
znaczników. Jeżeli w trakcie obsługi przerwania używane są jakieś rejestry, musi zostać przechowana ( i oddana! ) ich
pierwotna wartość. Dzięki takiemu postępowaniu przerwanie pozostaje niewidoczne dla programu użytkownika. Uff, możemy
spać spokojnie.
Obsługę przerwania można porównać do wykonania "fuchy" przez sprytnego (i
nieuczciwego) pracownika. Cichcem wykonuje on podczas swej normalnej pracy jakąś dodatkową robotę. Poczynania swe
maskuje starannie, by nikt, zwłaszcza szef tego nie zauważył. Niestety, często odbija się to na jakości i wydajności
podstawowej pracy.
Blokowanie przerwań
Początkujący programista nie zauważa istnienia systemu przerwań. Czasami jednak
komputer zachowuje się jakoś dziwnie. Próbujemy na przykład napisać program, który wydaje dźwięki bez użycia POKEY'a:
opt %10101
consol equ $d01f
speakr equ %00001000
delay equ 200
org $480
play lda #0
loop eor #speakr
sta consol
ldx #delay
pause dex
bne pause
jmp loop
end.
Dla uproszczenia pominięto opcję wyjścia. Programik można zatrzymać przez SHIFT/BREAK lub brutalnie poprzez RESET. Przy
okazji drobna uwaga: MiniDebuger Quick Assemblera (uruchamiany opcją Run) przed przystąpieniem do wykonywania naszego
programu zeruje wstępnie wszystkie rejestry, aby ułatwić obserwowanie zmian. Na Twoim miejscu nie liczyłbym na to,
ponieważ systemy typu DOS nie robią tego. A zatem prawdziwy program zawsze powinien nadawać rejestrom wartości
początkowe, zwróć uwagę na wiersz z etykietą PLAY.
Powyższy prosty program steruje mambramą głośnika poprzez bit SPEAKER w komórce
CONSOL. Jak pamiętamy z poprzednich odcinków, rozkaz EOR użyty w ten sposób, będzie dawał w akumulatorze na przemian
wartości %00001000 i %00000000. Uwzględniając opóźnienie wprowadzone przez pętlę PAUSE, powinniśmy otrzymać dźwięk o
określonej wysokości, wynikającej z prędkości procesora. Tymczasem zamiast czystego dźwięku usłyszymy przykry charkot.
To przerwania angażujące procesor do innych prac zakłucają działanie naszego programu. Spróbujmy temu zaradzić
wyłączając przerwania IRQ poprzez umieszczenie rozkazu:
sei
bezpośrednio przed pętlą LOOP. I co? Dźwięk się nie zmienił, ale za to nie możemy przerwać programu przez SHIFT/BREAK,
ponieważ zgłoszenie przerwania nadchodzące z klawiatury nie jest już przez procesor obsługiwane. Można oczywiście użyć
rozkazu:
cli
przywracającego wrażliwość na przerwania IRQ, ale jak bez klawiatury skłonić program do jego wykonania? Tym razem już
tylko RESET nam pomoże. To pierwsze ostrzeżenie: wyłączając przerwania bez możliwości ich włączenia możesz łatwo
zablokować komputer!
W naszym przypadku, źródłem zakłóceń dźwięku jest przerwanie VBLANC należące do
grupy NMI. Związane jest ono z pionowym powrotem plamki kreślącej obraz telewizyjny i jest wywoływane przez ANTIC
dokładnie co 1/50 sekundy. Jest to jednak przerwanie niemaskowalne, nie można zablokować jego odbioru w procesorze. Cóż
więc począć? Trzeba wyłączyć źródło przerwania ! Do tego celu w ANTIC'u służy komórka:
nmien equ $d40e
usuń z naszego programu rozkaz SEI ( i tak niewielki z niego pożytek ), a zamiast niego wstaw:
lda #0
sta nmien
Zasembluj i uruchom program. Teraz wyraźnie słychać różnicę. Brzmienie dźwięku znacznie się poprawiło, choć jeszcze
daleki jest od ideału. Warto jednak pamiętać, że wyłączenie przerwań NMI powoduje zatrzymanie funkcji istotnych dla
życia systemu ATARI, takich jak zegar systemowy, liczniki programowe, aktualizacja rejestrów - cieni, pełna obsługa
klawiatury itd. Dalsze kłopoty wystąpią w chwili gdy zechcesz odtworzyć poprzedni stan rejestru NMIEN. Do światowej
skarbnicy nonsensu należy zaliczyć metodę przez niektórych programistów teoretyków:
lda nmien
pha
z zamiarem późniejszego odtworzenia:
pla
sta nmien
Niestety! Rejestr nmien nie jest komórką pamięci, a przy tym konsruktorzy nie wyposażyli go w możliwość odczytu.
Cokolwiek tam wstawić, odczytuje się zawsze 255, i nic z tego nie wynika. Tymczasem we współczesnych ATARI tylko dwa
bity w nmien mają znaczenie:
dli_en equ %10000000
vbl_en equ %01000000
Pierwszy zezwala na przerwanie wywoływane przez program ANTIC'a, zaś drugi na przerwania związane z pionowym powrotem
plamki w telewizorze. Ponieważ trudno zgadnąć, jak wyglądało dotychczasowe ustawienie, stosowane bywają dwie metody
przywracania przerwań NMI:
bezpieczna:
lda #vbl_en
sta nmien
i uprzejma:
lda #dli_en+vbl_en
sta nmien
Każda ma swoje wady związane z przerwaniem "DLI". Pierwsza zabija to przerwanie o ile było dotąd aktywne, druga może
uruchomić niepożądaną aktywność przerwania, choć dotąd było zablokowane. Wybór należy do Ciebie. Moja prywatna taktyka
polega na unikaniu ingerencji w NMIEN, o ile nie jest to niezbędnie konieczne. Wróćmy jednak do naszego dźwięku.
Prócz cyklicznych przerwań VBLANK źródłem zakłóceń jest nieustanne odczytywanie
pamięci obrazu przez procesor graficzny z użyciem techniki zwanej DMA, co oznacza bezpośrednie korzystanie z pamięci bez
udziału procesora. Jest to zjawisko podobne do przerwania, choć nim nie jest. Nasz przykładowy pracownik (procesor)
zasypia na chwilę, gdy w tym czasie złodzieje wynoszą towar z magazynu. Nie ma tu wykonywania żadnego innego programu,
lecz wskutek przerw o trudnych do przewidzenia odstępach i czasach trwania, właściwy program jest realizowany
nierytmicznie.
Aby wyłączyć transmisję DMA, dopiszmy po sekwencji blokującej NMI:
dmact equ $d400
dmact_ equ $22f
lda #0
sta dmact_
sta dmact
Nie wystarczy wpisanie wartości do rejestru - cienia, ponieważ (jak już wiemy) nie
działa teraz mechanizm przepisywania wartości z cieni do rejestrów sprzętowych. Zasembluj teraz i uruchom program.
Ładnie brzmi choć trochę mało widać.
Rozmnażanie przerwań IRQ
Istnieją dwa podstawowe sposoby kontaktów procesorów ze światem zewnętrznym:
nasłuchiwanie sygnałów, oraz przerwania. Nasłuchiwanie polega na okresowym sprawdzaniu rejestrów urządzeń zewnętrznych,
by nie przegapić nadejścia ważnego sygnału. Ta metoda jest prosta w realizacji, lecz angażuje czas procesora do
bezproduktywnego przeglądania rejestrów. Metoda przerwań jest trudniejsza w realizacji, lecz efektywniejsza w
działaniu. W komputerze ATARI połączono oba te sposoby.
Przerwanie IRQ, zgłaszane przez POKEY, może dotyczyć jednej z wielu sytuacji:
naciśnięcia klawisza, odebrania znaku od urządzenia zewnętrznego, wyzerowanie licznika, itd. Każde ze zdarzeń ma swoją
własną procedurę obsługi, lecz niestety jest tylko jedna linia zgłoszenia. Procesor zaczyna więc obsługę przerwania IRQ
zawsze od tego samego: systemowa procedura zawarta w ROM'ie sprawdza wszystkie możliwe źródła przerwań, a po
stwierdzeniu, które jest aktywne, kieruje sterowanie do odpowiedniej procedury szczegółowej, której adres znajduje się w
odpowiednim słowie (wektorze) na stronie drugiej.
Poniżej przedstawiam obszerne fragmenty zrealizowanego przy pomocy przerwań
sterownika złącza RS-232. Zastosowane zostało przerwanie zegara 1 POKEY'a. Omówi działanie tylko części nadawczej, gdyż
odbiór z punktu widzenia przerwań różni się w niewielu szczegółach.
Jeżeli czytelnicy wyrażą zainteresowanie opublikujemy opis wykonania takiego złącza, oraz odpowiedni program do jego
obsługi.
Instalowanie własnej procedury obsługi przerwania
Teoretycznie proste zadanie, w praktyce sprawia trochę kłopotów. Program po
wykonaniu zadania powinien zostawić system tak jak go zastał. Dlatego należy rozpocząć od zachowania dotychczasowej
zawartości wektora:
vtimr1 equ $210
lda vtimr1
sta oldvtm
lda vtimr1+1
sta oldvtr+1
Należy liczyć się z tym, że przerwanie nadejdzie właśnie podczas instalowania jego obsługi. Z pozoru mało prawdopodobny
przypadek wystąpienia przerwania gdy zmieniliśmy pierwszy bajt wektora, a drugiego jeszcze nie, zdarza się nad podziw
często, oczywiście następuje wtedy skok w trudny do przewidzenia zakątek pamięci, co kończy się z zasady "zawieszeniem"
się komputera. Najprościej uniknąć tej przykrej niespodzianki wyłączając na chwilę przerwania:
sei
lda <tx_int
sta vtimr1
lda >tx_int
sta vtimr1+1
cli
Do transmisji szeregowej wykorzystuje się złącze joysticka należy więc odpowiednio je przeprogramować (niektóre bity
jako wyjścia):
ldy #%00001100
jsp ppa
procedura ppa zostanie przedstawiona nieco dalej. Pozostaje uruchomić licznik POKEY'a:
bits equ 8
rate equ $68 600 baud
t1mask equ %00000001
audctl equ $d208
audc1 equ $d201
audf1 equ $d200
stimer equ $d209
irqen equ $d20e
irqen_ equ $10
ldy #0
sty audctl 64 kHz
sty audc1 quiet!
lda #rate
sta audf1
lda irqen_
ora #t1mask
sta irqen_
sta irqen enable
sta stimer
Od tej chwili każde przerwanie od zegara 1 będzie wywoływać naszą procedurę tx_int.
Obsługa przerwania.
tx_int equ *
bit tx_rdy
bmi tx_ret
bvc tx_bit
Nie ma potrzeby przechowywania zawartości akumulatora, ponieważ zrobiła to za nas
systemowa procedura rozpoznania przerwania. Zmienna tx_rdy decyduje o sposobie działania przerwania. Wartość 255 (czyli
-1, ustawiony 7bit) oznacza, że nie ma aktualnie żadnego znaku do wysłania. Nasza procedura w takim przypadku kończy się
bez zwłoki. Jeżeli 7 bit jest skasowany, lecz ustawiony 6, to nie wykona się skok BVC. Oznacza to początek wysyłania
znaku:
lda #bits
sta tx_cnt
lsr tx_rdy
clc
bcc tx_out (jmp)
Zainicjowany zostaje licznik bitów tx_cnt, skok do tx_out ze skasowanym znacznikim C
spowoduje wemitowanie bitu staru (0). Skasowanie 6 bitu zmiennej tx_rdy sprawi, że w następnych wywołaniach ten fragment
będzie pomijany zaś wykona się następny:
tx_bit lda tx_cnt
bpl tx_dct
* finish
lda #255
sta tx_rdy
pla
rti
Tu podejmuje się decyzję co do wysłania kolejnego bitu. Ujemna wartość w tx_cnt oznacza, że cały znak został już
wysłany. W takim razie zmienna tx_rdy znów przyjmuje wartość ujemną na znak, że nadajnik jest gotów do wysłania
następnego znaku. W przeciwnym razie należy zmniejszyć licznik:
tx_dct dec tx_cnt
sec
bmi tx_out
Jeżli licznik osiągnął wartość ujemną, to znaczy, że zostały już wysłane wszystkie bity danych, pora teraz na bit stopu
(1). Uzyskuje się to poprzez skok do tx_out z ustawionym znacznikiem C. Przy następnym przerwaniu ujemność licznika
spowoduje zkończenie procesu przesyłania znaku. Jeżli licznik jest jeszcze nieujemny, należy wysłać kolejny bit danych z
rejestru tx_dat:
lsr tx_dat
tx_out lda #0
rol @
asl @
asl @
sta pa
tx_ret pla
rti
Trzeba pamiętać, że po uruchomieniu zegara POKEY'a nasza procedura obsługi przerwania zaczyna żyć własnym życiem. Jest
wywoływana przez system w równych odstępach czasu, całkowicie niezależnie od innych naszych poczynań. Aby wysłać znak
należy go umieścić w akumulatorze i skoczyć do takiej oto prostej procedury:
send bit tx_rty
bpl send
Nieujemna wartość zmiennej tx_rty oznacza, że nie zakończyło się jeszcze nadawanie poprzedniego znaku. Trzeba czekać na
ustawienie 7 bitu - sygnał zakończenia
sta tx_dat
lsr tx_rdy
rts
Po umieszczeniu danej w rejestrze tx_dat kasujemy 7 bit zmiennej tx_rdy, by
zainicjować operację nadawania. I to wszystko. Nie czekając na efekt można powrócić do głównego programu, by zająć się
wyświetlaniem obrazu, sprawdzeniem klawiatury, procesu odbirczego itd., gdy tymczasem wstawiony do tx_dat bajt "sam się
nadaje".
Po zakończeniu transmisji całego ciągu bajtów (pliku) trzeba po sobie posprzątać
(w przypadku handlera "R:" czyni to funkcja CLOSE). Przedewszystkim czekamy na wysłanie ostatniego bajtu.:
w_last bit tx_rdy
bpl w_last
gdy to nastąpi wyłączamy czym prędzej zegar nr.1:
lda irqen_
and #255-t1mask
sta irqen_
sta irqen disable
następnie "niepostrzeżenie" oddajemy wektor przerwania:
sei
lda oldvtm
sta vtimr1
lda oldvtm+1
sta vtimr1+1
cli
i przywracamy normalny stan portu joysticka:
pa equ $d300
pac equ $d302
ldy #%00000000
ppa lda #$30
sta pac
sty pa
lda #$34
sta pac
rts
garść zmiennych robczych na koniec dopełnia całości:
tx_rdy dta b(255)
tx_cnt org *+1
tx_dat org *+1
oldvtm org *+2
|