Programowanie 6502 (4-6)


      Witam wszystkich cierpliwych i chętnych do nauki programowania czytelników. Ponieważ ten dzisiejszy (nasz drugi) odcinek kursu zawiera w rzeczywistości trzy następne odcinki autorstwa Janusza Bohdana Wiśniewskiego nie będę tu dalej już ględził, zapraszam do wnikliwej lektury i nauki...

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 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 rozkaz RTS nie ma przeważnie żadnego sensu.

Przyjrzyjmy 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 $8
ton_1  equ 100
ton_2  equ 80
ton_3  equ 67
ton_4  equ 50

       org $480

*--- program glowny

pocz   lda #ton_1
       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

* powrot 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ę czestotliwością, program główny przekazuje do podprogramu informację liczbową. Najwygodniej użyć do tego 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 assemblera (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 sugeruje jego nazwa. Kolejne rozkazy RTS będą pobierać dane w odwrotnej kolejności, niż były odkł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 wystarczająca (z dużym zapasem). Stos zajmuje pierwszą stronę 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 bardzo 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łopotów, ponieważ istnieje między nimi odpowiedniość podlegająca bardzo prostym regułom. Każdą czwórkę bitów liczby dwójkowej zastępuje się pojedynczym symbolem. I tak:

      %0000 = $0 (0)
      %0001 = $1 (1)
      %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 nawet trzeba wykorzystywać w swoich programach. Prosty przykład:

      jsr $f556

Uzupełnij jak zwykle o OPT, ORG z przodu i RTS, END z tylu. 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 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. Wiekszość 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 to 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 trzy 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:

*--- wyswietlenie napisu

      opt %10101

iocb   equ $340
io_com equ iocb+2
io_adr equ iocb+4
io_len equ iocb+8

chno   equ 0
pisz   equ 11
eol    equ $9b
ciov   equ $e456

      org $480

      ldx #chno
* rozkaz wyslania napisu
      lda #pisz
      sta io_com,x
* adres tekstu
      lda <tekst
      sta io_adr,x
      lda >tekst
      sta io_adr+1,x
* rozmiar tekstu
      lda <ile
      sta io_len,x
      lda >ile
      sta io_len+1,x
* wykonaj procedure we/wy
      jsr ciov
* powroc do QA
      rts

* tekst do wyswietlenia

tekst dta c'Oto ten tekst !'
      dta b(eol)
ile   equ *-tekst
* 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 0 (związany na stałe z ekranem). W komórce tu oznaczonej jako io_com przekazuje się zadanie (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 po 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-a 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 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 koleji równoległe umieszczanie kodu w pamięci ogranicza rozmieszczenie programu, gdyż QA broni się przed zniszczeniem. Dlatego w praktyce podczas zapisu kodu na dysk 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 NONAME, którą QA przyjmuje w chwili uruchomienia, jest nazwą awaryjną i należy unikać wykorzystania jej do nazwania rzeczywistych plików. Oznacza ona po prostu BRAK NAZWY i sygnalizuje, że nic tam jeszcze nie wpisaliśmy. Do zapisania kodu na kasecie trzeba oczywiście wpisać nazwę C:. W przypadku korzystania z COS-u dobrze wczytać i znow zapisać kod 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 sie **. 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 chwilę.

BUDOWA DYSKOWEGO PLIKU BINARNEGO

      Plik dyskowy w formacie DOS-u może składać się z dowolnej liczby niezależnych segmentów (bloków). Każdy segment ma na przedzie tzw.nagłówek, który określa obszar pamięci, gdzie DOS powinien umieścić dane tego bloku. Nagłówek może mieć długość 4 lub sześciu bajtów, w tym drugim przypadku dwa pierwsze bajty zawierają liczby 255 ($FF). Pierwszy segment pliku MUSI mieć nagłówek sześcio-bajtowy, bo stanowi to informację, o rodzaju pliku dla DOS-u, pozostałe wedle uznania. Końcowe cztery bajty nagłówka niosą adresy początku i końca obszaru pamięci, gdzie zostanie umieszczony ten segment. Bezpośrednio po nagłówku następują dane segmentu w ilości wynikającej z nagłówka (adres1 - adres2 + 1bajt). Inaczej mówiąc segment stanowi zapisany na dysku pewien obszar pamięci komputera uzupełniony o adresy określające jego położenie. Po ostatnim bajcie danych może następować nagłówek następnego segmentu, itd.

      W praktyce rzadko odczuwamy potrzebę rozmyślania o nagłówkach bloków. Robi to bowiem za nas asembler, który zapisuje każdy ciąg bajtów, poprzedzony rozkazem ORG, w postaci osobnego segmentu. Warto przy tym pamiętać, że QA przed pierwszym ORG programu wykonuje "w myśli" rozkaz ORG 0. Ten mechanizm pozwala pisac "programy" bez rozkazów maszynowych, np. taki:

      opt %100101

      org $8000

ekra  equ *
      dta d' *** Programowanie '
      dta d'procesora 6502 *** '

      org $600

dlis  dta b($70),b($70),b($70)
      dta b($42),a(ekra)
      dta b($41),a(dlis)

      org $58

      dta a(ekra)

      org $2c5

      dta b($02),b($6a)

      org $22f

      dta b($22),dta a(dlis)

      Taki "program" spowoduje wyświetlenie napisu na ekranie, chociaż nie zawiera ani jednego rozkazu maszynowego. Umieszcza on stosowne wartości w komórkach sterujących wyswietlaniem obrazu, wypełnia program dla ANTIC-a (dl) i ekran. Rozkaz DTA pozwala umieścić w programie dane dowolnego rodzaju np. d'tekst' buduje ciąg bajtów zawierających kody ekranowe (internal) znaków składających się na 'tekst', b(wyrażenie) tworzy bajt z podaną wartością, zaś a(wyrażenie) - słowo. Poszczególne składowe rozkazu DTA odziela się przecinkami.

Parametry asemblacji OPT %100101 pozwolą wyemitować kod tego programu na dysk lub kasetę. Takie ustawienie zmusza asembler do poprzedzania każdego segmentu bajtami nagłówkowymi.

Dwie uwagi:

      Po pierwsze, nie każdy DOS podejdzie do tego kodu jednakowo wyrozumiale. Np. SpartaDos będzie próbował go mimo wszystko uruchomić, skacząc do początkowych bajtów danych jak do programu.

      Po drugie zmieniony wygląd ekranu pozostaje trwale, trzeba użyć klawisza RESET, żeby przywrócić ekran do normalnego stanu. Dodanie do tego żywych efektów dźwiękowych będzie już wymagało wykonania prawdziwego programu:

pokey equ $d200
okres equ pokey+0
barwa equ pokey+1

      org $480

prog  lda #$aa
      sta barwa
l0    ldx #255
l1    stx okres
      ldy delay
l2    dey
      bne l2
      dex
      bne l1
      inc delay
      bpl l0
      stx barwa
      stx okres
      jmp (10)

      Ten fragment można dopisać bezpośrednio do poprzedniego. Spotykamy tu nowy tryb adresacji, który stosuje się tylko z rozkazem JMP. Jest to tak zwany skok pośredni: procesor przejdzie do wykonania rozkazu, którego adres znajduje się w słowie pod adresem 10. Aby taki program wykonał się od etykiety PROG, należy uzupełnić go o:

      org $2e0
      dta a(prog)

DOS, po załadowaniu wszystkich segmentów programu poszukuje w słowie $2e0 adresu początku programu. Wcześniej ustawił te komórki w taki sposób, by móc rozpoznać, czy program umieścił tam swoje dane. Jeżeli wczytywany program wstawił tam (jako jeden z bloków) jakiś adres, to system po załadowaniu WSZYSTKICH bloków skacze do podanego adresu rozkazem JMP lub JSR. Z uwagi na to właśnie, że niektóre systemy wywołują programy przez JMP, a nie JSR błędem jest kończenie programu użytkowego rozkazem RTS, który nie ma żadnego sensu w parze z JMP.

      Godny polecenia jest skok JMP(10), pod tym adresem bowiem zapisany jest adres początku głównej procedury DOS. Do testów pod QA jednak, zwłaszcza gdy używa się klawisza RESET, lepiej czasowo wpisać RTS. Jak widać na powyższym przykładzie, nie jest wymagana jakaś konkretna kolejność segmentów. W szczególności segmentu definjującego adres uruchomienia może znaleść się gdziekolwiek, nawet na początku programu. W praktyce jednak okazuje się, że zachowanie porządku przynosi pewne korzyści. Na przykład w SpartaDOS'ie program nie musi podawać adresu uruchomienia, o ile początek logiczny programu pokrywa się z fizycznym (innymi słowy jeżeli adres uruchomienia wskazuje na pierwszy bajt pierwszego segmentu). Można jednak zauważyć, że niektóre programy zaczynają się wykonywać jeszcze przed zakończeniem procesu wczytywania. Na przykład QA wyświetla swoją wizytówkę i sprawdza przydział pamięci zanim rozpocznie się odczyt właściwego programu. Takie działanie możliwe jest dzięki tzw. inicjalizacji miedzysegmentowej. Wczytujący się program powinien wpisać w słowo $2E2 (initad) adres procedury (uprzednio wczytanej do pamięci), a system niezwłocznie wykona tą procedurę (przed odczytaniem następnego fragmentu). Trzeba zwrócić uwagę na to że procedura do wykonania po wpisaniu słowa initad i dalsza część programu musi być w dwóch oddzielnych blokach.

      Dzięki temu mechanizmowi każdy segment może być wykonywany bądź inicjalizowany osobno, możliwe też jest jak w przypadku QA powtórne użycie obszaru pamięci po wykorzystanym już podprogramie do umieszczenia następnego segmentu. Nie ma żadnego ograniczenia na liczbę takich miedzyblokowych inicjalizacji. Jest to bardzo pożyteczny mechanizm choć trochę kłopotliwy gdy się pisze programy do uruchamiania takich plików. Zwłaszcza odczyt z kasety gdy procedura miedzyblokowa wykonuje się zbyt długo może ulec zaburzeniu. Z tego względu niektóre kopiery np. Ncopy wydłużają przerwy pomiędzy rekordami zapisywanymi na kasecie w miejscach gdzie zostanie wykryty blok umieszczany w initad.

BUDOWA PLIKU "BOOT"

      Programy samowczytujące zwane żargonowo "butami" zbudowane są znacznie prościej. Sześciobajtowy nagłówek programu określa jego długość, adres ładowania i uruchomienia. Odmiennie niż w przypadku pliku dyskowego, gdzie nagłówek był elementem zewnętrznym w stosunku do segmentu tutaj wchodzi on w skład wczytywanego bloku i jest razem z nim umieszczany w pamięci. Pierwszy bajt nagłówka jest przez system ignorowany zwykle więc zawiera 0. W drugim bajcie umieszcza się rozmiar pliku liczony w 128 bajtowych rekordach co odpowiada fizycznej strukturze standardowego zapisu na taśmie lub dysku (liczba 0 oznacza 256 rekordów). Zawsze więc wczytuje się wielokrotność 128 bajtów. "Boot" kasetowy nie różni się niczym od dyskowego.

      Ponieważ QA nie ma specjalnego trybu do tworzenia plików samowczytujacych trzeba wykorzystywać tryb "bez nagłówków" co uzyskuje się rozkazem:

      opt %1100101

Tym, że nasz program nie zapełni całkowicie wielokrotności 128 bajtów nie warto się przejmować. Trzeba tylko zadbać by podany rozmiar nie był krótszy od programu. Nasz przykład zmieści się całkowicie w jednym rekordzie (sektorze).

      org $700
      dta b(0),b(1)

Drugi i trzeci bajt zawiera adres ładowania czyli wskazuje miejsce w pamięci gdzie zostanie umieszczony nasz "but". System nie nakłada żadnych ograniczeń na ten adres ale przyjęło się umieszczać programy samowczytujące od siódmej strony pamięci.

      dta a($700)

Pozostałe dwa bajty nagłówka zawierają adres procedury inicjalizacji. O ile niczego nie popsujemy, to ta procedura będzie się później wykonywać zawsze w wyniku naciśnięcia klawisza RESET. Trzeba bowiem pamiętać, że samowczytujące się programy z reguły są odporne na RESET. Typowym przykładem takiego programu jest DOS.

      dta a(init)

Nasz krótki "but" może być wykorzystany w programie formatujacym dyski. By omyłkowa próba wystartowania systemu z pustej dyskietki nie powodowała zawieszenia komputera lub przykrych efektów dźwiękowych. Po wczytaniu do pamięci stosownej liczby sektorów (w naszm przypadku jednego) wynikającej z nagłówka system uruchamia procedurę "kontynuacji", która powinna znajdować się bezpośrednio po nagłówku. Tej procedurze można powierzyć różne zadania. Zwykle przejmuje ona wczytywanie dalszej części programu (bo rasowy "but" ma co najwyżej trzy sektory). Jeżeli tak jak w naszym przykładzie nie ma dalszej części to wystarczy ustawić adres dosvec by system wiedział gdzie skoczyć po wykonaniu wszystkich czynności wstępnych.

dosvec equ 10

       lda <start
       sta dosvec
       lda >start
       sta dosvec+1

Procedura kontynuacji przekazuje do systemu za pomoca flagi C informację o tym czy proces wczytywania zakończył się powodzeniem. Ustawienie tej flagi oznaczałoby porażkę a zatem:

      clc
      rts

i proces boot został pomyślnie zakończony. Teraz system wykona swoje tajemnicze operacje podczas których wywoła też naszą procedurę inicjalizacji. Jej adres zostanie skopiowany z dwóch końcowych bajtów nagłówka do słowa casini (2) w przypadku taśmy lub dosini (12) w przypadku dysku. Ta procedura przeznaczona jest do wykonywania podczas sekwencji RESET i powinna regenerować te elementy, które RESET unicestwia. Np. DOS uzupełnia HATABS o wpis urządzenia "D:". Błędem jest natomiast ustawienie komórek, które przy RESET nie ulegają zmianie jak dosvec lub boot (9). Niestety wiele programów tworzonych przez dyletantów tak właśnie postępuje. Wymusza to konieczność ich "przechytrzania" przez programy rezydentne takie jak XLF czy BH co zwiększa stopień skomplikowania tych ostatnich. Dla naszych potrzeb wystarczy RTS, ale dla lepszego wyobrażenia czy i kiedy się to wykonuje zmieńmy kolor obrzeża ekranu.

ramka equ 712
znaki equ 709

init  lda znaki
      sta ramka
      rts

Teraz system spokojnie przekaże sterowanie do głównego programu, którego adres znajdzie w dosvec. A ten program wyświetli komunikat:

tekst dta c'Tu nic nie ma! Stuknij ESC...',b($9b)

ciov  equ $e456
iocb  equ $340
chn0  equ 0
pisz  equ 9

start ldx #chn0
      lda #pisz
      sta iocb+2,x
      lda <tekst
      sta iocb+4,x
      lda >tekst
      sta iocb+5,x
      lda #255
      sta iocb+8,x
      sta iocb+9,x
      jsr ciov

i zaczeka na wymianę dyskietki na właściwą, oraz naciśnięcie klawisza ESC

esc   equ 28
ch    equ 764

      lda #esc
wkey  cmp ch
      bne wkey

po czym wykona zimny start.

cold  equ $e477

      jmp cold

      end

Warto zauważyć, że nasz progrm nie ulega klawiszowi reset ma natomiast opcje "wyjścia". Wiadomo przecież, że 99% uszkodzeń komputerów zdarza się podczas wyłączania sprzętu. Dlatego troskliwy użytkownik robi to jak najrzadziej, a dobrze wychowany program pomaga mu w tym a nie przeszkadza.

      Kod rozkazu 9 przekazany do ciov oznacza polecenie wysłania napisu o długości nie większej niż podana w iocb+8 i +9. Przesyłanie tekstu zostaje samoczynnie przerwane przy napotkaniu pierwszego znaku końca wiersza ($9b). Jest to wygodne przy wypisywaniu jednego wiersza tekstu o nie sprecyzowanej długości. Dla testów zapiszemy nasz "but" na kasecie wpisując w QA nazwę C: (w menu file/obj) i wykonując Assembly. Nagrany plik należy uruchamiać poprzez wykonanie zimnego startu przy wciśniętych klawiszach start i option. Do wywołania procedury zimnego startu nie trzeba wcale wyłąnczać komputera. Istnieje mnóstwo mniej lub bardziej pomysłowych sposobów. W DOS lub COS wystarczy napisac run E477 w BASIC praktykuje się BYE i RESET w QA można ustawić adres Setup/Run na E477 i wykonać run z głównego menu. Mając w pamięci XLF-a można przywołać wglądownicę wpisać coś do komórki 580 schować wglądownicę (by w przyszłości cały XLF się nie wczytywał) i nacisnąć RESET.

      Aby umieścić nasz "but" na dyskietce najprościej jest użyć programu Turbo Watson lub jego młodszego kuzyna Wacia który prezentowany był w TA 4/91, należy wczytać sektor pliku zawierającego wersję OBJ naszego programu włożyć do napędu sformatowaną pustą dyskietkę przeznaczoną do testów zmienić numer sektora na 001 i zapisać go. Dyskietka uruchamiana przy wciśniętym klawiszu option zgłosi się natychmiast, zaś bez option dopiero po rozkazie DOS. Podczas zimnego startu komputer podejmuje probę wczytania "buta" z dyskietki, a o ile trzymamy wciśnięty klawisz START także z kasety. Brak napędu dysków kwitowany jest charakterystycznym "pryczeniem". Powodzenie odczytu programu z dysku powoduje ustawienie bitu zerowego ( wartość 1 ), w komórce 9 (boot) zaś powodzenie odczytu z kasety ustawia bit 1 (wartość 2). Od tej wartości zależy późniejsze zachowanie się systemu w procedurze RESET zwanej też ciepłym startem. 2 powoduje wykonanie procedury inicjalizujacej o adresie zapisanym w casini, a 1 w dosini. Jak nietrudno się domyślić wartość 3 wymusza wykonanie obydwu tych procedur.

      Komórka boot bardzo ważna określa zatem typ inicjalizacji. Jej poprawne ustawienie warunkuje prawidłową pracę systemu. Tymczasem do redakcji wciąż napływają cudowne pomysły np. na nieśmiertelny mrugający kursor, którego podtrzymanie zasadza się na wpisaniu swojego adresu do dosini i zmianie komórki boot na 1. Takie programy wędrują prosto do kosza. Ich autorzy posiadacze magnetofonów nie zdają sobie sprawy z efektu jaki ich program wywoła w DOS'ie.

MIAŁA BABA KOGUTA

      ...i wsadziła go do boota! Użytkowników ATARI z pamięcią taśmową można poznać z daleka po charakterystycznym skrzywieniu sylwetki. Lewa reka jest nieco dłuższa wyciągnięta w kierunku wyłącznika zsilania palce prawej układają się w rodzaj charakterystycznych widełek, które nieomylnie trafiają w klawisze OPTION i START. A przecież jest to postawa unikalna w świecie komputerów. Maszyny PC włącza się rano a wyłącza wieczorem. Są także komputery które nie usypjają nigdy.... Ożywia je system operacyjny, którego zadaniem jest uruchamianie kolejnych programów zgodnie z żądaniami użytkowników. Atari XE/XL jest w pełni przystosowany do takiego stylu pracy lecz niestety brak jest ogólnie uznanego standardu któremu wierni byliby programiści. Tymczasem zasada jest prosta. Istnieje tylko jeden "boot" jest nim dyskowy lub kasetowy system operacyjny. Każdy inny program powinien się uruchamiać pod kontrolą tego systemu i po zakończeniu działania do niego powrócić. Niedopuszczalny jest brak w programie opcji wyjścia (zmuszanie użytkownika do wyłączenia zasilania stanowi jawny zamach na jego czas i portfel bo zwiększa ryzyko awarii).

KANAŁY

      Nie te na Marsie, nie w Wenecji lecz tu w pamięci naszego Atari kanały wyjścia wejścia są niezwykle ważne dla działania komputera a na dodatek wcale ich nie ma. Mianem kanału określa się bowiem coś ulotnego: metodę realizacji połączenia pomiędzy uniwersalną procedurą wejścia/wyjścia zwaną CIO, a sterownikiem konkretnego urządzenia. Dzięki tej metodzie możliwe jest jednakowe traktowanie wszystkich urządzeń zewnętrznych niezależnie od ich fizycznych właściwości. Prostym przykładem użycia kanału będzie wysłanie tą drogą napisu "Hej to ja!". Zobaczmy to na żywym programie:

      opt %10101

ciov  equ $e456
iocb  equ $340
chn1  equ $10

CIOV stanowi tzw. wektor pod adresem $E456 znajduje się rozkaz skoku do właściwej procedury CIO. IOCB określa początek zestawu ośmiu obszarów zwanych blokami sterowania we/wy (Input Output Control Block). Każdy z nich ma rozmiar 16 ($10) bajtów i zawiera informacje niezbędne do funkcjonowania danego kanału. Istnieje zatem 8 kanałów o numerach od 0 do 7 każdy opisany osobnym obszarem IOCB.

      org $480

      ldx #chn1

Odwołując się do CIO wskazujemy zawsze rejestrem X, o który kanał chodzi. Liczba ta jest numerem kanału pomnożonym przez 16 tak by zsumowana z adresem początku obszarów IOCB dała adres interesującego nas obszaru. W tym programie posłużymy się kanałem numer 1. Przed rozpoczęciem przesyłania danych należy kanał otworzyć w wyniku czego zostanie on logicznie związany ze sterownikiem danego urządzenia:

open  equ 3
in    equ %0100
out   equ %1000
comm  equ iocb+2
addr  equ iocb+4
mode  equ iocb+10

      lda #open
      sta comm,x
      lda <name
      sta addr,x
      lda >name
      sta addr+1,x
      lda #out
      sta mode,x
      jsr ciov

COMM oznacza jednobajtowy rejestr rozkazu - w nim zawsze podaje się kod operacji żądanej od CIO.

ADDR jest dwubajtowym adresem obszaru danych, na którym ma operować dana funkcja CIO. W przypadku OPEN (otwarcia) pod adresem NAME dostarczamy nazwę urządzenia. Bajt mode wskazuje na tryb zamierzonego przesłania danych:

      IN - oznacza odczyt
      OUT - zapis

Aby umożliwić transmisję w obie strony podaje się IN + OUT, lecz nie każde urządzenie na to pozwala.

Przez tak otwarty kanał można teraz przesyłać dane co też niezwłocznie uczynimy:

pisz  equ 11
size  equ iocb+8

      lda #pisz
      sta comm,x
      lda <text
      sta addr,x
      lda >text
      sta addr+1,x
      lda <len
      sta size,x
      lda >len
      sta size+1,x
      jsr ciov__

      Ponieważ CIO pozostawia nienaruszoną zawartość rejestru X można go bez obawy nadal używać do indeksowania odwołań do IOCB. W słowie ADDR podaje się teraz adres danych przeznaczonych do przesłania. Istnieją dwa rozkazy zapisu: kod 11 zawany binarnym: i 9 czyli tekstowy. Ten pierwszy wymaga podania w słowie size dokładnej liczby bajtów, które zostaną przesłane bez wnikania w ich rodzaj. Drugi rozkaz, który stosuje się do danych tekstowych polega na tym że w przypadku podania większego rozmiaru niż długość wiersza transmisja przerywa się z chwilą napotkania i wysłania znaku końca wiersza (o kodzie 155). W obu przypadkach po powrocie z CIO słowo size będzie zawierać liczbę faktycznie przesłanych bajtów. Po wysłaniu porcji danych kanał pozostaje otwarty i aktywny wciąż związany z danym urządzeniem i gotów do dalszych rozkazów. Aby zakończyć całą operację trzeba kanał zamknąć by mogłbyć znów w przyszłości użyty do kontaktów z innym urządzeniem:

close equ 12

      lda #close
      sta comm,x
      jsr ciov
      rts

      Nie wolno tej czynności lekceważyć gdyż w przypadku urządzeń, które zapisują dane blokami (jak napęd dysków) dopiero close spowoduje fizyczny zapis kompletnej informacji. Wyjątek stanowi zawsze otwarty kanał nr. 0 używany przez system dla urządzenia "E:" w trybie IN+OUT. Nie należy go nigdy otwierać ani zamykać. Dopiszmy do naszego programu jeszcze przykładowy tekst:

eoln  equ 155

text  dta c'Hej to ja!'
      dta b(eoln)
len   equ *-text

Symbol * który w wyrażeniach oznacza bieżący wskaźnik lokacji programu pozwala nadać etykiecie len wartość równą długości textu bez potrzeby liczenia znaków.

Pozostaje jeszcze tylko nazwa urządzenia czy raczej ściślej nazwa pliku wraz z określeniem urządzenia. Tu mamy szeroki wybór jeżeli napiszemy:

name  dta c'E:'

to tekst przykładowy pojawi się na ekranie. Jeśli podamy:

name  dta c'P:'

to zostanie on wydrukowany na drukarce. Jeżeli napiszemy:

name  dta c'D:DOWOL.TXT'

to tekst zostanie zapisany na dyskietce w pliku o nazwie "DOWOL.TXT". Myślę że takie uniezależnienie od fizycznych parametrów urządzenia bardzo upraszcza konstruowanie programów. Nie jest to oczywiscie przypadek lecz efekt przemyśleń twórców systemu operacyjnego małego Atari. A dla tych, którzy jeszcze tego nie zauważyli...

      end

      I to już koniec przykładowego programu celowo pominięto tu badanie czy dana operacja wykonana przez CIO zakończyła się pomyślnie. W prawdziwym programie każde odwołanie do CIO należy wzbogacić:

      jsr ciov
      bmi error

gdyż CIO w przypadku niepowodzenia ustawia znacznik N. Fragment programu od etykiety error powinien reagować w odpowiedni sposób na porażkę wnioskując rodzaj błędu z rejestru Y, który zawiera jego numer. Tę samą liczbę można znaleść w IOCB+3,X.

NAKŁADKI

      Trzecim rodzajem programów obok omówionych już systemów operacyjnych (boot) i programów użytkowych (w szerokim rozumieniu tego słowa) są rezydentne rozszerzenia systemu zwane nakładkami. Taki program jest tworem pośrednim gdyż wczytuje się i uruchamia jak program użytkowy, lecz przyczepia się do systemu pozostając w pamięci jako jego część. Być może najdzie nas chęć wykonania rezydentnego sterownika jakiegoś nowego urządzenia np. "B:" co można uznać za skrót słowa "border", które oznacza ramkę ekranu. Nic łatwiejszego:

      opt %100101
      org $9000

Tym razem nie będziemy asemblować programu do pamięci lecz wprost na dysk, ponieważ zainstalowanie go pod adresem określonym w memlo spowodowało by zniszczenie QA. Program w chwili uruchomienia znajdzie się pod adresem $9000 skąd dopiero należy go przenieść w nieznane z góry miejsce, którego adres znajdziemy w memlo.

      Niech nasze nietypowe urządzenie zapisuje wysłane do niego dane w komórce 712 odpowiadajacej za kolor ramki ekranu zaś przy odczycie niech pobiera bajty z rejestru liczb losowych w przypadku 0 meldując błąd 136 co oznacza koniec danych.

random equ $d20a
newdev equ $eebc
border equ 712
memlo  equ $2e7
eof    equ 136
dosini equ 12

start  equ *

poniższy fragment stanowi procedurę inicjalizacji, która zastąpi dotychczasową sekwencję wykonywaną po naciśnięciu klawisza RESET.

init   jsr 0

pierwszą czynnością, którą musi wykonać nowa procedura inicjalizacji (podepniemy ją do wektora dosini) jest wywołanie starej procedury inicjalizacji. Podczas wstępnej fazy instalowania naszej nakładki zostanie w miejscu argumentu 0 umieszczony adres dotychczasowej procedury odczytany z dosini. Nastepną czynnością będzie wpisanie do tablicy HATABS informacji o urządzeniu "B:"

       ldx #'B'
       ldy bhadr
       lda bhadr+1
       jsr newdev

Wreszcie poprawienie memlo tak aby adres ten wskazywał obszar powyżej naszego programu:

       lda newml
       sta memlo
       lda newml+1
       sta memlo+1
       rts

Zestaw procedur sterownika:

  • OPEN
  • CLOSE
  • GET
  • PUT
  • STATUS
  • SPECIAL
Zwróć uwagę, że mają wiele wspólnych fragmentów kodu (zostały zoptymalizowane pod kątem oszczędności pamięci):

close  lda #0
put    sta border
open   equ *
status equ *
spec   equ *
ok     ldy #1
       rts
get    lda random
       bne ok
       ldy #eof
       rts

       brk (koniec programu)

Pojedyńczy rozkaz brk jest wymagany dla programu przesuwającego jako znacznik końca kodu. Bezpośrednio po nim następuje blok adresów zastepujących użycie w programie rozkazów w trybie natychmiastowym z argumentami "<" i ">" (takie rozkazy nie mogłyby być przemieszczone).

newml  dta a(endbh)
bhadr  dta a(bhtab)

oraz tablica sterownika, której adres zostanie umieszczony w HATABS.

bhtab  dta a(open-1)
       dta a(close-1)
       dta a(get-1)
       dta a(put-1)
       dta a(status-1)
       dta a(spec-1)

Klasyczna tablica sterownika zawiera jeszcze na końcu rozkaz skoku JMP do bliżej nieokreślonej procedury inicjalizującej, ale z całą pewnością nikt i nic z niego nie korzysta więc pozwoliłem sobie go pominąć.

endbh  equ *
       dta a(0)

pojedyńczy adres 0 informuje program przesuwający, że skończył się blok adresów przesuwalnych. Dalej mogą się znaleść teksty komunikatów różne bufory itd. słowem takie dane, których nie trzeba modyfikować podczas przesuwania programu w pamięci. Dziwnym zrządzeniem losu ten program nie zawiera takich danych. Tu kończy się część rezydentna. Dalsza część programu wykona się jednorazowo w chwili uruchomienia lecz nie wchodzi w skład rezydenta i nie musi być przesuwana.

setini ldx #1
si     lda dosini,x
       sta init+1,x
       lda memlo,x
       sta dosini,x
       dex
       bpl si
       rts

Procedura setini ustawia nowy adres inicjalizacji systemu zapamiętawszy przedtem stary. Na tym kończy się program sterownika, lecz nie przerywajmy pisania bo, oto specjalny prezent. Poniższy program potrafi przemieszczać w pamięci dowolne programy z góry w dół na granicę memlo pod warunkiem zachowania kilku prostych zasad:

  1. program zapiszesz w jednym ciągłym obszrze pamięci wraz z relokatorem
  2. wykonanie twego programu musi się rozpocząć od jego pierwszego bajtu
  3. brak rozkazów z < i > (w zamian korzystanie z danych adresowych)
  4. brk na końcu argumentu kodu
  5. tuż po brk tablica adresów przesuwalnych (wszystkie wskazują na obszar objęty przemieszczaniem ) zakończona adresem spoza tego zakresu.
Relokator następuje tuż po segmencie danych i musi mieć zdefiniowane dwie etykiety:
  • star__ określa początek programu do przesuwania (znajdzie się on pod adresem zawartym w memlo)
  • user__ wskazuje na procedurę, która będzie wykonana PRZED rozpoczęciem przemieszczania programu
Wszystkie etykiety użyte w relokatorze kończą się dwoma znakami podkreślenia dla uniknięcia konfliktów z nazwami w przesuwanym programie.

star__ equ start
user__ equ setini

*----------------------------------*
*                                  *
*         Relocator 1.0            *
*            by JBW                *
*            may'88                *
*                                  *
*----------------------------------*

*--- page 0 ---

byte__ equ $ce
datf__ equ $cf
dist__ equ $d0 (2)
srce__ equ $d2 (2)
dest__ equ $d4 (2)
addr__ equ $d6 (2)

*--- system ---

runa__ equ $2e0 (2)
melo__ equ $2e7 (2)

*--- move ---

move__ equ *
       jsr user__

* disable RUN comm
       lda #$60 (RTS)
       sta move__

* clear data flag
       lda #0
       sta datf__

* destination
       lda melo__
       sta dest__
       lda melo__+1
       sta dest__+1

* code source, distance
       sec
       lda <star__
       sta srce__
       sbc dest__
       sta dist__
       lda >star__
       sta srce__+1
       sbc dest__+1
       sta dist__+1

*** move process ***
       ldy #0
       beq movl__ (JMP)
seda__ sec
       ror datf__
       mocl__ equ *
       lda srce__
       cmp <move__
       lda srce__+1
       sbc >move__
       bcc dchk__

* done !
       jmp (melo__)

* data flag check
dchk__ bit datf__
       bvs mov1__
       bmi tye3__
inst__ equ *
       lda (srce__),y
       sta byte__
       sta (dest__),y
       jsr inca__
       tax
       beq seda__

* inst type check
       cmp #$20 JSR
       beq tpe3__
       cmp #$40 RTI
       beq movl__
       cmp #$60 RTS
       beq movl__
       and #$0d
       cmp #$08 x8,xA
       beq movl__
       bcc movl__
       txa
       and #$1f
       cmp #$09
       beq mov1__

* 3-byte instruction
tpe3__ equ *
       lda (srce__),y
       iny
       cmp <star__
       lda (srce__),y
       dey
       sbc >star__
       bcc mov2__
       lda (srce__),y
       iny
       cmp <move__
       lda (srce__),y
       dey
       sbc >move__
       bcs mov2__

* alter abs adresses
       lda dist__
       ldx dist__+1
       bcc mova__

* move w/o changes
mov2__ bit datf__
       bmi seda__
       lda #0
       tax

* move 2b address
mova__ equ *
       sta addr__
       stx addr__+1
       sec
       lda (srce__),y
       sbc addr__
       sta (dest__),y
       jsr inca__
       lda (srce__),y
       sbc addr__+1
       jmp sd__

* move 1b data
mov1__ equ *
       lda (srce__),y
sd__   sta (dest__),y
       jsr inca__
       jmp movl__

*--- inc srce,dest ---

inca__ inc srce__
       bne *+4
       inc srce__+1
       inc dest__
       bne *+4
       inc dest__+1
       rts

*--- start ---

       org runa__
       dta a(move__)

       end.

       Podczas asemblacji powstanie plik typu com, który można uruchomić spod DOS'a. Zaprezentowane tu urządzenie "B:" jest pomocne przy testach własnych programów I/O. Pozwala zapisywać dane mrugając do taktu ramką obrazu i odczytywać losowe pliki nie zawierające zer (oczywiście dane odczytywane nie mają żadnego związku z zapisywanymi).

  

       Jest to już drugi odcinek naszego sześcioczęściowego cyklu. Mam nadzieję że jakoś dotrwacie. Wydaje mi się, że sposób przekazywania a i same informacje zawarte w cyklu są bardzo ciekawe i pomocne dla ludzi uczących się programowania procesora 6502 w języku asemblera. Wierzę, że jak do tej pory wszystko jest jasne i klarowne, gdyby jednak tak nie było zawsze jestem skłonny podyskutować w korespondencji indywidualnej.

Zbycho Jabol/Dial