Mikrokomputer CA80
programowanie, assemblerowa piaskownica

przegląd wybranych procedur systemowych



charakterystyka i informacje ogólne
książki / dokumentacja (literatura)
ulotki i broszurki MIK (literatura)
skany artykułów z prasy (literatura)
programator EPROM (aplikacje)
dwupunktowy termometr (aplikacje)
licznik-timer Z80-CTC i dioda RGB (aplikacje)
Direct Memory Loader - ładowarka do pamięci (akcesoria)
próbnik stanów logicznych MIK03C (akcesoria)
zdalnie sterowana klawiatura (akcesoria)
przegląd procedur systemowych (programowanie)

☘☘☘☘☘☘☘☘☘☘☘

    Stronka ta skoncentrowana jest na podstawach programowania CA80, do tego w końcu ów komputerek stworzono. Dość dokładny opis procedur systemowych CA80 znajduje się w tomiku MIK05, tam też znajdziemy ich kod źródłowy oraz mapę zajętości górnych obszarów pamięci RAM. Pełny listing monitora to MIK08 i to jest kolejna broszurka, z której lekturą naprawdę warto się zmierzyć.
Przykłady poniżej mocno nawiązują do tego, co znajdziemy w MIK05, nie chciałam jednak powielać prostych programików z MIK, prezentowane tu aplikacje są nieco bardziej rozbudowane i celowo podkręcone, demonstrując przy okazji kilka sympatycznych właściwości translatora SB-Assembler .


-- ♠ --
szkic typowego programu - podstawa do klonowania

; example_draft.asm
        .cr z80                     ; oczywiscie procesor Z80
        .tf example_draft.hex,int   ; kompilacja do intel hex
        .lf example_draft.lst       ; poprosimy listing
        .sf example_draft.sym       ; i tablice symboli
                                    ; plik z deklaracjami 
        .in ca80.inc                ; procedur i stałych 
                                    ; systemowych                                            
        .sm     code                ; typowy start kodu uzytkownika
        .or     $c000               ; bank U12, adres $C000
        ;
        ld  SP,$ff66                ; ustawienie stosu 
        ;
        .co
        tu dajemy upust swej kreatywności
        .ec
        ;
        jp  $                       ; martwa pętla 
        rst $30                     ; lub powrót do Monitora
        ;
        

Wyjątkowo, oprócz źródła example_draft.asm wskazuję pliki wynikowe:

example_draft.hex - plik z kompilatem *.hex (może być *.bin) zależnie od tego, jak dalej chcemy przenieść binaria do CA80. *.bin to raczej dla emulatora EPROM AVT-270, *.hex dla Ładowarki do Pamieci lub wysyłki via RS232

example_draft.lst - listing, włącznie z liniami plików zagnieżdżonych dyrektywą .in. Przy mnemonikach widzimy kody maszynowe instrukcji oraz koszt ich wykonania w taktach zegarowych. Na bardzo upartego z listingu idzie bezpośrednio wpisać programik do CA80 zleceniem *D, a jest to tym prostsze, że SB-Asm kolejne bajty słów-parametrów 16-bitowych rozkazów (np. ld HL,nnnn) układa w ludzkiej kolejności młodszy-starszy, można przepisywać niejako na żywca.

example_draft.sym - spis symboli z odniesieniami, przydaje się przy dokumentowaniu aplikacji, czasem ułatwia przeprowadzenie modernizacji (w nowomowie: redesign) na poziomie wersji źródłowej, pokazuje liczniki użyć danego literału (RefCount), a najciekawsze są te z zerowymi wystąpieniami oraz te maksymalnie nękane w naszym kodzie.

Każdy program użytkownika zaczynamy jawnym ustawieniem stosu, w CA80 przyjęła się wartość $FF66. Druga zwyczajowo przyjęta rzecz to lokalizacja programu - bank U12 (to numer scalaka na płytce), od adresu $C000. Teoretycznie mamy tu 16kB ($4000) pamięci na nasz kod maszynowy, no łoł. Krzemowa rzeczywistość jest jednak szara i prozaiczna - w znakomitej większości przypadków bank ów obsadzony jest układem 6264 o pojemności 8kB, z racji niepełnego dekodowania obszary $C000...$DFFF oraz $E000...$FFFF są tożsame. Oznacza to także tyle, że choć dno stosu ustawiamy na $FF66 to tak naprawdę fizycznie wykorzystany zostanie adres $DF66 (kto chce niech sprawdzi zleceniem *D). Piszę to, aby zasygnalizować, że na stos nie mamy wcale tak wiele przestrzeni jak mogłoby się wydawać z teoretycznego rozmiaru banku. Pracujący stos będzie pomykał w kierunku niższych adresów - jeżeli damy ciała w jego obsłudze, na przykład źle zbilansowanymi ilościowo rozkazami push|pop, stos zamaże nam kod w RAM i będzie po zabawie. Szczęściarze posiadający konfigurację pamięci typu kostka 62256 (32kB) w podstawce U12 (to raczej w odniesieniu do nowego CA80) mają ciągły obszar RAM z zakresie $8000...$FFFF i tam hulaj dusza.

