Ratkaistu: / tehtävää

Bitit ja lukujärjestelmät

Osaamistavoitteet: Tämän materiaalin läpikäytyäsi tiedät miten tietoa esitetään tietokoneessa binäärilukuina ja osaat tehdä muunnoksia tietokonetekniikassa käytettyjen lukujärjestelmien välillä.
Aiemmilla ohjelmoinnin kursseilla opimme mitkä ovat tietokoneen (ja eri ohjelmointikielten) tuntemat lukujärjestelmät: binääri-, kymmen- ja heksadesimaaliluvut. Näistä kymmenjärjestelmä on mukana vain meidän ihmisten, ts. ohjelmoijien ja käyttäjien, takia. Tietokone vaatimattomana laskimena ei tarvitse kuin binääriluvut ja laiteläheisten ohjelmoijien tulee tietää miten tietokone ymmärtää binääriluvut ja niiden bitit.

Bitit tietokoneen muistissa

Bitti on tietoalkion (=informaation) pienin käsiteltävissä oleva osa. Bitillä on kaksi toisensa poissulkevaa loogista tilaa, 0 ja 1. Looginen tila tässä tarkoittaa sitä, että on suorittimen, laiteohjelmiston/käyttöjärjestelmän ja ohjelmoijan päätettävissä mitä bitin tilat tarkoittavat.
Yksittäinen bitti siis esittää aika vähän informaatiota, mutta kun bitit asetetaan jonoon binääriluvuksi, voidaankin esittää aika monipuolisesti informaatiota. Edellisellä luennoilla opimme, että suorittimen arkkitehtuuri määrittelee käsiteltävän tietoalkoon koon (sanan pituus) n-bittisenä binäärilukuna.
Esimerkiksi sulautetussa järjestelmässä oheislaitteen kanssa pelatessa, looginen tila 0 voisi tarkoittaa että oheislaite on valmis vastaanottamaan komentoja ja 1 että laite on busy, eli vielä suorittamassa edellistä komentoa.

Muistipaikka

Yleisesti on siis suoraviivaista ja sikäli hyödyllistä, että tietokoneen muisti vastaavasti organisoidaan muistipaikkoihin, joiden koko on sanan pituus. Tällöin sanan pituudesta (dataväylä) ja muistin koosta (osoiteväylä) saadaan tarvittava muistiväylän leveys.
Esimerkinomaisesti alla pätkä arkkitehtuuriltaan 8-bittisen tietokoneen muistia. Nyt muistia on vain 16 muistipaikkaa, joten osoiteväylän leveys on 4-bittiä (2^4=16). (Näin pieni muisti voisi olla vaikka haihtumaton EEPROM-muisti sulautetussa laitteessa.)
Muistiosoite Sisältö Tulkinta 10-järjestelmässä
0000 00000011 3
0001 11111111 -1
0010 10110111 -73
0011 00110110 54
.. .. ..
1111 00110111 55
Ylläolevassa muistissa muistiosoitteessa 3 (0011) on tallessa arvo 54 (00110110). Tällainen tilanne voisi syntyä, kun olisimme ohjelmassa esitelleet 8-bittisen muuttujan, joka olisi alustettu arvoon 54. Kääntäjä ja käyttöjärjestelmä (menemättä yksityiskohtiin) päätyivät siihen, että kyseinen arvo talletetaan muistipaikkaan nro 3, josta se on ohjelman käytettävissä sen ajonaikana.
Esimerkki. Muistipaikan osoitteen voisi helposti ajatella katuosoitteena. Jokaisella 
kadunvarren talolla on järjestysnumero, joka yksilöllisesti osoittaa talon, ja jokaiseen
taloon voidaan tallettaa yksi tieto. Ylläolevassa muistiesimerkissä talossa nro 3 olisi 
tallessa siis luku 54, josta se voidaan käydä kysymässä tai kertomassa tilalle uusi luku.
(Palaamme tähän taloesimerkkiin myöhemmässä materiaalissa..)

