Programowanie 6502 (7-9)


      Witam wszystkich wytrwałych czytelników w trzecim już wydaniu magazynu dyskowego SERIOUS i zapraszam do studiowania kolejnego już odcinaka kursu assemblera. Jednocześnie dla wszystkich, którzy niedoczytali się tego do tej pory, informacja, że autorem kursu jest jeden z lepszych programistów ATARI Janusz Bohdan Wiśniewski, a sam kurs był już raz publikowany na łamach naszego czasopisma "Tajemnice ATARI". Ja tylko staram się udostępnić go wszystkim młodym ataryniarzom, którzy nie mieli okazji się z nim zapoznać, a do których dociera nasz magazyn.

Zbycho Jabol/DIAL

PRAWDA O DODAWANIU BINARNYM

      Za wykonanie operacji dodawania w procesorze odpowiada wydzielona część układu, zwana ALU. Choć dodawane są do siebie dwa bajty, w istocie jednak każdy bit traktowany jest z osobna, jednakowo. Wygodnie jest więc spojrzeć na ALU jak na zespół jednobitowych sumatorów. Każdy z nich ma trzy wejścia:

  • argument 1
  • argument 2
  • wejście przeniesienia
oraz dwa wyjścia:
  • wynik
  • wyjście przeniesienia
Wejścia traktowane są równorzędnie, sumator po prostu dodaje bity, które w postaci sygnałów elektrycznych pojawiają się na nich.Są zatem cztery możliwości:
suma bitów
wyjście C
wynik
0
0
0
1
0
1
2
1
0
3
1
1

Myślę, że każdy bez trudu zauważy, że bity na wyjściach odpowiadają reprezentacji dwójkowej liczby otrzymanej z dodania bitów wejściowych. Sekretem, który scala sumatory w osmiobitową grupę, jest szeregowe połączenie wyjść z wejściami przeniesienia tak,że przeniesienie z młodszego bitu przekazywane jest do starszego. Wejście przeniesienia najmłodszego bitu przyłączone jest do znacznika C w ten sposób, że przyjmuje jego dotychczasową wartość, zaś wyjście przeniesienia z najstarszego bitu przekazuje swój stan jako nową wartość C.

      Przećwicz sobie, używając kartki i ołówka, kilka operacji dodawania, tak jak robi to ALU. Oczywiście bit przeniesienia wchodzący do ALU (czyli znacznik C) musi być zerem, aby wynik był poprawny. Wykorzystaj QA do sprawdzenia wyników np:

      opt %10101
      org $480

      clc
      lda #%100100
      adc #%100100
      brk
      rts

      end

Rozkaz ADC inicjuje wykonanie przez ALU operacji dodawania. Pierwszy argument pochodzi z akumulatora, drugi zaś z komórki pamięci wskazanej odpowiednim trybem adresowania. Wynik zostaje zapisany do akumulatora, w miejsce argumentu. Rozkaz BRK spowoduje zawieszenie programu bez zerowania rejestrów, można więc odczytać wynik w dolnym wierszu ekranu (A=...).

      Przełożenie zapisu szesnastkowego, używanego przez system QA, na dwójkowy nie powino sprawić nikomu kłopotu. W najgorszym razie można się posłużyć kalkulatorem XLF-a. Powtórne wykonanie RUN spowoduje zakończenie programu i przestawienie rejestrów w stan początkowy. Używając rozkazu BRK nigdy nie zapominaj, że RUN zawsze wykonuje program maszynowy od adresu widniejącego na dole ekranu ( P=... ), a nie zawsze od tego, który masz na myśli!

      Ostatnie dodawanie daje wynik 0. Może to początkowo wydać się dziwne, dodawano wszak dwie liczby różne od 0, zdziwienie to jednak minie, gdy uświadomisz sobie, że %100000000 jest liczbą dziewięciobitową, nie może więc zmieścić się w jednym bajcie: wystający bit znalazł się w znaczniku C. Ogólnie: jeśli dodawanie kończy się z ustawionym znacznikiem C, to znaczy, że wynik nie zmieścił się w przeznaczonym nań miejscu.

LICZBY ZE ZNAKIEM

      Można wszkże spojrzeć na ostatni przykład inaczej: Umówmy się, że istnieją liczby dodatnie i ujemne. Te ostatnie rozpoznaje się po tym, że ich siódmy bit (najstarszy) ma wartość 1. %10011100 jest taką właśnie liczbą. Ponieważ %1100100 to tyle co dziesiętnie 100, a w sumie dają 0, stąd łatwo wydedukować, że %10011100 oznacza liczbę -100. Ogólnie: aby przekształcić dwójkowy zapis liczby w liczbę przeciwną, należy zanegować wszystkie bity ( czyli zera zamienić na jedynki, a jedynki na zera) i do tak otrzymanej liczby dodać 1.

Na przykładzie liczby 100 wygląda to tak:

      %01100100  liczba 100
      %10011011  negacja
      %10011100  zwiekszona o 1

Realizacja tego przekształcenia w języku asemblera:

      lda #100       100
      eor #%11111111 negacja
      clc
      adc #1         dodaj 1

