Programowanie 6502 (10-12)


      Już po raz czwarty, mam przyjemność zaprosić czytelników do lektury kursu programowania procesora 6502, poprowadzonego już dosyć dawno temu, przez jednego z lepszych programistów Atari Janusza Bohdana Wiśniewskiego. Wszystkich czytelników, którzy z kursem stykają się po raz pierwszy, chcę poinformować, że kurs ten był już raz publikowany na łamach czasopisma "Tajemnice ATARI". Korzystając z tego, że jestem w telewizji, chciałbym dzisiejszy odcinek kursu, zwłaszcza w części dotyczącej programu Eol Eater zadedykować koledze Pigule z grupy Shapoon. Na koniec zostawiłem sobie przykrą wiadomość. Dzisiejszy odcinek jest już odcinkiem przedostatnim....

      Chciałbym jednak zwrócić uwagę wszystkich zawiedzionych na fakt, że redakcja "Tajemnic ATARI" cały ten materiał publikowała w piętnastu wydaniach swojego magazynu. My doceniając Waszą niecierpliwość, a także mniejszą częstotliwość ukazywania się naszego maga robimy to rozkładając cały materiał na pięć odcinków.

      Nie zwlekając już dłużej, zapraszam wszystkich do lektury tego naprawdę ciekawego materiału.

Zbycho Jabol/DIAL

TA Copy

      Kolejny przykład programu z klocków: uniwersalny duplikator plików. Znaczna jego część pochodzi z programu TA Poker, którym zajmowaliśmy się w poprzednim odcinku.

 * TA COPY          autor:JBW
 * (c) 1992 Tajemnice ATARI

         opt %100101

 *---procedury w ROM

 afp      equ $D800
 fpi      equ $D9D2
 ciov     equ $E456

 *---rejestry pakietu FP

 fr0      equ $D4
 cix      equ $F2
 inbuff   equ $F3

 *---system

 runadr   equ $2E0
 initad   equ $2E2
 dosrun   equ $A
 dosini   equ $C
 iocb     equ $340

 io_com   equ iocb+2
 io_sta   equ iocb+3
 io_adr   equ iocb+4 (2)
 io_len   equ iocb+8 (2)
 io_mod   equ iocb+10
 io_aux   equ iocb+11

Widziałem w życiu dużo kopierów, lecz niewiele z nich można nazwać przyjaznymi. Często dla osiągnięcia jak największej pojemności bufora programy te niszczą wszystko co oprócz nich żyło w komputerze. Nasz program będzie krótkim podręcznym kopierem wykorzystującym pamięć pomiędzy MEMLO i MEMHI, nie kolidującym więc z innymi programami (systemem operacyjnym, nakładkami, itp).
 memhi    equ $2E5
 memlo    equ $2E7
 driv     equ $301
 skctl    equ $D20F

 *---stale

 chn0     equ $00
 chn1     equ $10
 gett     equ 5
 putt     equ 9
 getb     equ 7
 putb     equ 11
 eol      equ 155
 eof      equ 136
 shift    equ %00001000

Zestaw przydatnych definicji poszerzył się o kilka nowych. Na stronie zerowej obok roboczych komórek BYTE, ADDR, WORD wyrosły nowe ważne dla kopiera zmienne globalne:
 *---strona zerowa

 byta     equ $CD
 addr     equ $CC
 word     equ $CE

 used     equ $D0
 size     equ $D2

Opisują one aktualny stan kopiera, powinny być wyraźnie wyodrębnione i sugestywnie nazwane, by nie przyszła nam ochota użyć ich do jakichś innych celów. ESED będzie zawierać rozmiar aktualnie wczytanego pliku,zaś SIZE wielkość dostępnej pamięci.
 *---numery komunikatów

 nul_m    equ 0
 tit_m    equ 1
 get_m    equ 2
 put_m    equ 3
 err_m    equ 4
 mem_m    equ 5
 sta_m    equ 6

Dużym zmianom ulegną komunikaty ( kopier jest bardziej rozmowny od Pokera).Oprócz nagłówka i odstępu potrzebne są: wyświetlenie stanu, zachęta do wczytania pliku, zachęta do zapisania, komunikat o błędzie we/wy i o przepełnieniu pamięci.

PLAN

      Planowanie zasad działania programu można utożsamić ze stworzeniem jego pętli głównej, która rozdziela zadania pomiędzy poszczególne procedury.

          org $8000

 main     jsr init
 * główna pętla
 loop     jsr close
 * wypisz status
          jsr dsp_stat
 * pobierz nazwę pliku
          ldx #get_m odczyt
          lda used
          ora used+1
          beq *+3
          inx        zapis
          jsr get_text
          bmi loop
 * nazwa pusta?
          dec io_len,x
          bne io
 * zmiana trybu lub koniec
          lda used
          ora used+1
          beq quit
          lda #0
          sta used
          sta used+1
          beq loop (jmp)
 * zapis czy odczyt?
 io       lda used
          ora used+1
          beq rd
 * zapis
          jsr write
          jmp loop
 * odczyt
 rd       jsr read
          jmp loop
 * powrót do DOS-u
 quit     jmp (dosrun)