Nimitysuutisia

Nyt, erikokoisille binääriluvuille on tietotekniikassa yleisesti määritelty nimityksiä:
Varsinkin englanninkieliset nimet on syytä tuntea osana tieto/sähkötekniikan insinöörin ammattitaitoa ja niitä käytetäänkin digitaali- ja tietotekniikassa yleisesti.

Binääriluvun tulkinta

Ikävä kyllä se vielä riitä, että osaamme yhdistää ohjelmamme muuttujan nimen tietokoneen muistipaikkaan ja sen arvoon. Lisäksi tietokoneen pitää osata tulkita muistissa oleva binääriluku.

Bittijärjestys

Binääriluvun esittämisessä suorittimen laskentayksikölle on vielä pari mutkaa matkassa, sillä pitää myös sopia miten suoritin tulkitsee muistipaikan bittijonon.
Kymmenjärjestelmässä olemme sopineet, että merkittävin numero on vasemmanpuoleisin numero ja siitä oikealle päin laskevassa järjestyksessä. Esimerkiksi, luvussa 217 isoin painoarvo on luvulla 2, joka kuvaa satasia. Luku on siis 2 x 100 + 1 x 10 + 7 x 1 = 217. Mutta, mikään muu kuin keskinäinen sopimus ei estä meitä lukemasta lukua myös toisesta suunnasta 7 x 100 + 1 x 10 + 2 x 1 = 712.
Sama koskee tietysti binäärilukuja. Nyt esimerkiksi luku 1011 voidaan lukea samoin vasemmalta oikealle tai toisinpäin. Suunnasta riippuen luvun arvo voi muuttua, joka sotkee asiat jos sopimukseen ei ole päästy.
"MSB and LSB"
Lukusuunnan määritelyssä puhutaan bittijärjestyksestä, jonka määräämiseksi tarvitaan kaksi määritelmää:
No, bittijärjestyksen ovat meidän puolestamme suorittimen arkkitehtuurin suunnittelijat aikoinaan päättäneet. Koska suunta voi vielä nykyäänkin vaihdella, se on kerrottu suorittimen käsikirjoissa. Yleensä ohjelmoijan ei tarvitse kiinnittää suorittimen osalta asiaan huomiota, koska ohjelmointikielissä voidaan määritellä bittijärjestys halutuksi ja kääntäjät sitten muokkaavat koodin suorittimelle sopivaksi. Mutta, sulautetuissa järjestelmissä oheislaitteiden kanssa pelaillessa bittijärjestykseen joutuu kiinnittämään huomiota.

Tavujärjestys

Sekään ei vielä riitä, että tiedämme missä järjestyksessä binääriluvun bittijono pitää tulkita. Erityisesti sulautetuissa järjestelmissä pääsemme usein käsittelemään lukuja, jotka ovat sanan pituutta suurempia!
Esimerkiksi 8-bittisessä sulautetun järjestelmän suoritinarkkitehtuurissa voimme yleisesti hyvinkin käsitellä 16- tai 32-bittisiä lukuja.
Ratkaisu on pätkäistä luku sanan pituuden sallimiin pätkiin, jolloin 8-bittisessä arkkitehtuurissa 16-bittisestä luvusta saadaan kaksi 8-bittistä lukua ja 32-bittisestä neljä 8-bittistä lukua. Muistiin nämä luvut sijoitetaan sitten peräkkäisiin muistipaikkoihin. Esimerkkinä, 8-bittisissä Arduinoissa C-kielen kääntäjät yleisesti sallivat käyttää isompiakin lukuja, ja kääntäjä sitten muokkaa koodin mikrokontrollerille sopivaksi (tällöin konekielessä isompien lukujen käsittelyssä joudutaan operaatiot tekemään vaiheittain.)
Palataan hetkeksi yllä esitettyyn muistiin:
Muistiosoite Muistipaikan sisältö Tulkinta 10-järjestelmässä
0000 00000011 3
0001 11111111 -1
0010 10110111 -73
0011 00110110 54
Nyt tässä muistidumpissa (raakakopio muistin sisällöstä) on neljä tavua, jotka voivat tarkoittaa neljää 8-bittistä lukua, kahta 16-bittistä lukua (muistipaikat 0-1 ja 2-3) ja yhtä 32-bittistä lukua (muistipaikat 0-3). Ok, mutta.. vastaavasti herää kysymys että missä järjestyksessä tavut luetaan? Onko 32-bittisen luvun ensimmäinen tavu muistipaikassa 0 vai 3?
No, arvatenkin myös tavujärjestys (eli tavujen lukusuunta) on vastaavasti suoritinarkkitehtuurin suunnittelun yhteydessä päätetty asia. Tässäkin käytetään kahta yleistä termiä:
Esimerkki. PC-työasemien nykyinen x86-64 -prosessoriarkkitehtuuri on little endian, 
mutta bittijärjestys taas eniten merkitsevä ensin, eli bitit luetaan vasemmalta 
oikealle MSB->LSB.

