Tajemnice ATARI

Programowanie 6502


Prawda o dodawaniu binarnym

   Za wykonanie operacji dodawania w procesorze odpowiada wydzielona część układu, zwana ALU. Choć dodawane są do siebie dwa bajty, w istocie jednak każdy bit traktowany jest z osobna, jednakowo. Wygodnie jest więc spojrzeć na ALU jak na zespół jednobitowych sumatorów.

 Każdy z nich ma trzy wejścia: argument 1, argument 2, wejście przeniesienia, oraz dwa wyjścia: wynik, wyjście przeniesienia. Wejścia traktowane są równorzędnie, sumator po prostu dodaje bity, które w postaci sygnałów elektrycznych pojawiają się na nich. Są zatem cztery możliwości:

Suma bitów wy C wynik
0 0 0
1 0 1
2 1 0
3 1 1
   Myślę, że każdy bez trudu zauważy, że bity na wyjściach odpowiadają reprezentacji dwójkowej liczby otrzymanej z dodania bitów wejściowych. Sekretem, który scala sumatory w ośmiobitową grupę, jest szeregowe połączenie wyjść z wejściami przeniesienia tak, że przeniesienie z młodszego bitu przekazywane jest do starszego. Wejście przeniesienia najmłodszego bitu przyłączone jest do znacznika C w ten sposób, że przyjmuje jego dotychczasową wartość, zaś wyjście przeniesienia z najstarszego bitu przekazuje swój stan jako nową wartość C.

   Przećwicz sobie, używając kartki i ołówka, kilka operacji dodawania, tak, jak robi to ALU, np. %100100 + %100100, %1100100 + 1010000, %10011100 + %1100100. Oczywiście, bit przeniesienia wchodzący do ALU (czyli znacznik C) musi być zerem, aby wynik był poprawny. Wykorzystaj Quick Assembler do sprawdzenia wyników, np.:

OPT %10101
ORG $480

CLC
LDA #%100100
ADC #%100100
BRK
RTS

END

   Rozkaz ADC inicjuje wykonanie przez ALU operacji dodawania. Pierwszy argument pochodzi z akumulatora, drugi zaś z komórki pamięci wskazanej odpowiednim trybem adresowania. Wynik zostaje zapisany do akumulatora, w miejsce argumentu. Rozkaz BRK spowoduje zawieszenie programu bez zerowania rejestrów, można więc odczytać wynik w dolnym wierszu ekranu (A=..). Przełożenie zapisu szesnastkowego, używanego przez system QA, na dwójkowy nie powinno sprawić nikomu kłopotu. W najgorszym razie można się posłużyć kalkulatorem XLFrienda. Powtórne wykonanie Run spowoduje zakończenie programu i przestawienie rejestrów w stan początkowy. Używając rozkazu BRK nigdy nie zapominaj, że Run z a w s z e wykonuje program maszynowy od adresu widniejącego w dole ekranu (P=....), a nie zawsze od tego, który masz na myśli!

   Ostatnie dodawanie daje wynik 0. Może to początkowo wydać się dziwne, dodawano wszak dwie liczby różne od zera, zdziwienie to jednak minie, gdy uświadomisz sobie, że %100000000 jest liczbą dziewięciobitową, nie może się więc zmieścić w jednym bajcie: wystający bit znalazł się w znaczniku C. Ogólnie: jeżeli dodawanie kończy się z ustawionym znacznikiem C, to znaczy, że wynik nie zmieścił się w przeznaczonym nań miejscu.

Liczby ze znakiem

   Można wszakże spojrzeć na ostatni przykład inaczej: Umówmy się, że istnieją liczby dodatnie i ujemne. Te ostatnie rozpoznaje się po tym, że ich siódmy bit (najstarszy) ma wartość 1. %10011100 jest taką właśnie liczbą. Ponieważ %1100100 to tyle co dziesiętne 100, a w sumie dają 0, stąd łatwo wydedukować ,że %10011100 oznacza liczbę -100.

   Ogólnie, aby przekształcić dwójkowy zapis liczby w liczbę jej przeciwną, należy zanegować wszystkie bity (czyli zera zamienić w jedynki, a jedynki w zera) i do tak otrzymanej liczby dodać 1. Na przykładzie liczby 100 wygląda to tak:

%01100100 (liczba 100)
%10011011 (negacja)
%10011100 (zwiększona o 1)

   Realizacja tego przekształcenia w języku asemblera:

LDA #1100100
EOR #%11111111 negacja
CLC
ADC #1 dodaj 1

   Przy okazji: uważna analiza przedstawionej wyżej zasady działania ALU pozwala na wniosek, że para rozkazów

CLC
ADC #1

powodująca zwiększenie o 1 zawartości akumulatora działa tak samo, jak

SEC
ADC #0

