Tajemnice ATARI

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

numer pozycji 7 6 5 4 3 2 1 0
liczba 1 1 1 0 0 0 0 1
wagi 128 64 32 16 8 4 2 1
iloczyny 1*128 1*64 1*32 0*16 0*8 0*4 0*2 1*1
suma 225 = 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:

bit 6 5 4 3 2 1 0 opis parametru dziesiętnie
. . . . . 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



Powrót na start | Powrót do spisu treści | Powrót na stronę główną

Pixel 2001