Pętla zaczyna się od wywołania procedury close, zamykającej kanał nr1. Jest to wygodny sposób zabezpieczenia się przed roztargnionym użytkownikiem tego kanału. DSP_STAT informuje klienta o aktualnym stanie kopiera. Tuż za nim następuje pytanie o nazwę pliku. Tekst pytania zależy od wartości słowa USED. Zero oznacza pusty bufor zatem prosimy o plik do odczytu. Jeżeli natomiast ORA da niezerowy wynik, to znaczy, że bufor coś zawiera, pytamy więc o plik do zapisu. Aby uprościć maksymalnie sposób obsługi kopiera, przyjąłem wprowadzenie pustego wiersza jako jedyny rozkaz sterujący. W trybie zapisu oznacza on polecenie przejścia do trybu odczytu (i zarazem wyczyszczenie bufora), natomiast w trybie odczytu powoduje zakończenie pracy i przejście do programu nadrzędnego. Przełączenie z trybu odczytu na zapis odbywa się samoczynnie po poprawnym odczytaniu pliku. Ten "pomysł" został wymuszony przez specyfikę standartowego wejścia ATARI (urządzenie E:) dostępnego przez kanał 0. Oddaje ono wprowadzony tekst dopiero po zatwierdzeniu go klawiszem RETURN. Najkrótszy zatem tekst to właśnie sam RETURN! Wiele kopierów stosuje tu rozkazy literowe np. Q. Wtedy oczywiście trudno jest wczytać i zapisać plik o nazwie Q. Procedury READ i WRITE odpowiadają za odczyt i zapis pliku. Główna pętla wywoła jedną z nich w zależności od stanu słowa USED. Klocki wyświetlania i wprowadzania tekstów nie ulegają żadnym zmianom:
 *---wypisz tekst
 dsp_msg  equ *
 * odszukaj tekst nr. X
          ldy #0
 fm0      dex
          bmi mout
 fmes     lda data,y
          iny
          cmp #eol
          bne fmes
          beq fm0 (jmp)
 * wypisz
 mout     txa
          ldx #chn0
          sta io_len,x
          clc
          tya
          adc dtaa
          sta io_adr,x
          lda #0
          sta io_len+1,x
          adc dtaa+1
          sta io_adr+1,x
          lda #putt
          sta io_com,x
          jmp ciov

 *---pobierz tekst

 get_text jsr dsp_msg
          ldx #chn0
          lda #gett
          sta io_com,x
          lda txta
          sta io_adr,x
          lda txta+1
          sta io_adr+1,x
          sta io_len+1,x
          jmp ciov

Przybędzie natomiast procedura do wypisywania liczb w formacie szesnastkowym. Jej ważną cechą jest bardzo duża szybkość działania. Oczywiście w tym zastosowaniu predkość nie gra większej roli więc kto woli ten może użyć procedur z ROM i wyświetlać liczby dziesiętnie.
 *---wypisywanie liczb

 pwor     jsr phex
          txa
 phex     pha
          jsr pxdig
          pla
          lsr @
          lsr @
          lsr @
          lsr @
 pxdig    and #%00001111
          ora #'0'
          cmp #'9'+1
          bcc *+4
          adc #6
          sta stat,y
          dey
          rts

Jest to w zasadzie zespół trzech procedur: PXDIG wyświetla pojedynczą cyfrę szesnastkową, PHEX wyświetla bajt (dwie cyfry), a PWOR słowo ( cztery cyfry ).

Zastosowana tu została prosta "sztuczka". PHEX wywołuje dwukrotnie procedurę PXDIG. Po pierwszym wywołaniu przez JSR następuje przesunięcie zawartości bajtu o cztery bity, by starsza połówka znalazła się na miejscu młodszej. Tu powinna nastąpić sekwencja JSR PXDIG, RTS, lecz taki sam skutek da przecież prostrze JMP PXDIG (RTS w PXDIG znajdzie na stosie adres powrotu w miejsce wywołania PHEX). Z koleji jednak rozkaz JMP do następnej instrukcji można bez szkody pominąć. W ten sposób procedura PWOR dwakroć wywoła procedurę PHEX. Liczby nie są wyświetlane na ekranie, lecz przygotowywane w obszarze komunikatów. Wyświetla je następny klocek:

 *--- wypisz status

 dsp_stat equ *
 * wykorzystywane
          lda used
          ldx used+1
          ldy <use_+3
          jsr pwor
 * rozmiar bufora
          sec
          lda memhi
          sbc bufa
          sta size
          lda memhi+1
          sbc bufa+1
          sta size+1
          tax
          lda size
          ldy <siz_+3
          jsr pwor
 * wypisz
          ldx #nul_m pusty
          jsr dsp_msg
          ldx #sta_m status
          jmp dsp_msg

Wykorzystanie procedury PWOR polega na ustawieniu rejestru indeksowego Y na pozycję ostatniej cyfry, w rejestrach A i X przekazuje się słowo do wypisania. W licznych programach zwykle bardziej skomplikowanych od naszego kopiera operacje wejścia/wyjścia wywoływane są z wielu podprogramów. Ponieważ po każdym takim wywołaniu trzeba zbadać kod zakończenia operacji (rejestr Y) wygodnie jest stworzyć procedurę inicjującą operację we/wy połączoną z reakcją na błąd.
 *--- CIO z ew.komunikatem

 mcio     jsr ciov
          bpl ciok
          cpy #136
          beq iook
 error    ldx #err_m
 derr     jsr dsp_msg
          ldy #255
          rts
 ciok     ldx #mem_m
          lda used
          ora used+1
          beq derr
 iook     ldy #1
          rts

