1.3 Kod binarny i heksadecymalny

Wspominałem wcześniej, że komputer operuje wyłącznie na liczbach. Nawet rożne czcionki i znaki wypisywane na ekran zapisywane są w postaci liczb. W całej elektronice bazowym systemem liczbowym jest system binarny (dwójkowy). Zapis liczb w tym systemie opiera się na zastosowaniu tylko dwóch znaków 0 i 1. Tak więc przykładowa liczba wygląda następująco 0110. Oczywiście stosując się do ogólnych zasad zapisu liczb nie musimy pisać nie znaczących zer po lewej stronie. Stosowanie systemu dwójkowego w elektronice wiąże się z podstawowymi elementami, z jakich są zbudowane układy scalone. Są to oczywiście tranzystory. Tranzystor w jednym z podstawowych trybów pracy może być stosowany do zapamiętywania jednego z dwóch stanów. Czyli stosując grupę takich tranzystorów do zapamiętania pewnej liczby uzyskujemy naturalnie kod binarny.

Jak nietrudno zauważyć liczba złożona ze znaków 0 i 1 może wyglądać również jak liczba z systemu dziesiętnego. Dla rozróżnienia liczb zapisanych w różnych systemach wprowadzony został dodatkowy znak określający dany system liczbowy. Dla liczb binarnych jest to litera mała b. Liczby w systemie dziesiętnym przyjęło się zapisywać bez żadnej dodatkowej litery lub z dodatkową literą d na końcu. Przykładowo liczba dziesiętna 10 zapisana dwójkowo ma postać 1010b.

Programując w asemblerze trzeba dość sprawnie posługiwać się przynajmniej jedną z metod przeliczania między różnymi systemami liczbowymi. Ja przedstawię tutaj podstawową z nich. Jest to metoda rozpisywania liczb na potęgi.

Konwersje między systemem dwójkowym i dziesiętnym

W systemie dziesiętnym liczby zapisujemy za pomocą 10 znaków (cyfr) 0,1,2,3,4,5,6,7,8,9. Samą liczbę zapisujemy w postaci szeregu cyfr umieszczając mniej znaczące po prawej stronie. Każda kolejna cyfra występująca w liczbie ma przypisaną do siebie wagę, którą możemy rozpisać jako x^y, gdzie x jest bazą systemu liczbowego czyli w obecnym przypadku 10, a y jest numerem pozycji cyfry licząc od 0. Pokażę teraz przykładowe rozpisanie liczby z systemu dziesiętnego:

Przykładową liczbą będzie 1014. Składa się ona z cyfr kolejno od najmniej znaczącej 4, 1, 0 i 1. Tym cyfrom odpowiadają kolejne wagi określane jako jedności, dziesiątki, setki i tysiące. Matematyczny zapis wag przedstawia się właśnie w postaci potęg:

jedności 10^0 = 1
dziesiątki 10^1 = 10
setki 10^2 = 100
tysiące 10^3 = 1000
itd.


Ideą metody rozpisywania na potęgi jest rozpisanie liczby na działanie o postaci:
10141*(10^3) + 0*(10^2) + 1*(10^1) + 4*(10^0)

Obecnie nie widać w tym sensu, ale chciałem tylko pokazać schemat działania tej metody na liczbie dziesiętnej. To co opisałem może wydawać się sprawą bardzo trywialną, ale jest to jedynie konsekwencją tego iż system dziesiętny jest dla nas naturalnym systemem liczbowym, którego każdy się uczy od początku nauki w szkole. W taki sam sposób można rozkładać liczby w dowolnym systemie liczbowym. Takie rozpisanie ułatwia wtedy przeliczanie takiej liczby na dowolny inny system liczbowy.

Jako przykład pokażę rozpisanie liczby binarnej 10100111b.
Wagi w systemie binarnym wyglądają następująco:

2^0 = 1,
2^1 = 2,
2^2 = 4,
2^3 = 8,
2^4 = 16,
2^5 = 32,
2^6 = 64,
2^7 = 128,
2^8 = 256,
...

Co łatwo można odtworzyć ponieważ kolejna waga jest równa poprzedniej pomnożonej przez 2. Ten ciąg liczb jest zwykle znany na pamięć przez każdego dobrego programistę asemblera. Nie ma jednak sensu uczyć się go na siłę, ponieważ z czasem sam wejdzie do głowy. Tak więc rozpisując liczbę binarną 10100111b wykonujemy następujące działanie:

