PROGRAMOWANIE PROCESORA 6502 W KOMPUTERACH ATARI XL/XE cz. IV
Ludzie listy piszą...
Wnikliwi czytelnicy dopatrzyli się nieścisłości w poprzednich odcinkach. Napisałem bowiem kiedyś, że znacznik V jest zmieniany tylko pod wpływem rozkazów ADC, SBC, PLP, BIT, zaś na znacznik C wpływają tylko:
ADC, SBC, PLP, ROL, ROR, ASL, LSR. Zapomniałem przy tym zupełnie o rozkazach modyfikujących bity rejestru znaczników. Rozkazy te należą do grupy rozkazów wewnętrznych procesora (nie komunikują się z pamięcią). Pozwalają wymusić konkretne stany niektórych znaczników. Oto ich wykaz:
CLC |
zeruje |
znacznik C |
SEC |
ustawia |
znacznik C |
CLI |
zeruje |
znacznik I |
SEI |
ustawia |
znacznik I |
CLV |
zeruje |
znacznik V |
CLD |
zeruje |
znacznik D |
SED |
ustawia |
znacznik D |
Jak widać, nie każdy znacznik da się dowolnie ustawić. Znaczenie i sposób wykorzystania tych rozkazów będą omawiane w miarę nadchodzących potrzeb.
Inni czytelnicy, użytkownicy QA, donoszą, że mimo starannego przepisania przykładów z pierwszego odcinka (przykłady polegały na zmianach kolorów w trybie tekstowym) nic im się na ekranie nie zmieniło. Myślę, że są oni w błędzie. Quick Assembler bowiem używa osobnego ekranu dla testów użytkownika i osobnego dla siebie. Nieporozumienie polega na tym, że program po zmianie kolorów natychmiast przekazuje sterowanie do QA i ekran testowy znika. Dzieje się to tak szybko, że oko nie zdąży zarejestrować efektu. Ekran użytkownika jest wszakże starannie przechowywany i zawsze można do niego zajrzeć naciskając klawisze SHIFT/CONTROL/SPACJA (podręcznik, s. 37).
Nie podejrzewam bowiem miłych mych czytelników, że uruchamiają programy od niewłaściwego adresu (przykłady zaczynają się od 1152, czyli $480 szesnastkowo, zatem adres Setup/Run musi być ustawiony na 480).
Ponieważ w wielu listach poruszany jest problem braku kursu programowania w podręczniku dołączonym do pakietu QA, pragnę podkreślić, że podręcznik ten jest ukierunkowany "gałkologicznie", to znaczy ma za zadanie wyjaśnić, co gdzie nacisnąć, aby zestaw zaczął działać. Sztuki programowania, niestety, nie sposób nauczyć na 64 stronach (obawiam się zresztą, że i 640 stron byłoby za mało). Niniejszy cykl przybliża nieco zagadnienia programowania, ale żeby zostać orłem, to nie wystarczy. Programowanie w języku asemblera jest dość trudne, więc lepiej łyknąć trochę teorii (podręcznik [l]), należy też znać dobrze wnętrze swego komputera (polecam książkę [2]), trzeba orientować się w sposobie wykorzystania poszczególnych obszarów pamięci (kłania się [3]), dobrze jest znać różne kruczki i sztuczki (zawarte w [4]) pomaga też ogonie oczytanie informatyczne (warto przeczytać [5], [6], [7], [8]).
[1] Rodney Zaks - Programming the 6502
[2] Lutz Eichler, Bernd Grohmann - ATARI 600XL/800XL Intern
[3] Ian Chadwick - Mapping the ATARI
[4] pr. zbiorowa - De Re ATARI
[5] Niklaus Wirth - Algorytmy + Struktury danych = Programy
[6] Dennie van Tassel - Praktyka programowania
[7] Niklaus Wirth - Wstęp do programowania systematycznego
[8] Edsger W. Dijkstra - Umiejętność programowania
Szczególnie natomiast odradzam programowania w języku asemblera tym osobom, dla których BASIC okazał się za trudny. Jest z tym podobnie, jak ze skokami akrobatycznymi do wody: trzeba najpierw nauczyć się pływać.
Podprogramy i stos
Możliwość definiowania powtarzalnych ciągów rozkazów w postaci wydzielonych fragmentów (tzw. procedur czyli podprogramów) i wywoływania ich z dowolnego miejsca w programie, tak często niedoceniana, ma podstawowe znaczenie dla przejrzystości i niezawodności programu. Używa się pary rozkazów: JSR (skok do podprogramu) i RTS (powrót z podprogramu). Wykonanie rozkazu JSR, znanego także jako "skok ze śladem", składa się z dwóch etapów. W pierwszym procesor umieszcza na stosie adres służący do odnalezienia dalszego ciągu programu głównego (rozkazu następującego po JSR). Drugi etap - to skok, analogicznie jak przy rozkazie JMP: jako następny wykona się rozkaz spod adresu danego argumentem. Rozkaz RTS przywraca wykonywanie przerwanej JSR-em sekwencji rozkazów poprzez pobranie ze stosu zapamiętanego tam adresu. Oczywiście rozkazy te można wykonywać tylko w rozumnej kolejności, bo nie poprzedzony JSR-em RTS nie ma przeważnie żadnego sensu. Przyjrzyj się przykładowi:
OPT %10101
*--- system
ZEGAR EQU 20
POKEY EQU $D200
OKRES EQU POKEY+0
BARWA EQU POKEY+1
*-- stale
CZAS EQU 5
BARW EQU $A0
GLOS EQU $08
TON_l EQU 100
TON_2 EQU 80
TON_3 EQU 67
TON_4 EQU 50
ORG $480
*--- program główny
POCZ LDA #TON_l
JSR GRAJ
LDA #TON_2
JSR GRAJ
LDA #TON_3
JSR GRAJ
LDA #TON_4
JSR GRAJ
RTS
*-- podprogram: graj dzwiek
* (okres dzwieku w A)
GRAJ EQU *
* wlacz granie
STA OKRES
LDA #BARW+GLOS
STA BARWA
* poczekaj
CLC
LDA ZEGAR
ADC #CZAS
CZEK CMP ZEGAR
BNE CZEK
* zamilcz
LDA #0
STA BARWA
* powrót z podprogramu
RTS
END
Główny program składa się z czterech wywołań procedury GRAJ. Każde wywołanie powoduje wyemitowanie dźwięku o czasie trwania określonym etykietą CZAS. Ponieważ dźwięki różnią się częstotliwością, program główny przekazuje do podprogramu informację liczbową. Najwygodniej użyć w tym celu rejestru, który inicjuje się przed wywołaniem procedury. W tym przypadku jest to akumulator. Rozkaz RTS w procedurze GRAJ powoduje powrót do głównego programu, w miejsce wywołania, tuż po odpowiednim rozkazie JSR. Rozkaz RTS w głównym programie powoduje powrót do asemblera (o ile
jest to QA, to Twój testowany program jest wywoływany właśnie rozkazem JSR).
Jeżeli podprogram wywołuje kolejny podprogram, używając rozkazu JSR, to nic nie szkodzi, ponieważ kolejne adresy odkładają się na stosie nie niszcząc poprzednich, dokładnie tak, jak to sugeruje jego nazwa. Kolejne rozkazy RTS będą pobierać dane w odwrotnej kolejności, niż były układane (adres z pierwszego JSR-a zostanie zdjęty na końcu).
Stos jest w stanie pomieścić 256 bajtów (czyli 128 adresów) i "chodzi w kółko", to znaczy przy przepełnieniu zaczyna "zjadać" najwcześniej składowane dane. W praktyce nie dostajemy do dyspozycji całego stosu, bo nasz program wywoływany jest zwykle przez jakiś inny podprogram głównego programu systemu operacyjnego. Trzeba więc zadbać, by podprogramy nie wywoływały się nawzajem bez końca. W praktyce, dla większości poprawnie napisanych programów, pojemność stosu jest (z dużym zapasem) wystarczająca.
Stos zajmuje stronę 1 pamięci komputera i jest w złym guście umieszczać tam cokolwiek innego.
Notacja szesnastkowa
Powyższy program zawiera kilka liczb rozpoczynających się od znaku "$". Są one zapisane w notacji szesnastkowej. Wielu adeptów sztuki programowania podchodzi do liczb szesnastkowych jak do jeża. Tymczasem ten sposób zapisu jest prosty i wdzięczny. Przy zwięzłości przewyższającej nawet system dziesiętny daje wyraźny podział na bajty, typowy dla notacji dwójkowej. Pozwala to wyodrębniać starszy i młodszy bajt liczby dwubajtowej (adresu) bez żadnych przeliczeń.
Na pierwszy rzut oka można stwierdzić, na której stronie pamięci znajduje się dany adres, np.: $480 - czwarta, $612 - szósta. Jeżeli nieobca Ci jest idea zapisu dwójkowego, to szesnastkowy przełkniesz bez kłopotu, ponieważ istnieje między nimi odpowiedniość podległa prostym regułom. Każdą czwórkę bitów liczby dwójkowej zastępuje się pojedynczym symbolem, I tak:
%0000 = $0 (0)
%0001 = $1 (l)
%0010 = $2 (2)
%0011 = $3 (3)
%0100 = $4 (4)
%0101 = $5 (5)
%0110 = $6 (6)
%0111 = $7 (7)
%1000 = $8 (8)
%1001 = $9 (9)
%1010 = $A (10)
%1011 = $B (11)
%1100 = $C (12)
%1101 = $D (13)
%1110 = $E (14)
%1111 = $F (15)
To pokrywa wszystkie możliwe kombinacje czterech bitów. Pamiętać tylko należy, że bity w liczbie dwójkowej liczy się od prawej strony (gdyby ich brakło do pełnej czwórki, można z lewej uzupełnić zerami).
Zatem liczba %1110010001110111 to po ludzku $E477. Proste? Układ dźwiękowy ATARI, zwany POKEY, zajmuje w pamięci komputera stronę o numerze $D2. Jego rejestry znajdują się pod adresami $D200, $D201, $D202, itd... Tak właśnie odwołuje się do nich program przykładowy.
Procedury systemowe
Pamięć stała (ROM) w naszym komputerze zawiera szereg procedur, które można, a często nawet trzeba wykorzystywać w swoich programach. Prosty przykład:
JSR $F556
Uzupełnij jak zwykle o OPT, ORG z przodu i RTS, END z tyłu. Co to robi? W większości komputerów ta procedura generuje dźwięk ostrzegawczy, znany jako BELL. Jeżeli Twój komputer działa inaczej, to być może procedura BELL siedzi tam pod innym adresem (albo Twój głośnik nie działa!). Wykorzystanie takiej procedury odbywa się na ryzyko programisty, bo komputery różnią się między sobą. Bywają jednak adresy pewne, których niezmienność gwarantowana jest przez firmę ATARI. Większość z Was zna z pewnością procedury, które mają swe początki pod adresami $E471 (Self Test), $E474 (ciepły start, w znacznej mierze podobny do sytuacji po naciśnięciu klawisza RESET), $E477 (zimny start, wymuszający stan komputera jak bezpośrednio po włączeniu). Procedury z nich
trochę nietypowe, bo nie wykazują ochoty wracać do wywołującego je programu (nie kończą się rozkazem RTS), przeto nie poświęcimy im wiele uwagi. Lecz widać przy okazji, że ich adresy początkowe odległe są o 3 bajty. Początki tych oficjalnych procedur ułożone są bowiem w rodzaj tablicy, której każdy element jest rozkazem skoku JMP (taki rozkaz ma właśnie długość trzech bajtów) do właściwego miejsca, gdzie kontynuowana jest dana procedura.
Ta tablica skoków (zwanych w literaturze wektorami) zaczyna się pod adresem $E450, a kończy gdzieś, hen... Różne źródła różnie o tym mówią. W moim komputerze ostatni rozkaz JMP leży pod adresem $E48C, nie wszystkie jednak z nich są dobrze udokumentowane.
Jedną z najprzydatniejszych procedur systemowych jest procedura wejścia/wyjścia znana jako CIO lub CIOV. Zaczyna się ona pod adresem $E456. Można jej użyć np. do wyświetlenia tekstu na ekranie:
*--- wyswietlanie napisu
OPT %l0l0l
IOCB EQU $340
IO_COM EQU IOCB+2
IO_ADR EQU IOCB+4
IO_LEN EQU IOCB+8
CHN0 EQU 0
PISZ EQU 11
EOL EQU $9B
CIOV EQU $E456
ORG $480
LDX #CHN0
* rozkaz wyslania napisu
LDA #PISZ
STA IO_COM,X
* adres tekstu
LDA TEXT
STA IO_ADR+1,X
* rozmiar tekstu
LDA ILE
STA IO_LEN+1,X
* wykonaj procedure WE/WY
JSR CIOV
* powroc do QA
RTS
* tekst do wyswietlenia
TEXT DTA C'Oto ten tekst!'
DTA B(EOL)
ILE EQU *-TEXT
* koniec programu
END
Informacje niezbędne dla wykonania operacji przekazuje się w tak zwanym bloku sterowania WE/WY, zwanym IOCB. Ponieważ tych bloków jest kilka, trzeba wskazać właściwy poprzez odpowiednie ustawienie rejestru X. Wartość 0 oznacza blok nr 0 (związany na stałe z ekranem). W komórce określonej tu jako IO_COM przekazuje się żądanie (11 oznacza wysłanie informacji na ekran). W słowach IO_ADR i IO_LEN przekazujemy informację o tym, co wysłać i ile tego jest. Pozostałych bajtów bloku IOCB (ma on długość 16) nie musimy w tym przypadku wypełniać. Po wyświetleniu tekstu procedura CIOV powraca do naszego programu.
Dyskowy plik binarny
Dotychczasowe przykłady uruchamiane były pod kontrolą QA. Nie jest to trudne, bo wystarczy ustawić Setup/Run tak, aby wykonywanie programu rozpoczęło się od właściwego adresu (najczęściej pokrywa się on z argumentem pierwszego ORG-a). Uzyskanie samodzielnego programu, który można wywołać bezpośrednio spod DOS-u lub COS-u też nie jest trudne, choć wymaga kilku zabiegów.
Przede wszystkim zmiany wymaga argument rozkazu OPT. Zapis pliku w formacie dyskowym następuje podczas drugiego przebiegu asemblacji, o ile w argumencie OPT jest ustawiony bit 5, a wyzerowany 6. Rozkaz ten będzie więc miał postać
OPT %01xxxxx
gdzie literkami x oznaczono bity związane z generowaniem kodu do pamięci, drukowaniem, wyświetlaniem na ekranie i trybem "listowania". Ponieważ są one całkowicie niezależne od bitów włączających zapis na dysk, można je ustawiać dowolnie, wedle potrzeb. Ale bez przesady! Podczas nagrywania kodu na taśmę pełne listowanie programu może być przyczyną przestojów, co spowoduje wzrost odstępów między rekordami. Z kolei równoległe umieszczanie kodu w pamięci ogranicza rozmieszczenie programu, gdyż QA broni się przed zniszczeniem. Dlatego w praktyce podczas zapisu kodu na dysku, a zwłaszcza na kasecie, pozostałe opcje się wyłącza. Wskazane jest wykonać najpierw próbną asemblację bez kodu, aby upewnić się, że program jest syntaktycznie poprawny.
Ostatnią operacją przed stworzeniem pliku wynikowego z programem może być nadanie pożądanej nazwy temu plikowi. Robi się to w menu File/Obj, gdzie figuruje zwykle nazwa domyślna, którą QA ustawia zawsze podczas Load i Save wzorując się na nazwie pliku z programem (domyślne rozszerzenie będzie .OBJ). Nazwa N0NAME, którą QA przyjmuje w chwili uruchomienia, jest nazwą awaryjną i należy unikać wykorzystania jej do nazywania rzeczywistych plików. Oznacza ona po prostu BRAK NAZWY i sygnalizuje, że nic tam jeszcze nie wpisywaliśmy. Dla zapisania kodu na kasecie trzeba oczywiście wpisać C:. W przypadku korzystania z COS-u dobrze jest wczytać i znów zapisać kod programu kopierem NameCopy, aby nadać nazwę, ponieważ QA sam tego nie robi.
Czy już wszystko gotowe? Ostatni przykład programu świetnie się nadaje do takiego eksperymentu. Gdy plik wynikowy już gotów, można wyjść z QA. Pod DOS-em (mam na myśli system dystrybuowany przez AVALON wraz z QA) pisze się po prostu nazwę pliku, a o ile rozszerzenie jest .COM, to można je pominąć. Pod COS-em pisze się **.
I co? I nic. To dobrze. DOS lub COS załadował program do pamięci pod właściwy adres, lecz na tym koniec. Aby go teraz uruchomić, napisz RUN 480. Jak zrobić, żeby program sam się uruchamiał, będzie za miesiąc.
Janusz B. Wiśniewski
|