ponieważ ustawienie znacznika C przed dodawaniem jest równoznaczne z dodaniem dodatkowej jedynki do sumy argumentów.

   Jeżeli traktujemy bajt jako ośmiobitową liczbę dwójkową bez znaku, to w oczywisty sposób może ona przybierać wartości z zakresu od 0 do 255. Jeśli jednak chcemy bajt widzieć jako liczbę ze znakiem, to największą liczbą dodatnią (skasowany 7. bit) jest %01111111, czyli 127, zaś najmniejszą liczbą ujemną jest % 10000000, czyli -128. Oczywiście różnica między liczbami ze znakiem i bez znaku istnieje tylko w naszej głowie, liczba -1 i liczba 255, to w gruncie rzeczy to samo.

Prawda o odejmowaniu

   Informacja o tym, jakoby procesor 6502 posiadał rozkaz odejmowania, jest nieco przesadzona. W rzeczywistości rozkaz SBC jest rozkazem dodawania, różni się od ADC tylko tym, że drugi argument (ze wskazanej komórki pamięci) przed dodaniem zostaje zanegowany. Gdy więc spróbujemy wykonać takie "odejmowanie", np.:

CLC
LDA #15
SBC #15

   to otrzymamy nie 0, lecz liczbę %11111111, czyli -1. Aby otrzymać poprawny wynik odejmowania, trzeba "pomóc" procesorowi i dodać jeszcze 1. Najprościej zrobić to przez ustawienie na wstępie znacznika C (rozkazem SEC). Dopiero takie współdziałanie (procesor neguje argument, a my dodajemy 1) powoduje zamianę argumentu w liczbę przeciwną i w rezultacie właściwe odejmowanie.

   Wiedza o metodzie odejmowania pomaga czasem w kłopotliwych sytuacjach. Dodawanie jest przemienne, ale odejmowanie nie. Gdy trzeba odjąć zawartość komórki x od akumulatora, piszemy:

SEC
SBC x

   To proste. Co jednak zrobić, by odjąć zawartość akumulatora od komórki x? Zdarzyło mi się widzieć rozwiązanie z użyciem dodatkowej komórki:

STA y
SEC
LDA x
SBC y

lub bez angażowania dodatkowej pamięci:

LDX x
STA x
SEC
TXA
SBC x

   Tu jednak zamazuje się dotychczasową zawartość x. Tymczasem najprościej można to zrobić to tak:

SEC
EOR #1255
ADC x

Arytmetyka wielobajtowa

   Zakres liczb 0..255 (lub -128..127), które dają się wyrazić jednym bajtem, rzadko wystarcza w praktyce, tym bardziej, że adresy zapisuje się na ogół dwoma bajtami. Dobrze byłoby więc mieć pod ręką 16-bitową ALU, a w najgorszym razie dwie 8-bitowe i wyjście przeniesienia jednej połączyć z wejściem drugiej. Mając tylko jedną ALU, trzeba sobie radzić na raty. W poniższym przykładzie dodamy liczbę 920 (23 * 40) do adresu pamięci obrazu, aby otrzymać adres ostatniego wiersza na ekranie.

EKRAN EQU $58
ADRES EQU $CC

   Na wstępie należy wyzerować wejście przeniesienia:

CLC

   Dodawanie rozpoczyna się od młodszych bajtów:

LDA EKRAN
ADC <920
STA ADRES

   Znaczek "<" tłumaczony jest przez asembler tak jak "#", z tą różnicą, że użyty zostanie tylko młodszy bajt argumentu. W wyniku dodawania młodszych bajtów znacznik C uzyska wartość przeniesienia z najstarszego bitu, która weźmie udział w dodawaniu starszych bajtów:

LDA EKRAN+1
ADC >920
STA ADRES+1

   Znak ">" ma znaczenie podobne do "<", lecz tym razem wzięty zostanie starszy bajt argumentu. Aby zweryfikować efekt tego działania umieść jakiś znak, np.

LDA #'!'-32

w miejscu o dwie kolumny odległym...

LDY #2

od wyliczonego adresu:

STA (ADRES),Y

   W wyniku wykonania tego rozkazu pojawi się w trzeciej kolumnie ostatniego wiersza ekranu znak "!". Teraz postaw pytajnik dwa wiersze wyżej:

SEC
LDA ADRES
SBC <80
STA ADRES
LDA ADRES+l
SBC >80
STA ADRES+1
LDA #'?'-32
STA (ADRES),Y

   Oczywiście każdy widzi, że >80 to po prostu 0, lecz użyty tu zapis lepiej obrazuje, do czego ta liczba służy. Nie można także pominąć tego drugiego odejmowania, choć argument jest równy 0, wszak wartość znacznika C pozostała po pierwszym odejmowaniu, wskazująca na obecność lub brak przeniesienia (przy odejmowaniu zwanego "pożyczką"), może spowodować zmniejszenie starszego bajtu. Pamiętajmy (jak to pokazano wyżej), że wyzerowany C powoduje dodatkowe zmniejszenie wyniku (pożyczkę). Odwrotnie, ustawiony C pozostawi bajt ADRES+1 bez zmian. Bazując na tej wiedzy można nieco skrócić powyższy fragment:

      SEC
      LDA ADRES
      SBC #80
      STA ADRES
      BCS PYT
      DEC ADRES+l