Poniżej znajdziemy kolejne programiki demonstracyjne procedur systemowych CA80, to swego rodzaju API zdejmujące z nas problemy z obsługa klawiatury, wyświetlacza czy z wprowadzaniem danych (liczb/parametrów) do programu. Warto z nich korzystać, ponieważ zapewnia to kompatybilność kodu pomiędzy starą i nową wersją CA80 no i jest w sumie oszczędnością czasu, mamy gotowce na podstawowe życiowe sytuacje.

Wszystkie programy demonstracyjne z GitHub bienata/CA80 można sobie skompilować jedną linijką:

for przyklad in example_* ; do ./sba ${przyklad}; done

-- ♠ --
procedura COM - $01ab - drukowanie jednego kodu 7-seg.

Procedura COM wyświetla siedmiosegmentowy kod znaku przekazany w rejestrze C zgodnie z ustaloną jako parametr PWYS pozycją wyświetlacza. Przykład kręciołka na wyświetlaczu widzimy poniżej.

example_COM.asm
; example_COM.asm
        .cr z80                     
        .tf example_COM.hex,int   
        .lf example_COM.lst
        .sf example_COM.sym       
        .in ca80.inc                
        .sm code                    ; 
        .or $c000                   ; U12/C000
        ld  SP,$ff66                ; 
        ;
.begin      
        ld B,semiTableEnd-semiTable ; B-licznik elementów tabeli
        ld HL,semiTable             ; adres tabeli znaczków
.loop       
        ld C,(HL)                   ; weź element tabeli
        call COM                    ; pokaż znaczek
        .db $17                     ; pozycja lewa skrajna (7)
        call COM                    ; pokaż znaczek
        .db $15                     ; pozycja lewa (5)
        call delay                  ; opóźnienie
        inc HL                      ; indeks++
        djnz .loop                  ; while (--licznik)
        jp .begin
        ;
semiTable:
        .db $01,$02,$04,$08,$10,$20,$40,$80
semiTableEnd:        
        ;
delay:  push    BC
        push    AF
        ld      B,$FF
.delay        
        halt            ; 2ms
        djnz    .delay  ; while( --B )
        pop     AF
        pop     BC
        ret
        ;

Wywołanie COM jest proste i nie wymaga komentarza, zerknijmy za to na procedurę delay z rozkazem halt. To znana w CA80 sztuczka generująca opóźnienia wykorzystująca fakt, że w tle cały czas pracują dla nas przerwania NMI. A jak wiadomo, procesor Z80 z błogostanu wywołanego rozkazem halt może wyjść albo na skutek sprzętowego resetu, albo właśnie przyjęcia przerwania. NMI w systemie CA80 pojawiają się co 2 ms, wykonanie halt zastopuje program główny do najbliższego przerwania, dając finalne 2 ms opóźnienia. Wołając halt w pętli, jak w przykładzie powyżej możemy generować praktycznie dowolne opóźnienia, oczywiście niestety marnując moc obliczeniową procesora, w tym przykładzie jest to akurat akceptowalne.

Zobaczmy też, jak inicjowany jest rejestr B, służący jako licznik obiegów pętli koordynowanej rozkazem djnz. Translatorowi podajemy wyrażenie arytmetyczne, różnicę adresów etykiet `semiTableEnd` i `semiTable`, ponieważ elementy są jednobajtowe wynik odejmowania da nam ilość elementów w tabeli. Ewentualne dołożenie kolejny elementów automatycznie przesunie etykietę (pamiętamy, aby ona była za ostatnim elementem tabeli, na pierwszym wolnym kolejnym adresie), translator sam wyliczy jaką wartością należy załadować licznik w rejestrze B. Sztuka jest bardzo przydatna, należy tylko mieć na uwadze, że maksymalna wyliczona wartość nie może być większa od $FF pod rygorem błędu translacji.

Działanie programu example_COM.asm widzimy na filmiku:



-- ♠ --
procedura PRINT - $01d4 - drukowanie komunikatu

Procedura PRINT wyprowadza na wyświetlacz ciąg znaków (komunikat) wskazany adresem w rejestrze HL i ograniczony znacznikiem $FF (dalej EOM /end of message/). Treść komunikatu (kolejne znaki) to kody siedmiosegmentowe wyświetlacza. Przykład wykorzystania procedury poniżej (już okrojony do niezbędnego minimum):

example_PRINT.asm
; example_PRINT.asm
ADDR_LO .eq 0
ADDR_HI .eq 1               
.begin  
        ld IX,messageTable          ; adres tabeli komunikatów
        ld B,messageTableLength     ; licznik komunikatów 
.loop        
        ld L,(IX+ADDR_LO)           ; do HL adres wybranego via wskaznik 
        ld H,(IX+ADDR_HI)           ; z IX komunikatu w kolejności LSB, MSB 
        call PRINT                  ; pokaż znaki
        .db $80                     ; na cały wyświetlacz
        call delay                  ; opóznienie
        inc IX                      ;
        inc IX                      ; IX += 2 kolejny wskaźnik              
        djnz .loop                  ; while(--licznik)          
        jp .begin
        ;
messageTable:
        .dw message1 
        .dw message2
        .dw message3
messageTableLength  .eq $-messageTable/2
        ;
message1:
        .db     $00,$00,$00, $74, $79, $38, $38, $5c, EOM        
