Bittioperaatiot C-kielessä¶
Osaamistavoitteet: Tämän materiaalin läpikäytyäsi tiedät miten bittioperaatioita tehdään C-kielellä ja miksi ne ovat tärkeitä laiteläheisessä ohjelmoinnissa.
Bittioperaatiot on meidän varsin tärkeää ymmärtää, niillä on oma merkittävä roolinsa tietokonetekniikassa yleisesti digitaalipiirien toiminnan toteuttamisessa. Mutta myös ohjelmoidessa sulautettuja me tarvitsemme seuraavia: AND, OR, NOT, XOR ja lisäksi siirto-operaatiot (engl. shift). Yleinen käyttötarkoitus on ohjata tietokoneen tai oheislaitteen digitaalipiirejä ohjelmallisesti.
Hox! Jo ohjelmoinnista tutuista loogisista operaatioista, esimerkiksi if-lauseen totuusarvo, bittioperaatiot erottaa se, että ne tehdään (binääri)luvun jokaiselle bitille erikseen, eikä esimerkiksi tarkastella koko numeroarvoa sellaisenaan.
Siirto-operaatiot¶
Siirto-operaatiot ovat C-kielessä:
- Siirto "vasemmalle" lukua n m:n bitin verran
n << m
- Siirto "oikealle" lukua n m:n bitin verran
n >> m
Esimerkkejä.
01100101 << 1 = 11001010 (desimaalilukuna 101 << 1 = 202) 01100101 << 2 = 10010100 (desimaalilukuna 101 << 2 = 148 ??) 01100101 >> 1 = 00110010 (desimaalilukuna 101 >> 1 = 50) 01100101 >> 2 = 00011001 (desimaalilukuna 101 << 2 = 25)
Kuten huomaamme, siirto-operaatio pudottaa molempiin suuntiin ''reunan yli" bittejä. Tämä johtuu siitä, että muuttujan tyyppi ei salli enempää kuin sovitun määrän bittejä. Tässä siis toteutuu aiemmin mainittu giljotiini myös.
Siirto vasemmalle yhden bitin verran vastaa siis lukualueen rajoissa luvun kertomista kahdella ja siirto oikealle yhden bitin verran vastaa luvun jakamista kahdella.
Loogiset bittioperaatiot¶
Loogiset bittioperaatiot ovat C-kielessä:
&
AND|
OR^
XOR~
NOT eli negaatio
Ja ne käyttäytyvät seuraavasti. Operaatio kohdistetaan luvun / lukujen jokaiseen vastinbittiin erikseen.
00001111 AND: molempien operandien toisiaan vastaavat bitit ovat ykkösiä & 10101010 -------- 00001010 00001111 OR: jommankumman operadin bitti on yksi | 10101010 -------- 10101111 00001111 XOR: operadien toisiaan vastaavat bitit ovat "erit" (0 ja 1 tai 1 ja 0) ^ 10101010 -------- 10100101 ~ 00001111 NOT: käännetään ykköset nolliksi ja päinvastoin -------- 11110000
Hox! On hyvin tavallista koodissa sekoittaa &-operaattori (bittioperaatio AND) ja &&-operaattori (looginen AND).
Bittimaskit¶
Sijoitusoperaatiot ja ym. bittioperaatiot kohdistuivat kaikkiin luvun bitteihin asettaen niille jommankumman tilan. Tämä ei ole oikein kätevää, jos haluamme koskea jonkin muuttujan yksittäiseen bittiin.
// muuttuja alustetaan arvoon 76
muuttuja = 76; // binäärilukuna 01001100
// muuttujassa haluttaisiin asettaa eka bitti päälle
muuttuja = 1; // binäärilukuna 00000001, eli usean eri bitin arvo muuttui!
Ilman bittimaskeja, vaihtaaksemme jonkin yksittäisen bitin tilan, joutuisimme selvittämään ensin kaikkien muiden bittien tilan ja pitämään ne ennallaan. Aika iso operaatio koodata itse, kun pitäisi testata jokaisen bitin tila erikseen esim. if-lauseella.
Käyttämällä maskia meidän ei tarvitse välittää muiden bittien arvoista. Nyt bittimaskilla valitaan halutut bitit muuttujasta, joihin operaatio kohdistuu. Tämä tapahtuu merkitsemällä maskiin ne halutut bitit ykköseksi ja muut pysyvät nollina. Ts. luomme binääriluvun joka vastaa haluttua maskia.
8-bittisellä maskilla 00100001 operaatio kohdistuu 1. ja 6. bittiin. Taas maskilla 11000000 operaatio kohdistuu 7. ja 8. bittiin.
Bittimaski voidaan luoda joko sijoitusoperaatiolla tai bittioperaatiolla muuttujaan tai vakioarvoon.
uint8_t maski = 0x22; // 00100010
uint8_t maski = (1 << 5) | (1 << 1) ; // siirrolla maski jossa 2. ja 6. bitti merkitty (00100010)
#define MASKI 0x22 // Vakio nimeltä MASKI, jossa heksaluku 0x22 vastaavat bitit
#define MASKI ((1 << 5) | (1 << 1)) // siirrola maski vakiossa (hox! sulkeiden käyttö)
Tai-operaatio avattuna:
00100000
| 00000010
--------
00100010 = 0x22
Bittimaskin käyttäminen¶
Bittimaskeja laiteläheisessä ohjelmoinnissa käytetään ohjaamaan oheislaitetta asettamalla sen ohjausväylän valitut bitit haluttuun arvoon ja tulkitsemaan oheislaitteen tilaa tai sen lähettämiä viestejä. Esimerkiksi, kirjoitusoperaatiota voisi vastata bitin tila 1 ja lukuoperaatiota tila 0. Eli tarvitsisi operaatiosta riippuen vaihtaa vain tämän bitin arvo! Bittimaskilla kohdistuu operaatio juuri haluttuun bittiin ja muut jää ennalleen. Tämä tiedoksi tässä vaiheessa, asiaan palaamme jatkossa..
Bittioperaatioilla voidaan maskia hyväksikäyttäen siis..
1. Asettaa vain halutut bitit ykkösiksi ("päälle") TAI-operaatiolla.
Oletetaan:
uint8_t ohjausrekisteri = 0x3; // 00000011
uint8_t maski = 0x10; // 00010000
Vaadittava bittioperaatio on:
ohjausrekisteri = ohjausrekisteri | maski;
Operaatio avattuna:
00000011 ohjausrekisteri ennen
| 00010000 maski
--------
00010011 ohjausrekisteri jälkeen
Eli lopputuloksena, ohjausrekisteri-muuttujasta vain maskin osoittama bitti asetettiin ykköseksi ja muut bitit jäivät siihen tilaan missä olivat. (Tämän jälkeen ohjausrekisterin muuttunut arvo pitäisi lähettää laitteelle.)
2. Asettaa vain halutut bitit nollaksi ("pois päältä") AND- ja NOT-operaatioilla.
Oletetaan:
uint8_t ohjausrekisteri = 0x3F; // 00111111
uint8_t maski = 0x12; // 00010010
Vaadittava bittioperaatio on:
ohjausrekisteri = ohjausrekisteri & ~(maski);
Operaatio avattuna:
~ 00010010 maski
--------
11101101 maskin negaatio
00111111 ohjausrekisteri ennen
& 11101101 maskin negaatio
--------
00101101 ohjausrekisteri jälkeen
Eli lopputuloksena, ohjausrekisteri-muuttujasta vain maskin osoittamat bitit asetettiin nollaksi ja muut bitit jäivät siihen tilaan missä olivat.
Koodiesimerkki¶
Koodiesimerkki reaalimaailmasta... sulautetun laitteen tietyn ledin asetus päälle ja pois, niin ettei muiden ledien tiloihin kosketa.
Määritellään numeraaliset vakiot, jotka kuvaavat ledejä vastaavia bittejä:
#define LED_POWER 0 // Eli nyt power-lediä vastaa bitti nro 1.
#define LED_MEASURE 1 // lediä vastaa bitti nro 2.
#define LED_DEBUG 4 // ...
Operaatiot (ledien tila näkyy ledit-muuttujassa:
uint8_t ledit |= (1 << LED_DEBUG); // debug-ledi päälle TAI-operaatiolla itsensä kanssa
ledit &= ~(1 << LED_DEBUG); // debug-ledi pois päältä AND- ja NOT-operaatioilla
Puretaanpas nämä hieman mystisen näköiset bittioperaatiot
uint8_t ledit |= (1 << LED_DEBUG); // debug-ledi päälle
Tarkoittaa samaa kuin:
ledit = ledit | (1 << LED_DEBUG);
Bittimaski: (1 << LED_DEBUG) = (1 << 4) = 00010000, kun korvataan vakio LED_DEBUG sen numeroarvolla
Sovitaan, että bitit joiden arvoa emme tiedä emmekä niistä välitä, merkitään x:llä.
Nyt ledit-muuttujan alkutilanne on siis xxxxxxxx
Tehdään TAI-operaatio:
ledit xxxxxxxx
maski 00010000 |
--------
xxx1xxxx
Nyt siis x:n TAI-operaatio 0:n kanssa ei muuta x:n arvoa miksikään,
mutta TAI-operaatio ykkösen kanssa pakottaa x:n ykköseksi.
Joten, lopputuloksena bitit josta emme olleet kiinnostuneita pysyivät x:nä
ja valittu bitti "pakotettiin" ykköseen.
Entäs ledin asetus pois päältä..
ledit &= ~(1 << LED_DEBUG); // debug-ledi pois päältä
Tarkoittaa samaa kuin:
ledit = ledit & ~(1 << LED_DEBUG);
Nyt taas ledit-muuttujan alkutilanne on xxxxxxxx
Bittimaski, kun korvataan vakio sen numeroarvolla (1 << 4) = 00010000
Tehdään ensin maskin negaatio, jonka tulos on 11101111
Sitten JA-operaatio negaation kanssa:
ledit xxxxxxxx
maski 11101111 &
--------
xxx0xxxx
Nyt siis x:n JA-operaatio ykkösen kanssa ei muuta x:n arvoa miksikään,
biteissä 1-4 ja 6-8, mutta JA-operaatio nollan kanssa pakottaa 5. bitin nollaan.
Joten, lopputuloksena bitit, joista emme olleet kinnostuneita, pysyivät x:nä
ja valittu bitti "pakotettiin" nollaan.
Noin, nyt osaat jo ohjata C-kielellä sulautetun laitteen tai sen oheislaitteen toimintaa! Tässä voisi olla kakkukahvin paikka..
Maskit ja rekisterit¶
Sulautetuissa järjestelmissä oheislaitteet usein viestivät CPU:n kanssa erityisten omien varattujen muistipaikkojensa, eli rekisterien, kautta. Näistä lisää tulevassa materiaalissa, mutta otetaan tässä esimerkki miten bittioperaatioilla käsitellään rekisterien arvoja. Esimerkiksi ohjataan oheislaitteen toimintaa tai luetaan siltä dataa.
Alla kuvassa 16-bittisen rekisterin kuvaus, jossa on identifioitu väreillä kolme bittiryhmää
x3..x0
, y7..y0
ja z3..z0
. Oheislaite itse määrittää mitä nämä eri ryhmät ja niiden bitit merkitsevät (ja se ei meitä tässä vaiheessa vielä kiinnosta), mutta esimerkiksi ryhmässä x voisi olla osoitelinja, ryhmä y voisi olla datalinja ja ryhmä z voisi olla ohjauslinjan bitit. (Tulevilla luennoilla pureudumme asiaan tarkemmin, mutta usein oheislaitteiden väylät on kuvattu muuttujina mikrokontrollerin muistiin, jolloin niitä helppo käsitellä koodissa. )Yllä kuvassa siis esitettiin miten voimme bittioperaatioilla irroittaa halutut arvot 16-bittisestä (rekisteri)muuttujasta. Halutut bitit
y7..y0
merkittiin maskilla ja käytimme JA-bittioperaatioita saadaksemme halutut bitit irti muista rekisterimuuttujan arvoista. Sitten tarvitsemme vielä siirto-operaation, koska muutoin binäärilukumme alin bitti y0 (LSB) ei vastaa pienintä kakkosen potenssia. Ts. Ilman siirto-operaatiota LSB-bitin y0 painoarvo on 2^4 eikä 2^0, jolloin sen numeroarvo on väärä. Lopuksi¶
Tämä jokseenkin kuiva materiaali tarjosi meille tärkeitä eväitä sulautettujen ohjelmointiin. Bittimaskit ovat yksi yleisimmin käytetyistä C-kielen keinoista ohjata oheislaitteita. Usein eri oheislaitteiden kirjastot piilottavat nämä bittioperaatiot ohjelmoijalle nätimpien funktio-kutsujen alle, mutta kirjastojen lähdekoodia tarkastellessa sieltä alta ne kyllä löytyy!
Alla operaatioiden yleinen suoritusjärjestys C-kielessä. Tämä taulukko voi auttaa bugien selvittelyssä, mutta yleisesti on parempi käyttää sulkeita tekemään koodista luettavaa ja pitämään hyönteiset poissa koodista. Erityisesti vakioiden kanssa suositaan sulkeita, josta lisää myöhemmin..
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? Yleisellä operaattorilla Q:
(a Q b) Q c
vai a Q (b Q c)
?Anna palautetta
Kommentteja materiaalista?