PYT   LDA #'?'-32
      STA (ADRES),Y

   Dotyczy to, rzecz jasna, tylko odejmowania liczby jednobajtowej od dwubajtowej. Analogicznie można uprościć dodawanie liczby krótkiej do długiej. Jak? Pomyślcie sami. Oczywiście arytmetyka nie kończy się na liczbach dwubajtowych. Można sumować (lub odejmować) ze sobą dowolnie długie ciągi bajtów, pamiętając o kolejności (od najmłodszego bajtu do najstarszego) i bacząc, by pomiędzy operacjami na poszczególnych bajtach nie zamazać znacznika C. Przykład często popełnianego błędu przy dodawaniu liczb 4-bajtowych:

      LDX #0
      CLC
DODAJ LDA Nl,X
      ADC N2,X
      STA N1,X
      INX
      CPX #4
      BNE DODAJ

   Rozkaz CPX zeruje znacznik C, w wyniku czego całe dodawanie jest do kitu!

Porównania

   Rozkaz porównania CMP jest ze sposobu działania zbliżony do rozkazu SBC, lecz nie wymaga wstępnego ustawiania znacznika C (Jest on ustawiany samoczynnie). Pomaga to przy porównywaniu pojedynczych bajtów, lecz komplikuje użycie tego rozkazu w porównaniach liczb wielobajtowych. Drugą ważną różnicą jest to, że wynik odejmowania nigdzie się nie zapisuje; akumulator (a w przypadku bliźniaczych rozkazów CPX, CPY - rejestr X, Y) i argument pozostają niezmienione.

   W wyniku wykonania rozkazu CMP ustalają się wartości znaczników C, Z, N, co bywa wykorzystywane w następnych rozkazach (zwykle po porównaniu następuje skok warunkowy). Znaczniki zachowują się identycznie jak w przypadku rozkazu SBC. Ustawiony znacznik Z oznacza, że porównywane liczby były jednakie. Znacznik C bierze się pod uwagę przy porównaniu liczb bez znaku. Skasowany oznacza, że zawartość akumulatora była mniejsza od argumentu. W przypadku liczb ze znakiem rozpatruje się znacznik N, którego ustawienie mówi, że zawartość akumulatora była mniejsza niż argument.    Aby porównać liczbę wielobajtową należy posłużyć się rozkazem odejmowania. Tylko pierwszą parę bajtów można obsłużyć rozkazem CMP, z czego jest takt pożytek, że nie trzeba dbać o znacznik C.

LDA N1
CMP N2

   To porównanie ustawia odpowiednio znacznik przeniesienia dla kolejnych rozkazów.

LDA N1+1
SBC N2+1

i tak dalej, aż do ostatniego (najstarszego) bajtu. Znacznik C dla liczb bez znaku, a N dla liczb ze znakiem wskazuje, czy pierwsza z liczb była mniejsza od drugiej, czy nie mniejsza. Niestety, ta metoda nie pozwala odróżnić równości od większości, gdyż znacznik Z ustawiany bywa dla każdego bajtu z osobna.

Pożytek ze znacznika V

   Kiedy patrzymy na bajt jak na liczbę ze znakiem, najstarszy bit określa znak tej liczby. Skasowany bit oznacza liczbę nieujemną. A zatem wartość bezwzględna liczby wynika z pozostałych siedmiu bitów. Lecz nasz sposób interpretacji liczb nijak nie wpływa na ALU; wszelkie operacje są wykonywane jednakowo. To prowadzi nieraz do nieporozumień:

CLC
LDA #60 liczba dodatnia
ADC #80 liczba dodatnia
BRK ujemny wynik! (-74)
RTS

   Suma dwóch liczb dodatnich powinna być też liczbą dodatnią, lecz wynik przekroczył zakres dopuszczalny dla jednobajtowej liczby ze znakiem (127). Otrzymany wynik (poprawny jako liczba bez znaku) jest bez sensu w konwencji liczb ze znakiem. Tę sytuację sygnalizuje właśnie znacznik V. Jest on zwany znacznikiem przepełnienia i wskazuje, że liczba zmieniła znak w sposób niekontrolowany. Dzieje się tak, gdy z dodawania liczb dodatnich wychodzi liczba ujemna, lub gdy różnica liczby ujemnej i dodatniej (powinna być jeszcze bardziej ujemna) jest liczbą nieujemną.

   Poza podanym zastosowaniem oraz unikalnym użyciem po rozkazie, BIT (V przyjmuje w wyniku wykonania tego rozkazu wartość szóstego bitu argumentu), znacznik V nie znajduje innego wykorzystania.

   A przy okazji, aż dziw bierze, jakie niestworzone dyrdymały wypisują różni autorzy na temat znacznika V.

Janusz B. Wiśniewski

Powrót na start | Powrót do spisu treści | Powrót na stronę główną

Pixel 2001