message2:
        .db     $5c,$5c,$5c, $5c, $00, $00, $00, $00, EOM        
message3:
        .db     $00, $00, $00, $00, $5c,$5c,$5c, $5c, EOM     

Tu, oczywiście oprócz wywołania procedury PRINT pragnę pokazać jak SB-Assembler potrafi nam ułatwić życie, a przynajmniej zwiększyć czytelność kodu źródłowego. Zacznijmy od sztuki z automatycznym obliczaniem rozmiaru tabeli. W tym wypadku mamy do czynienia z trzema komunikatami message1,2,3 których adresy zgromadzone są w tabelce `messageTable`. Adresy są szesnastobitowe, więc jak nietrudno zgadnąć - rozmiar tabeli wyliczamy odejmując adres jej końca (pierwszego bajtu po ostatnim elemencie) od adresu początku. Całość dzielimy przez dwa, tak właśnie wyliczana jest wartość `messageTableLength`. I tu zapewne marszczymy brwi - no ale jak? Przecież wyrażenie `$-messageTable/2` to na logikę (i szkolna kolejność operacji) różnica pomiędzy bieżącym wskaźnikiem i połową adresu początku... No, tu trzeba się nieco przyzwyczaić do filozofii SB-Asm i tego, że wylicza on wartości po kolei, więc najpierw odejmie, a potem produkt podzieli przez dwa. Niezbyt to fajne i trzeba zwyczajnie uważać.

Drugi niby drobiazg to sposób zapisu mnemoników Z80. Program przechowuje adres bieżącego komunikatu w rejestrze indeksowym IX, kolejne komunikaty (adresy) wybiera inkrementując ten rejestr o 2. Proszę zwrócić uwagę na sposób przepisania do rejestrów H i L wartości z komórek wskazywanych przez IX. Mamy w Z80 rozkaz `ld r,(IX+d)` gdzie r jest jednym z rejestrów roboczych, d - to przesunięcie/displacement o zakresie -128..+127. Oczywiście można zapisać +0, +1 i też będzie ładnie, ale wprowadzając stałe odpowiednio ADDR_LO, ADDR_HI oszczędzamy czytelnikowi zgadywania co poetka miała na myśli. Fragment kodu:

ADDR_LO .eq 0
ADDR_HI .eq 1               
        ld L,(IX+ADDR_LO)           ; do HL adres wybranego via wskaznik 
        ld H,(IX+ADDR_HI)           ; z IX komunikatu w kolejności LSB, MSB 

jest chyba czytelniejszy - do L trafia młodszy bajt komórki wskazanej IX, do rejestru H - bajt starszy. Oczywiści nie należy przesadzać z tego rodzaju parametryzacją, ale stosowana z umiarem - może naprawdę ułatwić życie osobie czytającej nasze źródła.

Filmik demonstrujący zastosowanie procedury PRINT:



-- ♠ --
procedura CLR - $0010 - kasowanie zawartości wyświetlacza

Procedura CLR zeruje zawartość bufora wyświetlacza w/g wskazanej pozycji PWYS. Samo wywołanie jest banalne i widać je poniżej, warto natomiast powiedzieć kilka zdań o tym jak posługiwać się parametrem PWYS (to te stałe `.db NN` za większością wywołań procedur systemowych). PWYS w wielkim skrócie steruje pracą procedur ekranowych, informując je od której pozycji wyświetlacza mają zacząć pisanie/kasowanie - to młodsze 4 bity oraz iloma pozycjami wyświetlacza ma zająć się procedura - to cztery starsze bity. Przykładowo: cały wyświetlacz czyli 8 pozycji, począwszy od 0 daje nam finalnie $80. Dwie cyfry hex począwszy od prawej wyświetlimy z PWYS=$20, a cztery cyfry od lewej podając PWYS=$44. Komentarze przy wywołaniach CLR są chyba jasne:

example_CLR.asm
; example_CLR.asm
.begin  
        ld HL, message1             ; testowa treść
        call PRINT                  ; pokaż 
        .db $80                     ; na cały wyświetlacz
        call delay                  
        call CLR                    ; 7 6 5 4 3 2 1 0
        .db  $22                    ; _._._._.#.#._._.
        call delay                  
        call CLR 
        .db  $24                    ; _._.#.#._._._._.
        call delay                  
        call CLR 
        .db  $20                    ; _._._._._._.#.#.
        call delay                  
        call CLR 
        .db  $26                    ; #.#._._._._._._
        call delay                  
        jp .begin
message1:
        .db     $5c,$5c,$5c, $5c, $63, $63, $63, $63, EOM        

Filmik demonstracyjny:



-- ♠ --

procedura CO - $01e0 - wyświetlenie cyfry hex z rejestru C

Procedura CO drukuje cyfrę szesnastkową (0..$F) podaną w rejestrze C zgodnie z ustawioną pozycją wyświetlacza. Przykładowy programik:

example_CO.asm
; example_CO.asm
.begin  
        ld A,$10
        ld (.displayPos),A          ; inicjacja PWYS
.loopPositions        
        ld B,$10                    ; zacznij od F