Nie jest to całkiem trywialne ponieważ tylko w przypadku zapisywania pliku pożądane jest pozytywne zakończenie operacji ! W przypadku odczytu bowiem kopier nie znając z góry wielkości pliku żąda wypełnienia całego bufora. Jeżeli więc ta operacja się powiedzie można z dużym prawdopodobieństwem przyjąć, że plik jest większy. Dlatego procedura MCIO melduje w takim przypadku niedobór pamięci. Poprawne zakończenie odczytu to błąd nr.136 (napotkany koniec pliku). Świadczy on o tym, że wszystko zostało przeczytane. Dla uproszczenia kontroli poprawności w procedurach, które wywołują MCIO modyfikowany jest kod w rejestrze Y: teraz już naprawdę ujemna wartość oznacza niepowodzenie operacji.
 *--- zamknij kanał

 close    ldx #chn1
          lda #12
          sta io_com,x
          jsr ciov
          lda #3
          sta skctl cicho!
          tya
          bmi error
          rts

Procedura zamykająca kanał wycisza przy okazji niemiły pisk słyszalny wskutek błędu w systemie ATARI.
 *--- otwórz kanał

 open     ldx #chn1
          sta io_mod,x
          lda #3
          sta io_com,x
 * szukaj dwukropka
          ldy #':'
          cpy text+1
          beq seti
          cpy text+2
          beq seti
          lda #0
 * ustaw iocb
 seti     clc
          adc dnma
          sta io_adr,x
          lda #0
          adc dnma+1
          sta io_adr+1,x
          lda skctl
          and #shift
          asl @
          asl @
          asl @
          asl @
          sta io_aux,x
          jsr ciov
          bmi error
 * przygotuj na potem
          lda io_mod,x
          ora #3
          sta io_com,x
          lda bufa
          sta io_adr,x
          lda bufa+1
          sta io_adr+1,x
          tya
          rts

Otwarcie kanału to bodaj najbardziej skomplikowany klocek. Ponieważ procedura jest ta sama dla zapisu i odczytu w akumulatorze przekazuje się żądany tryb pracy kanału 4 lub 8. Procedura sprawdza obecność znaku ":" w nazwie pliku jeżeli go brak to dołączana jest z przodu domyślna nazwa urządzenia. Jeżeli użytkownik trzyma wciśniety klawisz SHIFT to w IO_AUX umieszczone jest 0 (długie przerwy dla urządzenia C:) w przeciwnym razie 128. Jeśli otwarcie powiedzie się, to można przygotować jeszcze kod rozkazu dla przyszłych odczytów lub zapisów (7 lub 11) oraz adres bufora.

Uwaga: ponieważ OPEN i CLOSE korzystają z części ERROR procedury MCIO należy te klocki traktować jako niepodzielną grupę. Z uwagi na skoki względne pożądane jest bliskie ich sąsiectwo.

 *--- wczytaj plik

 read     lda #4
          jsr open
          bmi rret
          lda size
          sta io_len,x
          lda size+1
          sta io_len+1,x
          jsr mcio
          bmi rret
          lda io_len,x
          sta used
          lda io_len+1,x
          sta used+1
 rret     rts

Wczytanie pliku rozpoczyna się ustawieniem żądanego rozmiaru danych (SIZE - cały bufor), a kończy się zarejestrowaniem faktycznej długości pliku zwracanej przez system w IO_LEN. Zapis to już czcza formalność i nie wymaga chyba komentarza.
 *--- zapisz plik

 write    lda #8
          jsr open
          bmi wret
          lda used
          sta io_len,x
          lda used+1
          sta io_len+1,x
          jsr mcio
 wret     rts

Pozostaje jeszcze tylko do napisania procedura INIT, która w sprytny sposób wydedukuje numer stacji dysków z której wczytujemy nasz kopier (jeśli to nie stacja to nic nie szkodzi) wyzeruje słowo USED i wyświetli nagłówek programu.
 *--- ustawienie początkowe

 init     lda #'0'
          ora driv
          sta dnam+1
          lda #0
          sta used
          sta used+1
          ldx tit_m tytul
          jmp dsp_msg

 *--- koniec programu

          brk

Niezbedne dane
 *--- dane adresowe

 txta     dta a(text)
 dtaa     dta a(data)
 bufa     dta a(buff)
 dnma     dta a(dnam)
          dta a(0)

 *--- dane

 data     dta b(eol)
          dta c' TA COPY 1.0 '*
          dta b(eol)
          dta c'Source:',b(eol)
          dta c'Target:',b(eol)
          dta c'I/O error!'
          dta b(eol)
          dta c'Out of memory!'
          dta b(eol)
 stat     dta c'Used $'
 use_     equ *-stat
          dta c'.... bytes of $'
 siz_     equ *-stat
          dta c'....',b(eol)

 dnam     dta c'D0:'
 text     org *+120
 buff     equ *

Leniwi mogą tu zakończyć pracę:
          org runadr
          dta a(main)
          end