1*(2^7) + 0*(2^6) + 1*(2^5) + 0*(2^4) + 0*(2^3) + 1*(2^2) + 1*(2^1) + 1*(2^0)

po przeliczeniu potęg:

1*128 + 0*64 + 1*32 + 0*16 + 0*8 + 1*4 + 1*2 + 1*1 = 128 + 32 + 4 + 2 + 1 = 167

Gdyby liczba binarna była dłuższa to należało by się posłużyć większą ilością potęg liczby 2. Na szczęście praktycznie podczas pisania programów nie będzie potrzeby przeliczania większych liczb. Do takiej konwersji wystarczy skrawek papieru lub umiejętność dodania kilku liczb w pamięci i na pewno nie jest to coś bardzo trudnego.

Odwrotne przeliczanie z liczby dziesiętnej na dwójkową wymaga od nas umiejętności dzielenia liczb przez 2 co też nie powinno być dla nikogo trudne. Posłużę się otrzymaną poprzednio liczbą 167. Przy przeliczaniu na system binarny będziemy otrzymywać cyfry w kolejności od najmniej znaczącej. Konkretnie liczby te będziemy otrzymywać jako resztę z dzielenia 167 przez 2. Części całkowitej wyniku z dzielenia będzie trzeba użyć przy następnym dzieleniu. Najlepiej jak to wyjaśnię na podstawie schematu działania.

Na początku mamy liczbę 167. Dzielimy ją na dwa co daje 167 / 2 = 83 reszta 1. Reszta jest pierwszą z cyfr naszej liczby binarnej. W kolejnym kroku dzielimy liczbę 83 na dwa. 83 / 2 = 41 reszta 1. Kolejną cyfrą jest znowu jedynka co się właściwie zgadza. Całą procedurę przeliczenia liczby 167 na system binarny przedstawiam poniżej.

Działanie Wynik Aktualnie przeliczona liczba dwójkowa
Część całkowita Reszta
167 / 2 83 1b        1b
83 / 2 41 1b       11b
41 / 2 20 1b      111b
20 / 2 10 0b     0111b
10 / 2 5 0b    00111b
5 / 2 2 1b   100111b
2 / 2 1 0b  0100111b
1 / 1 0 1b 10100111b

Konwersje między systemem szesnastkowym i dziesiętnym

Jako że liczby zapisane w postaci binarnej są zwykle bardzo długie i niewygodnie jest nimi operować, to w programowaniu częściej posługuje się systemem liczbowym szesnastkowym (hexadecymalnym), który jest bardziej zwięzły w zapisie. Ten system liczbowy jest bardzo blisko związany z systemem dwójkowym po przez fakt, że jego podstawa czyli liczba 16 jest potęgą liczby 2. Z tego faktu wynika bardzo łatwa procedura konwersji między tymi systemami liczbowymi.

Do kodowania liczb w systemie szesnastkowym stosuje się następujące znaki: 0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F. Aby odróżnić liczbę w zapisie szesnastkowym na jej końcu dodaje się literę h (zapis 0x10 jest inną formą zapisu liczb w systemie hexadecymalnym, 0x10 = 10h). Przykładowo liczba 167 zapisana w systemie szesnastkowym ma postać A7h. Do konwersji na liczbę dziesiętną można posłużyć się algorytmem rozkładania na potęgi, co przedstawię poniżej dla liczby 2A7h:

2A7h = 2h*(16^2) + Ah*(16^1) + 7h*(16^0)

Liczby od A do F konwertujemy bezpośrednio na system dziesiętny jako A=10, B=11, C=12, D=13, E=14, F=15. Mamy więc równanie:

2A7h = 2*(16^2) + 10*(16^1) + 7*(16^0) = 2*256 + 10*16 + 7*1 = 512 + 160 + 7 = 679

Trochę jest z tym liczenia i na pewno nie jest to takie łatwe. Można popełnić przy tym jakiś błąd, więc ja polecam posługiwać się przy takiej konwersji kalkulatorem, który jest standardowo dołączany do każdego systemu operacyjnego. Konwersja w odwrotną stronę przebiega podobnie jak poprzednio dla liczb binarnych. Z tą różnicą, że teraz dzielimy liczbę konwertowaną przez podstawę hexadecymalną czyli 16.

Działanie Wynik Aktualnie przeliczona liczba szesnastkowa
Część całkowita Reszta
679 / 16 42 7        7h
42 / 16 2 10 (Ah)       A7h
2 / 16 0 2      2A7h