.loopDigits        
        call CLR                    ; skasuj wyswietlacz
        .db $80
        ld A,B                      ; do wyświetlania 
        dec A                       ; wartość -1 (zakres F..0)  
        ld C,A
        call CO                     ; nie mylić z "COM" :-)
.displayPos .bs  1                  ; skrajna prawa
        call delay                  
        djnz .loopDigits            ; while (--B)
        ; zmodyfikuj pwys
        ld A,(.displayPos)          ; weź aktualną pozycję
        inc A                       ; kolejna
        cp  $18                     ; czy skrajny lewy ?
        jp  Z,.begin                ; tak, zacznij od nowa
        ld (.displayPos),A          ; zapisz nową
        jp .loopPositions           ; rób cyferki na nowej pozycji 

Zwróćmy uwagę na nieco odbiegający od poprzednich układ instrukcji przy wywołaniu procedury systemowej. Po tradycyjnym `call PROCEDURA` mieliśmy równie tradycyjne `.db PWYS`, a tu:

        call CO                     
.displayPos .bs  1                  

Zastosowany tu zabieg to megaprosty przykład programu samomodyfikującego się, ano tak. Zarezerwowany bajt zaraz za rozkazem `call nnnn` jest opatrzony etykietą (displayPos) i zawartość tej komórki jest modyfikowana w trakcie działania programu, wybierane są kolejne pozycje wyświetlacza na których pojawiają się cyfry 0..F. Z technicznego punktu widzenia ten bajt nie jest zmienną programu rezerwowaną w dedykowanym segmencie, on należy do segmentu CODE, a jednak go zmieniamy i to skutecznie. Prezentuję to jako ciekawostkę, takie programy są trudne w uruchamianiu, no i oczywiście można je puszczać tylko w pamięci RAM, ten fragment wgrany do EPROM zwyczajnie nie zadziała. Na filmiku poniżej widzimy pracujący programik:



Można chwilę się nim pobawić, na przykład opatrując komentarzem wywołanie `call CLR` wraz z dotyczącym go parametrem PWYS. Program będzie wtenczas radośnie mazał po wyświetlaczu, pozostawiając jako ostatnio ustawioną zawartość, wygląda to mniej więcej tak:



-- ♠ --

procedura LBYTE - $0018 - wyświetlenie dwucyfrowej liczby hex z akumulatora

Procedura LBYTE wyprowadza szesnastkową zawartość rejestru A na wyświetlacz według zadanej pozycji wyświetlacza, program:

example_LBYTE.asm
; example_LBYTE
.begin      
        ld B,bytesTableLength   ; B-licznik elementów tabeli
        ld HL,bytesTable        ; adres tabeli bajtow
.loop       
        ld A,(HL)                   ; wez element tabeli
        call LBYTE                  ; pokaż wartość
        .db $20                     ; pozycja prawa skrajna
        call delay                  ; opóznienie
        inc HL                      ; indeks++
        djnz .loop                  ; while (--licznik)
        jp .begin
        ;
bytesTable:
        .db $00,$11,$22,$33,$44,$55,$66,$77
        .db $88,$99,$aa,$bb,$cc,$dd,$ee,$ff        
bytesTableLength    .eq $-bytesTable

Tu odkrywczego w sumie nic nie ma, zauważmy tylko, że skoro LBYTE drukuje ośmiobitową liczbę hex (00..FF), to z powodzeniem wydrukuje dwucyfrową liczbę BCD (00..99), kwestia jak się umówimy odnośnie interpretacji zawartości akumulatora. Tak ogólnie to procedura jest użyteczna do pokazywania wszelkich współczynników czy innych parametrów, filmik z działania następujący:



-- ♠ --

procedura LADR - $0020 - wyświetlenie czterocyfrowej liczby hex z HL

Procedura LADR wyprowadza na wyświetlacz szesnastkową zawartość rejestru HL (w kolejności H,L) zgodnie z zadaną pozycją wyświetlacza.

example_LADR.asm
; example_LADR
.begin      
        ld B,bytesTableLength   ; B-licznik elementów tabeli
        ld HL,bytesTable        ; adres tabeli bajtow
.loop       
        call LADR                   ; pokaż adres elementu 
        .db $44                     ; po lewej stronie
        ld A,(HL)                   ; wez element tabeli
        call LBYTE                  ; pokaż wartość
        .db $20                     ; po prawej
        call delay                  ; opóznienie
        inc HL                      ; indeks++
        djnz .loop                  ; while (--licznik)
        jp .begin
        ;
bytesTable:
        .db $00,$11,$22,$33,$44,$55,$66,$77
        .db $88,$99,$aa,$bb,$cc,$dd,$ee,$ff        
bytesTableLength    .eq $-bytesTable

Przykład jak widać jest mocną wariacją na temat programu z LBYTE, jedyne co wprowadziłam to pokazywanie nie tylko zawartości elementu tabeli (LBYTE) ale i jego szesnastobitowego adresu w pamięci, właśnie procedurką LADR. Całość w działaniu znajdziemy na filmiku:



-- ♠ --

procedura CSTS - $FFC3 - sprawdzenie stanu klawiatury

Procedura CSTS dokonuje szybkiego przeskanowania klawiatury, gdy stwierdzi wciśnięty klawisz wychodzi z ustawioną flagą CY i kodem klawisza w akumulatorze, gdy nic nie wciśnięto CY=0, oto prosty programik:
example_CSTS.asm
; example_CSTS
        ld  SP,$ff66                ; 
