Programowanie 6502 (13-15)


      Na ile sposobów można rozumieć liczbę? Niezliczenie wiele! Może to być wysokość dziwęku, numer buta, dzień tygodnia wielkość gwiazdy... Co tylko zapragniesz! Niestety, im więcej znaczeń, tym większy kłopot. Im więcej języków, tym trudniej się porozumieć.

ASCII

      Mówiliśmy już, że jednym z możliwych sposobów interpretacji liczby zawartrej w jednym bajcie pamięci jest znak pisarski, taki jak cyfra, litera, kreska, kropka, itd. W takim ujęciu mówimy o liczbach, że stanowią kody znaków, a prościej: ich numery. Trzeba pamiętać, że odbierany przez nas efekt wizualny, np. kształt litery "X", jest szczególnym sposobem odwzorowania na ekranie liczby 120 przez procesor obrazu. Znaczna część cywilizowanego świata podporządkowała się umowie, że liczba 65 oznacza znak "A", liczba 66 - "B", itd. Cyfra 0 ma numer porządkowy 48, cyfra 1 - 49 itd. Ten standard chyba najszerzej rozpowszechniony nazywa się ascii. Jest tych znaków 128 (kody od 0 do 127) lecz te od 0 do 31 są "niewidzialne", ponieważ służą nie do wyświetlania, lecz do wymuszania pewnych działań, jak ruch kursora na ekranie lub ustawienie zgęszczonego druku w drukarce. Jeżeli posługujemy się asemblerem, to zbędne jest pamiętanie kodów poszczególnych znaków. Aby wziąść do akumulatora kod litery "A", piszemy:

       lda #'A'


a QA wygeneruje identyczny rozkaz maszynowy jak w przypadku:
       lda #65


Symbol 'A' jest więc traktowany tak, jak liczba. Może być stosowany wszędzie, gdzie asembler spodziewa się wartości liczbowej. Poniższa procedura służy do zamiany małych liter na duże, innym znakom nie czyni krzywdy. Domniema się, że znak został umieszczony w akumulatorze, tam też pojawia się odpowiedź.
 upper cmp #'a'
       bcc ok mniejsze od a
       cmp #'z'+1
       bcs ok wieksze niż z
       sec
       sbc #'a'-'A'
 ok    rts

Proszę zauważyć, że asembler sam oblicza wyrażenia 'a', 'z'+1, 'a'-'A'. W rozkazach programu maszynowego zostaną użyte odpowiednio liczby: 97,123, 32, lecz o tym nie potrzeba nam wiedzieć. Zapamiętajmy: budowa wyrażeń w języku asemblera nie ma wpływu na złożoność programu wynikowego. Nie oszczędzajmy więc kosztem czytelności! Otrzymałem ostatnio list, w którym Czytelnik pyta, czy programista powinien mieć w głowie tablicę ASCII. Zapewne czytał przewrotny tekst o "Prawdziwych programistach" To jakieś nieporozumienie. Kochani, programista powinien mieć w głowie rozum!

Dla umieszczania w programie całych ciągów znaków służy rozkaz DTA typu C:

       dta c'Tajemnice ATARI'


Ponieważ 128 znaków nie wyczerpuje wszystkich możliwości bajtu, istnieje wiele odmian "poszerzonego ASCII" lub "ASCII 8-bitowego". To poszerzenie polega na dodaniu 128 dowolnych znaków. Tak, to nie żart. Znaki 0 ... 127 mają obowiązek być zawsze takie same, lecz dla kodów od 128 do 255 nie ma jednolitego standartu. To jest bardzo wygodne. Rosjanie umieszczają tu cyrylicę, Polacy swoje ą i ę, Niemcy umlauty i basy, zwykle też zmieszczą się jakieś znaczki semigraficzne, symbole matematyczne itp.

Przy przenoszeniu danych między komputerami różnych systemów zawsze trzymamy się bezpiecznego 7-bitowego zestawu. Na nim też oparte są wszystkie języki programowania. Idylla. Jaka szkoda, że nasze ATARI nie używa ASCII.

ATASCII

      To bodajże największa (prócz nie standardowego złącza wy/we) krzywda wyrządzona temu komputerowi przez jego twórców. Duża niezgodność "ATari ASCII" z ASCII powoduje, że nie jest możliwe bezpośrednie przenoszenie danych między naszym ulubieńcem i resztą świata. Trudności napotyka też podłączenie typowych urządzeń zewnętrznych, np. drukarek, modemów, itp. Niektórzy mówią o 256-znakowym zestawie ATASCII, lecz nie wierzcie im. Mamy do czynienia ze 128 różnymi znakami, a najstarszy bit daje tylko informację o kolorze - te same znaki mogą być wyświetlane w inny sposób. Na dodatek w niektórych trybach liczba znaków jest redukowana do 64, by tym kosztem uzyskać "aż" dwa bity definjujące kolor znaku.

Zestaw znaków ANTIC'a

      Jakby tych atrakcji było mało, procesor obrazu w naszym komputerze posługuje się zgoła odrębnym zestawem znaków. Muszę przyznać, że go rozumiem. W niektórych trybach każą mu wyświetlać znaki w czterech kolorach. Kod znaku ma zatem tylko 6-bitów (dwa przeznaczono na numer koloru). A przecież znaki 0 ... 63 możliwe do wyrażenia tym sposobem nie obejmują liter! Dlatego dokonano przetasowania. Kody Antica 0 ... 63 odpowiadają znakom 32 ... 95 zestawu ATASCII. Znaki ATASCII 0 ... 31 mają teraz kody 64 ... 95, tylko znaki 96 ... 127 pozostały na swoim miejscu.