Przy okazji... uważna analiza przedstawionej wyżej zasady działania ALU pozwala na wniosek, że para rozkazów:

      clc
      adc #1

powodująca zwiększenie o 1 zawartości akumulatora działa tak samo, jak:

      sec
      adc #0

ponieważ ustawienie znacznika C przed dodawaniem jest równoznaczne z dodaniem dodatkowej jedynki do sumy argumentów.

Jeżeli traktujemy bajt jako osmiobitową liczbę bez znaku, to w oczywisty sposób może ona przybierać wartości z zakresu od 0 do 255.Jeśli jednak chcemy bajt widzieć jako liczbę ze znakiem to największą liczbą dodatnią ( skasowany 7 bit ) jest %01111111,czyli 127, zaś najmniejszą liczbą ujemną jest %10000000 czyli-128. Oczywiście różnica między liczbami ze znakiem i bez znaku istnieje tylko w naszej głowie, liczba -1 i 255 to w gruncie rzeczy to samo.

PRAWDA O ODEJMOWANIU

      Informacja o tym, jakoby procesor 6502 posiadał rozkaz odejmowania, jest nieco przesadzona. W rzeczywistości rozkaz SBC jest rozkazem dodawania, różni się od ADC tylko tym, że drugi argument (ze wskazanej komórki pamięci) przed dodawaniem zostaje zanegowany. Gdy więc spróbujemy wykonać takie "odejmowanie", np:

      clc
      lda #5
      sbc #5

to otrzymamy nie 0, lecz liczbę %11111111 czyli-1. Aby otrzymać poprawny wynik odejmowania, trzeba pomóc procesorowi i dodać jeszcze 1. Najprościej to zrobić przez ustawienie na wstępie znacznika C ( rozkazem SEC ). Dopiero takie współdziałanie ( procesor neguje argument, a my dodajemy 1 ) powoduje zamianę argumentu w liczbę przeciwną i w rezultacie właściwe odejmowanie. Wiedza o metodzie odejmowania pomaga czasem w kłopotliwych sytuacjach. Dodawanie jest przemiene,ale odejmowanie nie. Gdy trzeba odjąć zawartość komórki x od akumulatora, piszemy:

      sec
      sbc x

To proste. Co jednak zrobić, by odjąć zawartość akumulatora od komórki x? Zdarzyło mnie się widzieć rozwiązanie z użyciem dodatkowej komórki:

      sta y
      sec
      lda x
      sbc y

lub bez angażowania dodatkowej pamięci

      ldx x
      sta x
      sec
      txa
      sbc x

Tu jednak zamazuje się dotychczasową zawartość x. Tymczasem najprościej to można zrobić tak:

      sec
      eor #255
      adc x

ARTMETYKA WIELOBAJTOWA

      Zakres liczb 0..255 (lub -128..127), które dają się wyrazić jednym bajtem, rzadko wystarczają w praktyce, tym bardziej, że adresy zapisuje się na ogół dwoma bajtami. Dobrze byłoby więc mieć pod reką 16-bitowa ALU, a w najgorszym razie dwie 8-bitowe i wyjście przeniesienia jednej połączyc z wejściem drugiej. Mając tylko jedną ALU, trzeba sobie radzić na raty. W poniższym przykładzie dodamy liczbę 920 (23*40) do adresu pamięci obrazu, aby otrzymać adres ostatniego wiersza na ekranie.

ekran equ $58
adres equ $cc

Na wstępie należy wyzerować wejście przeniesienia:

      clc

Dodawanie rozpoczyna się od młodszych bajtów:

      lda ekran
      adc <920
      sta adres

Znaczek "<" tłumaczony jest przez asembler tak jak "#", z tą różnicą, że użyty zostanie tylko młodszy bajt argumentu. W wyniku dodawania młodszych bajtów znacznik C uzyska wartość przeniesienia z najstarszego bitu, która weźmie udział w dodawaniu starszych bajtów:

      lda ekran+1
      adc >920
      sta adres+1

Znak ">" ma znaczenie podobne do "<" lecz tym razem wzięty zostanie starszy bajt argumentu. Aby zweryfikować efekt tego działania umieść jakiś znak, np:

      lda #'!'-32

w miejscu o dwie kolumny odległym...

      ldy #2

od wyliczonego adresu:

      sta (adres),y

W wyniku wykonania tego rozkazu pojawi się w trzeciej kolumnie ostatniego wiersza ekranu znak "!". Teraz postaw pytajnik dwa wiersze wyżej:

      sec
      lda adres
      sbc <80
      sta adres
      lda adres+1
      sbc >80
      sta adres+1
      lda #'?'-32
      sta (adres),y