Warto tylko zmienić adres w rozkazie org by poszerzyć bufor. Ambitnym proponuję połączyć kopier z RELOCATOR'em. Trzeba w nim zadeklarowac STAR_ jako main zas USER_ jako rret.

Klocki raz jeszcze

      W ostatnim już odcinku na temat modularnego programowania pokażę dwa pożyteczne programy, które przy minimalnym wysiłku można uzyskać z klocków przedstawionych do tej pory.

      Oba programy należą do grupy tzw. transkoderów, czyli programów przekształcających dane z jakieją postaci w drugą. Styl pracy takiego kodera do złudzenia przypomina zwykły kopier. Różnica polega na zamianie danych w pewien sekretny sposób. Ponieważ w tym przypadku pamięć nie jest z gumy stosujemy w przypadku transkoderów zasadę oszczędności miejsca: jeżeli komputer zmniejsza rozmiar danych to, należy ją przeprowadzać podczas odczytu (więcej się zmieści) w przeciwnym razie kodujemy dane podczas wysyłania ich do pliku wyjściowego żeby nie rosły w pamięci. Oczywiście modyfikacja danych wymaga czasu, takie działanie podczas transmisji powoduje zwolnienie odczytu lub zapisu, jeśli więc dane nie zmieniają objętości lub zmieniają ją nieznacznie, to można dokonać przekodowania całości w pamięci pomiędzy operacjami odczytu i zapisu jak to robi poniższy program.

EOL EATER

      Ten miły program służy do wycinania z plików wszystkich znaków o kodzie 155, stosowanych w systemie ATARI dla oznaczenia końca wiersza. Podstawowym zadaniem Eatera jest kompresja danych do gry The Jet. Plansze do gry tworzy się dowolnym edytorem, lecz powstające przy tym końce linii są dla gry niepotrzebnym (choć nieszkodliwym) balastem. Po przepuszczeniu planszy The Jet przez Eol Eatera staje się ona krótsza o tyle bajtów, ile liczyła wierszy.

 * TA EOL EATER
 * autor: JBW @ De Jet
 * (c) 1992 Tajemnice ATARI

Początek podobny do TA Copy, nie? W rzeczywistości różnią się w niewielu szczegółach. Najlepiej więc wziąść tekst kopiera, wywalić z niego klocek read a zamiast niego wstawić:
 *---wczytaj plik

 read     lda #4
          jsr open
          bmi rret
          lda size
          sta io_len,x
          lda size+1
          sta io_len+1,x
          jsr mcio
          bmi rret

Na razie tak samo, lecz przytaczam dla porządku:
 * ustaw adresy

          clc
          lda bufa
          sta addr
          sta word
          adc io_len,x
          sta used
          lda bufa+1
          sta addr+1
          sta word+1
          adc io_len+1,x
          sta used+1

Tym razem słowo USED przechowuje chwilowo adres końca wczytanego pliku, aby było wiadomo gdzie skończyć. Do przebiegania po danych wejściowych służy słowo WORD. Zaś ADDR wskazuje miejsce w buforze gdzie wpisywane są bajty rezultatu. Ponieważ ten drugi wskaźnik nigdy nie prześcignie pierwszego,można bez obawy wykorzystać wspólny bufor.
 eat_loop equ *
          ldy #0
          lda (word),y
          cmp #eol
          beq eat_eol
          sta (addr),y
          inc addr
          bne *+4
          inc addr+1
 eat_eol  equ *

Metoda konwersji jest prosta: jeżeli wykryjemy EOL, to wystarczy po prostu nie przepisywac tego bajtu (nie zmienia się adres docelowy ADDR). Zawsze natomiast zwiększa się adres źródłowy WORD. Porównanie go ze strażnikiem USED pozwala na zakończenie procesu we właściwym miejscu.
          inc word
          bne *+4
          inc word+1
          lda word
          cmp used
          lda word+1
          sbc used+1
          bcc eat_loop

Pozostaje obliczyć nową długość pliku:
          sec
          lda addr
          sbc bufa
          sta used
          lda addr+1
          sbc bufa+1
          sta used+1
 rret     rts

I kosmetyczna poprawka w danych by wyświetlała się prawidłowa wineta programu:
 *---dane

 data     dta b(eol)
          dta c' TA Eol Eater '*
          dta b(eol)

zamiast tytułu "TA Copy" (reszta bez zmian).

Autorem procedury wycinającej Eole jest Dariusz Żołna.

Dygresja: posiadacze "Panthera" mogą rzecz całą wykonać dużo prościej. Wystarczy zdefiniować tabelę konwersji z jednym wpisem: #155=  co się czyta "zmień wszystkie EOL na nic" aktywizować ją, a następnie "wydrukować" ułożoną planszę na plik dyskowy lub kasetowy opcją File/Print.

HEX DATA Coder

      Narzędzie to służy do zamiany programów maszynowych na wiersze DATA tak, by mogły być łatwo prezentowane na łamach czasopism. Każdy bajt pliku wejściowego zostanie zamieniony na odpowiadającą mu parę cyfr szesnaskowych. Te pary zgrupowane są po 13 w wierszu, a każdy wiersz zaczyna się od numeru i słowa DATA. Pierwszy wiersz zawiera pusty rozkaz REM (jest to minimum niezbędne dla ZGRYWUSA+) i ma numer 1000. Pozostałe wiersze numerowane są z krokiem 10.

 * TA Hex DATA   autor: JBW
 * (c) 1992 Tajemnice ATARI