Ten pokraczny "standard" znaków bywa w literaturze zwany peek-poke, ekranowy, internal (wewnętrzny) lub iocode. Początkujący programista nie zdaje sobie sprawy z jego istnienia. Pisze:

 PRINT "ATARI"


i na ekranie widzi:
 ATARI


To systemowe procedury wyświetlania tekstów dokonują po cichu konwersji naszego napisu. Ale próba bezpośredniego wysłania znaku na ekran zawodzi.
 ekran   equ $58

         ldy #2
         lda #'A'
         sta (ekran),y

a na ekranie pojawia się coś całkiem innego. Aby otrzymać znak "A" trzeba wysłać, zgodnie z nabytą wiedzą, znak o kodzie o 32 mniejszym, czyli
        lda #'A'-32


Można definiować całe napisy zgodne z zestawem Antica za pomocą DTA typu D, np.
        dta d'Tajemnice ATARI'


Ten tekst (odmiennie niż dta c) można przenieśc bezpośrednio na ekran. Wielu programistów stosuje tę sztukę dla przyspieszenia i skrócenia programu, odpada bowiem konieczność konwersji. Ja jednak nie jestem zwolennikiem tej metody, gdyż w praktyce niejednokrotnie trzeba odwoływać się do systemu, na przykład podając nazwę programu (koniecznie ATASCI!) lub pobierając listę plików poleceniem DIR. Operowanie na zestawie znaków ATASCII daje wymierne korzyści. Więc jednak konwersja. Procedura zmieniająca znak w akumulatorze z kodu ATASCII na ANTIC (7-bitowy) mogłaby wyglądać tak:
 konwer1 cmp #32
         bcc dod64
         cmp #96
         bcc ode32
         rts
 dod64   clc
         adc #64
         rts
 ode32   sec
         sbc #32
         rts

Nie uwzględnia ona jednak bitu koloru (musi on być w tym wypadku zerem). Po odpowiednim usprawnieniu otrzymujemy taki oto podprogram (krótki i szybki, lecz o ileż mniej zrozumiały!).
 konwer2 asl @
         php
         cmp #192
         bcs ok
         sbc #63
         bcs ok
         adc #192
 ok      plp
         ror @
         rts

Niekiedy okazuje się, że taka konwersja, przy wypełnianiu znakami całego ekranu, jest zbyt czasochłonna. Można się wówczas posłóżyć 256 bajtową tablicą, którą wstępnie wypełniamy przy pomocy poniższej procedury:
         ldx #0
 wyp     txa
         jsr konwer2
         sta tab,x
         inx
         bne wyp

Tablicę należy zadeklarować jako:
 tab     org *+256


Odtąd można się już posługiwać taką błyskawiczną procedurą:
 konwer3 tax
         lda tab,x
         rts

Nawiasem mówiąc, czasem i to jest zbyt wolne. Nie pozostaje nic innego, jak zmusić ANTIC, by wyświetlał dane bezpośrednio w kodach ATASCII. Jak? Wiem ale nie powiem. A może ktoś z Was sam zgadnie?

Kody klawiatury

      Klawiatura ma swoją własną interpretację znaków i przynajmniej w tym jednym wypadku jest to w pełni usprawiedliwione. Ponieważ elektrycznie zbudowana jest ona na zasadzie matrycy krzyżujących się przewodów, naturalnie sygnał od naciśniętego klawisza niesie informację o wierszu i kolumnie na przecięciu których znajduje się naciśnięty klawisz. Klawisze leżące blisko siebie mają podobne kody. Ich kolejność nie jest więc alfabetyczna, lecz, rzec można topograficzna. Nie ma żadnej, dającej się ująć w ramy algorytmu zależności między tymi kodami, a zestawem ATASCII. Jedynym ratunkiem jest tabela konwersji, której adres znajduje się we współczesnych komputerach w słowie $79.

UWAGA: W tym miejscu autor odsyła nas do lektury mapy pamięci także publikowanej w "Tajemnicach ATARI". Oczywiście wszyscy ci czytelnicy, którzy posiadają w swoich zbiorach czasopismo "Tajemnice ATARI" mogą sobie i cały kurs programowania tamże wyczytać, my publikujemy ten materiał dla ludzi którzy nie mieli okazji zdobyć tego legendarnego dla nas już czasopisma. Jak tam wyżej kolega Wiśniewski wspominał, wektor zawarty w słowie $79 wskazuje na tablicę znajdującą się w pamięci ROM komputera. W tablicy tej umieszczono kolejno:

  • Znaki ATASCII odpowiadające kodom klawiszy (jest ich 64).
  • Znaki ATASCII odpowiadające kodom klawiszy wciśniętych wraz z SHIFT.
  • Znaki ATASCII odpowiadające kodom klawiszy wciśniętych wraz z CONTROL.
I co z tego? Poniżej zamieszczamy procedurę oczekiwania na naciśnięcie klawisza, napisaną w asemblerze, która wydaje się być przydatną w bardzo wielu programach:
 ktab    equ $79
 ch      equ $2fc
 getk    lda ch       odczytaj ch
         cmp #255     czy jest znak?
         beq getk     nie: czekaj
         ldy #255     daj znać, że klawisz odebrany...
         sty ch
         tay          kod klawisza jako
         lda (ktab),y index do tablicy
         rts          w ROM'ie.