Oczywiście każdy widzi, że >80 to po prostu 0, lecz użyty tu zapis lepiej obrazuje, do czego ta liczba służy. Nie można także pominąć tego drugiego odejmowania, choć argument jest równy 0, wszak wartość znacznika C pozostała wskazując na obecność lub brak przeniesienia (przy odejmowaniu zwanego pożyczką), może spowodować zmniejszenie starszego bajtu. Pamiętajmy (jak to pokazano wyżej ), że wyzerowany C powoduje dodatkowe zmniejszenie wyniku (pożyczkę). I odwrotnie, ustawiony C pozostawi bajt adres+1 bez zmian. Bazując na tej wiedzy można nieco skrócić powyższy fragment:

      sec
      lda adres
      sbc #80
      sta adres
      bcs pyt
      dec adres+1
pyt   lda #'?'-32
      sta (adres),y

Dotyczy to rzecz jasna, tylko odejmowania liczby jednobajtowej od dwubajtowej. Analogicznie można uprościć dodawanie liczby krótkiej do długiej. Jak? Pomyślcie sami. Oczywiście artmetyka nie kończy się na liczbach dwubajtowych. Można sumować (lub odejmować) ze sobą dowolnie długie ciągi bajtów. Pamiętając o kolejności (od najmłodszego bajtu do najstarszego) i bacząc, by pomiędzy operacjami na poszczególnych bajtach nie zamazać znacznika C. Przykład często popełnianego błędu przy dodawaniu liczb 4-bajtowych:

      ldx #0
      clc
dodaj lda n1,x
      adc n2,x
      sta n1,x
      inx
      cpx #4
      bne dodaj

Rozkaz CPX zeruje znacznik C, w wyniku czego całe dodawanie jest do kitu!

PORÓWNANIA

      Rozkaz porównania CMP jest ze sposobu działania zbliżony do rozkazu SBC, lecz nie wymaga wstępnego ustawienia znacznika C (jest on ustawiany samoczynnie). Pomaga to przy porównywaniu pojedynczych bajtów, lecz komplikuje użycie tego rozkazu w porównaniach liczb wielobajtowych. Drugą ważną różnicą jest to, że wynik tego odejmowania nigdzie się nie zapisuje: akumulator (a w przypadku bliźniaczych rozkazów CPX, CPY rejestr X, Y) i argument pozostają niezmienione. W wyniku wykonania rozkazu CMP ustalają się wartości znaczników C, Z, N, co bywa wykrzystywane w następnych rozkazach (zwykle po porównaniu następuje skok warunkowy). Znaczniki zachowują się identycznie jak w przypadku rozkazu SBC. Ustawiony znacznik Z oznacza, że porównywane liczby były jednakowe. Znacznik C bierze się pod uwagę przy porównywaniu liczb bez znaku. Skasowany oznacza, że zawartość akumulatora była mniejsza od argumentu. W przypadku liczb ze znakiem rozpatruje się znacznik N, którego ustawienie mówi,że zawartość akumulatora była mniejsza niż argument. Aby porównać liczbę wielobajtową należy posłużyć się rozkazem odejmowania. Tylko pierwszą parę bajtów można obsłużyć rozkazem CMP z czego jest taki pożytek, że nie trzeba dbać o znacznik C.

      lda n1
      cmp n2

To porównanie ustawia odpowiednio znacznik przeniesienia dla kolejnych rozkazów.

      lda n1+1
      sbc n2+1

i tak dalej, aż do ostatniego (najstarszego) bajtu. Znacznik C dla liczb bez znaku, a N dla liczb ze znakiem wskazuje, czy pierwsza liczba była mniejsza od drugiej, czy nie mniejsza. Niestety, ta metoda nie pozwala odróżnić równości od wiekszości, gdyż znacznik Z ustawiany bywa dla każdego bajtu z osobna.

POŻYTEK ZE ZNACZNIKA V

      Kiedy patrzymy na bajt jak na liczbę ze znakiem, najstarszy bit określa znak tej liczby. Skasowany bit oznacza liczbę nieujemną. A zatem wartość bezwzgledna liczby wynika z pozostałych siedmiu bitów. Lecz nasz sposób interpretacji nijak nie wpływa na ALU wszelkie operacje są wykonywane jednakowo. To prowadzi nieraz do nieporozumień:

      clc
      lda #60
  liczba dodatnia
      adc #80   liczba dodatnia
      brk       ujemny wynik (-74)
      rts

Suma dwóch liczb dodatnich powinna być też liczbą dodatnią, lecz wynik przekroczył zakres dopuszczalny dla jednobajtowej liczby ze znakiem (127). Otrzymany wynik (poprawny jako liczba bez znaku) jest bez sensu w konwencji liczb ze znakiem. Te sytuacje sygnalizuje właśnie znacznik V. Jest on zwany znacznikem przepełnienia i wskazuje, że liczba zmieniła znak w sposób niekontrolowany. Dzieje się tak, gdy z dodawania liczb dodatnich wychodzi liczba ujemna, lub gdy różnica liczby ujemnej i dodatniej (powinna być jeszcze bardziej ujemna) jest liczbą nieujemną. Poza podanym zastosowaniem oraz unikalnym użyciem po rozkazie BIT (V przyjmuje w wyniku wykonania tego rozkazu wartość szóstego bitu argumentu), znacznik V nie znajduje innego wykorzystania. A przy okazji, aż dziw bierze, jakie niestworzone dyrdymały wypisują różni autorzy na temat znacznika V.

