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+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
|