.begin  
        ld HL, welcomeMessage       ; ------
        call PRINT                  ; pokaż 
        .db $80                     ; na cały wyświetlacz
        call CSTS                   ; sprawdz klawisze
        jr  NC,.begin               ; czekaj dalej
        ; w A kod klawisza
        cp  $C
        call Z,whenCKeyPressed      ; dla C
        cp  $D
        call Z,whenDKeyPressed      ; D
        cp  $E
        call Z,whenEKeyPressed      ; E
        jr .begin                   ; powtarzaj
        ;
whenCKeyPressed:
        ld HL, keyCmessage          ; właściwy komunikat
        jr whenPressedCommon        ; do cześci wspólnej
whenDKeyPressed:
        ld HL, keyDmessage
        jr whenPressedCommon
whenEKeyPressed:
        ld HL, keyEmessage        
whenPressedCommon:        
        call PRINT
        .db $80
.waitRelease:
        call CSTS
        jr C,.waitRelease
        ret
        ;       
welcomeMessage:
        .db     $80,$80,$80,$80,$80,$80,$80,$80, EOM        
keyCmessage:        
        .db     $01,$01,$01,$01,$01,$01,$01,$01, EOM        
keyDmessage:        
        .db     $40,$40,$40,$40,$40,$40,$40,$40, EOM        
keyEmessage:        
        .db     $08,$08,$08,$08,$08,$08,$08,$08, EOM  

W tym programie warto zastanowić się nad obsługą warunków - zależnie od wciśniętego guzika wołamy niby-różne procedury do ich obsługi. Tak naprawdę to procedura jest jedna, ale u nas ma trzy (może dowolnie więcej) punktów wejścia. Oczywiście dalsza kontrola sterowania wymaga użycia lamerskich skoków bezwarunkowych. A co do skoków - zwróćmy też uwagę na wykorzystanie skoków relatywnych, adres docelowy nie jest wartością bezwzględną (czyli gdzie należy wylądować) ale raczej - odległością, czyli jak daleko, w przód lub tył. Wykorzystanie skoków relatywnych (jr - jump relative) jest fajne, bo daje nam w efekcie kod relokowalny, który może być bez ponownej translacji uruchomiony w dowolnym miejscu pamięci, przynajmniej teoretycznie. W przykładzie powyżej całą zabawę psuje nieco ... `call nnnn` i pomimo że i program główny i procedurka obsługująca klawisze korzystają ze skoków relatywnych, to pomiędzy nimi jest twarda zależność w postaci stałego adresu dla `whenXXXKeyPressed`. Działanie procedury CSTS na żywo:



-- ♠ --

procedura CI - $FFC6 - pobranie znaku z klawiatury

Procedura CI czeka na naciśnięcie klawisza, gdy to się stanie generowany jest sygnał dźwiękowy, w akumulatorze mamy kod tablicowy guzika. Dla zwykłych przycisków flaga Z procesora jest ustawiana na 0. Gdy procedura ustawi Z=1 to oznacza wciśnięcie [=] - tu flaga CY jest 1 lub [.] gdy flaga CY=0. Program testowy:
example_CI.asm
; example_CI
CODE_DOT    .eq     $80             ; .
CODE_EQU    .eq     $48             ; =
        ld  SP,$ff66                ; 
.begin  
        call CLR                    
        .db $80                     ; na cały wyświetlacz
        call CI                     ; pobierz znaczek z kbd
        jr Z,.dotOrEqual             ; [.] lub [=]
        ;cała reszta, kod znaku jest w Aku :)
        call LBYTE
        .db $20
        call delay
        jr .begin
        ; obsluga .=
.dotOrEqual:        
        ld  C,CODE_EQU       ; może ustaw =
        jr  C,.dotOrEqualNext
        ld  C,CODE_DOT       ; a jednak ustaw .
.dotOrEqualNext        
        call COM             ; pokaż kod 7-seg
        .db $10
        call delay
        jr  .begin           ; i nazat

Program komentarza specjalnego nie potrzebuje, zauważmy jednak ciemne strony klecenia w assemblerze - linijki od etykiety `dotOrEqual`, literał C pojawia się ciurkiem, raz to rejestr C raz flaga CY, początkującym często od tego witki opadają, ale z biegiem czasu zaczynamy taki kod ze zrozumieniem czytać, a póki co film:



Dodatkowy film poniżej wręcz pretensjonalnie pokazuje, że procedura CI zwraca sterowanie po wciśnięciu klawisza, takie zachowanie należy mieć na uwadze projektując interfejs dla człowieka w naszej aplikacji.



-- ♠ --

procedura TI - $0007 - pobranie cyfry z klawiatury z echem

Procedura TI pobiera z klawiatury cyfrę szesnastkową wyświetlając jednocześnie jej echo na zadanej pozycji wyświetlacza. Informacja o guzikach [=][.] - jak dla procedury CI. Procedura widzi tylko cyfry 0..F, a programik testowy wygląda następująco:
example_TI.asm
; example_TI
CODE_DOT    .eq     $80             ; .
CODE_EQU    .eq     $48             ; =
        ld  SP,$ff66                ; 
