Nauka asemblera. Część czwarta.

  Mamy już algorytm, więc czas na program.


Są różne sposoby przekazywania parametrów do funkcji, a w naszym przypadku do procedury czyli podprogramu. Wykorzystano tutaj rejestry procesora, w których umieszczamy adres danych do przesłania (DE), adres pod który należy te dane przesłać (HL) oraz długość bloku danych (BC). Należy zwrócić uwagę na fakt, że analizowany program pracuje w ZX Spectrum, a dane będą odbierane przez CA80 zleceniem *8E. Oczywiście zamiast Sinclair'a możemy użyć inny komputer z procesorem Z80 (albo innym, ale wtedy musimy zmodyfikować program) np. drugi CA80. Czym są dwuliterowe skróty BC, DE i HL? Są to szesnastobitowe rejestry procesora Z80 złożone z par rejestrów ośmiobitowych. Rejestry to wewnętrzna pamięć procesora. Odczyt (i zapis) danych z RAM trwa wiele dłużej niż z rejestrów, poza tym lista rozkazów Z80 zawiera operacje znacznie ułatwiające pracę. Musimy pamiętać, co przechowują rejestry i chronić ich zawartość, żeby program działał poprawnie. 


  • HL - adres ładowania dla CA80,
  • DE - adres danych do przesłania (wskaźnik na tablicę danych jednobajtowych),
  • BC - długość przesyłanego bloku (rozmiar tablicy).


Kiedy parametry są już w rejestrach, wywołujemy procedurę ZAPIS. Do tego celu służy rozkaz CALL, który najpierw zapisuje na stosie (stos to obszar w RAM, w którym procesor przechowuje adresy powrotu z procedur (CALL -> RET itp.) lub zawartości chronionych rejestrów (PUSH -> POP). Wierzchołek stosu (czyli pierwsze wolne miejsce na stosie) jest wskazywany przez rejestr SP - wskaźnik stosu). Stosem zajmiemy się innym razem.

ZX serial setup()
Procedura ZAPIS.


Zaczynamy od wysłania adresu i długości bloku. Procedura ZAP wysyła osiem bitów z rejestru A (A - akumulator, jest to specjalny rejestr procesora, który ma dostęp do ALU - jednostki arytmetyczno-logicznej. Bez niego nie moglibyśmy wykonać żadnych obliczeń), używając rejestr D jako iterator. To znaczy, że przed wywołaniem ZAP musimy gdzieś zapamiętać wartość z rejestru D. Najprościej zapisać DE na stosie rozkazem PUSH DE. Do rejestru A ładujemy zawartość L (LD A,L). Dopiero teraz wywołujemy ZAP rozkazem CALL. ZAP to etykieta, czyli słowne oznaczenie adresu. CALL ZAP moglibyśmy zapisać jako CALL 0FF95H lub CALL 0xff95. Kod wynikowy otrzymalibyśmy identyczny, ale użycie etykiet poprawia czytelność programu źródłowego i ułatwia dodawanie rozkazów, lub inne modyfikacje. Nie musimy zmieniać adresów za każdym razem, kiedy zmieni się długość naszego programu. ZAP to właściwie pętla for(i = 8; i > 0; i--) (iteratorem jest D). W pętli wysyłamy wartości kolejnych bitów akumulatora, które przesuwamy do flagi CF (flagi znajdują się w rejestrze stanu procesora - F) rozkazem RRCA. W praktyce przesunięcie liczby o jeden bit w prawo, jest równoważne dzieleniu przez dwa. Reszta z dzielenia ustawia lub zeruje CF. Flagi służą nam do warunkowego wykonania fragmentów programu (tak jak if() w C). JR C,ZAPJED to skok względny (relatywny) warunkowy, który umożliwia zmianę PC (licznika programu) o wartość z przedziału <-128, +127>. Nie musimy jej ręcznie obliczać, wystarczy podać etykietę, a asembler policzy za nas. Słownie: jeżeli CF = 1 skocz do ZAPJED. Jeżeli warunek nie jest spełniony, wysyłamy zero. OUT (RESD),A używamy tutaj w sposób nietypowy, bo wartość rejestru A jest nieistotna. Dekoder adresów wystawiając stan niski na wyjściu RESD, zeruje tylko przerzutnik D w układzie SN7474. Żadne urządzenie wejścia-wyjścia nie jest ustawiane do odczytu szyny danych. Wysłanie jedynki różni się tylko adresem w rozkazie OUT. Tym razem stan niski pojawi się na SETD, czyli przerzutnik D zostanie ustawiony.