BASIC CZYLI JAK DOBRYM KUMPLEM JEST QA

      Często asembler bywa wykorzystywany do tworzenia krótkich programów maszynowych, które dołączane są następnie do programu w Basic'u. Wielu programistów korzysta z tej możliwości,bo różnica w szybkości działania między programem w Basic'u, a programem w języku maszynowym jest oszołamiająca. Rozkazy maszynowe pozwalają także zrealizować prosto takie zadania, które w Basic'u są trudne do wykonania lub wręcz niemożliwe. Wywołanie programu maszynowego umożliwia funkcja Basic'a: USR (adres), która zwraca wartość liczbową z zakresu 0,,65535. adres wskazuje miejsce w pamięci, gdzie znajduje się program maszynowy. Należy podkreślić, że jest to funkcja, a nie rozkaz,nie może więc stać sama, lecz tylko jako wyrażenie będące argumentem jakiegoś rozkazu, np.:

X=3*USR(1536)+2

Oczywiście musisz wpierw zadbać, aby pod wskazanym adresem ( tutaj: 1536) znalazł się pożądany program maszynowy Do tego zadania można zatrudnić Quick Assembler.Z objęć interpretera Basic'a przejdź więc do DOS'u, pisząc: DOS, zachowawszy wcześniej ewentualny program w Basic'u, ponieważ wywołanie QA spowoduje jego skasowanie. Uruchom QA i napisz taki program:

      opt %10101

      org $600
      pla
      rts

      end

Nie jest chyba dla nikogo tajemnicą, że $600 i 1536 oznaczają tą samą liczbę. Najprostrzy program musi się składać conajmniej z dwóch rozkazów, a nie samego RTS, bo Basic przed przekazaniem sterowania naszemu programowi umieszcza na stosie oprócz adresu powrotu jeszcze jeden bajt, który należy usunąć przy pomocy rozkazu PLA. Ten program można zatytułować "nie rób nic".

      Wykonaj Assembly, aby umieścić ten krótki program w pamięci. Opuść QA i wróć do Basic'a. Program na szóstej stronie pozostaje nienaruszony, ponieważ ani DOS ani Basic nie korzystają z tego obszaru pamięci. Najprostszy sposób wywołania naszego "programu", to:

? USR (1536)

w wyniku czego Basic wyświetli liczbę zwracaną przez tę funkcję. W najprostszym przypadku (jak ten) będzie to "echo", czyli adres wywołanego programu. Jest on na wstępie umieszczany w słowie $D4 (rejestr FR0), co stanowi efekt uboczny transkodowania podanego adresu. Po zakończeniu wykonywania programu maszynowego Basic rozumie zawartość słowa FR0 jako rezultat funkcji USR. Aby przekazać tą drogą jakąś istotną wiadomość, program powinien umieścić swoją liczbę w słowie FR0, np.:

fr0   equ $d4
dlptr equ $230

      org $600

dlp   pla
      lda dlptr
      sta fr0
      lda dlptr+1
      sta fr0+1
      rts

      Ten program ma za zadanie przekazać adres "display list". Rozkazy OPT i END dla uproszczenia pomijam, zakładając, że każdy wie gdzie i jak je dopisać. Żeby oszczędzić sobie czasu przy przechodzeniu z QA do Basic'a, a zwłaszcza z powrotem, proponuję zrobić tak:

  1. Ustawić w QA Setup/Memhi tak, aby rozmiar bufora wynosił około 2000 bajtów (to w zupełności wystarczy dla naszych testów)

  2. Umieścić tę samą liczbę w krótkim "programie":

          opt %10101
          org 8
          dta b($fe)
          org $2e7
          dta a($7000)
          end

    i wykonać Assembly, co spowoduje ustawienie systemowego wskaźnika MEMLO na podaną wartość oraz ustawienie komórki WARMST, aby zapobiec zanikaniu programu w Basic'u. Jeśli edytor zawiera już jakiś program, to ten można dopisać po nim, gdyż rozkaz END "odetnie" całą resztę. Po asemblacji można cały ten program usunąć, przywracając stary tekst do łask. Kto ma w pamięci XLF'a, ten może odpowiednie "poki" wpisać wglądownicą.

  3. Przygotowany tekst programu dla USR'a zasemblować w wyniku czego QA umieści go na szóstej stronie.

  4. Ustawić adres Setup/Run na $A000. Jest to adres startu interpretera Basic'a.

  5. Wykonać Run, w wyniku czego QA uruchomi Basic jako testowany program!