W akumulatorze znajduje się kod ATASCII, który odpowiada wciśniętemu klawiszowi, pobrany za pomocą tablicy, o której cały czas się rozpisujemy. Wielokrotnie zdarza się, że dla pisanego programu nie ma znaczenia to, czy podczas naciskania klawisza był wciśnięty klawisz SHIFT lub CONTROL. W tym przypadku wystarczy przed rozkazem tay wpisać and #%00111111 (ignoruj SHIFT i CONTROL).

Warto tylko podkreślić, że korzystanie z systemowego wejścia (przez kanał otwarty dla urządzenia "K:") uwalnia nas od zawiłych problemów: otrzymujemy bowiem gotowe znaki w standardzie ATASCII.

Znak bez znaku

      Ponieważ każdy znak można utożsamiać z jego jednobajtowym kodem i z drugiej strony każdy bajt zawiera kod jakiegoś znaku, istnieje tendencja do zamiennego stosowania nazw "znak" i "bajt". Zwłaszcza w językach wysokiego poziomu, gdzie rzadziej operuje się na bajtach, terminem "znak" określa się liczbę jednobajtową, na każdą inną liczbę mówiąc poprostu "liczba".

To prowadzi do zabawnego paradoksu: liczby, które określamy mianem znaków, mogą mieć znak (plus, minus), lub go nie mieć. Nie jest to oczywiście cecha liczb lecz naszego subiektywnego na nie spojrzenia. Pisałem już o tym: jeśli myślimy, że bajt zawiera liczbę od 0 do 255, to jest ona zawsze nieujemna, ale jeśli umówić się, że ustawiony bit 7 oznacza liczbę ujemną to mamy do czynienia z zakresem -128 ... 127. Jaka to różnica dla komputera? Żadna. Procesor 6502 wykonuje operacje artmetyczne w obu trybach na raz! Weźmy liczby 30 i 140. Ich suma powinna dać 170. W dwójkowych rejestrach procesora odbędzie się to tak:

                00011110
               +10001100
               =10101010

Dla tych, którzy chcą to sprawdzić przypominam technikę dodawania na papierze. Sumujemy odpowiednie cyfry poczynając od końca:
        0 + 0     = 0
        1 + 0     = 1
        1 + 1     = 0 przenosimy 1
        1 + 1 + 1 = 1 przenosimy 1
        1 + 0 + 1 = 0 przenosimy 1
        0 + 0 + 1 = 1
        0 + 0     = 0
        0 + 1     = 1

Wynik, liczba %10101010, to rzeczywiście 170. Jeśli jednak uświadomimy sobie, że liczba 140 jest w rozumieniu ze znakiem równa -116, to mamy do czynienia z działaniem 30+(-116)=-86. Nikogo chyba nie zdziwi, że dwójkowo ten wynik zapisuje się %10101010. Również porównywanie takich liczb odbywa się zwykle bez kłopotów, gdyż
         lda #30
         cmp #170

spowoduje skasowanie znacznika C (jako, że 30<170) i jednocześnie skasowanie znacznika N (ponieważ 30>86). Dla liczb bez znaku, by skoczyć "gdy mniejsze" stosujemy więc rozkaz BCC, a dla liczb ze znakiem BMI. Nasz procesor jest przygotowany na obydwie ewentualności. Trzeba jednak pamiętać, że porównywanie ze znakiem daje niepoprawny wynik w sytuacjach, w których odejmowanie tych samych liczb ustawia znacznik V. Dotyczy to porównywania liczb o różnych znakach, gdy suma ich wartości bezwzględnych przekracza 127. Z tego powodu porównania ze znakiem mają bardzo ograniczone zastosowanie (głównie do niewielkich liczb).

Nietrudno w literaturze natknąć się na przepis własnoręcznego wykonywania liczb ujemnych. Np. -5 robimy tak: bierzemy liczbę 5 (%00000101),zamieniamy wszystkie bity na przeciwne (%11111010) i dodajemy jedynkę (%11111011). To właśnie jest -5. Ta metoda wydaje mi się jednak zbyt skomplikowana, przez co trudna do zapamiętania. Ja rozumuję tak: -5 to 0-5, czyli:

   (1)00000000
 -    00000101
 =    11111011

Jedynka w nawiasie oznacza dodatkowy bit, z którego można zaczerpnąć "pożyczkę" w procesorze 6502 jest to znacznik C. Ta metoda ma dodatkową zaletę: można zwalić całą robotę na komputer. Rozkaz
          lda #%100000000-5


umieści w akumulatorze liczbę -5. Sposób zawiłego ręcznego przekształcania liczb ma więc tylko znaczenie ciekawostkowe. Nie ma potrzeby się go uczyć.
BCD

      Kolejną stosowaną w komputerze ATARI konwencją dotyczącą liczb, to tryb dziesiętny. Tym się różni od pozostałych, że nie wszystkie z 256 kombinacji bitów są dozwolone. W tym ujęciu pojedyńczym elementem jest połówka bajtu (cztery bity), która wyraża jedną cyfrę z zakresu 0 ... 9. A zatem bajt zawiera liczbę dziesiętną (zapisaną dwójkowo) z zakresu 0 ... 99. Ten temat jest jednak na tyle obszerny, że poświęcę mu więcej miejsca w którymś z następnych odcinków.