Ponieważ taka konwersja danych powoduje ponad dwukrotne zwiększenie ich objetości, nie wykonuję jej w pamięci lecz dopiero na żywo podczas zapisu pliku docelowego. Znów punktem wyjścia będzie TA Copy, lecz tym razem pozostawimy procedurę read bez zmian, wymienimy natomiast klocek write.
 *---zapisz plik

 write    lda #8
          jsr open
          bmi wret

Pierwszą czynnością, po pozytywnym otwarciu pliku będzie zapisanie wiersza z rozkazem REM:
 * zapisz REM

          lda d0_a
          sta io_adr,x
          lda d0_a+1
          sta io_adr+1,x
          lda d0_l
          sta io_len+1,x
          jsr ciov
          bmi wret

Podczas zapisywania pliku fragmentami, wystarczy modyfikować w bloku IOCB tylko adres i długość danych, pozostałe bowiem parametry system pozostawia nienaruszone. Również wartość rejestru X nie ulega zmianie wewnątrz procedury CIOV. Dane szesnastkowe gromadzi się w buforze długości jednego wiersza. Trzeba na wstępie ustalić jego numer początkowy. Najprościej przenieść go z wiersza REM. Nie można zadeklarować numeru startowego po prostu jako danych w buforze, ponieważ przy powtórnym zapisie pliku numer musi wrócić do swojej pierwotnej wartości.
 * ustaw numer poczatkowy

          ldy #3
 setn     lda d0,y
          sta d1,y
          dey
          bpl setn

Przygotowanie do konwersji polega na ustawieniu komórki BYTE, która będzie używana do wskazywania aktualnego miejsca w wierszu DATA, gdzie wpisywane są dane. Ponieważ wykorzystana jest sprytnie procedura PHEX (wypisuje ona cyfry w napisie informacyjnym STAT), indeks jest obliczany względem początku tego napisu. Słowo WORD będzie służyć jako strażnik końca dla adresu ADDR, którym przebiegać będziemy po buforze.
 * prolog konwersji

          lda <dat_
          sta byte
          clc
          lda bufa
          sta addr
          adc used
          sta word
          lda bufa+1
          sta addr+1
          adc used+1
          sta word+1
          lda d1_a
          sta io_adr,x
          lda d1_a+1
          sta io_adr+1,x

Główna pętla konwersji pobiera kolejne bajty i wysyła je na wyjście za pomocą procedury BOUT. Operacje przerywa się (ważne!) gdy BOUT zamelduje błąd.
 * petla konwersji

 wri_     lda addr
          cmp word
          lda addr+1
          sbc word+1
          bcs wend     koniec?
          ldy #0
          lda (addr),y
          inc addr
          bne *+4
          inc addr+1
          jsr bout     wypisz
          bpl wri_     gdy ok.
 wret     rts

Procedura BOUT nie wysyła przez CIOV każdego bajtu z osobna, lecz dopiero cały kompletny wiersz. Dlatego zakończenie konwersji wymaga na ogół wysłania ostatniego niepełnego wiersza:
 wend     lda byte
          cmp <dat_+1
          jmp xclo

Rozkaz CMP, tuż przed skokiem, wymusza odpowiednie zachowanie programu w miejscu XCLO. Rozkaz JMP nie zmienia przecież znaczników. Zapobiega to wysłaniu pustego wiersza, jeśli zgromadzono w nim dopiero 0 bajtów. Sztuczka z zastosowaniem "<" zamiast "#" pozwala odwołać się do wartości, która nie jest jeszcze w tym miejscu znana asemblerowi.

BOUT stanowi typowy przykład techniki znanej jako przesyłanie z buforowaniem. Polega ono na gromadzeniu danych w wydzielonym miejscu pamięci zwanym buforem, a faktycznym przesłaniu ich do urządzenia wyjściowego porcjami o wielkości zależnej od pojemności bufora. Takie rozwiązanie w istotny sposób przyspiesza i usprawnia operacje we/wy Oczywiście typowe urządzenia zewnetrzne wyposażone są w swe własne bufory, mamy tu więc do czynienia z buforowaniem wielopoziomowym. Dopóki zatem wypisywanie cyfr nie dojdzie do końca bufora (strażnikiem jest etykieta REM_) nie ma potrzeby wywoływać CIOV.

 * zapisz bajt

 bout     inc byte
          ldy byte
          inc byte
          jsr phex
          lda byte
          cmp <rem-1
 xclo     bcc wrok

Wypełnił się bufor! Trzeba zwiększyć numer wiersza. Drobne oszustwo polega na zwiększeniu o 1 trzycyfrowego licznika. Czwarta cyfra 0 jest tylko atrapą i nigdy nie ulegnie zmianie.
 * kolejny numer wiersza

          ldy #2
 advn     clc
          lda #1
          adc d1,y
          sta d1,y
          cmp #'9'+1
          bcc pdln
          lda #'0'
          sta d1,y
          dey
          bpl advn
          jmp quit     za wiele!