Po skończonej zabawie z Basic'em będzie można wrócić do QA rozkazem DOS. Można też wielokrotnie przerywać pracę programu w Basic'u kombinacją klawiszy SHIFT/BREAK (by na przykład porównać efekty działania programu z tekstem w edytorze) i powracać do Basic'a poprzez Run, nie tracąc ani tekstu w QA, ani programu w Basic'u (miłośnikom MAC'a proponuję wypróbowanie tej sztuczki w swoim assemblerze).

UWAGA! Należy się powstrzymać od użycia klawisza RESET. Można co prawda ponownie wywołać krok 2. co pozwoli powrócić do Basic'a, ale później mogą być kłopoty z rozkazem DOS.

Teraz wykonanie ? USR(1536) powinno spowodować pojawienie się liczby 39968. Pod tym adresem znajduje się zwykle program ANTIC'a, czyli DL. Wypróbuj to tak:

      20 D=1:DL=USR(1536)
      25 FOR I=8-D TO D STEP (D-4)/3
      30 POKE DL,16*I:NEXT I
      35 D=8-D:GOTO 25

Jeżeli wszystko jest dobrze, program wywoła miarowe ruchy ekranu. Można je zatrzymać klawiszem BREAK.

ZAMIESZKAĆ NA ZAWSZE W BASIC'U

      Byłoby niepraktycznie postępować za każdym razem w opisany wyżej sposób. Na ogół asembler stosuje się tylko na etapie tworzenia i poprawiania programu maszynowego. Potem wygodniej jest umieścić go na stałe w tekście Basic'owym. Istnieją rozmaite sposoby umieszczania danych programu maszynowego w wierszach programu Basic'a. Oto jeden z nich:

      50 ? "100 DATA ";:A=1536
      55 B=PEEK(A):? B;
      60 IF B=96 THEN ?:END
      65 ? ",";:A=A+1:GOTO 55

Wykonanie tego programu (GOTO 50) zaowocuje pojawieniem się wiersza DATA, zawierającego dane kodu maszynowego. Wystarczy teraz wjechać na ten wiersz kursorem i nacisnąć RETURN, by stał się częścią programu. Należy tylko jeszcze dopisać rozkazy, które przeniosą te kody z wiersza DATA na szóstą stronę:

      10 FOR I=0 TO 11: READ X
      15 POKE 1536+I,X:NEXT I

(wiersze 50..65 nie są już potrzebne).

      Ta metoda ma dwie wady. Po pierwsze, ciąg cyfr i przecinków w DATA nie jest jeszcze programem maszynowym, lecz musi zostać przekształcony w ciąg bajtów. Po drugie, operacja przepisywania trwa długo (a w przypadku dużej liczby danych nawet bardzo długo).

ZNACZKI DZIWACZKI

      Można też umieścić dane maszynowe w BASIC'u w inny sposób:

      50 ? "10 DIM X$(12):X$=";:A=1536
      55 ? CHR$(34);
      60 B=PEEK(A):? CHR$(27);CHR$(B);
      65 A=A+1:IF B<>96 THEN 60
      70 ? CHR$(34):END

Zaakceptowanie powstałego w ten sposób wiersza przyłącza go do programu w BASIC'u (wiersze 50...100 stają się niepotrzebne). Wiersz 15 należy usunąć. Teraz nasz program maszynowy zawarty jest w zmiennej X$. W swej naturalnej postaci. Gotowy do wykonania. Bez przenoszenia. Ale gdzie to konkretnie jest? Mówi o tym funkcja ADR(), która zwraca adres zmiennej BASIC'a. A więc wystarczy poprawić:

      20 D=1:DL=USR(ADR(X$))

by nasz program przykładowy znów pląsał bez kłopotów. Ten sposób traktowania danych maszynowych ma dużo zalet i jeszcze więcej wad.

      Zalety:
Dane zajmują mniej miejsca w tekście programu, nie wymagają przepisywania w docelowy obszar pamięci, dane kilku osobnych procedur nie kolidują ze sobą

      Wady:
Dane są mniej czytelne, program maszynowy praktycznie znika po NEW, program zawierający kody $9B (koniec wiersza) i $22 (cudzysłów) nie da się łatwo przedstawić w ten sposób.

      Najpoważniejsza jednak wada tej metody ujawnia się,gdy zdamy sobie sprawę, że dla większości programów nie jest obojętne,w którym miejscu pamięci się znajdują. Inaczej mówiąc do zastosowania tej techniki nadają się tylko takie programy, których kod nie zależy od wartości ORG. Takie programy nazywane bywają "przemieszczalnymi". Użycie dowolnego rozkazu odwołującego się do wnętrza programu (JSR, JMP, czy nawet LDA) niweczy tę właściwość, np. fragment:

      JMP TUTAJ
      ...
TUTAJ RTS

NIE jest przemieszczalny, bo argument rozkazu JMP w oczywisty sposób zależy od położenia programu. Natomiast równoważny (także pod względem długości) fragment:

      CLC
      BCC TUTAJ
      ...
TUTAJ RTS

jest przemieszczalny, ponieważ asembler tłumaczy rozkaz skoku względnego zawsze jednakowo niezależnie od położenia programu w pamięci.

      Wobec wymienionych niedogodności umieszczenie programów w stałych tekstowych należy zawsze poprzedzić głębokim namysłem. Z drugiej strony, z uwagi na dostępną w słowie FR0 informację o położeniu programu, zawsze da się go tak skonstruować, aby był przemieszczalny.

PRZEKAZYWANIE PARAMETRÓW

      Dosyć często oprócz odebrania rezultatu funkcji chcielibyśmy przekzać do niej parametry, które miałby by wpływ na jej działanie.Typowym przykładem są bardzo przydatne w programowaniu, nieobecne w ATARI BASIC, bitowe operacje logiczne. Wywołanie funkcji AND dla dwóch argumentów zapiszemy przykładowo tak: ? USR (1536,X,1) To pozwoli stwierdzić czy liczba X jest nieparzysta. BASIC przekaże parametry (traktowane jak liczby 16-bitowe bez znaku), wpychając je kolejno na stos, od ostatniego do pierwszego. Autorzy BASIC'a wpadli przy tym na wielce dowcipny pomysł, żeby zapamiętywać najpierw młodszy bajt, później starszy co uniemożliwia bezpośredni dostęp do parametrów. Po umieszczeniu (i zliczeniu) tych liczb BASIC kładzie na stosie jeszcze jeden bajt, określający, ile jest parametrów. Ten właśnie bajt równy 0, musieliśmy zdejmować ze stosu w podprogramie wywoływanym przez USR(1536), gdzie nie było wcale parametrów. Dopiero na spodzie, pod tymi wszystkimi danymi znajduje się adres powrotu dla rozkazu RTS. Napisz DOS i jesteś znów w QA.

      OPT %10101
      ORG $600

Na wstępie godzi się sprawdzić, czy podano właściwą liczbę parametrów. W przeciwnym wypadku cała praca na nic, bo program się na ogół zawiesi (dlaczego?).

BELL  EQU $F556
      PLA
     zdejm liczbę
      TAX
      CPX #2
  czy 2 parametry?
      BEQ AND
      BNE Q
   (JMP)
FLUSH PLA
      PLA
Q     DEX
      BPL FLUSH
      JMP BELL

W przypadku podania złej liczby parametrów nasza funkcja wrzaśnie i wróci do BASIC'a.

Teraz dopiero można zabrać się do wykonania właściwej operacji. Będzie to iloczyn logiczny dwu liczb 16-bitowych Najwłaściwszym miejscem przechowywania pośrednich wyników wydaje się rejestr FR0.

FR0   EQU $D4

AND   PLA
      STA FR0+1
      PLA
      STA FR0
      PLA
      AND FR0+1
      STA FR0+1
      PLA
      AND FR0
      STA FR0
      RTS

      END

Przejdź do BASIC'a (skacząc do adresu $A000). Wykonaj test, który upewni Cię, że program działa dobrze:

      10 ? "Podaj liczbe":INPUT X
      20 IF USR(1536,X,1) THEN ? "nie";
      30 ? "parzysta":GOTO 10

PROGRAMY Z KLOCKÓW

      Każdy, kto napisał już w życiu kilka programów spostrzega że pewne procedury powtarzają się w wielu z nich. A zatem przy pisaniu kolejnego dzieła korzysta się nieraz z gotowych rozwiązań. Quick Assembler z jego operacjami na blokach tekstu wydaje się wręcz stworzony do tego by składać nowe programy ze sprawdzonych fragmentów starych. Jednak podczas takiego montowania programu napotyka się często na szereg trudności. Kawałki nie chcą pasować do siebie z uwagi na konflikty nazw, odmienne wykorzystywanie pewnych komórek pamięci, różny sposób przekazywania parametrów. Można temu zapobiec jeśli już w trakcie pisania takiego często wykorzystywanego modułu będziemy pamiętać o kilku prostych zasadach. Pokażę to na przykładzie prostego programu.

STWÓRZ PLIK NAGŁÓWKOWY

      Często odwołujemy się do procedur systemowych (takich jak CIOV,WARMST, SETVBV). Sięgamy też do tablic i pojedyńczych komórek (takich jak IOCB, BOOT, CONSOL). Ustalmy dla nich podobnie jak dla procedur stałe nazwy (najlepiej te najczęściej spotykane w literaturze). Będzie łatwiej zapamiętać, co dany kawałek robi. Przy okazji nasz program stanie się czytelny i zrozumiały dla innych. Te procedury wykorzystuje się w wielu programach. Stwórz osobny plik deklaracji EQU tych najważniejszych etykiet. Można go dołanczać do każdego pisanego programu (najwygodniej z RAMdysku rozkazem ICL). Jeżeli piszesz następny program korzystający z innych procedur i komórek możesz dopisać je do dotychczasowego zestawu. Dzięki zastosowaniu nazw uświęconych tradycją nie ma obawy, że zostały lub zostaną użyte w innym znaczeniu.

Często w programach używamy stałych. Niech was ręka boska broni przed pisaniem "LDX #16"! Cóż to jest takiego? To może oznaczać wszystko. Jeśli napiszesz "LDX #CHN1" to każdy (nawet ja) zorjentuje się o co chodzi.

Nie żałuj komentarzy. Wiesz, że nie cierpisz na sklerozę i będziesz jutro a nawet pojutrze pamiętać co robi twój program. Ale zapomnisz za miesiąc albo za rok. Ostatnio przeżyłem takie właśnie rozczarowanie siegnąwszy do swych dawnych programów by skorzystać z zawartych w nich pomysłów. Niestety! Programy były tak napisane, że nie potrafiłem z nich nic zrozumieć.

Wiemy które komórki strony zerowej nadają się do wykorzystania. W wiekszości programów używa się zmiennych roboczych leżących w tym obszarze. Warto stworzyć uniwersalny zestaw. Nadaj im nazwy raz na zawsze. Przyda się z pewnością kilka komórek jednobajtowych (można je stosownie do przeznaczenia nazwać BYTE, CHAR, FLAG lub po polsku BAJT, ZNAK, ZNACZNIK, LICZNIK) i dwubajtowych (ADDR, PTR, WORD, albo ADRES WSK, SLOWO).

Nie martw się, nasz uniwersalny plik nagłówkowy nie będzie od razu doskonały. Każdy następny program przynosi nowe doświadczenia. Gromadź je cierpliwie modyfikując zestaw definicji. Od czasu do czasu sprawdź czy twoje stare programy asemblują się jeszcze z tak ulepszonym zestawem. Będą jeżeli zmiany polegały tylko na dodawaniu nowych unikalnych definicji. Użytkownicy stacji dysków mogą ten plik dołanczać za pomocą rozkazu ICL. Wielbiciele magnetofonów będą zapewne woleli wgrać go do edytora i dobudować do niego resztę programu.

TA POKER

      Program przykładowy będzie umożliwiał wykonanie instrukcji POKE z poziomu DOS-u. Czasem chciałoby się jej użyć by np. zmienić kolory lub uruchomić silnik magnetofonu a BASIC jest akurat odłączony... Jak każdy, tak i nasz program wykonuje operacje wyjścia/wejścia (trudno sobie wyobrazić taki program, który nie pobiera żadnych danych i nie objawia w żaden sposób efektów swej pracy). Najpospoliciej używaną ich formą jest wyświetlanie komunikatów i pytań. Procedura, która to robi może (raz opracowana) być bez zmian przenoszona z programu do programu. Lepiej wszakże zachować ją (i jej podobne) w osobnym pliku zwanym biblioteką, który można później wykorzystywać podobnie jak nagłówek dołanczając do różnych programów. Najlepsze efekty daje konstrukcja "inteligentnych" procedur. Nasza będzie umiała znaleść i wyświetlić odpowiedni komunikat, znając tylko jego kolejny numer. W tym przypadku będą potrzebne cztery:

  • pusty dla wprowadzania odstepów
  • tytułowy na przywitanie
  • pytanie o adres
  • pytanie o bajt
W innym programie teksty mogą być inne procedura się nie zmieni. Jest wskazane by wykonanie programu zaczynało się od początku kodu. Nie jest to oczywiście konieczne, w typowych przypadkach lecz czasami bardzo się przydaje. Ułatwia na przykład dołączenie RELOCATOR'a zrezygnowanie w SpartaDOS'ie z podawania adresu startu... Aby to uzyskać można umieścić główny blok programu na początku. Równie dobre byłoby rozpoczęcie od JMP MAIN do miejsca gdzie faktycznie zaczyna się program.

PLAN

      Rozpoczynając pisanie rozumiałem, że trzeba na wstępie wykonać jakieś operacje przygotowujące, lecz nie byłem pewien jakie. Stąd "JSR INIT", potem coś się wymyśli. Główna pętla jest dość prosta. Konstruując ją myślę o dostępnych mi klockach-procedurach i staram się przede wszystkim z nich układać akcję programu. Ich nieobecnością na razie się nie przejmuję. W najgorszym razie te fragmenty, których nie znajdę w bibliotece będę musiał dopisać (i umieścić w bibliotece!). Rozpoczynam od wyświetlania pustego wiersza (robi to na ekranie odstęp po poprzednich tekstach). Wykona to procedura DSP_TXT Pobranie adresu polega na wyświetleniu pytania i odebraniu tekstu (zrobi to procedura GET_TEXT) i zdekodowaniu go, by otrzymać liczbę. Wprowadzenie przez użytkownika pustego tekstu (sam RETURN) będzie oznaczało rezygnację. Długość tekstu można znaleść w stosownych komórkach tablicy IOCB (rejestr X wskazuje konkretny blok). Ponieważ nie da się wprowadzić dłuższego tekstu niż 120 bajtów cała liczba mieści się w jednym bajcie. Jest ona równa co najmniej 1 bo dla tekstu wlicza się także kończący RETURN. Procedura DECO zwróci wprowadzane słowo w rejestrach A i Y. Analogicznie pobiera się bajt i POKE! Ponowny skok na początek pozwala na wprowadzenie wielu "poków".

PODPROGAMY

      Odszukanie tekstu na podstawie numeru jest proste jeżeli każdy tekst zajmuje jedną linijkę (kończy się znakiem End Of Line). Dodatkowo ułatwia sprawę fakt, że wszystkie teksty zajmują mniej niż 256 bajtów dzięki czemu można je objąć zakresem zmian jednego rejestru indeksowego Y. Do wyświetlania tekstu posłużyłem się rozkazem WE/WY "put_text", który powoduje wyświetlenie jednej linijki tekstu. Do procedury CIOV przekazuję jakąkolwiek duża długość, a operacja przebiega tylko do pierwszego EOL.

      Analogicznie przebiega odebranie tekstu. Dla rozkazu WE/WY gett podaję się dowolną długość byle większą od spodziewanej, a w odpowiedzi w słowie IO_LEN otrzymujemy rozmiar przesłanych danych. Do zamiany wprowadzonego tekstu (kodów ATASCII cyfr) najwygodniej użyć procedur artmetycznych zawartych w ROM. Procedura AFP zamienia ciąg znaków na liczbę rzeczywistą w tzw. formacie zmiennoprzecinkowym (w skrócie FP). Adres tekstu przekazuje się w słowie INBUF zaś bajt CIX zawiera odległość liczby od początku tekstu. Rezultat znajdzie się w rejestrze FR0. Procedura FPI zamienia liczbę z formatu FP na dogodną dla nas dwubajtową postać binarną. Rezultat znajdziemy także we FR0.

KOŃCÓWKA

      Często zachodzi potrzeba przesunięcia programu w pamięci na granicę MEMLO. Dotyczy to zwłaszcza kopierów, które dzięki temu potrafią wykorzystać całą dostępną pamięć. Ponieważ pokazany na przykładzie POKER-a schemat postępowania jest typowy także dla prostych programów kopjujacych i transkodujących ma także (choć tym razem to zbędne) strukturę przystosowaną do RELOCATOR'a przedstawionego wcześniej. I na koniec dane tekstowe, bufor dla wprowadzania liczb i adres uruchomienia dla DOS.

* TA POKER      autor: JBW
* (c) 1992 Tajemnice ATARI

      opt %100101

*---procedury w ROM

afp    equ $D800
fpi    equ $D9D2
ciov   equ $E456

*---rejestry pakietu FP

fr0    equ $D4
cix    equ $F2
inbuff equ $F3

*---system

runad  equ $2E0
initad equ $2E2
doarun equ $A
dosini equ $C
iocb   equ $340
io_com equ iocb+2
io_sta equ iocb+3
io_adr equ iocb+4 (2)
io_len equ iocb+8 (2)
io_mod equ iocb+10
io_aux equ iocb+11

*---stale

chn0   equ 0    chanel $0
gett   equ 5    get text
putt   equ 9    put text
getb   equ 7    binary get
putb   equ 11   binary put
eol    equ 155  end of line

*---strona zerowa

byte   equ $cb
addr   equ $cc
word   equ $ce

*---numery komunikatow

nul_m  equ 0
tit_m  equ 1
adr_m  equ 2
byt_m  equ 3

      org $8000

main  jsr init

* petla glowna
loop  ldx #nul_m
      jsr dsp_msg

* pobierz adres
      ldx #adr_m
      jsr get_text
      bmi loop

* pusty wiersz?
      dec io_len,x
      beq quit    koniec

* dekoduj adres
      jsr deco
      sta addr
      sty addr+1

* pobierz bajt
      ldx #byt_m
      jsr get_text
      bmi loop

* pusty wiersz?
      dec io_len,x
      beq loop    od nowa

* dekoduj bajt
      jsr deco

* wstaw bajt pod adres
      ldx #0
      sta (addr,x)

* jeszcze raz
      jmp loop

* powrot do DOS-u
quit  jmp (dosrun)

*---wypisz tekst

dsp_msg equ *
* odszukaj tekst nr. X
      ldy #0
fm0   dex
      bmi mout
fmes  lda data,y
      iny
      cmp #eol
      bne fmes
      beq fm0    (jmp)
* wypisz
mout  txa
      ldx #chn0
      sta io_len,x
      clc
      tya
      adc dtaa
      sta io_adr,x
      lda #0
      sta io_len+1,x
      adc dtaa+1
      sta io_adr+1,x
      lda #putt
      sta io_com,x
      jmp ciov

*---pobierz tekst

get_text jsr dsp_msg
      ldx #chn0
      lda #gett
      sta io_com,x
      lda txta
      sta io_adr,x
      lda txta+1
      sta io_adr+1,x
      sta io_len+1,x
      jmp ciov

*---dekodowanie

deco  lda txta
      sta inbuff
      lda txta+1
      sta inbuff+1
      lda #0
      sta cix
      jsr afp
      jsr fpi
      lda fr0
      ldy fr0+1
      rts

*---procedura poczatkowa

init  ldx #tit_m
      jmp dsp_msg

*---znak konca

      brk

*---dane adresowe

dtaa  dta a(data)
txta  dta a(text)
      dta a(0)

*---dane

data  equ *
      dta b(eol)
      dta c' TA POKER '
      dta b(eol)
      dta c'Address:'
      dta b(eol)
      dta c'Byte:',b(eol)

text  org *+120

*---adres uruchomienia

      org runad
      dta a(main)

      end

      Na zakończenie dzisiejszego nudzenia pragnę poinformować wszystkich, że nieuchronnie zbliżamy się do połowy kursu. Następne odcinki do lektury których serdecznie zapraszam, będą coraz bardziej pasjonujące...