Bardzo ciekawe dane

      Prawdziwi programiści pogardzają liczbami BCD. Wszak komputery myślą dwójkowo, czyż to nie wystarczający argument? Kod BCD został stworzony specjalnie dla maminsynków, którzy nie mają dość rozumu, żeby się nauczyć myśleć jak komputer...

      A teraz poważnie: BCD, czyli liczby dziesiętne kodowane dwójkowo (Binary Coded Decimal), stanowią pomost, ogniwo pośrednie pomiędzy wewnętrzną reprezentacją liczby w komputerze, a postacią, w jakiej zwykli śmiertelnicy wolą oglądać liczby: ciągiem cyfr dzieśiętnych. Liczba wyświetlona na ekranie lub wydrukowana na papierze składa się z cyfr, będących znakami z zestawu ASCII. Dla sterownika obrazu lub drukarki nie ma przytym rozróżnienia między literkami, cyframi i innymi znaczkami dziwaczkami. Każdy jest po prostu jakimś zestawem ośmiu bitów czyli bajtem. Wyobraźmy sobie jednak, że wyświetlamy (drukujemy) TYLKO liczby dodatnie. Przyjrzyjmy się kodom potrzebnych znaków:

 '0' = %00110000
 '1' = %00110001
 '2' = %00110010
 '3' = %00110011
 '4' = %00110100
 '5' = %00110101
 '6' = %00110110
 '7' = %00110111
 '8' = %00111000
 '9' = %00111001

Wyraźnie widać, że cztery starsze bity są zawsze takie same. A zatem w przypadku przechowywania w pamięci danych tego typu połowa informacji jest zupełnie zbędna. Do zidentyfikowania znaku o którym wiemy, że jest cyfrą dziesiętną, wystarczą cztery bity. Więc może by tak umieścić po dwie cyfry w każdym bajcie? Wtedy np.
 '17' = %00010111
 '46' = %01000110
 '92' = %10010010
 itd...

Na zapisanie liczby 1993 potrzeba tylko dwóch bajtów:
 '1993' = %0001100110010011


Taką koncepcję zapisu liczb dziesiętnych nazywamy właśnie BCD. W tym miejscu warto uczynić dwa spostrzeżenia.
  • Nie wszystkie możliwe kombinacje czterech bitów są wykorzystane (dozwolone). Wskutek tego "gęstoś zapisu" liczb w pamięci jest w tym systemie mniejsza niż przy naturalnym dla komputera, dwójkowym sposobie. Jeden bajt BCD może przechowć liczbę od 0 do 99 (a dwójkowo 0-255).
  • Wartość liczby odczytanej z bajtu będzie różna w zależności od sposobu interpretacji. Przykład: %01000110 oznacza binarne 70, a tylko 46 w BCD; z koleji 64 zapisuje się dwójkowo jako %01000000, a w BCD jako %01100100.
Po co tyle zamętu?

Dane w formacie BCD są bardzo łatwe do prezentacji wizualnej (zamiany na cyfry ASCII). Wystarczy podzielić bajt na połówki, do każdej doczepić cztery bity wzięte np. z cyfry '0' i gotowe. Natomiast dane dwójkowe są trudne do prezentacji w formie dziesiętnej i najczęściej przekształca się je w celu wyświetlenia właśnie do formatu BCD, co nie jest łatwe i zajmuje masę czasu. Dlatego, o ile inne względy nie przemawiają przeciwko temu, wygodnie jest przechowywać liczby w postaci BCD. Prosta procedura wyświetlająca młodszą cyfrę BCD może wyglądać tak:

 savmsc equ 88

 disp_1 and #%00001111
        ora #'0'-32
        sta (savmsc),y
        dey
        rts

Aby nie zmieniać algorytmu dodatkowymi zabiegami, wyświetlamy cyfrę w pobliżu początku pamięci obrazu (wektor savmsc). Odjęcie liczby 32 od kodu '0' jest najprostszym sposobem zamiany z ASCII na kod Antic'a. Przed wywołaniem procedury należy oczywiście umieścić stosowną liczbę w akumulatorze i zainicjować rejestr Y. Zmniejszenie wartości tego rejestru po wyświetleniu cyfry ma taki skutek, że kolejne wywołania procedury disp_1 będą umieszczać następne cyfry na lewo od poprzednich. Wyświetlenie całego bajtu, czyli dwóch cyfr BCD:
 disp_2 pha
        jsr disp_1
        pla
        lsr @
        lsr @
        lsr @
        lsr @
        jsr disp_1
        rts

Pierwsze wywołanie disp_1 wyświetla młodszą cyfrę, ale niszczy przy tym starszą, stąd konieczność przechowania bajtu na stosie. Przesunięcie bitów o cztery pozycje w prawo sprawi, że za drugim razem starsza cyfra znajdzie się na miejscu młodszej.

Zwolennicy upraszczania zauważą zapewne ,że można zamienić sekwencję:

        JSR DISP_1
        RTS

na równoważny jej rozkaz
        JMP DISP_1


Rozkaz ten z koleji można całkowicie pominąć, jeśli umieścić procedurę disp_2 bezpośrednio przed disp_1. Komu wydaje się jednak zbyt skomplikowane, niech lepiej zaniecha tych uproszczeń.

Aby zadelarować dane typu BCD (assemblery na ogół nie oferują dla nich osobnego formatu), trzeba posłużyć się prostą sztuczką. Należy zastosować tryb szesnastkowy z tym zastrzeżeniem, że używa się tylko cyfr od '0' do '9'. A więc liczbę 1956 zapiszemy jako:

 year    dta a($1956)


wyświetlenie takiej liczby jest łatwe i przyjemne:
 len     equ 2

 disp_y  ldx #0
         ldy #5
 dsp     lda year,x
         jsr disp_2
         inx
         cpx #len
         bcc dsp
         rts