999 wierszy danych umożliwia zakodowanie programu o długości ponad 12kB. Kto uważa, że to mało może zmodyfikować program wprowadzając dodatkową cyfrę lub zagospodarowując wspomnianą wyżej atrapę. We fragmencie wywołującym CIOV musimy pamiętać, że nie zawsze wiersz jest pełny, trzeba więc obliczyć jego aktualną długość.
 * zapisz wiersz

 pdln     sec
          lda byte
          sbc <d1-stat
          sta io_len,x
          jsr ciov
          bmi wret
          lda #0
          sta io_len,x
          lda #eol
          jsr ciov
          bmi wret
          lda <dat_
          sta byte
 wrok     ldy #1
          rts

Podanie w io_len zerowej długości danych spowoduje przesłanie pojedyńczego znaku, który znajduje się w akumulatorze. Dane adresowe należy uzupełnić o:
 d0_a     dta a(d0)
 d1_a     dta a(d1)

Umieszczamy je oczywiście przed
         dta a(0)

Blok danych tekstowych wymaga kilku zmian. Na wszelki wypadek przytoczę go w całości by było jasne, co powielić a co poprawić.
 *-dane

 data     equ *
          dta b(eol)
          dta c' TA Hex DATA '*
          dta b(eol)
          dta c'Source:',b(eol)
          dta c'Target:',b(eol)
          dta c'I/O error!'
          dta b(eol)
          dta c'Out of memory!'
          dta b(eol)
 stat     dta c'Used $'
 use_     equ *-stat
          dta c'.... bytes of $'
 siz_     equ *-stat
          dta c'....',b(eol)

 d1       dta c'.... DATA '
 dat_     equ *-stat
          org *+26
 d1_l     equ *-d1

 rem_     equ *-stat
 d0       dta c'1000 REM',b(eol)
 d0_l     equ *-d0


 dnam     dta c'D0:'
 text     org *+120

 buff     equ *

I to wszystko. Każdy z trzech omówionych programów (copy, eol eater, hex data) można bez trudu połączyć z relokatorem, który zapewnia optymalne wykorzystanie pamięci komputera. Poniżej jest ulepszona wersja relokatora umożliwiająca powtórne uruchomienie programu po powrocie do DOS'u rozkazem RUN. Wykorzystane zostało słowo INITAD ($2e2) dzięki czemu działalność relokatora ma miejsce przed faktycznym uruchomieniem programu. Relokator oblicza adres pod którym znajdzie się program i wpisuje go do RUNAD ($2e0).To sprawia, że DOS na ogół samoczynnie uruchomi nasz program od tego adresu. Również ponowne wskoczenie do programu przez RUN odbędzie się z wykorzystaniem tego adresu.
 main__   equ main
 user__   equ rret

 *-------------------------*
 *    Relocator 1.1        *
 *       by JBW            *
 *     1992-02-10          *
 *-------------------------*

 *--- page 0 ---

 byte__   equ $ce
 datf__   equ $cf
 dist__   equ $d0 (2)
 srce__   equ $d2 (2)
 dest__   equ $d4 (2)
 addr__   equ $d6 (2)

 *--- system ---

 runa__   equ $2e0 (2)
 inia__   equ $2e2 (2)
 melo__   equ $2e7 (2)

 *--- move ---

 move__   equ *
          jsr user__
 * clear data flag
          lda #0
          sta datf__
 * destination
          lda melo__
          sta dest__
          sta runa__
          lda melo__+1
          sta dest__+1
          sta runa__+1
 * code source, distance
          sec
          lda <main__
          sta srce__
          sbc dest__
          sta dist__
          lda >main__
          sta srce__+1
          sbc dest__+1
          sta dist__+1
 *** move process ***
          ldy #0
          beq movl__ (JMP)
 seda__   sec
          ror datf__
 movl__   equ *
          lda srce__
          cmp <move__
          lda srce__+1
          sbc >move__
          bcc dchk__
 * done  !
          rts
 * data flag check
 dchk__   bit datf__
          bvs mov1__
          bmi tpe3__
 inst__   equ *
          lda (srce__),y
          sta byte__
          sta (dest__),y
          jsr inca__
          tax
          beq seda__
 * instr type check
          cmp #$20 jsr
          beq tpe3__
          cmp #$40 rti
          beq movl__
          cmp #$60 rts
          beq movl__
          and #$0d
          cmp #$08 x8,xA
          beq movl__
          bcc mov1__
 * 3-byte instruction
 tpe3__   equ *
          lda (srce__),y
          iny
          cmp <main__
          lda (srce__),y
          dey
          sbc >main__
          bcc mov2__
          lda (srce__),y
          iny
          cmp <move__+1
          lda (srce__),y
          dey
          sbc >move__+1
          bcs mov2__
 * alter abs adresses
          lda dist__
          ldx dist__+1
          bcc mova__
 * move w/o changes
 mov2__   bit datf__
          bmi seda__
          lda #0
          tax
 * move 2b address
 mova__   equ *
          sta addr__
          stx addr__+1
          sec
          lda (srce__),y
          sbc addr__
          sta (dest__),y
          jsr inca__
          lda (srce__),y
          sbc addr__+1
          jmp sd__
 * move 1b data
 mov1__   equ *
          lda (srce__),y
 sd__     sta (dest__),y
          jsr inca__
          jmp movl__

 *--- inc srce,dest ---

 inca__   inc srce__
          bne *+4
          inc srce__+1
          inc dest__
          bne *+4
          inc dest__+1
          rts

 *--- start ---

          org inia__
          dta a(move__)

          end

