PROGRAMOWANIE PROCESORA 6502 W KOMPUTERACH ATARI XL/XE - cz.III
Cóż to są właściwie te tryby adresowania?
Tryb adresowania to inaczej sposób, metoda na poinformowanie procesora, do którego miejsca w pamięci ma się przy wykonaniu rozkazu odwołać. Asembler poznaje ten tryb ze sposobu zapisu argumentu, np.:
ADC 1410
to rozkaz dodania do akumulatora zawartości komórki pamięci o adresie 1410. Niewątpliwie jednak, o ile zadbamy, by w rejestrze X była liczba 10, taki sam efekt wywoła rozkaz
ADC 1400,X
Jeśli zaś wiemy, że słowo pod adresem 204 zawiera liczbę 1200, a rejestr Y - 210, to możemy napisać nawet
ADC (204),Y
lub
ADC 1200,Y
z tym samym skutkiem. Wybór sposobu adresowania zależy od naszej inwencji, która z kolei napędzana jest przez potrzeby. W istocie język maszynowy, który jest wynikiem działania asemblera, ma odrębne rozkazy dla każdego trybu adresowania. Nie ma przeto jednego, stałego kodu operacji, np. ADC, lecz wiele odrębnych rozkazów ADC, z których asembler wybiera właściwy, na podstawie analizy tekstu programu. Ale to już jego zmartwienie, nie nasze.
Nie wszystkie co prawda rozkazy akceptują każdy tryb adresacji, ale nie sądzę, by warto sobie tym było zaprzątać głowę. Jeżeli jakiś tryb adresowania jest nielegalny dla danego rozkazu, to asembler poinformuje nas o tym. Warto tylko wiedzieć, jakim zestawem trybów w ogóle dysponujemy (odnosi się to, rzecz jasna tylko do rozkazów z argumentami). Oto zestaw trybów dostępu do danych:
1. Natychmiastowy, np. LDA #10, musi to być liczba jednobajtowa. Argument stanowi część rozkazu. Nielegalny z rozkazami, które zmieniają wartość argumentu (np. INC i DEC, rozkazy zwiększania i zmniejszania bajtu w pamięci).
2. Bezwzględny długi, czyli z argumentem leżącym gdziekolwiek w pamięci, np. LDA 580. Najprostszy, dopuszczalny z większością rozkazów. Rozkaz maszynowy zawiera prócz bajtu kodu (identyfikującego rozkaz) dwubąjtowy adres argumentu. Teoretycznie argument może leżeć nawet na stronie zerowej, lecz w QA jest to trudne do uzyskania, bo dla adresów mniejszych od 256 asembler wybiera tryb krótki.
3. Bezwzględny krótki, czyli z argumentem leżącym na stronie zerowej, np. LDA 9. Asembler automatycznie wybiera odpowiedni kod rozkazu, zależnie od argumentu. Rozkaz maszynowy tego typu jest krótszy i szybciej się wykonuje od swego długiego odpowiednika, ponieważ zawiera tylko jeden (młodszy) bajt adresu (starszy bajt zawsze jest zerem).
4. Indeksowy długi, czyli z argumentem leżącym gdziekolwiek w pamięci. Adres, podany w rozkazie, zwiększany jest dodatkowo o liczbę zawartą w rejestrze X lub Y. Są zatem dwie odmiany takiego rozkazu, np. LDA 560,X i LDA 560,Y. Rejestr, którego symbol stoi po przecinku, zwie się w tym kontekście rejestrem indeksowym.
5. Indeksowy krótki, czyli z argumentem leżącym na stronie zerowej, którego adres zmodyfikowany jest dodatkowo zawartością rejestru X lub Y. Są zatem dwie odmiany takiego rozkazu, np. LDY 32,X i LDX 32,Y. Zwróć uwagę na dwa fakty, które początkującego programistę mogą tu
Po pierwsze: wiele rozkazów dopuszcza taki tryb z rejestrem indeksowym X, lecz tylko nieliczne z Y (dokładnie dwa: LDX i STX). Dlatego program, w którym często używamy np. rozkazu LDA 28,Y będzie dłuższy od analogicznego, w którym zastąpiono te rozkazy przez LDA 12,X. Wynika to z faktu, że asembler zastosuje dla LDA 12,Y jedynie dostępny tryb indeksowy długi, dopisując zera w starszej połówce adresu.
Po drugie (i ciekawsze): procesor odwołuje się rozkazem krótkim ZAWSZE
do strony zerowej, a zatem w wyniku wykonania rozkazów
LDX #lOO
LDA 200,X
w akumulatorze znajdzie się zawartość komórki pamięci o adresie 44, a nie 300, jak niektórzy byliby skłonni sądzić. Po prostu procesor zawsze zeruje starszy bajt adresu argumentu. Natomiast nawet starzy wyjadacze bywają niekiedy zaskoczeni faktem, że rozkazy
LDY #100
LDA 200,Y
umieszczą jednak w akumulatorze bajt spod adresu 300. Po prostu nie istnieje krótka odmiana rozkazu LDA 200,Y i asembler, nieświadom naszych intencji użyje tu trybu z adresem dwubajtowym.
6. Pośredni indeksowy, czyli z argumentem, którego adres znajduje się nie w rozkazie, lecz w słowie na stronie zerowej określonym przez argument. Są dwie odmiany tego rozkazu, różniące się zastosowanym rejestrem indeksowym, oraz (niestety), także innymi szczegółami.
Odmiana z rejestrem X, np. LDA (20,X) odwołuje się do pamięci poprzez tablicę adresów, która mieści się na stronie zerowej (oznacza to, że fragment strony zerowej traktowany jest jako tablica adresów, a nie, że z założenia są tam jakieś sensowne dane). Jeśli więc napisać
LDX #6
LDA (88,X)
to w akumulatorze znajdzie się bajt spod adresu, którego adres siedzi w komórkach 94 i 95. Jeśli to wydaje Ci się dziwne, zawiłe i mało przydatne, to całkowicie się z tym zgadzam. Można na palcach jednej nogi wyliczyć zastosowania tego rozkazu. Myślę, że w większości programów nie został użyty ani razu.
Natomiast odmiana z rejestrem T, np. LDA (20),Y jest bodaj najulubieńszym rozkazem wszystkich programistów-asemblerzystów. Odwołuje się on bowiem do tablicy bajtów, której adres siedzi na stronie zerowej w słowie określonym przez argument. Tak więc fragment programu
LDY #6
LDA (88),Y
umieści w akumulatorze szósty bajt z pamięci ekranu. Adres pośredni dla tych rozkazów MUSI leżeć zawsze na stronie zerowej.
Jak się należy pętlić
W dotychczasowych przykładach pokazano kilka różnych konstrukcji pętli (iteracji). Wybór właściwego sposobu zapętlania jest umiejętnością ważną, być może nawet podstawową. Uruchamiając poniższe przykłady nie zapomnijmy (jak poprzednio) o rozkazach OPT, ORG, itd...
Dla liczby przebiegów mniejszej od 128, np. wobec 66 znaków, które mają pojawić się na ekranie, postępujemy najprościej:
EKRAN EQU 88 (2)
ILE EQU 66
LDA #'a'
LDY #ILE-1
L STA (EKRAN),Y
DEY
BPL L
Kierunek od 68 do O pozwala uniknąć końcowego porównania. Po obsłużeniu (wyświetlanie znaków jest tu oczywiście tylko przykładem) ostatniego, zerowego elementu, Y zmienia się z O na -l, co spowoduje zakończenie pętu. Dla 256 znaków rejestr indeksowy musi przebiec wszystkie wartości. To również jest proste:
LDA #'b'
LDY #O
L STA (EKRAN),Y
INY
BNE L
gdyż 255 zwiększone o i daje zero. Natomiast dla liczb z przedziału 128..255 sytuacja nieco się komplikuje, ponieważ liczby te są... ujemne! W każdym razie tak to "rozumie" procesor, dla którego oznaką ujemności jest ustawiony siódmy (najstarszy) bit liczby. A zatem nie obejdzie się bez dodatkowego rozkazu porównania, jak podczas wyświetlania 192 znaków:
ILE EQU 192
LDY #O
LDA #'c'
L STA (EKRAN),Y
INY
CPY #ILE
BNE L
Próba uproszczenia, np.
LDY #ILE
LDA #'d'
L STA (EKRAN),Y
DEY
BNE L
Spowoduje zgubienie jednego znaku, ponieważ pętla nie wykona się dla Y = 0. Zastąpienie natomiast w tej "poprawionej" wersji BNE przez BPL sprawiłoby, że pętla wykona się tylko raz, bo już na początku siódmy bit Y jest ustawiony. Spróbuj, przekonasz się.
W przypadku, gdy jest do zapełnienia znakami cały ekran, trzeba to robić po kawałku. Gdyby jednak użyć metody z poprzedniego odcinka, polegającej na zapełnianiu kolejnych stron, to zostałaby końcówka wymagająca oddzielnej pętli, ponieważ rozmiar ekranu tekstowego nie jest całkowitą wielokrotnością strony. Lepiej jest obsługiwać ekran w sposób naturalny z ludzkiego punktu widzenia, czyli po wierszu:
OPT 21
WYS EQU 24
SZER EQU 40
EKRAN EQU 88
ADRES EQU 204
CZYM EQU 'e'
ORG 1152
* przeniesienie
* adresu ekranu
LDA EKRAN
STA ADRES
LDA EKRAN+1
STA ADRES+1
* wypełnienie
* 24 wierszy
LDX #WYS
WIERSZ LDA #CZYM
LDY #SZER-1
ZNAK STA (ADRES),Y
DEY
BPL ZNAK
CLC
LDA ADRES
ADC SZER
STA ADRES+1
DEX
BNE WIERSZ
Fragment od rozkazu CLC do STA ADRES+1 stanowi klasyczny przykład dodawania liczb dwubajtowych. Ponieważ rozkaz dodawania bajtów ADC zawsze uwzględnia przeniesienie z poprzedniego dodawania, można tym sposobem dokonywać operacji arytmetycznych na dowolnie długich ciągach bajtów. Przeniesienie (informuje o nim znacznik C) musi zostać wyzerowane przed operacją na pierwszym z bajtów i temu właśnie służy rozkaz CLC.
W tym przypadku do słowa w pamięci dodaje się argument natychmiastowy: znaczki "<" i ">" oznaczają odpowiednio młodszy i starszy bajt liczby długiej (asembler QA tworzy taki sam kod jak w przypadku znaczka #). Można także analogicznie dodawać dwie długie liczby zamieszkujące pamięć.
Parę słów o liczbach w notacji dwójkowej
System dwójkowy, podobnie jak dziesiętny, jest systemem pozycyjnym, to znaczy, że wartość reprezentowana przez pojedynczą cyfrę zależy od pozycji, na której się ta cyfra znajduje. Na przykład w liczbie 472 czwórka oznacza "4 razy 100", siódemka zaś, (choć jako cyfra jest większą) tylko "7 razy 10". Ów mnożnik, który określa ciężar danej cyfry w liczbie nazywa się wagą. Waga i-tej pozycji w dowolnym systemie jest równa podstawie systemu podniesionej do potęgi równej numerowi pozycji. Przypominam, że pozycje (cyfry) numeruje się od prawej strony, a skrajna ma numer 0. Podobnie więc jak w systemie dziesiętnym mamy wagi: 1 = 100, 10 = 101, 100 = 102, itd., tak i w systemie dwójkowym kolejne wagi są:
1 = 20, 2 = 21, 4 = 22, 8 = 23, itd. Dla
rozszyfrowania zatem dowolnego zapisu liczby wystarczy zsumować kolejne iloczyny cyfr i ich wag. Weźmy, dla przykładu, liczbę %11100001 ("%" oznacza zapis dwójkowy):
1 |
1 |
1 |
0 |
0 |
0 |
0 |
1 |
128 |
64 |
32 |
16 |
8 |
4 |
2 |
1 |
1*128 |
1*64 |
1*32 |
0*16 |
0*8 |
0*4 |
0*2 |
1*1 |
128 + |
64 + |
32 + |
0 + |
0 + |
0 + |
0 + |
1 |
Proste, prawda ? Łatwo zrozumieć, w jaki sposób konstruuje się wszystkie wartości od 0 do 255.
W pakiecie Quick Assembler, (którego konsekwentnie używamy we wszystkich przykładach) ustawia się parametry asemblacji przy użyciu rozkazu OPT. Argumentem rozkazu jest liczba, której poszczególne bity opisują tryb pracy asemblera. Oto ich znaczenie:
|
. |
. |
. |
. |
. |
x |
x |
zakres listowania: |
|
|
|
|
|
|
|
0 |
0 |
- wyłączone |
0 |
|
|
|
|
|
|
0 |
1 |
- tylko błędy |
1 |
|
|
|
|
|
|
1 |
0 |
- cały plik główny |
2 |
|
|
|
|
|
|
1 |
1 |
- cały asemblowany tekst |
3 |
|
. |
. |
. |
. |
x |
. |
. |
listowanie na ekren |
0 lub 4 |
|
. |
. |
. |
x |
. |
. |
. |
listowanie na drukarkę |
0 lub 8 |
|
. |
. |
x |
. |
. |
. |
. |
umieszczenie kodu w pamięci |
0 lub 16 |
|
x |
x |
. |
. |
. |
. |
. |
zapis na dysku (taśmie): |
|
|
0 |
0 |
|
|
|
|
|
- wyłączony |
0 |
|
0 |
1 |
|
|
|
|
|
- plik binarny (DOS-owy) |
32 |
|
1 |
0 |
|
|
|
|
|
- dołączenie do pliku j.w. |
64 |
|
1 |
1 |
|
|
|
|
|
- binaria bez nagłówków |
96 |
A zatem rozkaz OPT 21, którego użycie w przykładowych programach było zalecane, tłumaczy się:
16 + 4 + 1 czyli asemblacja z umieszczeniem kodu w pamięci i wyświetleniem błędów na ekranie. Ładnie napisany program definiuje parametry osobno:
LSTERR EQU %00000101
LSTALL EQU %00000110
LSTPRN EQU %00001110
OBJMEM EQU %00010000
OBJDSK EQU %00100000
Można je następnie wykorzystywać bez zastanawiania się nad znaczeniem bitów, np.:
OPT OBJMEM+LSTERR
* mruganie na ekranie
* (migający kursor)
KLAW EQU 764
NIC EQU 255
ZEGAR EQU 20
A_EKR EQU 88
INVER EQU %10000000
ORG 1152
* przykładowo 5. znak
LDY #5
* sprawdź klawiaturę
ZNOWU LDA KLAW
CMP #NIC
BNE KONIEC
* sprawdź zegar
LDX ZEGAR
TXA
AND #%lllll
BNE ZNOWU
* bity 0..4 są zerami
* raz na 32 jednostki,
* zmień 7. bit znaku,
* oznaczający inwersję
CLC
LDA (A_EKR),Y
ADC #INVER
STA (A_EKR),Y
* poczekaj na zmianę
POCZEK CPX ZEGAR
BEQ POCZEK
* powtórz wszystko
JMP ZNOWU
* koniec mrugania
KONIEC LDA #NIC
STA KLAW
RTS
* koniec programu
END
O sposobie interpretacji
Powyższy program mruga znakiem na ekranie, w tym celu pobiera bajt, dodaje do niego maskę bitową, a otrzymana liczbę wysyła znów na ekran. Oczywiście ta wstrząsająca różnorodność Istnieje tylko w naszym umyśle, procesor 6502 bowiem wykonuje operacje zawsze na liczbach i nie ma biedak pojęcia, do czego nam one posłużą. Każda taka wyrażona bajtem liczba jest, rzecz jasna, numerem jakiegoś znaku, skoro znaków jest właśnie 256. ANTTC, procesor obrazu w ATARI wyświetla w telewizorze znaki, których numery znajduje w obszarze pamięci należącej do ekranu (jej adres jest w słowie 88). Tego, że bajt %10000000, stanowiący maskę bitową, jest tym samym, co liczba 128, nie trzeba już chyba dogłębnie tłumaczyć. Ponieważ zaś zestaw znaków dzieli się wyraźnie na dwie części po 128 znaków odpowiadających sobie pod względem kształtu, a różniących się barwą, nietrudno zgadnąć, dlaczego zwiększanie numeru o 128 powoduje efekt mrugania. Łatwe też jest do wykazania, że dwukrotne dodanie liczby 128 (czyli razem 256) przywraca początkowy stan danego bajtu.
Janusz B. Wiśniewski
|