Jak łatwo zauważyć, nie nakłada się tu żadnego ograniczenia na długość liczby może ona zajmować dowolnie wiele kolejnych bajtów. W naszym przypadku są dwa, odlicza je rejestr X.

Artmetyka BCD

Liczby dwójkowe podlegają ścisłym prawom, w myśl których:

  %00001001
 +%00000001
 =%00001010

czyli szesnastkowo $09+$01=$0A (dziesiętnie 9+1=10). W przypadku cyfr BCD dla osiągnięcia poprawnego wyniku chciałoby się żeby:
  %00001001
 +%00000001
 =%00010000

A więc szesnastkowo $9+$1=$10 (dziesiętnie 9+1=16). Aby zmusić nasz procesor do dodawania w tak niezwykły sposób, należy ustawić znacznik 'D'. Spróbuj zasemblować i wykonać:
       sed
       clc
       lda #%1001
       adc #%0001
       cld
       brk

i zaobserwuj wynik. Pod wpływem znacznika 'D' dodawanie przebiega zgodnie z regułami BCD, przepełnienie ponad 9 powoduje przeniesienie jedynki z młodszej połówki bajtu do starszej, ze starszej zaś do znacznika C. Pamiętaj, że znacznik 'D' ma wpływ wyłącznie na rozkazy ADC i SBC, zaś inne (jak INC, DEC) mu nie ulegają. Istnieje zdrowy zwyczaj włączania trybu dziesiętnego TYLKO na czas wykonywania operacji na liczbach BCD, natychmiast potem należy go wyłączyć (rozkazem CLD), aby zapobiec nieporozumieniom w czasie przyszłych rachunków.

Konwersja liczby dwójkowej na BCD

      Często, gdy zachodzi potrzeba wyświetlenia liczby, stajemy przed koniecznością jej zamiany z postaci binarnej na BCD. Typowym przykładem jest kod błędu, zwracany przez procedury we/wy w rejestrze Y.

 byte   equ $cb
 word   equ $cc

        sty byte
        jsr convr
        ldy #5
        lda word
        jsr disp_2
        lda word+1
        jsr disp_1

Przepisujemy kod błędu do komórki byte procedura konwersji umieszcza kod w słowie word. Ponieważ liczba z zakresu 0-255 jest co najwyżej trzycyfrowa, wywołuje się najpierw wyświetlenie dwóch cyfr, a z drugiego bajtu jednej. Procedura konwersji może mieć taką postać:
 convr  lda #0
        sta word
        sta word+1
        ldx #8
        sed
 cv1    asl byte
        lda word
        adc word
        sta word
        rol word+1
        dex
        bne cv1
        cld
        rts

