"Lekcja assemblera"
------------------( Jaskier/Taquart )-
NUMER 1/92: Magiczne liczby ze znakiem. Pewnie każdy tak o nich myśli. Jak w ogóle one wyglądają? Otóż zupełnie tak samo jak liczby bez znaku, tyle że najwyższy bit oznacza, czy wartość należy traktować jako liczbę ujemną (bit ustawiony), czy nie. Nie jest prawdą, jakoby nasz procesor dokonywał operacji identycznie na liczbach ze znakiem, czy bez, a jedynie od naszej interpretacji zależy różnica. Nasz procesor pracuje prawie poprawnie tylko na tych liczbach ze znakiem, których bit znaku znajduje się w najwyższym bicie ich najstarszego bajtu. Na innych liczbach ze znakiem bardzo trudno jest operować. Jak zatem należy pracować na tych liczbach, na których nasz procesor trochę się zna?
Po pierwsze jak łatwo zauważyć znak liczby znajduje się w jej najstarszym bajcie, dzięki czemu operacje na wszystkich młodszych bajtach wykonujemy tak, jakby liczba była bez znaku. Dopiero, kiedy dojdziemy do najstarszego bajtu, wszelke operacje dodawania, odejmowania, czy porównywania dokonujemy w sposób specjalny. Jak się to robi?
Jak łatwo się domyślić, znacznik C nie ma tutaj takiego zastosowania, jak w przypadku liczb bez znaku. Spróbujmy bowiem do liczby 16 dodać liczbę -16 ($f0). W przypadku liczb bez znaku wartość 0 z ustawionym C jest sensowna, ale w przypadku liczb ze znakiem nie ma to sensu, gdyż żadne przepełnienie przecież nie nastąpiło. Może więc należy tutaj znacznik C traktować odwrotnie? Również nie. Spróbujmy na przykład zrobić 1+1. Również nie nastąpiło przepełnienie a znacznik C jest skasowany. Zanim podam prawidłową odpowiedź zastanówmy się najpierw, co będzie znaczyło przepełnienie w przypadku liczby ze znakiem. Jeżeli odbyło się ono przy okazji operacji porównywania, to nic takiego. Jeżeli jednak nastąpiło to przy okazji operacji arytmetycznej to znak, że w żaden sposób nie da się liczby zmieścić w przyznanej jej ilości bajtów. Albo więc zwiększymy przydzieloną liczbie lilość bajtów albo też cały wynik można sobie wsadzić tam, gdzie słońce nie dochodzi. Jeżeli nasz program przystosowany jest do zmiennej ilości bajtów zajmowanych przez liczbę, to wynik, który dał nam przepełnienie zapisujemy normalnie, tak jakby był dobry (bo jest dobry tyle, że już nie ma znaku), a do obecnie najstarszego bajtu liczby wpisujemy zero i uzupełniamy odpowiednim bitem znaku. Najlepiej jest to zrobić następująco:
PHP PLA AND #$7f EOR #$80 STA wynik+ (za plusem stoi nr. bajtu).
Teraz pora na rozwiązanie zagadki. Otóż w przypadku liczb ze znakiem rolę znacznika C przejmuje znacznik V. Jeżeli w wyniku operacji arytmetycznej miała powstać liczba dodatnia, ale wynik przekroczył 127, to znacznik V zostanie ustawiony. Podobnie, jeżeli w wyniku miała powstać liczba ujemna, ale wynik był mniejszy od -128. W obydwu przypadkach N pokazuje zły znak liczby, gdyż procesor nie zdaje sobie sprawy z naszych intencji i do N zawsze kopiuje najwyższy bit wyniku, który tutaj też jest nieprawidłowy. Znacznik N jest tutaj ważny głównie w przypadku porównań. Jeżeli wykonujemy porównywanie wielobajtowych liczb ze znakiem, to robimy to standardowo (pierwszy bajt CMP, następne SBC), jednakże końcowy wynik interpretujemy następująco: jeżeli tylko jeden ze znaczników N i V został ustawiony, to liczba, której bajty ciągle ładowaliśmy do akumulatora jest mniejsza od tej, z którą ją porównywaliśmy. Jeżeli oba znaczniki zostały ustawione lub skasowane, to znaczy, że liczba z akumulatora jest większa bądź równa tej, z którą ją porównywaliśmy. Może się to wydawać skomplikowane, jeżeli jednak uświadomimy sobie, że porównywanie, to w istocie odejmowanie, to stanie się to wszystko proste. Jeżeli V jest ustawiony, to znaczy, że N wskazuje niepoprawny znak i musimy go zmienić. Sam zaś znak liczby, to po prostu wynik odejmowania. Jeżeli jest ujemny, to znaczy, że od liczby mniejszej odejmowaliśmy większą. Teraz jest to proste?
Muszę jeszcze powiedzieć coś o porównywaniu liczb jednobajtowych. Otóż nie należy tego robić rozkazem CMP. To największy błąd jaki popełniły Tajemnice Atari w swoich publikacjach. Otóż ponieważ rozkaz CMP nie zapisuje nigdzie wyniku, to również nie przejmuje się znacznikiem V. Jeżeli nastąpiło przepełnienie, to wynik będzie nieprawidłowy. Należy tutaj stosować rozkaz SBC. Rozkazu CMP można jedynie używać, kiedy wiemy, że nie nastąpi przepełnienie.
Ostatnia sprawa. Rozkazy ADC i SBC za każdym razem prawidłowo ustawiają znacznik V. Do czego jest więc potrzebny rozkaz CLV. Wiecie? Bo ja nie. Przecież nikt chyba nie będzie próbował przed operacją dodawania liczb zerować znacznika V. Co prawda V zastępuje tutaj znacznik C, ale nie aż do tego stopnia. C nadal pełni swoje ustawowe funkcje.
Electron twierdzi, że podstawowa wersja 6502 posiada nóżkę SETV, która pozwala zewnętrznie ustawić znacznik V. Jest to ponoć wykorzystywane w stacji Commodore 1541.
Numer 6-7/92: Podana jest tutaj (aż dwa razy) dostępna na Atari tablica kolorów. W obu przypadkach zawiera ten sam błąd. Kolor1 i kolor $f (15), to dokładnie ten sam kolor. Nie ma między nimi ŻADNEJ różnicy!
Numer 10/92: Cytat: "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." Jest to prawdziwe w przypadku przerwań NMI, ale całkowicie fałszywe w przypadku przerwań IRQ o których traktuje ten artykuł. Każde wywołanie przerwania automatycznie powoduje ustawienie znacznika I w rejestrze P procesora, przez co następne przerwanie nie może wywołać się przed zakończeniem poprzedniego.
Numer 11-12/92: BCD- kolejne magiczne liczby. Przedstawiona w artykule konwersja liczby dwójowej na BCD (procedura CONVR) jest trochę niezrozumiała. Tyczy się to szczególnie następującej części:
CV1 ASL BYTE LDA WORD ADC WORD STA WORD ROL WORD+1
Właśnie tutaj robiona jest (przy zapalonym znaczniku D) konwersja liczby binarnej na postać dziesiętną. Wygląda ona faktycznie dość nietypowo. Rozkazy rolowania bitów mieszają się z dodawaniem (w systemie BCD). Zauważmy jednak, że w komórce WORD+1 największa wartość jaka może się znaleźć, to 2, gdyż konwertujemy liczbę binarną jednobajtową, a ona nie jest większa niż 255. Tym samym rozkaz ROL WORD+1 działa tak samo jak sekwencja:
LDA WORD+1 ADC WORD+1 STA WORD+1
która wygląda tutaj już bardziej zrozumiale. Po prostu słowo WORD jest w ośmiu obiegach pętli za każdym razem mnożone przez 2 (ponieważ WORD+WORD=2*WORD). Rozkaz ROL WORD+1 jest tutaj dany tylko dla oszczędności kodu. Teraz wszystko jest już jasne. Wartośc w komórce BYTE jest za każdym obiegiem pętli mnożona przez 2. To samo dzieje się ze słowem WORD. Po ośmiu obiegach wszystkie bity z BYTE znajdą swoje uznanie, w postaci znacznika C, podczas mnożenia słowa WORD. Jeżeli BYTE miało zapalony najstarszy bit, to słowo WORD po pierwszym obiegu będzie miało wartość $0001. Po drugim $0002, następnie $0004, $0008, $0016, $0032, $0064, $00128. Następny bit będzie miał tylko 7 możliwości na pomnożenie swojej wartści, czyli zostanie wartością $0064. Wszystkie wartości są na bieżaco sumowane tak, że jeżeli BYTE miało 2 najwyższe bity zapalone, to po dwóch obiegach pętli WORD będzie równe $0003. Proste? Jeżeli nie, to spójrzcie na przykład konwersji na postać dziesiętną liczby dwubajtowej:
CONVR EQU * LDA #0 STA WORD STA WORD+1 STA WORD+2 LDX #16 SED CV1 ASL BYTE ROL BYTE+1 LDA WORD ADC WORD STA WORD LDA WORD+1 ADC WORD+1 STA WORD+1 ROL WORD+2 DEX BNE CV1 CLD RTS
Jak natomiast przedstawia się sprawa odwrotna? Konwersja liczby BCD na postać binarną? To jest już proste. Wystarczy brać kolejne czwórki bitów licząc od najstarszej i dodawać do już obliczonej liczby. Przed każdą taką operacją należy wynik mnożyć przez 10. Oto program:
ILD- ilośc bajtów liczby w postaci BCD,
ILB- spodziewana ilość bajtów po konw.
SLOWO- konwertowana liczba w postaci BCD,
WYNIK- a jak myślisz? Długość ILB bajtów,
DAT- pomocnicze słowo długości ILB bajtów.
LDX #ILD-1 LDY #ILB-1 LDA #0 L1 STA WYNIK,Y DEY BPL L1 L2 JSR MNOZ LDA SLOWO,X LSR @ LSR @ LSR @ LSR @ JSR DODAJ JSR MNOZ LDA SLOWO,X AND #15 JSR DODAJ DEX BPL L2 RTS DODAJ EQU * CLC ADC WYNIK STA WYNIK LDY #1 L3 CPY #ILB BEQ L4 LDA WYNIK,Y ADC WYNIK,Y STA WYNIK,Y INY BNE L3 (to w istocie JMP L3) L4 RTS MNOZ LDY #0 CLC L5 ROL WYNIK,Y LDA WYNIK,Y STA DAT,Y INY CPY #ILB BNE L5 LDY #0 CLC L6 ROL WYNIK,Y INY CPY #ILB BNE L6 LDY #0 CLC L7 ROL WYNIK,Y INY CPY #ILB BNE L7 LDY #0 CLC L8 LDA DAT,Y ADC WYNIK,Y STA WYNIK,Y INY CPY #ILB BNE L8 RTS
Ta procedura jest tak skomplikowana, gdyż jest uniwersalna. Procedura dla zakresu 0-255 wygląda tak:
LDA SLOWO+1 ASL @ ASL @ STA DAT ASL @ ASL @ ASL @ STA WYNIK ASL @ ADC WYNIK ADC DAT STA WYNIK LDA SLOWO AND #$F0 LSR @ STA DAT LSR @ LSR @ ADC DAT ADC WYNIK STA WYNIK LDA SLOWO AND #15 ADC WYNIK STA WYNIK RTS
Numer 4/93: MulDiv. Pokazana w tym artykule "szybka" procedura mnożenia jest tak wolna, że aż się chce płakać. Oto szybka procedura:
DEC A LDA #0 LSR B BCC *+4 ostatnie 4 rozkazy trzeba ADC A powtórzyć 8 razy ROR @ ROR B
Dane: A, B- jednobajtowe liczby na stronie zerowej. Starszy bajt wyniku znajduje się w akumulatorze, a młodszy w komórce B. Procedura ta w maksimum zajmuje 108 cykli, a w minimum 92.
I to wszystko co miałem do powiedzenia na temat błedów w Tajemnicach Atari. W następnych odcinkach zajmiemy się już prawdziwym kodowaniem.
Jaskier/Taquart