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¶
Sulautetuissa järjestelmissä kommunikaatio oheislaitteiden kanssa voidaan hoitaa kahdella eri tavalla: Muistiinkuvatun I/O ja Port-Mapped I/O.
Port-Mapped I/O -järjestelmässä oheislaitteisiin päästään käsiksi erillisessä osoiteavaruudessa, joka on erotettu järjestelmän päämuistista. Tämä tarkoittaa, että prosessori käyttää erityisiä I/O-käskyjä (esim. IN ja OUT -käskyjä x86-arkkitehtuurissa) kommunikoidakseen laitteiden kanssa. Jokaiselle laitteelle annetaan ainutlaatuinen I/O-porttiosoite, ja prosessori lukee tai kirjoittaa näihin portteihin laitteiden kanssa toimiessaan.
Yleisin tapa kommunikoida oheislaitteiden kanssa on kuitenkin Muistiinkuvatun I/O:n avulla (engl. Memory mapped Input/Output).
Sulautetuissa järjestelmissä yleisin tapa kommunikoida oheislaitteiden kanssa on kuitenkin muistiinkuvattu I/O (engl. Memory-Mapped I/O). Muistamme aiemmasta tietokonearkkitehtuurin käsittelystä, että suoritin kommunikoi muistin ja oheislaitteiden kanssa väylien kautta. Suoritin asettaa osoitteen osoiteväylälle, siirtää dataa dataväylän kautta ja käyttää ohjaussignaaleja ilmaisemaan toimintoja, kuten lukemista ja kirjoittamista.
Muistiinkuvatussa järjestelmässä kaikki suorittimen muistiosoiteavaruuden osoitteet eivät vastaa RAM-muistia. Osa osoitealueista on sen sijaan varattu oheislaitteille. Kun suoritin lukee tällaisesta osoitteesta tai kirjoittaa siihen, osoitteenpurku- ja ohjauslogiikka ohjaa operaation järjestelmän väylien kautta vastaavalle oheislaitteelle RAM-muistin sijaan.
C-ohjelman näkökulmasta oheislaitteen käyttäminen voi tämän vuoksi näyttää hyvin samankaltaiselta kuin muistissa olevan arvon käyttäminen. Näin oheislaitteen käyttöön näkyviin asetettuja laitteistopaikkoja kutsutaan yleisesti oheislaite-rekistereiksi.
GPIO port mapping in RP2040.
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.
Oletetaan, että näytöllä on rekistereitä, joiden yksittäiset bitit vastaavat kuvassa esitettyjä signaaleja. Rajapinta voitaisiin esimerkiksi toteuttaa seuraavasti:
- Kaksi 8-bittistä muistiinkuvattua rekisteriä:
- ohjausrekisteri, jossa on bitit signaaleille E, RW ja RS
- datarekisteri, jossa on bitit signaaleille D7-D0
- Tai yksi 16-bittinen muistiinkuvattu rekisteri, jossa on bitit sekä signaaleille D7-D0 että ohjaussignaaleille E, RW ja RS
Oleellinen ajatus on, ettei ohjelma käsittele fyysisiä signaalijohtimia suoraan. Sen sijaan ohjelma lukee ja kirjoittaa rekisterien bittejä, ja laitteisto yhdistää rekisterien arvot vastaaviin signaaleihin.
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ä.
Hox! Oheislaiterekisterien arvot voivat muuttua ohjelmasta riippumatta, koska rekisterit ovat yhteydessä laitteistoon. Tämän vuoksi muistiinkuvattuja rekistereitä käsitellään C-kielessä tyypillisesti
volatile-määreen avulla. Se kertoo kääntäjälle, että arvo voi muuttua odottamattomasti eikä rekisteriin kohdistuvia luku- tai kirjoitusoperaatioita saa optimoida pois tavallisen muuttujan tavoin.Palaamme muistiinkuvattujen rekisterien tarkkaan C-kieliseen käsittelytapaan myöhemmin kurssilla.
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 << mtarkoittaen luvun n bittien siirtoa vasemmalle (kohti MSB:tä) m:n bitin verran.n >> mtarkoittaen 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! Arvon siirtäminen yhden bitin verran MSB:tä kohti vastaa kertomista kahdella, kunhan merkittäviä bittejä ei siirry pois luvusta. Yhden bitin siirto LSB:tä kohti vastaa kokonaislukujakoa kahdella siten, että vähiten merkitsevä bitti häviää.
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
|, * TAI/OR: operaattori|, totta kun vähintään toinen biteistä on yksi
00001111 | 10101010 -------- 10101111
- "ERI"/XOR: operaattori
^, * "ERI"/XOR: operaattori^, tulos on 1 kun toisiaan vastaavat bitit ovat erilaiset (toinen on 0 ja toinen 1)
00001111 ^ 10101010 -------- 10100101
- negaatio/NOT: operaattori
~, eli käännetään ykköset nolliksi ja päinvastoin
~ 00001111 -------- 11110000
Hox! Jos käytät etumerkillisiä lukuja, ole varovainen! Bittioperaatiot voivat muuttaa luvun merkkibittiä ja siten myös luvun etumerkkiä.
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 76543210
--------
maski 00100010
Bittimaski voidaan luoda käyttämällä suoraa vakioarvoa:
uint8_t maski = 0x22;
tai bittisiirtojen avulla:
uint8_t maski = (1 << 5) | (1 << 1);
OR-operaatio voidaan avata seuraavasti:
00100000 (1 << 5) = 0x20 | 00000010 (1 << 1) = 0x02 -------- 00100010 = 0x22
Sama maski voidaan määritellä myös makrona:
#define MASKI 0x22
tai:
#define MASKI ((1 << 5) | (1 << 1))
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). Seuraavissa esimerkeissä x tarkoittaa merkintää 'don't care': bitin nykyinen arvo voi olla joko 0 tai 1.Bittimaskin avulla valitaan bittipaikat, joita haluamme käsitellä. Maski itsessään ei määritä, mitä näille biteille tapahtuu. Maskin kanssa käytetty bittioperaatio määrittää, asetetaanko, nollataanko, vaihdetaanko vai irrotetaanko valitut bitit. Kahdessa ensimmäisessä alla olevassa esimerkissä operaatiot on muodostettu siten, että maskin ulkopuoliset bitit säilyvät muuttumattomina.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; // vastaava lyhyempi muoto 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); // vastaava lyhyempi muoto lcd &= ~(MASK_E);
// MASK_E:n negaatio
// Tässä esitetään vain tarkastelun kannalta olennaiset 8 bittiä.
~ 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.
Edellinen taulukko on saatu tästä lähteestä (Wei-keng Liao).
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?
Taulukossa esitetty assosiatiivisuus kertoo, miten saman prioriteetin operaattorit ryhmitellään lausekkeessa: joko vasemmalta oikealle tai oikealta vasemmalle.
4. Bittien tilan vaihtaminen XOR-operaatiolla
XOR-operaatiolla voidaan vaihtaa valittujen bittien tila päinvastaiseksi:
- 0 muuttuu arvoksi 1
- 1 muuttuu arvoksi 0
Maskin ulkopuoliset bitit säilyvät muuttumattomina, koska XOR-operaatio nollan kanssa säilyttää alkuperäisen bitin arvon.
Esimerkki: Vaihdetaan bitin E tila:
lcd ^= MASK_E;
// Operaatio avattuna:
xxxxx1xx lcd
^ 00000100 MASK_E
--------
xxxxx0xx lcd
Tässä
!e tarkoittaa bitin e vastakkaista arvoa.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?