Tajemnice ATARI

PROGRAMOWANIE PROCESORA 6502
W KOMPUTERACH ATARI XL/XE

    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ś tam głupiego programu! Większość przerwań powodowanych jest przez zdarzenia wymagające natychmiastowej reakcji. Procesor PRZERYWA więc wykonywanie programu użytkownika (naszego programu!), zamiast niego realizując tzw. procedurę obsługi przerwania. Po zakończeniu tej procedury powraca do wykonania przerwanego programu, jakby 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 i adres kolejnego rozkazu, który miał zostać 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 opcji-wyjścia. Programik można zatrzymać przez SHIFT/BREAK lub (brutalnie) przez RESET. Przy okazji drobna uwaga. Mini-Debugger Quick Assemblera (uruchamiany opcją Run) przed przystąpieniem do wykonania 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 powinien zawsze nadawać rejestrom wartości początkowe (patrz: wiersz z etykietą PLAY).

    Powyższy, prosty program steruje membraną 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 chrobot. To przerwania, angażujące procesor do innych prac, zakłócają 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!

    Źródłem zakłóceń dźwięku w naszym przypadku jest przede wszystkim przerwanie VBLANK, należące do grupy NMI. Związane jest ono z pionowym powrotem plamki kreślącej obraz telewizyjny i jest wywoływane przez ANTIC regularnie co 1/50 sekundy. Jest to jednak przerwanie niemaskowalne, a więc 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 (niewielki tu z niego pożytek), z 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 dalekie 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 pojawią się w chwili, gdy zechcesz odtworzyć poprzedni stan rejestru NMIEN. Do światowej skarbnicy nonsensu należy zaliczyć metodę proponowaną przez niektórych programistów-teoretyków:
       LDA NMIEN
       PHA
z zamiarem późniejszego odtworzenia przez
       PIA
       STA MMIEN
    Niestety! Rejestr NMIEN nie jest komórką pamięci, a przy tym konstruktorzy 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 przerwania 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 dotąd było 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. Zainteresowanych tematem praktycznego zastosowania przerwań NMI odsyłam do cyklu "Piszemy Demo", tymczasem zaś wróćmy 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 po prostu na chwilę, gdy w tym czasie złodzieje wynoszą towar z magazynu. Nie ma tu wykonywania żadnego dodatkowego 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 wpisać wartości do rejestru-cienia, ponieważ jak już wiemy nie działa teraz mechanizm przepisywania wartości z cieni do właściwych rejestrów sprzętowych. Zasembluj i uruchom program. Ładnie brzmi, choć trochę mało widać.

Rozmnażanie przerwań IRQ

    Istnieją dwa podstawowe sposoby kontaktu 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 dla 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, wy-zerowania 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 odpo wiedniej procedury szczegółowej, której adres znajduje w odpowiednim słowie (wektorze) na stronie 2.

    Poniżej przedstawiam obszerne fragmenty zrealizowanego przy pomocy systemu przerwań sterownika złącza RS (patrz str. 5 i wcześniejsze). Zastosowane zostało przerwanie zegara nr 1 POKEY-a. Omówię działanie tylko części nadawczej, gdyż odbiór z punktu widzenia wykorzystania przerwań różni się w niewielu szczegółach.

Instalowanie własnej procedury obsługi przerwania

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

         LDA VTIMR1
         STA OLDVTM 
         LDA VTIMR1+1
         STA OLDVTM+1
    Należy się liczyć 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 już 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" komputera. Najprościej uniknąć tej przykrej niespodzianki, wyłączając na chwilkę przerwania:
         SEI
         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
         JSR PPA
        Procedura PPA zostanie przedstawiona nieco dalej. Pozostaje uruchomić licznik POKEY-a (podobnie, jak to pokazano w TA 9/92):
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 64kHz
         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 7 bit) 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 znacznikiem C spowoduje wyemitowanie bitu startu (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ż wysiany. 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żeli licznik osiągnął wartość ujemną, to znaczy, że zostały już wysłane wszystkie bity danych, pora teraz na bit stopu (l). Uzyskuje się to przez skok do TX_OUT z ustawionym znacznikiem C. Przy następnym przerwaniu ujemność licznika spowoduje zakończenie procesu przesyłania znaku. Jeżeli 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 uruchomienia "zegara" POKEY-a nasza procedura obsługi przerwania zaczyna żyć własnym życiem. Jest wywoływana przez system w regularnych 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_RDY
         BPL SEND
    Nieujemna wartość zmiennej TX_RDY oznacza, że nie zakończyło się jeszcze nadawanie poprzedniego znaku. Trzeba zaczekać 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 odbiorczego 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). Przede wszystkim 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 l:
         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 roboczych na koniec dopełnia całości:
TX_RDY   DTA B(255)
TX_CNT   ORG *+1
TX_DAT   ORG *+1
OLDVTM   ORG *+2
         END

Kilka przestróg

    Często się zdarza, że nowicjusz, oczarowany niewątpliwą urodą świata przerwań, pragnie wykorzystać świeżo nabytą wiedzę za wszelką cenę. Nie dajcie się zwariować! Przerwania, efemeryczne ze swej natury, nie pozwalają się śledzić zwykłymi debuggerami, dają nieoczekiwane rezultaty z najmniej spodziewanych przyczyn, są więc bardzo trudne do testowania i poprawiania. Jeżeli rzecz da się wykonać innym sposobem, zrezygnuj z przerwań.

    Nieuzasadnione użycie przerwań jest typowym objawem nieudolności programisty. Ostatnio widziałem program w którym kursor zmienia kolory przy pomocy przerwania VBLANK:
MY_VBL LDA TIMER
       STA COLOR
       JMP SYSVBV
    Oczywiście wymaga to dodatkowych rozkazów dla zainstalowania przerwania (przy tym tak, by nas nie "przyłapało"):
       LDA <MY_VBL
       STA WBLKI 
       LDA >MY_VBL 
       STA WBLKI+1
    Artysta "zapomniał" na dodatek o utrzymaniu działania dotychczasowej procedury VBLANK (poprzez zapamiętanie starego wektora i skok do niego z końca swojej procedury). Zapewne chodziło o oszczędność kodu... Tymczasem rzecz cała sprowadza się do umieszczenia w pętli oczekiwania na naciśnięcie klawisza dwóch wspomnianych wyżej rozkazów:
       LDA TIMER
       STA COLOR
    Stosowanie przerwania w tym przypadku można śmiało nazwać wytaczaniem armaty na wróble!

    Przerwania zwalniają działanie systemu. Typowym przykładem jest zbyt gęste przerwanie zegara POKEY-a. Przyznam, że opisana obsługa złącza RS działa na granicy wydolności komputera. Jeżeli kolejne przerwanie "następuje na pięty" poprzedniemu, spowoduje rychłe przepełnienie stosu odkładanymi adresami powrotu, a w efekcie śmierć systemu.

    Przerwania mogą być przerywane przez inne przerwania! Zbytnie obciążenie systemu przerwań powoduje zawieszanie się szybkich urządzeń WE/WY, zwłaszcza stacji dysków. Większość nadsyłanych do redakcji zegarów (ulubiony, poza dziesbinem, temat naszych Czytelników) obarczona jest tą wadą. Jeżeli już nie można się obyć bez przerwania, powinno być ono obsłużone w rekordowo krótkim czasie.

    Tu przerwał, lecz róg trzymał...

Janusz B. Wiśniewski



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

Pixel 2002