PROGRAMOWANIE PROCESORA 6502 W KOMPUTERACH ATARI XL/XE
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. Kto nie ma poprzedniego numeru TA, może go nabyć korespondencyjnie (szczegóły na str. 10). Tych fragmentów programu, które zostały w nim wydrukowane, nie będę już dziś powtarzał.
Oba programy należą do grupy tzw. transkoderów, czyli programów przekształcających dane z jakiejś jednej 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ż pamięć nie jest z gumy, stosujemy w przypadku transkoderów zasadę oszczędności miejsca: jeżeli konwersja 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, by 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 robi to poniższy
1. 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 zastosowaniem Eatera jest kompresja danych do gry The Jet, którą wydrukowano na str. 28. 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 (podkreślam: samej planszy, a nie gry) The Jet przez Eol Eater-a 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 te programy 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 to 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 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 przepisywać 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 w stosownym 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 winieta programu
*--- dane
data dta b(eol)
dta c' TA Eol Eater '*
dta b(eol)
zamiast tytułu "TA Copy" (reszta danych bez zmian).
Autorem procedury wcinającej Eole jest Dariusz Żołna.
Dygresja: posiadacze Panthera mogą rzecz całą wykonać dużo prościej. Wystarczy zdefiniować tabelę konwersji z jedynym wpisem:
#155=
co się czyta "zamień wszystkie EOL na nic", aktywizować ją, a następnie "wydrukować" ułożoną planszę na plik dyskowy lub kasetowy opcją File/Print.
2. 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 szesnastkowych. 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 objętoś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_1
sta io_len,x
lda >d0_1
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 o 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 swej pierwotnej wartości.
* ustaw numer początkowy
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 (wpisuje 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. Operację przerywa się (ważne!) gdy BOUT zamelduje błąd.
* pętla 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żeli 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 przesył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 zewnętrzne wyposażone są w swe własne bufory, mamy tu więc do czynienia z buforowaniem wielopoziomowym. Dopóki zatem wpisywanie 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 - zero jest tylko atrapą i nigdy nie ulega 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 12 KB. 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
snc <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 pojedynczego 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_1 equ *-d1
rem_ equ *-stat
d0 dta c'1000 REM',b(eol)
d0_1 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 Relocator-em, który zapewnia optymalne wykorzystanie pamięci komputera. Relokator był już prezentowany w TA. Dziś wersja nieznacznie zmodyfikowana. Umożliwia ona powtórne uruchomienie programu po powrocie do DOS-u (np. 2.5 z CP) lub COS-u, rozkazem RUN. Wykorzystane zostało słowo INITAD ($2E2), dzięki czemu działalność Re-lokatora ma miejsce PRZED faktycznym uruchomieniem programu. Relokator oblicza adres, pod którym znajdzie się program i wpisuje go do RUNAD ($2EO). To sprawia, że DOS na ogół samoczynnie uruchomi nasz program od tego adresu. Również ponowne "wskoczenie" do programu przez RUN (w wielu systemach jest taka opcja) 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__
TXA
AND #$1F
CMP #$09
BEQ 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 Relokatora, może obniżyć adres podany w początkowym ORG, by uzyskać nieco więcej wolnej pamięci dla programu.
Janusz B. Wiśniewski
|