Sopimus

Jotta kurssilla ymmärrämme toisiamme, niin määritellään tässä yhteiset lukusuunnat:
Esimerkki. Tulkinta 16-bittiselle luvulle.
MSB    ->    LSB
1100101000101100 = -13780

Pyrkimys säästeliäisyyteen

Jees, hieno homma. Mutta miksi nämä tavut ja sanat kiinnostavat meitä.. eikö riittäisi, että käytetään aina vain 64-bittistä arkkitehtuuria sovelluksissa, niin ei lopu lukualue kesken ja osaahan ne kääntäjätkin hanskata isot luvut?
Työasemalla ohjelmoidessahan asia ei juuri liikuta, koska resursseja kuten muistia ja prosessoritehoa on riittämiin. Kun taas sulautetuissa järjestelmissä mikrokontrollereissa on yleensä pieni määrä RAM-muistia, tyyliin muutama kilotavu, joka pitää jakaa sovelluksen lisäksi firmiksen (ja käyttöjärjestelmän kesken). Näin ollen muistin käyttöä joudutaankin sulautetuissa sovelluksissa miettimään tarkemmin.
Oleellinen asia tässä on esittää ohjelmassa juuri sopivan kokoisia muuttujia. Nyt, jos tarvitsemamme lukualue mahtuu 8-bittiseen muuttujaan, niin miksi esitellä ohjelmassa sitä varten 64-bittinen muuttuja? Jälkimmäisessä tapauksessa muistia kuluu kahdeksan tavua, kun taas sovelluksessa oikeastaan pärjättäisiin yhdellä tavulla. Työasemaohjelmointiin tottuneelle muutaman tavun säästö voi tuntua olemattomalta saavutukselta, mutta tarkastellaan asiaa sulautettujen maailmassa esimerkin kautta.
Esimerkki. ATmelin ATtiny-sarjan mikrokontrollereissa RAM-muistia on jopa alle yhden kilotavun 
(1024 tavua). Lasketaanpa hieman. Nyt jos käytetään 32-bittisiä muuttujia, niitä mahtuu muistiin 
maksimissaan (1024 tavua / 4 tavua = 256). Jos taas käytetään 16-bittisiä muuttujia, niitä mahtuisi 
muistiin (1024 / 2 tavua = 512). Mutta eikös satakin muuttujaa ole jo aika paljon yhteen ohjelmaan? 

No, riippuu sovelluksesta. Esimerkiksi jos kerätään nopeassa tahdissa (useita kertoja sekunnissa) 
vaikkapa kiihtyvyysanturidataa kolmelta akselilta (X, Y, Z) taulukkoon, ei muutaman kilotavun 
muisti enää riittäne..
Tähän asiaan palaamme jo heti seuraavassa materiaalissa, kun esittelemme C-kielen muuttujatyypit.