Zastosowana tu metoda polega na "przelewaniu" bitów z liczby binarnej do liczby w formacie BCD. Wychodzące w wyniku ASL (ten rozkaz działa zawsze binarnie) bity chwyta się za pomocą znacznika C do tworzonego słowa word, gdzie kolejne mnożenia przez 2 (za pomocą dodawania w trybie BCD) nadadzą im właściwą wartość. Przykładowo, bit 7 z bajtu byte wejdzie w pierwszym obiegu pętli jako 1 (lub 0) do słowa word. W wyniku pozostałych siedmiu mnożeń będzie pomnożony przez 128, czyli osiągnie swą wagę z bajtu byte. Każdy następny bit dostąpi odpowiednio mniej mnożeń, aż do bitu nr.0, który wejdzie na najniższą pozycję i tam już pozostanie.

      Inną metodą konwersji może być "przelewanie jednostek". Algorytm ten polega na zmniejszaniu liczby binarnej (np. przez DEC) z równoczesnym zwiększaniem liczby BCD (rozkazem ADC #1 w trybie dziesiętnym) aż do osiągnięcia przez tę pierwszą wartość zera. Ta metoda prosta i łatwa do zrozumienia, jest jednak znacznie mniej efektywna od tej jaką pokazuje procedura CONVR. O trybie dziesiętnym należy wiedzieć i pamiętać nawet wtedy gdy go nie używamy. Szczególnie w procedurach obsługi przerwań warto mieć na uwadze to, że mogą być one wywoływane w dowolnej chwili. Także w takiej kiedy jest ustawiony znacznik D. Dlatego każda szanująca się obsługa przerwania zaczyna się od sakramentalnego CLD.

PRZERWANIA

      W niektórych dziedzinach przerywanie uważane bywa za niezdrowe. Dla procesora wszakże jest to zwykła rzecz. Przerwania są wręcz niezbędne. To w komputerze coś, jak rytm serca.

      Przerwanie w rozumieniu informatycznym oznacza pewne zdarzenie, nierzadko losowe, które wymaga od procesora natychmiastowego zainteresowania. Klasyczne przerwanie objawia się sygnałem elektrycznym podanym na nóżkę procesora. Procesor 6502 ma dwie takie nóżki zwane IRQ i NMI. Różnica między nimi polega na tym, że można programowo wyłączyć wrażliwość procesora na sygnał IRQ, podczas gdy wejście NMI pozostaje zawsze aktywne. Żródłami sygnałów przerwań, są w komputerze ATARI układy pomocnicze: POKEY (IRQ) i ANTIC (NMI). Nadchodzące od nich sygnały mówią o takich zdarzeniach, jak naciśnięcie klawisza, rozpoczęcie pionowego powrotu plamki kreślącej obraz, itd. To system nerwowy procesora, jedyny jego kontakt z realnym światem.

      Fakt istnienia (pojawiania się) przerwań mąci idyllę programisty, nie jest on bowiem w stanie przewidzieć, w którym momencie wykonywania jego programu nadejdzie sygnał, skupiający na sobie całą uwagę procesora. Przerwanie jest zawsze ważniejsze od jakiegoś głupiego programu! Większość przerwań wywoływane jest przez zdarzenia wymagające natychmiastwej reakcji. Procesor PRZERYWA więc wykonywanie programu użytkownika (naszego programu!), zamiast nigo realizuje tak zwaną procedurę obsługi przerwania. Po zakończeniu tej procedury powraca do wykonania przerwanego programu, jakgdyby nic się nie zdarzyło. Aby przerwanie nie miało wpływu na wykonanie naszego programu, procedura jego obsługi jest zobowiązana do pozostawienia procesora DOKŁADNIE w takim stanie, w jakim go zastała. Sprzyja temu "odruchowy" mechanizm reakcji na sygnał przerwania: procesor umieszcza na stosie rejestr znaczników oraz adres kolejnego rozkazu, który miał być wykonany w chwili wystąpienia przerwania. Specjalny rozkaz powrotu z przerwania, którym musi się kończyć każda procedura obsługi, RTI powoduje odtworzenie adresu programu i znaczników. Jeżeli w trakcie obsługi przerwania używane są jakieś rejestry, musi zostać przechowana ( i oddana! ) ich pierwotna wartość. Dzięki takiemu postępowaniu przerwanie pozostaje niewidoczne dla programu użytkownika. Uff, możemy spać spokojnie.

      Obsługę przerwania można porównać do wykonania "fuchy" przez sprytnego (i nieuczciwego) pracownika. Cichcem wykonuje on podczas swej normalnej pracy jakąś dodatkową robotę. Poczynania swe maskuje starannie, by nikt, zwłaszcza szef tego nie zauważył. Niestety, często odbija się to na jakości i wydajności podstawowej pracy.

Blokowanie przerwań

      Początkujący programista nie zauważa istnienia systemu przerwań. Czasami jednak komputer zachowuje się jakoś dziwnie. Próbujemy na przykład napisać program, który wydaje dźwięki bez użycia POKEY'a:

        opt %10101

 consol equ $d01f
 speakr equ %00001000
 delay  equ 200

        org $480

 play   lda #0
 loop   eor #speakr
        sta consol
        ldx #delay
 pause  dex
        bne pause
        jmp loop

        end.

Dla uproszczenia pominięto opcję wyjścia. Programik można zatrzymać przez SHIFT/BREAK lub brutalnie poprzez RESET. Przy okazji drobna uwaga: MiniDebuger Quick Assemblera (uruchamiany opcją Run) przed przystąpieniem do wykonywania naszego programu zeruje wstępnie wszystkie rejestry, aby ułatwić obserwowanie zmian. Na Twoim miejscu nie liczyłbym na to, ponieważ systemy typu DOS nie robią tego. A zatem prawdziwy program zawsze powinien nadawać rejestrom wartości początkowe, zwróć uwagę na wiersz z etykietą PLAY.

      Powyższy prosty program steruje mambramą głośnika poprzez bit SPEAKER w komórce CONSOL. Jak pamiętamy z poprzednich odcinków, rozkaz EOR użyty w ten sposób, będzie dawał w akumulatorze na przemian wartości %00001000 i %00000000. Uwzględniając opóźnienie wprowadzone przez pętlę PAUSE, powinniśmy otrzymać dźwięk o określonej wysokości, wynikającej z prędkości procesora. Tymczasem zamiast czystego dźwięku usłyszymy przykry charkot. To przerwania angażujące procesor do innych prac zakłucają działanie naszego programu. Spróbujmy temu zaradzić wyłączając przerwania IRQ poprzez umieszczenie rozkazu:

        sei

bezpośrednio przed pętlą LOOP. I co? Dźwięk się nie zmienił, ale za to nie możemy przerwać programu przez SHIFT/BREAK, ponieważ zgłoszenie przerwania nadchodzące z klawiatury nie jest już przez procesor obsługiwane. Można oczywiście użyć rozkazu:
        cli

przywracającego wrażliwość na przerwania IRQ, ale jak bez klawiatury skłonić program do jego wykonania? Tym razem już tylko RESET nam pomoże. To pierwsze ostrzeżenie: wyłączając przerwania bez możliwości ich włączenia możesz łatwo zablokować komputer!

      W naszym przypadku, źródłem zakłóceń dźwięku jest przerwanie VBLANC należące do grupy NMI. Związane jest ono z pionowym powrotem plamki kreślącej obraz telewizyjny i jest wywoływane przez ANTIC dokładnie co 1/50 sekundy. Jest to jednak przerwanie niemaskowalne, nie można zablokować jego odbioru w procesorze. Cóż więc począć? Trzeba wyłączyć źródło przerwania ! Do tego celu w ANTIC'u służy komórka:

 nmien  equ $d40e

usuń z naszego programu rozkaz SEI ( i tak niewielki z niego pożytek ), a zamiast niego wstaw:
        lda #0
        sta nmien

Zasembluj i uruchom program. Teraz wyraźnie słychać różnicę. Brzmienie dźwięku znacznie się poprawiło, choć jeszcze daleki jest od ideału. Warto jednak pamiętać, że wyłączenie przerwań NMI powoduje zatrzymanie funkcji istotnych dla życia systemu ATARI, takich jak zegar systemowy, liczniki programowe, aktualizacja rejestrów - cieni, pełna obsługa klawiatury itd. Dalsze kłopoty wystąpią w chwili gdy zechcesz odtworzyć poprzedni stan rejestru NMIEN. Do światowej skarbnicy nonsensu należy zaliczyć metodę przez niektórych programistów teoretyków:
        lda nmien
        pha

z zamiarem późniejszego odtworzenia:
        pla
        sta nmien

Niestety! Rejestr nmien nie jest komórką pamięci, a przy tym konsruktorzy nie wyposażyli go w możliwość odczytu. Cokolwiek tam wstawić, odczytuje się zawsze 255, i nic z tego nie wynika. Tymczasem we współczesnych ATARI tylko dwa bity w nmien mają znaczenie:
 dli_en equ %10000000
 vbl_en equ %01000000

Pierwszy zezwala na przerwanie wywoływane przez program ANTIC'a, zaś drugi na przerwania związane z pionowym powrotem plamki w telewizorze. Ponieważ trudno zgadnąć, jak wyglądało dotychczasowe ustawienie, stosowane bywają dwie metody przywracania przerwań NMI:

      bezpieczna:

        lda #vbl_en
        sta nmien

      i uprzejma:
        lda #dli_en+vbl_en
        sta nmien

Każda ma swoje wady związane z przerwaniem "DLI". Pierwsza zabija to przerwanie o ile było dotąd aktywne, druga może uruchomić niepożądaną aktywność przerwania, choć dotąd było zablokowane. Wybór należy do Ciebie. Moja prywatna taktyka polega na unikaniu ingerencji w NMIEN, o ile nie jest to niezbędnie konieczne. Wróćmy jednak do naszego dźwięku.

      Prócz cyklicznych przerwań VBLANK źródłem zakłóceń jest nieustanne odczytywanie pamięci obrazu przez procesor graficzny z użyciem techniki zwanej DMA, co oznacza bezpośrednie korzystanie z pamięci bez udziału procesora. Jest to zjawisko podobne do przerwania, choć nim nie jest. Nasz przykładowy pracownik (procesor) zasypia na chwilę, gdy w tym czasie złodzieje wynoszą towar z magazynu. Nie ma tu wykonywania żadnego innego programu, lecz wskutek przerw o trudnych do przewidzenia odstępach i czasach trwania, właściwy program jest realizowany nierytmicznie.

Aby wyłączyć transmisję DMA, dopiszmy po sekwencji blokującej NMI:

 dmact  equ $d400
 dmact_ equ $22f

        lda #0
        sta dmact_
        sta dmact

      Nie wystarczy wpisanie wartości do rejestru - cienia, ponieważ (jak już wiemy) nie działa teraz mechanizm przepisywania wartości z cieni do rejestrów sprzętowych. Zasembluj teraz i uruchom program. Ładnie brzmi choć trochę mało widać.

Rozmnażanie przerwań IRQ

      Istnieją dwa podstawowe sposoby kontaktów procesorów ze światem zewnętrznym: nasłuchiwanie sygnałów, oraz przerwania. Nasłuchiwanie polega na okresowym sprawdzaniu rejestrów urządzeń zewnętrznych, by nie przegapić nadejścia ważnego sygnału. Ta metoda jest prosta w realizacji, lecz angażuje czas procesora do bezproduktywnego przeglądania rejestrów. Metoda przerwań jest trudniejsza w realizacji, lecz efektywniejsza w działaniu. W komputerze ATARI połączono oba te sposoby.

      Przerwanie IRQ, zgłaszane przez POKEY, może dotyczyć jednej z wielu sytuacji: naciśnięcia klawisza, odebrania znaku od urządzenia zewnętrznego, wyzerowanie licznika, itd. Każde ze zdarzeń ma swoją własną procedurę obsługi, lecz niestety jest tylko jedna linia zgłoszenia. Procesor zaczyna więc obsługę przerwania IRQ zawsze od tego samego: systemowa procedura zawarta w ROM'ie sprawdza wszystkie możliwe źródła przerwań, a po stwierdzeniu, które jest aktywne, kieruje sterowanie do odpowiedniej procedury szczegółowej, której adres znajduje się w odpowiednim słowie (wektorze) na stronie drugiej.

      Poniżej przedstawiam obszerne fragmenty zrealizowanego przy pomocy przerwań sterownika złącza RS-232. Zastosowane zostało przerwanie zegara 1 POKEY'a. Omówi działanie tylko części nadawczej, gdyż odbiór z punktu widzenia przerwań różni się w niewielu szczegółach.

Jeżeli czytelnicy wyrażą zainteresowanie opublikujemy opis wykonania takiego złącza, oraz odpowiedni program do jego obsługi.

Instalowanie własnej procedury obsługi przerwania

      Teoretycznie proste zadanie, w praktyce sprawia trochę kłopotów. Program po wykonaniu zadania powinien zostawić system tak jak go zastał. Dlatego należy rozpocząć od zachowania dotychczasowej zawartości wektora:

 vtimr1 equ $210

        lda vtimr1
        sta oldvtm
        lda vtimr1+1
        sta oldvtr+1

Należy liczyć się z tym, że przerwanie nadejdzie właśnie podczas instalowania jego obsługi. Z pozoru mało prawdopodobny przypadek wystąpienia przerwania gdy zmieniliśmy pierwszy bajt wektora, a drugiego jeszcze nie, zdarza się nad podziw często, oczywiście następuje wtedy skok w trudny do przewidzenia zakątek pamięci, co kończy się z zasady "zawieszeniem" się komputera. Najprościej uniknąć tej przykrej niespodzianki wyłączając na chwilę przerwania:
        sei
        lda <tx_int
        sta vtimr1
        lda >tx_int
        sta vtimr1+1
        cli

Do transmisji szeregowej wykorzystuje się złącze joysticka należy więc odpowiednio je przeprogramować (niektóre bity jako wyjścia):
        ldy #%00001100
        jsp ppa

procedura ppa zostanie przedstawiona nieco dalej. Pozostaje uruchomić licznik POKEY'a:
 bits   equ 8
 rate   equ $68 600 baud
 t1mask equ %00000001
 audctl equ $d208
 audc1  equ $d201
 audf1  equ $d200
 stimer equ $d209
 irqen  equ $d20e
 irqen_ equ $10

        ldy #0
        sty audctl  64 kHz
        sty audc1   quiet!
        lda #rate
        sta audf1
        lda irqen_
        ora #t1mask
        sta irqen_
        sta irqen   enable
        sta stimer

Od tej chwili każde przerwanie od zegara 1 będzie wywoływać naszą procedurę tx_int.

Obsługa przerwania.

 tx_int equ *
        bit tx_rdy
        bmi tx_ret
        bvc tx_bit

      Nie ma potrzeby przechowywania zawartości akumulatora, ponieważ zrobiła to za nas systemowa procedura rozpoznania przerwania. Zmienna tx_rdy decyduje o sposobie działania przerwania. Wartość 255 (czyli -1, ustawiony 7bit) oznacza, że nie ma aktualnie żadnego znaku do wysłania. Nasza procedura w takim przypadku kończy się bez zwłoki. Jeżeli 7 bit jest skasowany, lecz ustawiony 6, to nie wykona się skok BVC. Oznacza to początek wysyłania znaku:
        lda #bits
        sta tx_cnt
        lsr tx_rdy
        clc
        bcc tx_out (jmp)

      Zainicjowany zostaje licznik bitów tx_cnt, skok do tx_out ze skasowanym znacznikim C spowoduje wemitowanie bitu staru (0). Skasowanie 6 bitu zmiennej tx_rdy sprawi, że w następnych wywołaniach ten fragment będzie pomijany zaś wykona się następny:
 tx_bit lda tx_cnt
        bpl tx_dct
 * finish
        lda #255
        sta tx_rdy
        pla
        rti

Tu podejmuje się decyzję co do wysłania kolejnego bitu. Ujemna wartość w tx_cnt oznacza, że cały znak został już wysłany. W takim razie zmienna tx_rdy znów przyjmuje wartość ujemną na znak, że nadajnik jest gotów do wysłania następnego znaku. W przeciwnym razie należy zmniejszyć licznik:
 tx_dct dec tx_cnt
        sec
        bmi tx_out

Jeżli licznik osiągnął wartość ujemną, to znaczy, że zostały już wysłane wszystkie bity danych, pora teraz na bit stopu (1). Uzyskuje się to poprzez skok do tx_out z ustawionym znacznikiem C. Przy następnym przerwaniu ujemność licznika spowoduje zkończenie procesu przesyłania znaku. Jeżli licznik jest jeszcze nieujemny, należy wysłać kolejny bit danych z rejestru tx_dat:
        lsr tx_dat
 tx_out lda #0
        rol @
        asl @
        asl @
        sta pa
 tx_ret pla
        rti

Trzeba pamiętać, że po uruchomieniu zegara POKEY'a nasza procedura obsługi przerwania zaczyna żyć własnym życiem. Jest wywoływana przez system w równych odstępach czasu, całkowicie niezależnie od innych naszych poczynań. Aby wysłać znak należy go umieścić w akumulatorze i skoczyć do takiej oto prostej procedury:
 send   bit tx_rty
        bpl send

Nieujemna wartość zmiennej tx_rty oznacza, że nie zakończyło się jeszcze nadawanie poprzedniego znaku. Trzeba czekać na ustawienie 7 bitu - sygnał zakończenia
        sta tx_dat
        lsr tx_rdy
        rts

      Po umieszczeniu danej w rejestrze tx_dat kasujemy 7 bit zmiennej tx_rdy, by zainicjować operację nadawania. I to wszystko. Nie czekając na efekt można powrócić do głównego programu, by zająć się wyświetlaniem obrazu, sprawdzeniem klawiatury, procesu odbirczego itd., gdy tymczasem wstawiony do tx_dat bajt "sam się nadaje".

      Po zakończeniu transmisji całego ciągu bajtów (pliku) trzeba po sobie posprzątać (w przypadku handlera "R:" czyni to funkcja CLOSE). Przedewszystkim czekamy na wysłanie ostatniego bajtu.:

 w_last bit tx_rdy
        bpl w_last

gdy to nastąpi wyłączamy czym prędzej zegar nr.1:
        lda irqen_
        and #255-t1mask
        sta irqen_
        sta irqen  disable

następnie "niepostrzeżenie" oddajemy wektor przerwania:
        sei
        lda oldvtm
        sta vtimr1
        lda oldvtm+1
        sta vtimr1+1
        cli

i przywracamy normalny stan portu joysticka:
 pa     equ $d300
 pac    equ $d302

        ldy #%00000000
 ppa    lda #$30
        sta pac
        sty pa
        lda #$34
        sta pac
        rts

garść zmiennych robczych na koniec dopełnia całości:
 tx_rdy dta b(255)
 tx_cnt org *+1
 tx_dat org *+1
 oldvtm org *+2