.begin  
        call TI                     ; pobierz znaczek z kbd
        .db $10                     ; echo na prawej pozycji
        jr Z,.dotOrEqual             ; [.] lub [=]
        jr .begin
        ; obsluga .=
.dotOrEqual:        
        ld  C,CODE_EQU       ; może ustaw =
        jr  C,.dotOrEqualNext
        ld  C,CODE_DOT       ; a jednak ustaw .
.dotOrEqualNext        
        call CLR
        .db $10
        call COM             ; pokaż kod 7-seg
        .db $10
        call delay
        jr  .begin           ; i nazat

Program jest bliźniaczy do CI, tylko bez kasowania wyświetlacza procedura CLR, a działa następująco:



-- ♠ --

procedura PARAM - $01f4 - pobranie słowa 16-bit do HL z echem

Procedura PARAM pobiera cztery cyfry szesnastkowe układając je w rejestrze HL, echo pokazywane jest zgodnie z ustawioną pozycją wyświetlacza. Wprowadzanie kończymy klawiszem [=] co ustawy CY=1 lub klawiszem [.] który ustawi CY=0. Zastosowanie:
example_PARAM.asm
; example_PARAM
ADDR_LO .eq 0
ADDR_HI .eq 1        
        ld  SP,$ff66                ; 
.begin  
        ld HL,addrPrompt            ; "Adr=____"
        call PRINT
        .db $80                     ; pokaż po lewo
        call PARAM
        .db $40
        jr NC,.begin                ; jak nie [=] to kontynuuj
        ; po [=] w HL mamy wprowadzony 16 bit adres
        ld A,H
        rlca                ; z wartości NNxx.xxxx zawijamy
        rlca                ; << i jest xxxx.xxNN    
        and $03             ; 
        ; tu A jest numerem komunikatu (nazwy banku) 0..3                
        add A               ; a := a*2, adresy są dwubajtowe
        ld  HL,bankNamesArray
        add L               ; wylicz adres nazwy banku
        ld L,A
        push HL             ; dziki transfer 16bit
        pop  IX             ; 
        ld  L,(IX+ADDR_LO)  ; pobierz ptr na komunikat   
        ld  H,(IX+ADDR_HI)  ; po kawałku
        call PRINT          ; i pokaż
        .db $80
        call delay
        jr  .begin           ; i nazat
        ;
bankNamesArray:
        .dw socketU9            ; "U9___rom"
        .dw socketU10           ; "U10__rAm"
        .dw socketU11           ; "U11__rAm"
        .dw socketU12           ; "U12__rAm"                        
        ;
socketU9:
        .db $3e,$6f,$00,$00,$00,$50,$5c,$54,EOM
socketU10:
        .db $3e,$06,$3f,$00,$00,$50,$77,$54,EOM
socketU11:
        .db $3e,$06,$06,$00,$00,$50,$77,$54,EOM
socketU12:
        .db $3e,$06,$5b,$00,$00,$50,$77,$54,EOM
addrPrompt:
        .db $77,$5e,$50,$48,$00,$00,$00,$00,EOM

Ten programik celowo jest zamotany, a mianowicie - wyświetlamy człowiekowi prompt 'Adr=', procedurką PARAM pobieramy cztery cyfry szesnastkowe, program potraktuje je jako adres pamięci CA80 i zależnie od wartości napisze nam do którego banku on należy. Dla przypomnienia, adresacja w CA80 jest następująca:
0000-3FFF - ROM, podstawka U9
4000-7FFF - RAM, podstawka U10
8000-BFFF - RAM, podstawka U11
C000-FFFF - RAM, podstawka U12
Program na okazję każdego z banków ma stosowny komunikat na wyświetlacz (`socketU9`...`socketU12`), adresy komunikatów zgromadzone są w tabeli `bankNamesArray`. Sztuka cała w tym, aby je dopasować do tego, co wprowadził człowiek. Bierzemy więc z liczby szesnastobitowej z rejestru HL dwa najstarsze bity, bo one właśnie odpowiadając liniom adresowym A15,A14 procesora Z80 identyfikują banki pamięci. Rotując w lewo dwa razy mamy z tego dwa najmłodsze bity wartości akumulatora, będą one z zakresu 0..3 czyli zaczynają pasować - toż to indeks komunikatu w tabeli. A skoro adresy komunikatów są dwubajtowe, każdy indeks mnożymy przez 2 (dodając samego do siebie) i tak dostajemy wskaźnik na miejsce, gdzie jest treść dedykowana danej kombinacji bitów wprowadzonego adresu, czyli nazwę (opis) banku.



-- ♠ --

procedura EXPR - $0213 - pobranie 16-bit parametrów na stos, z echem

Procedura EXPR pobiera dwubajtowe parametry, których ilość jest zadana via rejestr C, kolejno wprowadzane przez użytkownika liczby są odkładane na stosie, wierzchołek stosu to ostatnio wprowadzona wartość. Kolejne liczby separujemy klawiszem [.], wprowadzanie kończymy klawiszem [=]. Programik demonstracyjny:
example_EXPR.asm
; example_EXPR.asm
        .in utilities.inc           ; wszelkie przydasie                    
        ;
        ld  SP,$ff66                ; 