Powyższe działania nie są już takie proste i zwykle zajmują bardzo dużo czasu, więc odradzam ich wykonywania w pamięci, chyba że ktoś chce sprawdzić czy kalkulator działa poprawnie. Jak wcześniej polecam do wykonywania takich konwersji zastosowanie odpowiedniego kalkulatora.

Konwersja między systemem dwójkowym i szesnastkowym

Wiem, że dla ludzi rozpoczynających zabawę z asemblerem dość trudno jest operować na systemie szesnastkowym i często w kodzie umieszczają liczby w postaci dziesiętnej. Jednak często tak się zdarza, że potrzeba znać binarny lub hexadecymalny odpowiednik tej liczby, co narzuca potrzebę jej przeliczenia. Jednak przeliczanie liczb dwójkowych i szesnastkowych na dziesiętny i odwrotnie jest w większości przypadków trudnym zadaniem i zajmuje zbyt dużo czasu. Dobrym nawykiem jest nauczenie się operować wyłącznie dwoma systemami liczbowymi czyli dwójkowym i szesnastkowym. Przeliczanie liczb między tymi dwoma systemami jest dziecinnie proste i da się wykonać w kilka sekund w pamięci. Oczywiście jeżeli liczba ta jest bardzo duża, to trwa to trochę dłużej, jednak nie jest to wcale trudniejsze. Natomiast w przypadku przeliczania dużych liczb w systemie dziesiętnym wiąże się to z dzieleniem dość sporych wartości przez 2 lub 16.

Jako przykład przedstawię konwersje liczby szesnastkowej 2A7Fh na postać binarną. Jak już wspomniałem zaletą systemu szesnastkowego jest to, że jego baza jest jedną z kolejnych potęg bazy systemu dwójkowego 2^4 = 16. Z tego faktu wynika to, że każdy znak liczby szesnastkowej jest kodowany dokładnie czterema znakami liczby dwójkowej. Tak więc w przypadku liczby 2A7Fh konwersję można podzielić na 4 takie same etapy. Ilość tych etapów wynika z ilości cyfr, która dla tej liczby wynosi właśnie 4. Każdą z cyfr 2h, Ah, 7h i Fh rozkładamy niezależnie na postać binarną co nie powinno być trudne. Przy drobnej wprawie będzie to można wykonywać bez jakiegokolwiek liczenia. Tak więc zamieniamy kolejno:

Cyfra hex. 2 A 7 F
Postać bin. 0010 1010 0111 1111

Wynikiem jest złożenie postaci binarnych kolejnych cyfr w jedną liczbę. Wynik 2A7Fh = 0010101001111111b

Konwersja bin -> hex polega na podzieleniu liczby binarnej na ciągi po 4 cyfry i wyliczeniu dla tych ciągów odpowiadających mu cyfr w postaci szesnastkowej. Czyli jest to dokładnie odwrotny algorytm. Taka konwersja ma bardzo wiele zalet, ponieważ pozwala nam na szybkie sprawdzenie stanu dowolnego bitu liczby 2A7Fh bez potrzeby konwertowania jej w całości. Wiadomo bowiem, że na każdy znak hex przypada cztery znaki binarne. Chcąc poznać wartość dziesiątego znaku liczby binarnej wiemy, że wystąpi on na drugiej pozycji w trzecim znaku liczby szesnastkowej. Tak więc konwertujemy trzeci znak Ah = 1010b i widzimy iż drugi bit ma wartość 1b i to jest dziesiąty znak zapisu binarnego liczby 2A7Fh.

Podczas programowania w asemblerze bardzo często będzie potrzebna znajomość wartości konkretnych wartości bitów. Tak więc operując przede wszystkim na liczbach szesnastkowych, a nie dziesiętnych oszczędzamy sobie wykonywania bardzo dużej liczby operacji matematycznych i jednocześnie ograniczamy możliwość popełnienia błędu. Na koniec dodam jeszcze pewną tabelkę konwersji, która jest czymś podstawowym w programowaniu w asemblerze i jest ona pewnego rodzaju szybkim kalkulatorem. Z czasem jak będziemy programować w asemblerze wartości z tej tabelki zapiszą się w naszej pamięci na stałe i przeliczanie między systemami hex i bin nie będzie żadnym problemem.

dec hex bin
0 0 0000
1 1 0001
2 2 0010
3 3 0011
4 4 0100
5 5 0101
6 6 0110
7 7 0111
8 8 1000
9 9 1001
10 A 1010
11 B 1011
12 C 1100
13 D 1101
14 E 1110
15 F 1111