Bittioperaatiot C-kielessä¶
Osaamistavoitteet: Tämän materiaalin läpikäytyäsi tiedät miksi bittioperaatiot ovat tärkeitä laiteläheisessä ohjelmoinnissa ja miten niitä käytetään C-kielessä.
Muistiinkuvattu I/O¶
Hyvin usein sulautetuissa järjestelmissä kommunikaatio oheislaitteiden kanssa hoidetaan muistiinkuvatun I/O:n avulla (engl. Memory mapped Input/Output).
Muistamme aiemmasta yleisestä tietokoneen organisaation esittelystä, että oheislaitteet ovat kytkettyjä suorittimeen väylien kautta. Jotta oheislaitteiden kanssa työskentely olisi mahdollisimman helppoa ohjelmoijalle, osana samaisen tietokoneen arkkitehtuuria sen RAM-muistista voidaan varata muistilohkoja oheislaitteiden käyttöön. Kätevää, kuten kohta havaitaan. Vieläpä voidaan tehdä niin että ohjausosan logiikan avustuksella nämä lohkot kytketään (väylien kautta) suoraan oheislaitteen omaan sisäiseen muistiin. Tällaisesta ratkaisusta käytetään nimitystä muistiinkuvattu I/O.
Idean hienous on siinä, että suoritettavassa ohjelmassa voidaan kommunikoida oheislaitteen kanssa suoraan läpinäkyvästi, kirjoittamalla tai lukemalla näitä sille varattuja muistipaikkoja. Näistä laitekäyttöön varatuista muistipaikoista käytetään myös yleisnimitystä rekisteri.
Ja nyt voimme esimerkiksi lähettää viestin oheislaitteelle kirjoittamalla tietoa tarkoitukseen varattuun muistipaikkaan ja ohjauslogiikka hoitaa viestin perille oheislaitteelle. Vastaavasti viestit oheislaitteelta tietokoneelle voidaan lukea vastaavan muistipaikan kautta, kun ohjauslogiikka on hoitanut viestit näkyville varattuun muistipaikkaan. Hyvin yleinen tapa on ohjata oheislaitteita näiden muistipaikkojen kautta, esimerkiksi asettaa LCD-näytön taustavalo päälle muuttamalla ohjausbittien arvoja tarkoitukseen varatussa muistipaikassa. Siksi bittioperaatiot.
Tällaiset muistipaikat on (sulautetussa) järjestelmässä usein vakioitu ja kehitysympäristöjen valmiit kirjastot tarjoavatkin apuja, kuten valmiiksi esiteltyjä vakioita ja muuttujia, niiden käyttöön ohjelmissa.
Esimerkki. LCD-näytön ohjaus muistiinkuvatun I/O:n avulla.
Kuvan näytölle voisi olla RAM-muistissa muistipaikkoja varattuna seuraavasti:
- Kaksi 8-bittistä muistipaikkaa:
- 8-bittinen ohjausväylä, vaikka tarvitsemme vain johtimet/signaalit E, RW ja RS
- 8-bittinen dataväylä eli johtimet/signaalit D7-D0
- Yksi 16-bittinen muistipaikka: sisältää kaikki johtimet D7-D0, E, RW, RS
Nyt vain tarvitsisi esitellä ohjelmassa joko 8-bittiset muuttujat tai 16-bittinen muuttuja, jotka osoittavat haluttuun muistipaikkaan (miten tämä tapahtuu, kerrotaan myöhemmässä luentomateriaalissa). Sitten voisimme ohjelmassa näiden muuttujien avulla ohjata näyttöä ja kirjoittaa siihen tekstiä. Mutta, palataan tähän esimerkkiin hetken päästä..
C-kielen bittioperaatiot¶
Bittioperaatioihin C-kielessä kuuluvat digitaalitekniikasta johdetut loogiset operaattorit AND, OR, NOT, XOR ja lisäksi bittien siirto-operaatiot (engl. shift operations).
Siirto-operaatiot¶
C-kielessä bittien siirto-operaattorit ovat:
n << m
tarkoittaen luvun n bittien siirtoa vasemmalle (kohti MSB:tä) m:n bitin verran.n >> m
tarkoittaen luvun n bittien siirtoa oikealle (kohti LSB:tä) m:n bitin verran.
Esimerkkejä.
int8_t x = 5; // muuttuja x arvo 5 (eli 00000101)
x = x << 1; // siirto MSB:tä kohti yhden bitin verran
// arvo muuttuu 00000101 -> 00001010 = 10
int8_t x = 5;
x = x << 2; // arvo muuttuu 00000101 -> 00010100 = 20
int8_t x = 23; // x:n arvo 23 (00010111)
x = x >> 2; // giljotiini pudottaa alimmat bitit pois
// arvo muuttuu 00010111 -> 00000101 = 5
int8_t x = 118; // x = 01110110 = 118
x = x << 1; // arvo muuttuu 01110110 -> 11101100
// 2-komplementtiluvun merkkibitti vaihtui
// joten x:n arvio onkin nyt -20
Hox! Siirto MSB:tä kohti bitin verran vastaa siis luvun kokonaislukukertomista kahdella ja siirto LSB:tä kohti bitin verran luvun kokonaislukujakamista kahdella.
Esimerkki. Nyt näytölle on varattu yksi 16-bittinen muuttuja
uint16_t lcd
, josta haluamme lukea näytölle syötetyn merkin arvon (D7-D0). Tällöin meidän täytyy siirtää muuttujan bittejä, niin että tiputamme ohjaussignaalit pois ja saamme luettua muuttujasta 8-bittisen data-arvon. Tässä tarvittava bittioperaatio on
lcd = lcd >> 3;
, eli bittisiirto oikealle niin että bitit E, RW ja RS häviävät giljotiinissa.Katsotaanpa miltä bittisiirto näyttää avattuna. Tässä
d
tarkoittaa vastaavaa databittiä (D7-D0) ja x
sitä ettemme välitä kyseisen bitin arvosta. Nyt siis bitit E, RW ja RS eivät kiinnosta. // Bittisiirto avattuna ddddddddxxx lcd >> 3 ----------- 000dddddddd lcd
Lopputuloksena meillä on jäljellä vain databitit D7-D0, joiden paikka on muuttunut niin että D0 (eli datan pieni bitti) vastaa muuttujan
lcd
pienintä bittiä (eli LSB:tä). Näin muuttujassa on tallella vain haluttu arvo.Loogiset bittioperaatiot¶
Ohjelmoinnin Alkeet- ja digitaalitekniikan kursseilta tutuista loogisista operaatioista (AND, OR, XOR, NOT) bittioperaatiot erottaa se, että ne tehdään (binääri)luvun jokaiselle bitille erikseen, eikä esimerkiksi tarkastella koko muuttujan totuusarvoa sellaisenaan (Totuusarvoista lisää myöhemmin).
Loogisten bittioperaatioiden syntaksi C-kielessä on:
- JA/AND: operaattori
&
, totta kun molemmat bitit ovat yksi
00001111 = 15 & 10101010 = -86 -------- 00001010 = 10
- TAI/OR: operaattori
|
, totta kuin jompikumpi bitti on yksi
00001111 | 10101010 -------- 10101111
- "ERI"/XOR: operaattori
^
, totta jos toisiaan vastaavat bitit ovat "erit"
00001111 ^ 10101010 -------- 10100101
- negaatio/NOT: operaattori
~
, eli käännetään ykköset nolliksi ja päinvastoin
~ 00001111 -------- 11110000
Hox! On hyvin tavallista C-koodissa sekoittaa &-operaattori (bittioperaatio AND) ja looginen &&-operaattori (looginen AND).
Bittimaskit¶
Okei, bittioperaatioilla pääsemme todella pitkälle sulautettujen laitteiden ohjelmoinnissa, kun otamme vielä käyttöön apuvälineen nimeltä bittimaski. Bittimaski tarkoittaa (binääri)lukua, jolla merkitsemme halutut bitit, jolloin maskin avulla kohdistamme bittioperaation vain haluttuihin bitteihin!
Noh, mikä tässä on ongelma? Miksei voida vain sijoittaa muuttujaan uusi haluttu arvo? Katsotaanpas. Ylläolevaa näyttöä ohjatessa haluaisimme esimerkiksi muuttaa vain bitin E arvoa ja jättää muut ennalleen. Voimme toki tehdä tämän niin, että ensin selvitämme muuttujasta bitin E arvon (onko se 1 vai 0), vaihdamme arvon ohjelmakoodissa (1 -> 0 tai 0 -> 1) ja kirjoitamme muokatun arvon takaisin muuttujaan. Ja tilannehan eskaloituu kunnolla, jos haluamme käsitellä yo. muuttujasta useita bittejä ja niiden arvo(-kombinaatiot) pitäisi yksitellen selvittää. Nyt bittimaskit tarjoavat nokkelan keinon merkitä halutut bitit ja muuttaa niiden arvoa suoraviivaisesti.
Esimerkki. Haluamme käsitellä 8-bittisestä bittejä 1. ja 6., jolloin maski on seuraava.
bitti 87654321 -------- maski 00100001 binäärilukuna, eli 16 + 1 = 17 = 0x11
Bittimaski voidaan luoda joko sijoitusoperaatiolla tai bittioperaatiolla muuttujaan tai vakioarvoon.
uint8_t maski = 0x22; // maskiin merkitty bitit 00100010 = 0x20 + 0x02
uint8_t maski = (1 << 5) | (1 << 1) ; // bittisiirrolla 2. ja 6. bitti merkitty (00100010)
#define MASKI 0x22 // vastaava vakiolla MASKI
#define MASKI ((1 << 5) | (1 << 1)) // vastaava makro MASKI
// Yltä TAI-operaatio avattuna:
00100000 (1 << 5) = 0x20
| 00000010 (1 << 1) = 0x02
--------
00100010 = 0x22
Esimerkki. Palataanpa yo. näyttöön ja laaditaan sille data- ja ohjausbittejä vastaavat maskit.
D7-D0: MASK_DATA -> 11111111000 = 0x7F8 E: MASK_E -> 00000000100 = 0x4 RW: MASK_RW -> 00000000010 = 0x2 RS: MASK_RS -> 00000000001 = 0x1
Esimerkki. Luodaan kaikkia ohjausbittejä vastaava muuttuja
ohjausmaski
. uint16_t ohjausmaski = MASK_E | MASK_RW | MASK_RS;
// ..toisin sanoen ilman vakioita
uint16_t ohjausmaski = 0x4 | 0x2 | 0x1;
// Nyt siis TAI-operaatio
00000100 MASK_E
| 00000010 MASK_RW
| 00000001 MASK_RS
--------
00000111 ohjausmaski
Bittimaskin käyttö¶
Bittimaskilla voidaan tehdä muuttujalle/rekisterille erilaista muokkauksia yhdistämällä C-kielen bittioperaatioita yhdessä tai useammassa lauseessa.
Oletetaan nyt 8-bittinen rekisterimuuttuja
lcd
(pelkästään ohjaussignaalit), jonka bitit kuvataan merkinnällä x
(don't care). Tämä tarkoittaa että riippumatta onko x-bitin 1 tai 0, sen arvo asetetaan maskin mukaan. 1. OR-operaatio asettaa maskatut bitit päälle (tässä loogiseen tilaan 1)
Esimerkki yo. näytöllä, kun asetetaan bitti E (Enable) päälle, koskematta muiden bittien tilaan.
lcd = lcd | MASK_E;
// Operaatio avattuna:
xxxxxxxx lcd (x = dont care)
| 00000100 MASK_E
--------
xxxxx1xx lcd
Lopputuloksena, muuttujasta
lcd
vain maskin osoittama bitti pakotettiin tilaan 1 ja muut jäivät siihen tilaan x missä olivatkin. Kuten yllä esitettiin, TAI-operaatio 0:n kanssa ei muuta bitin arvoa. 2. Yhdistetyt AND- ja NOT-operaatiot asettavat maskatut bitit pois päältä (tässä loogiseen tilaan 0)
lcd = lcd & ~(MASK_E);
// MASK_E:n negaatio
~ 00000100 MASK_E
--------
11111011 ~MASK_E
// JA-operaatio muuttujan kanssa
xxxxxxxx lcd
& 11111011 ~MASK_E
--------
xxxxx0xx lcd
Lopputuloksena, muuttujasta lcd vain maskin osoittama bitti pakotettiin tilaan 0 ja muut jäivät siihen tilaan x missä olivatkin. Kuten yllä esitettiin, JA-operaatio 0:n kanssa muuttaa aina bitin arvon 0:ksi.
3. Bittien "irrotus" maskin avulla
Kun haluamme tarkastella yksittäisen tai useamman bitin arvoa, tarvitsemme keinoja bittien irrotukseen muuttujasta arvosta. Tämäkin onnistuu maskien avulla yhdistämällä bittioperaatioita.
// tarkastellaan bitin e arvoa uint16_t enable = (lcd & MASK_E) >> 2; // JA-operaatio näyttömuuttujan kanssa xxxxxxxx lcd & 00000100 MASK_E -------- 00000e00 enable (e:n arvo joko 0 tai 1) // siirto-operaatio 00000e00 enable >> 2 -------- 0000000e enable
Tämän jälkeen voimme ehtolauseella tarkistaa onko
enable
-muuttujan arvo 0 tai 1 (joka riippuu siitä mikä on bitin e arvo, kun muut ovat nollia) arvo. Huomataan, että tässä riittäisi tarkastella pelkästään muuttujan arvoa ilman bittisiirtoa, koska jos e=1, niin muuttujan enable
arvo > 0 aina. C-kielen operaattorien suoritusjärjestys¶
Ylläolevissa esimerkeissä käytimme jo useita eri operaatioita samassa C-kielen lauseessa, esimerkiksi sijoitus, bittisiirto/looginen operaatio ja negaatio. No, jotta hommassa ei sotkeutuisi pahan kerran, on juuri tässä kohden hyvä esittää operaatioiden yleinen suoritusjärjestys C-kielessä.
Ja jotta ao. taulukkoa ei tarvitsisi opetella ulkoa, aina on parempi käyttää sulkeita kertomaan haluttu operaattorien suoritusjärjestys, tekemään koodista luettavaa ja pitämään hyönteiset poissa ohjelmasta.
Taulukossa suoritussuunta tarkoittaa sitä, että jos meillä on saman prioriteetin operaattoreita useampi samassa lauseessa, niin miten päin niiden suoritusjärjestys tulkitaan, oikealta vasemmalle vai vasemmalta oikealle?
Lopuksi¶
Tämä materiaali tarjosi meille tärkeitä eväitä sulautettujen ohjelmointiin, sillä bittimaskit ovat yksi yleisimmin käytetyistä C-kielen keinoista ohjata oheislaitteita.
Usein eri oheislaitteiden kirjastot piilottavat nämä bittioperaatiot nätimpien funktiokutsujen alle, mutta kirjastojen lähdekoodia tarkastellessa sieltä alta ne kyllä löytyvät..
Anna palautetta
Kommentteja materiaalista?