.begin  
        ld HL,paramPrompt            ; "PAr=____"
        call PRINT
        .db $80                     ; pokaż po lewo
        ld C,parametersArrayCount   ; tyle parametrów ile elementów tabelki
        call EXPR
        .db $40
        ; pozdejmuj ze stosu i poukładaj w tabeli parametrów
        ; na wierzchołku stosu jest OSTATNIO wprowadzony parametr
        ld B,parametersArrayCount
        ld IX,parametersArrayLast
.saveParams:
        pop HL              
        ld (IX+ADDR_LO),L
        ld (IX+ADDR_HI),H
        dec IX
        dec IX          ; IX := IX-2
        djnz .saveParams
        ;
        ;prezentacja zgromadzonych parametrów
.showLoop:        
        ld B,parametersArrayCount        
.showParams:
        ld      C,$73       ; "P"
        call    COM
        .db     $17         ; po lewo        
        ld      A,parametersArrayCount
        sub     B           ; numer parametru (rosnąco)
        push    AF          ; zachowaj A
        inc     A           ; A+1, aby pokazywać od 1, jak dla ludzi
        call    LBYTE       ; dopisz za "P"
        .db     $25         ; też po lewo
        ; ustal adres parametru na podstawie numeru
        pop     AF          ; mamy stare A z numerem parametru
        ld HL,parametersArray ; i adres początku tabeli               
        add     A           ; A*2
        add     A,L         
        ld      L,A         ; w HL adres parametru
        push    HL          ; dziki transfer
        pop     IX
        LD      L,(IX+ADDR_LO)  ; pozyskaj z pamięci
        LD      H,(IX+ADDR_HI)
        call    LADR            ; pokaż po prawo
        .db     $40                 
        call    delay
        djnz    .showParams         ; while (--B)
        ;
        jp      .showLoop           
        ;        
paramPrompt:
        .db $73,$77,$50,$48,$00,$00,$00,$00,EOM
segment zmiennych programu
; example_EXPR.asm
        ; zmienne
        .sm RAM 
        .or $8000
        >BEGINARRAY parametersArray
        .bs 2
        .bs 2
        .bs 2
        .bs 2
        >ENDARRAY parametersArray,2

No i przy tym programiku, choć prostym pomału wychodzimy z piaskownicy, będzie konkretniej. Zacznijmy od tego, co tak naprawdę powyższy kod realizuje, a mianowicie: zależnie od narzuconej podczas kompilacji ilości parametrów pobiera owe procedurą EXPR i składuje w stosownej lokalizacji w pamięci RAM. Następnie w nieskończonej pętli wyświetla nazwę parametru, w ludzkiej postaci, licząc od 1 oraz jego szesnastobitową wartość. Tyle scenariusza, teraz o technikaliach.
Wspominałam wcześniej o automatycznym wyliczaniu rozmiaru tabeli zmiennych oraz dziwnościach związanych z kolejnością operacji arytmetycznych jaką stosuje SB-Assembler. Aby nie męczyć się tymi detalami - najprościej je jakoś ukryć, w końcu czego oczy nie widzą to i sercu nie żal. Zauważmy zatem pojawienie się na samym początku programu dyrektywy `.in` załączającej do naszej aplikacji plik o wymownej nazwie `utilities.inc`, czyli wszelkie przydasie. Interesujące nas aktualnie fragmenty poniżej:

utilities.inc
; utilities.inc
BEGINARRAY      .MA   arrayName
]1:       
                .EM
                ;
ENDARRAY        .MA   arrayName,itemSize
]1End:       
]1Size          .eq  ]1End-]1
]1Count         .eq  ]1Size/]2
]1Last          .eq  ]1End-]2
                .EM

