energy.scene.pl

Popularny magazyn na ośmiobitowe Atari

Wybierz muzykę:

Energy 1: 1 2 3 4 5
Energy 2: 1 2 3 4 5 6 7 8 9 10 11 12
Lekcja asemblera cz. 2

Jaskier
autor: jaskier

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.



Poprzednia strona: Lekcja asemblera cz. 1
Następna strona: Packowanie danych