Negatiiviset binääriluvut

Aiemmin olemme käyttäneet binäärilukuja positiivisten lukujen esittämiseen, mutta hei.. mites negatiiviset binääriluvut?
No, ei hätää, useita ratkaisuja on tietotekniikan esitetty, jotka perustuvat sopimukseen, että varataan binääriluvusta joku bitti merkkibitiksi esittämään etumerkkiä (+ / -). Yleisesti tämä bitti on MSB, jonka arvo 0 tarkoittaa positiivista lukua ja 1 negatiivistä lukua. Kun varaamme yhden bitin etumerkin käyttöön, jää meillä itse lukualueeseen n-1 -bittiä.
Esimerkiksi 8-bittisen luvun lukualue tippuu 7:n bittiin, eli positiivinen lukualue olisi enää 0..127. Tosin nyt saamme lukualueeseen tilalle etumerkin avulla lisäksi negatiiviset lukualueen -128..-1. Ilman etumerkkiä lukualue olisi 0-255, joten lukualue ikäänkuin siirtyy negatiiviseen suuntaan MSB:n arvon verran.

Suora esitystapa

Suorassa esitystavassa (engl. signed magnitude) varataan biteistä eniten merkitsevä (MSB) merkkibitiksi. Tästä seuraa, että nyt esimerkiksi 0010 olisi 2 ja 1010 olisi -2.
Huomataan, että tällöin nollalla on kaksi esitystä, eli 0000 (ikäänkuin +0) ja 1000 (ikäänkuin -0). Tämä on huono juttu, koska ohjelmassa joudutaan huomioimaan molemmat esitykset nollalle.

Yhden komplementti

Nyt negatiivinen luku saadaan ottamalla positiivisesta looginen negaatio. Esimerkiksi, kun luku 3 on 0011 binäärilukuna, niin -3 olisi 1100.
Tässäkin esitystavassa on nollalle kaksi esitystä, 0000 ja 1111, joten ei paras eikä hyvä meille.

Kahden komplementti

Kahden komplementti-esitystapa on yleisesti digitaalitekniikassa / tietotekniikassa käytetty, joten uhrataan sille hieman enemmän aikaa ja kurssimateriaalia.
Sovitaan taas, että eniten merkitsevä bitti (MSB) on merkkibitti eli 0 positiivinen ja 1 negatiivinen.
  1. Positiivisesta binääriluvusta otetaan negaatio (eli nollat ykkösiksi ja päinvastoin).
  2. Lisätään negaatioon 1.
Esimerkkejä 4-bittisillä luvuilla. 

Luku 1    0001       Luku 2    0010       
Negaatio  1110       Negaatio  1101  
Lisäys      +1       Lisäys      +1                  
          ----                 ----                
          1111 = -1            1110 = -2 
Muunnos takaisin negatiivisesta positiiviseksi mennään ihan samoin (kätevää!):
Luku -1   1111       Luku -2   1110      
Negaatio  0000       Negaatio  0001   
Lisäys      +1       Lisäys      +1                  
          ----                 ----                
          0001 = 1             0010 = 2
Esimerkki. Kuvassa 4-bittiset luvut ilman etumerkkiä ja 2-komplementtiesityksenä.
"Lukualueet"

Kätevyyttä

2-komplementtiesitys on myös siitä kätevä, että 2-komplementtilukujen yhteenlasku toimii samoin oli operandi positiivinen tai negatiivinen. Tämä helpottaa merkittävästi digitaalitekniikassa esimerkiksi laskentaa suorittavien logiikkapiirien toteutusta.
Esimerkki. Laske 2-komplementtiluvuilla 2+3 = ? ja 2-3 = 2+(-3) = ?

Luku +2 0010            Luku +2 0010
Luku +3 0011            Luku -3 1101
+       ----            +       ----
        0101 = 5                1111 = -1