ZX - CA80 CLK
Procedura CLK.

 

        Aby dokończyć przesłanie bitu, musimy wygenerować zbocze narastające sygnału zegarowego, więc wywołujemy procedurę CLK. Na stosie mamy już DE, adres powrotu z ZAP i adres powrotu z CLK. Znowu rozkazem OUT (RESS),A zerujemy przerzutnik D, ale nie ten sam (7474 ma dwa przerzutniki - jeden służy nam jako nadajnik danych, a drugi wysyła sygnał synchronizujący). Zlecenie *8E sprawdza stan linii CLK (STROB) oczekując na zmianę ze stanu niskiego na wysoki. Żeby operacja się powiodła, musimy zaczekać na gotowość odbiornika. Pamiętajmy, że w CA80 z częstotliwością 500 Hz są wywoływane NMI. Gdybyśmy zbyt wcześnie wysłali kolejny bit, mogłoby dojść do przekłamania. "Doświadczalnie stwierdzono", że wystarcza 50 pętli procedury DELAY.

Software delay.
Procedura DELAY.

 

        Tym razem musimy zadbać o akumulator, ponieważ zawiera on jeszcze nieprzesłane bity, a teraz posłuży jako licznik pętli. PUSH AF zapisuje na stosie parę AF czyli akumulator i flagi. Na stosie przybył adres powrotu z DELAY i AF czyli mamy już pięć warstw. Wykonujemy pustą pętlę for(A = 0x32; A > 0; A--).  Dekrementację akumulatora robi DEC A.  Po wyzerowaniu iteratora JR NZ,DEL się nie wykona (NZ - nie zero, to warunek skoku. Flaga Z jest ustawiana, kiedy wynik operacji jest zerowy). Pozostało zdjąć ze stosu AF (POP AF) i wrócić z DELAY (RET). Teraz ustawiamy CLK rozkazem OUT (SETS),A i znowu czekamy w DELAY. Wracamy do ZAP rozkazem RET, żeby przesłać kolejny bit. Tyle pracy, a wysłaliśmy dopiero pierwszy bit! Ale prześledziliśmy już wszystkie podprogramy, więc dalej pójdzie już gładko. :-) Po przesłaniu każdego bitu zmniejszamy D i sprawdzamy, czy już się wyzerował. Jeżeli tak, wracamy z ZAP. Wysłaliśmy już rejestr L - młodszą część adresu, więc teraz do A ładujemy H i wywołujemy ZAP. To samo musimy zrobić z BC, czyli LD A,C; ZAP; LD A,B; ZAP. Przesłaliśmy już cztery bajty!!!

ZX - CA80 serial
Procedura ZAP.


Uwaga, teraz mała zmyłka! Na stosie zapisaliśmy DE, a teraz ze stosu zdejmiemy HL... To nie jest błąd. Musi zgadzać nam się liczba zapisów i odczytów czyli operacji na stosie, ale nie musimy sztywno trzymać się źródeł danych. Czasem wygodnie jest przekazać dane między rejestrami przez stos. Od teraz wskaźnikiem danych do przesłania będzie HL.

ZX serial
Pętla główna.


Wykonujemy kolejną pętlę for(; BC>0; BC--) (jak widać najczęściej iterator dekrementujemy, bo najłatwiej jest wykryć jego wyzerowanie), w której wysyłamy kolejne bajty z naszej tablicy. Do akumulatora ładujemy zawartość komórki pamięci której adres jest w HL (LD A,(HL) ). Umieszczenie jakiejś wartości w nawiasach, oznacza że chodzi nam o adres. Gdybyśmy zapomnieli dodać nawias, asembler zgłosiłby błąd, ponieważ HL to para rejestrów - 16 bitów, a akumulator mieści tylko jeden bajt 8 bitów, czyli nie znalazłby odpowiedniego rozkazu. Znowu "wołamy" ZAP, po czym inkrementujemy HL (INC HL), dekrementujemy BC (DEC BC) i sprawdzamy, czy wszystko już przesłaliśmy. W tym celu wykorzystujemy sumę bitową. Do akumulatora ładujemy C i rozkazem OR B sprawdzamy, czy wszystkie bity są już zerami. Jeżeli nie, skaczemy do NAST (JR NZ,NAST). Po przesłaniu wszystkich bajtów wracamy do programu wywołującego RET. SP powinien mieć taką samą wartość, jak na początku.


Mam nadzieję, że teraz programowanie w asemblerze nie jest dla Ciebie "czarną magią".

Komentarze

Popularne posty z tego bloga

Uruchamiamy CA80 na RCbus.

Magnetofon CA80.