Kto rezygnuje z użycia Relocatora może obniżyć adres podany w początkowym org by uzyskać nieco więcej pamięci dla programu.

  

      W tym odcinku kursu, jego autor JBW pozwolił sobie na pewnien "luz". Ja aby zachować wierność autorowi opublikuję i ten odcinek, jednak i ja wzorem JBW pozwolę sobie na uwypuklenie pewnych faktów. Otóż materiały publikowane w magazynach dyskowych są raczej czytane dosyć pobierznie i na niektórych rzeczach dosyć trudno nam się skupić. Szczególnie dotyczy to materiałów wydawałoby się powszechnie znanych. Po opublikowaniu instrukcji użytkowania edytora Panther spotkałem się z zarzutami, że było to "przegięcie". O niesłuszności tego twierdzenia przekonał mnie ostatnio jeden z moich przyjaciół. Otóż osobnik ten dosyć szeroko znany na naszej scenie oświadczył mi, że w pewnej sytuacji musiał zrezygnować z wykorzystywania Panthera na korzyść Speed Script'a, gdyż w żaden inny sposób nie mógł się pozbyć znaku końca wiersza (tzw. EOL), który to znak jest przez Panther'a automatycznie wprowadzany do tekstu. Taka sytuacja jest dowodem na to, iż wspomniany osobnik nie przestudiował instrukcji Panther'a, ani orginalnej (być może jej nie miał....), ani tej publikowanej w SERIOUS'ie #1. Nasuwa się pytanie - dlaczego nie przestudiował? Bo on przecież już to wszystko wiedział! Dlaczego o tym wspominam? Ano dlatego, żeby się podbudować, żeby utwierdzić się w przekonaniu, że to ja jednak miałem rację. Wiele osób stawia mi ten sam zarzut odnośnie publikacji cyklu o programowaniu, lecz mnie samemu bardzo przydało się przypomnienie zawartych w nim wiadomości, a wypowiedź Krógera oceniająca cykl jako "kultowy" i twierdzenie, że on się na nim wychował, utwierdziło mnie w przekonaniu o słuszności decyzji publikacji cyklu z nadzieją, że jeszcze ktoś sie na nim "wychowa"...

      W tym miejscu chcę zwrócić się do mojego kolegi, którego przykładem posłużyłem się tam wyżej. Mam wielką nadzieję, że tylko MY dwaj wiemy kto jest kto! Nie chciałem tutaj wyciągać twojej POSTACI na światło dzienne, chciałem tylko użyć "życiowego", prawdziwego przykładu.

      Teraz już zapraszam wszystkich do poczytania z jakimi problemami ze strony swoich czytelników borykał się JBW.

Zbycho Jabol/DIAL

  

      Jeden z czytelników pyta : "Jaka instrukcja lub instrukcje asemblera odpowiadają rozkazowi basicowemu "GRAPHICS 0"? Oczywiście nie ma gotowej recepty. Sposób postępowania będzie zależał od kontekstu, czyli tego co się działo dotąd w komputerze oraz tego co zamierzamy dalej robić. Najprostrzą znaną mi metodą byłby skok:

      JMP GR0


do systemowej procedury, która wykonuje wspomnianą instrukcję Basica. Niestety, należałoby ją poprzedzić deklaracją GR0 EQU... ale nie znam stosownego adresu. W tej sytuacji stosuję więc trick, który polega na otwarciu kanału dla urządzenia "E:". System sam wykona akcję "GRAPHICS 0".
        org $480

 gr0    ldx #chn1
        lda #open
        sta io_com,x
        lda e
        sta io_adr+1,x
        lda #read+write
        sta io_mod,x
        jsr ciov

W tym momencie na ekranie jest już tryb 0. Oczywiście nie można tak po prostu pozostawić aktywnego kanału. Ponieważ nie będzie więcej używany, należy go zamknąć.
        lda #close
        sta io_com,x
        jsr ciov

Można to zrobić bez obawy, że "close" "zabierze" tryb z ekranu. Ta sztuczka została zastosowana między innymi w programie Panther. Powyższy program może służyć do przywrócenia standardowego ekranu z poziomu DOS-u. Wiele DOS'ów nie regeneruje ekranu po zmianie trybu np. na 8 w Basic lub jakimś programie o ile program sam tego nie zrobi. Należy dopisać zakończenie:
        jmp (dosvec)

 e      dta c'E:',b(eol)

        org runadr
        dta a(gr0)

        end

Taki program, wygenerowany po dołączeniu z przodu
       opt list_err+code_dsk


i zasemblowaniu na dysk pod nazwą CLS. COM nadaje się do uruchomienia z poziomu DOS.

      Czasem jednak zdarza się, że sekretny zamysł zmusi nas do tworzenia trybu 0 na własną rękę. Do tego niezbędna jest wiedza o budowie programu ANTIC'a. Myślę, że wszyscy już ją posiadamy. Umieszczenie DL w programie pozwala ulokować obszar ekranu praktycznie w dowolnym miejscu pamięci. Spróbujmy napisać taki "program" bez programu, który objawi się naszym oczom podczas wczytywania:

        opt list_err+code_dsk

 dl_org equ $9c20

        org dlptrs
        dta a(dl)