Powyżej zdefiniowane są dwa makra `>BEGINARRAY` oraz `>ENDARRAY`, które otwierają i zamykają definicję tabeli zmiennych. Rozmiar elementu może być w sumie dowolny, podajemy go w makrze domykającym definicję. Para makr definiuje dodatkowe symbole wypracowane na podstawie nazwy podanej jako parametr i tak: powstaje symbol `xxxSize`, który jest rozmiarem tabeli w bajtach, powstaje symbol `xxxCount`, którego wartość odpowiada ilości elementów tabeli (rozmiar tabeli/rozmiar elementu). Na koniec mała, aczkolwiek przydatna rzecz - symbol `xxxLast` - wskazuje na ostatni element tabeli (nie na pierwszy wolny adres za, ale na ostatni w obrębie tabeli). To przydatne jest gdy chcemy iterować po elementach tabeli od końca, co zaraz z resztą zobaczymy. Tej parki makr możemy używać w dowolnym segmencie pamięci, czy to ROM, czy RAM, bez znaczenia. Takoż bez znaczenia jest sposób alokacji adresów - można korzystać z `.db`, `.dw`. `.bs N`, wszelkie wyliczenia są przez translator dokonywane w drugim obiegu, gdzie następuje finalna, twarda adresacja wszelkich dostępnych etykiet. W naszym programie makr tych użyjemy do konstrukcji tabeli dwubajtowych wartości o nazwie `parametersArray`, lokalizację ustawimy im z daleka od kodu, na przykład od adresu $8000. Konsekwencją naszych makr będzie możliwość skorzystania z symboli `parametersArrayCount` - translator wyliczy 4 elementy tabeli, oraz `parametersArrayLast` - to zostanie ustawione na adres czwartego elementu tabeli.
Po skompletowaniu serii wartości na stosie procedura EXPR zwróci nam sterowanie no i teraz martw się babo, co z tą stertą liczb masz zrobić. Tu przydaje się właśnie sztuka z ***Last, możemy zdejmować ze stosu kolejne wartości i układać w pamięci począwszy od ostatniej lokalizacji. Po zakończeniu pętli (etykieta `saveParams`) mamy tabelkę `parametersArray` ładnie wypełnioną wprowadzonymi liczbami i to w odpowiedniej kolejności. Teraz należałoby je pokazać - to odbywa się w drugiej pętli opatrzonej etykietą `showParams`. Widzimy tu zastosowanie ***Count - w bardzo prosty sposób możemy zainicjować licznik pętli ilością elementów, wszystko jest już wyliczone. Licznik pętli (rejestr B) zmienia wartości w dół, my chcemy pokazywać kolejne nazwy Pxx w górę i to nie od zera (po komputerowemu) ale od jeden, bardziej po ludzku. Stąd właśnie konieczność dodania w locie +1 do wartości wyświetlanej na ekraniku. Zauważmy też, że program sam dostosuje się do zmian rozmiaru tabeli, polecam dla sportu dodać kilka nowych elementów (dyrektywą `.bs 2`) i zaobserwować jak zmieniły się wartości wyliczone przez translator. Oczywiście filmik z działania naszego programu:



-- ♠ --

procedura HILO - $023b - inkrementacja HL i porównanie z DE, iterator

Procedura HILO jest dość specyficznym stworzeniem na tle dotychczas opisywanych, pełni rolę pomocniczą i korzystamy z niej niejako w tle. Do pracy wymaga zainicjowanych dwóch par rejestrów HL oraz DE, z których to HL jest inkrementowany przy każdym wywołaniu procedury. Następnie zależnie od relacji z wartością rejestru DE ustawiana jest flaga CY=1 gdy DE<HL, CY=0 gdy DE≥HL. Fakt, że procedura operuje na wartościach szesnastobitowych doskonale pretenduje ją do wszelkich transferów obszarów pamięci czy wypełniania jej w zadanym zakresie adresów. Programik demonstracyjny to prosta kopiarka do RAM, podajemy adres początku, końca oraz adres docelowy, pod który należy dane przesłać:
example_HILO.asm
; example_HILO.inc
        ld  SP,$ff66                ; 
.begin  
        ld C,3                      ; beginAddr, endAddr, destAddr
        call EXPR
        .db $40
        ; na stosie w kolejnosci dest,end,begin
        pop IX          ; dest
        pop DE          ; end 
        pop HL          ; begin 
.memCopy:
        ld  A,(HL)      
        call    showProgress
        ld  (IX),A
        inc IX          ; dest++
        call    delay                
        call    HILO    ; begin++        
        jp NC,.memCopy
        ;
        call    CLR
        .db     $80
        ;        
        jp  $
        ; pokaz ADDR__NN podczas kopiowania
showProgress:
        push    AF
        push    HL
        push    DE
        call    LBYTE 
        .db     $20   
        call    LADR  
        .db     $44                 
        pop     DE
        pop     HL
        pop     AF
        ret     

W powyższym programie zagadek żadnych już nie ma, zwróćmy tylko uwagę, że pracujemy głównie z rejestrami - wartości adresów, które procedura EXPR chomikowała na stosie zbieramy do rejestrów roboczych, do bezpośredniego wykorzystania. No i jeżeli chcemy jakąś bardziej skomplikowaną pętlę koordynować procedurką HILO (taki niby-odpowiednik rozkazu `djnz`), to należy koniecznie zadbać o ochronę wykorzystywanych przez nią rejestrów HL oraz DE, widzimy to w procedurze `showProgress`, gdzie prologu i epilogu jest więcej niż faktycznego kodu roboczego. Kopiarka w działaniu prezentuje się następująco:



-- ♠ --

Powyższy opis jest ściśle związany z rozdziałem `4.0 Definicje procedur systemowych` tomiku MIK05 dokumentacji CA80 i proszę go potraktować jako nieco bardziej rozbudowany suplement. Chcąc naprawdę świadomie i efektywnie korzystać z procedur systemowych CA80 należy wspomniany rozdział dokładnie przeczytać i z każdą z procedurek samodzielnie poeksperymentować. Bardzo ważne jest zrozumienie filozofii APWYS/PWYS, czyli zmiennych koordynujących pisanie po wyświetlaczu. Proszę też zauważyć, że wszystkie prezentowane tu przykłady dotyczą wywołań systemowych, gdzie parametr PWYS jest przekazywany w formie stałej następującej po rozkazie `call nnnn`, to jest tak zwane (w MIK05) wywołanie z aktualizacją PWYS. Procedury obsługujące magnetofon kasetowy czyli ZMAG, ZEOF i OMAG muszą poczekać na swoją kolej i ... magnetofon.


#slowanawiatr, kwiecień 2019

Natasza Biecek 2004-2019/~, e-mail