(Suorassa menetelmässä ja 1-komplementti-esityksessä tämä ei aina toimi, kokeile löytää esimerkki jossa yhteenlasku ei toimi!)

Niksi-Pirkasta päivää

Ja sitten terveisiä Niksi-Pirkka osastolta.. negatiivisen binääriluvun muunnos kymmenjärjestelmään voidaan tehdä ilman laskintakin yhteenlaskua hyödyntäen. Idea on, että ajatellaan:
Kuvassa alla siis MSB-bitin arvo ajatellaan negatiivisena ja muiden positiivisena.
"Yhteenlaskua vain"
Nyt muunnos tapahtuu siten, että lasketaan yhteen bittien arvoa vastaavat kymmenjärjestelmän luvut.
Esimerkki. 8-bittiset luvut 99 ja -99. 

99 on binäärilukuna 01100011 eli 64 + 32 + 2 + 1 = 99
-99 on binäärilukuna 10011101 eli -128 + 16 + 8 + 4 + 1 = -99
Muunna 6-bittinen 2-komplementtiluku 101011 kymmenjärjestelmään yhteenlasku-menetelmällä.
Tarvitaan vain vastaus.
Varoitus: Et ole kirjautunut sisään. Et voi vastata.

Heksadesimaaliluvut

Toinen tietokonetekniikassa usein käytetty kantalukujärjestelmä on 16. Tällöin puhutaan heksadesimaalinumeroista, jotka käsittävät numerot 0-9 ja 10-15, joita jälkimmäisiä merkitään kirjaimilla A-F.
"Heksadesimaaliluvut"
Huomataan, että jokainen heksadesimaalinumero voidaan esittää 4:llä bitillä (ns. nibble). Tämä onkin yksi erittäin hyvä syy miksi heksadesimaalilukuja tietotekniikassa ja ohjelmoinnissa käytetään. Nyt voidaan yksi tavu esittää kahdella heksadesimaalinumerolla (vs. hankalasti biteiksi tulkittava kymmenjärjestelmän kokonaisluku vs. silmille epämukava binääriluku).
Muunnos heksalukujen ja binäärilukujen välillä on helppo kun tiedämme, että jokaista heksanumeroa siis vastaa 4 bittiä. Tarvitsee vain muuttaa heksanumerot yksi kerrallaan 4-bittisiksi binääriluvuiksi.
Esimerkki. binääriluku 01011101 on heksana 5D ja kymmenjärjestelmässä 93.
   5    D
0101 1101
0 + 64 + 0 + 16 + 8 + 4 + 0 + 1 = 93
Esimerkki. Muunna heksadesimaaliluku 173A4C binääriluvuksi?
   1    7    3    A    4    C   
0001 0111 0011 1010 0100 1100
Monissa ohjelmointikielissä heksaluvut esitetään käyttäen etuliitettä 0x. Esimerkiksi kaikki ao. luvut ovat valideja heksalukuja.
0x0
0x0123456789ABCDEF
0xA
0x1000
0x001
0xBEEF
0xC0DE
0xBA5EBA11

Lopuksi

.. joskus eri lukujärjestelmien kanssa laskeminen voi saada kummallisia käänteitä (http://www.xkcd.com/571)
"Lampaita laskiessa"
Nooh, nythän meille on selvää mitä kolmannessa ruudussa tapahtuu! Ystävämme laskee lampaita 16-bittisillä 2-komplementtiluvuilla ja käy niin ikävästi, että unta odotellessa positiivinen lukualue ehtii loppua, jolloin merkkibitti vaihtuu ja mennäänkin negatiiviselle lukualueelle.
...
0111111111111111 = 32767 lammasta
              +1 lammas
1000000000000000 = -32768 lammasta // etumerkki vaihtui kun MSB 0 -> 1
              +1 lammas
1000000000000001 = -32767 lammasta
...
?