Analogicznie należy zdefiniować dane DL:
        org dl_org
 dl     dta b($70),b($70),b($70)
        dta b($42),a(scr)
        dta b(2),b(2),b(2),b(2)
        dta b(2),b(2),b(2),b(2)
        dta b(2),b(2),b(2),b(2)
        dta b(2),b(2),b(2),b(2)
        dta b(2),b(2),b(2),b(2)
        dta b(2),b(2),b(2)
        dta b($41),a(dl)

Zwróć uwagę,że nasza DL ma się znaleść pod adresem $9c20 czyli tam, gdzie się zwykle mieści podczas pracy z BASIC'em. Jeżeli uruchomimy nasz program przy nieobecnym BASIC'u to DL i podążający za nią ekran znajdzie się w nietypowym w tych warunkach miejscu. Po zakończeniu programu cała ta struktura pozostanie oczywiście widoczna na monitorze podczas gdy inne adresy, jak SAVMSC ($58) wskazują całkiem co innego. To powoduje nieco zamieszania, gdyż komunikaty systemu będą się "pokazywać" w niewidocznym dla nas miejscu. Jedynym rozsądnym dla nas wyjściem z tarapatów będzie użycie przycisku RESET. Można, rzecz jasna, ustalić dl_org na $bc20, tak jak w systemie bez BASIC'a, ale wówczas program uruchomiony przy BASIC zachowa się jeszcze gorzej: pokaże "sieczkę", gdyż obszar ten znajdzie się w obrębie ROM. Jest to ważna, choć nie jedyna wada prezentowanego programu.
 scr    dta d'****** EKRAN TESTOWY'
        dta d' WIERSZ #01 ********'
        dta d'****** EKRAN TESTOWY'
        dta d' WIERSZ #02 ********'
        .....
        dta d'*******EKRAN TESTOWY'
        dta d' WIERSZ #24 ********'

Z brakującymi wierszami w miejscu kropek. Oczywiście zamiast przepisywać bezmyślnie "EKRAN TESTOWY" możesz pofolgować swojej wyobraźni i wypełnić ekran według własnego uznania, na przykład obrazkiem ze znaczków semigraficznych. W przykładzie każdy 40 znakowy wiersz został podzielony na dwie części po 20 znaków z powodu ograniczonej szerokości szpalty w TA. (I ja to zachowałem dla SERIOUS'a - Jabol). Jednak we własnym programie można, a nawet trzeba wpisywać każdy wiersz ekranu w jednej linijce. Pseudorozkaz DTA typu D służy do definiowania danych przeznaczonych do wyświetlania na ekranie w formie znaków. Kody tych znaków, generowane przez QA różnią się od kodów ASCII (uzyskiwanych przez DTA typu C), tak jak wymaga tego ANTIC. Trzeba o tym pamiętać, gdy sami umieszczamy dane w pamięci ekranu. Przy korzystaniu z CIO napisy podaje się w ASCII, gdyż system sam dokonuje konwersji. Tu dostrzegamy drugą poważną wadę tego sposobu wyświetlania. Program musi zawierać dane całego ekranu i DL, czyli bez mała 1kB choć czasem spora część ekranu świeci pustką lub powtarza ten sam wzór. Niestety, choć program jest już niemal gotowy, okazuje się, że niektóre DOS'y próbują ZAWSZE wykonać wczytany program, nawet gdy nie podamy adresu uruchomienia. Oto więc adres:
         org runadr
         dta a(dummy)

Jest to systemowa procedura inicjalizacji sterownika dysku, znana z literatury jako DISIV. Jest bardzo krótka i niewiele robi (czytaj: psuje), nie stwierdziłem jej szkodliwego działania w żadnych warunkach. Wykorzystuję ją zazwyczaj jako równoważnik rozkazu RTS którym jest zakończona, gdy nie mam pod ręką żadnego RTS-u. Pozostaje problem RESET-u, który trzeba wykonać dla przywrócenia właściwych parametrów ekranu. Pozostawienie tego zapominalskiemu użytkownikowi byłoby nieroztropne. Z drugiej strony trzeba zostawić trochę czasu na zapoznanie się z obrazem. 10 sekund powinno wystarczyć:
 t       equ 500 (10*50)

         org cdtmv2
         dta a(t)

Wpisanie wartości do licznika cdtmv2 inicjuje odliczanie czasu. Ponieważ jednostką jest tu 1/50 sekundy, więc na sekundę trzeba ich 50. Po odliczeniu do zera system wykona skok do adresu podanego w cdtma2. Trzeba tam wpisać (szybko zanim upłynie te 10 sekund).
         org cdtma2
         dta a(warmst)

         end

Proszę zauważyć, że w całym programie nie padł ani jeden rozkaz maszynowy! Pokazana metoda nie nadaje sie do krótkich programów uruchamianych DOS'em. Doskonała jest natomiast do czołówek gier, w trybie graficznym.

  

      Zapraszam wszystkich cierpliwych i wytrwałych czytelników do piątego wydania magazynu SERIOUS na ostatni już trzyczęściowy odcinek kursu programowania procesora 6502...