Bitn..... in C voor beginners.

© Harry Broeders.

Deze pagina is bestemd voor studenten van de THRijswijk.

Werktuigbouwkundig ingenieurs worden wel eens oneerbiedig aangesproken met de vakterm: "fietsenmaker". Zo bestaat er ook een vakterm voor E of TI ingenieurs die zich bezighouden met het programmeren van microcontrollers: bitn...... Op deze pagina wordt uitgelegd op welke manieren je met bitjes kunt spelen en hoe je dat in C (veilig ;-) moet doen.

Bitje veranderen.

De onderstaande voorbeelden veranderen een bitje in het PORTB register van de 68HC11. Op de EVM kast is deze PORTB verbonden met 8 ledjes (PB0 t/m PB7) zodat we meteen het resultaat van de bewerking kunnen zien.

Bitje setten.

Je kunt een bitje setten (1 maken) met behulp van een bitwise-or operator. Je moet het bitje dat je wilt setten or-en met 1 en de rest met 0. Om dus bijvoorbeeld ledje PB3 aan te zetten moeten we PORTB or-en met het binaire getal: 00001000.

int main() {
    typedef unsigned char byte;
    volatile byte* portb=(byte*)0x1004;
    *portb=*portb|0x08;
    return 0;
}

De betekenis en het nut van het keyword volatile wordt bij de eerste practicumopgave uitgelegd.

De regel:
   *portb=*portb|0x08;
kun je ook verkorten tot:
   *portb|=0x08;

Let op! Er zit een groot verschil tussen de bitwise-or operator | en de logical-or operator ||. Bij de bitwise-or wordt de or bewerking bit-voor-bit uitgevoerd. 0x2c|0x09 is dus gelijk aan 0x2d. Zet de getallen even om naar het binaire talstelsel als je het niet meteen ziet. Bij de logical-or wordt het getal omgezet naar een logische (binaire) waarde (true of false). Daarna wordt de or bewerking uitgevoerd met als resultaat true (1) of false (0). 0x2c||0x09 is dus gelijk aan 0x01. Als in het bovenstaande programma de | operator vervangen wordt door de || operator gaat ledje PB0 branden! Begrijp je dat?

Er is nog een verschil tussen de bitwise-or operator | en de logical-or operator ||. Bij de logical-or operator worden de operanden van links naar rechts uitgerekend en zodra het antwoord bekend is wordt de berekening gestopt. Dit wordt short-circuit evaluation genoemd. Bij de bitwise-or wordt de expressie altijd helemaal doorgerekend. Voorbeeld: Als de expressie fun1()||fun2() wordt uitgevoerd en fun1() geeft true terug dan wordt fun2() niet aangeroepen (het antwoord van de expressie is true). Als fun1()|fun2() wordt uitgevoerd dan worden fun1() en fun2() altijd beide aangeroepen (ook als fun1() allemaal enen teruggeeft).

Er is nog een subtiel verschil. Bij de logical-or operator ligt de evaluatievolgorde van de operanden vast maar bij de bitwise-or niet. Voorbeeld: Als de expressie fun1()||fun2() wordt uitgevoerd wordt fun1() als eerste aangeroepen (fun2() wordt mogelijk helemaal niet aangeroepen). Als fun1()|fun2() wordt uitgevoerd dan is het compiler afhankelijk of eerst fun1() of eerst fun2() wordt aangeroepen (ze worden wel gegarandeerd beide aangeroepen).

Bitje clearen.

Je kunt een bitje clearen (0 maken) met behulp van een bitwise-and operator. Je moet het bitje dat je wilt clearen and-en met 0 en de rest met 1. Om dus bijvoorbeeld ledje PB3 uit te zetten moeten we PORTB and-en met het binaire getal: 11110111.

int main() {
    typedef unsigned char byte;
    volatile byte* portb=(byte*)0x1004;
    *portb=*portb&0xf7;
    return 0;
}

De regel:
   *portb=*portb&0xf7;
kun je ook verkorten tot:
   *portb&=0xf7;

Het is ook mogelijk om bij het clearen hetzelfde bitpatroon te gebruiken als bij het setten. Je moet dan de compiler zelf de inverse laten uitrekenen door middel van de bitwise-not ~ operator:
   *portb&=~0x08;

Let op! Er zit een groot verschil tussen de bitwise-and operator & en de logical-and operator &&. Bij de bitwise-and wordt de and bewerking bit-voor-bit uitgevoerd. 0x2c&0x09 is dus gelijk aan 0x08. Zet de getallen even om naar het binaire talstelsel als je het niet meteen ziet. Bij de logical-and wordt het getal omgezet naar een logische (binaire) waarde (true of false). Daarna wordt de and bewerking uitgevoerd met als resultaat true (1) of false (0). 0x2c&&0x09 is dus gelijk aan 0x01.

Er is nog een verschil tussen de bitwise-and operator & en de logical-and operator &&. Bij de logical-and operator worden de operanden van links naar rechts uitgerekend en zodra het antwoord bekend is wordt de berekening gestopt. Dit wordt short-circuit evaluation genoemd. Bij de bitwise-and wordt de expressie altijd helemaal doorgerekend. Voorbeeld: Als de expressie fun1()&&fun2() wordt uitgevoerd en fun1() geeft false terug dan wordt fun2() niet aangeroepen (het antwoord van de expressie is false). Als fun1()&fun2() wordt uitgevoerd dan worden fun1() en fun2() altijd beide aangeroepen (ook als fun1() allemaal nullen teruggeeft).

Er is nog een subtiel verschil. Bij de logical-and operator ligt de evaluatievolgorde van de operanden vast maar bij de bitwise-and niet. Voorbeeld: Als de expressie fun1()&&fun2() wordt uitgevoerd wordt fun1() als eerste aangeroepen (fun2() wordt mogelijk helemaal niet aangeroepen). Als fun1()&fun2() wordt uitgevoerd dan is het compiler afhankelijk of eerst fun1() of eerst fun2() wordt aangeroepen (ze worden wel gegarandeerd beide aangeroepen).

Let op! Er zit een groot verschil tussen de bitwise-not operator ~ en de logical-not operator !. Bij de bitwise-not wordt de not bewerking bit-voor-bit uitgevoerd. ~0x2c is dus gelijk aan 0xd3. Zet de getallen even om naar het binaire talstelsel als je het niet meteen ziet. Bij de logical-not wordt het getal omgezet naar een logische (binaire) waarde (true of false). Daarna wordt de not bewerking uitgevoerd met als resultaat true (1) of false (0). !0x2c is dus gelijk aan 0x00.

Bitje flippen.

Je kunt een bitje flippen (inverteren) met behulp van een bitwise-exor operator. Je moet het bitje dat je wilt flippen exor-en met 1 en de rest met 0. Om dus bijvoorbeeld ledje PB3 te inverteren moeten we PORTB or-en met het binaire getal: 00001000.

int main() {
    typedef unsigned char byte;
    volatile byte* portb=(byte*)0x1004;
    *portb=*portb^0x08;
    return 0;
}

De regel:
   *portb=*portb^0x08;
kun je ook verkorten tot:
   *portb^=0x08;

Er bestaat in C vreemd genoeg geen logical-exor operator.

Meerdere bitjes veranderen.

Als je meerdere bitjes wilt setten, meerdere bitjes wilt clearen of meerdere bitjes te inverteren dan kun je dat doen door in het bitpatroon waarmee je respectievelijk de bitwise-or, bitwise-and of bitwise-exor uitvoert meerdere bitjes te setten.

In het onderstaande voorbeeld worden PB4 en PB2 geset, PB5 en PB1 gecleared en PB7, PB6 en PB0 geïnverteerd:

int main() {
    typedef unsigned char byte;
    volatile byte* portb=(byte*)0x1004;
    *portb|=0x14;
    *portb&=~0x22;
    *portb^=0xc1;
    return 0;
}

Bitje testen.

De onderstaande voorbeelden testen een bitje in het PORTC register van de 68HC11. Op de EVM kast is deze PORTC verbonden met 8 schakelaars (PC0 t/m PC7) zodat we de programma's eenvoudig kunnen testen. Als de test true oplevert worden ledje PB7 aangezet en als de test false oplevert wordt ledje PB0 aangezet zodat we meteen het resultaat van de test kunnen zien.

Is het bitje 1?

Je kunt testen of een bitje 1 is door dit bitje te "isoleren" van de andere bitjes in de betreffende variabele. De overige bits worden gemaskeerd. Je kunt een bitje isoleren door een bitwise-and bewerking. Het volgende voorbeeld zal als schakelelaar PC3 1 is ledje PB7 laten branden en anders ledje PB0 laten branden:

int main() {
    typedef unsigned char byte;
    volatile byte* portb=(byte*)0x1004;
    volatile byte* portc=(byte*)0x1003;
    if ((*portc&0x08)==0x08) {
        *portb=0x80;
    }
    else {
        *portb=0x01;
    }
    return 0;
}

De extra haakjes in de if instructie zijn noodzakelijk omdat de bitwise-and operator & een lagere prioriteit heeft dan de vergelijkings operator ==.

De regel:
    if ((*portc&0x08)==0x08) {
kun je ook verkorten tot:
    if (*portc&0x08) {

De expressie *portc&0x08 geeft namelijk als resultaat 0x08 als schakelaar PC3 1 is en 0x00 als schakelaar PC3 0 is. 0x08 is ongelijk aan nul en wordt dus gezien als de logische waarde true en 0x00 is gelijk aan nul en wordt dus gezien als de logische waarde false.

Is het bitje 0?

Je kunt testen of een bitje 0 is door dit bitje te "isoleren" van de andere bitjes in de betreffende variabele. De overige bits worden gemaskeerd. Je kunt een bitje isoleren door een bitwise-and bewerking. Het volgende voorbeeld zal als schakelelaar PC3 0 is ledje PB7 laten branden en anders ledje PB0 laten branden:

int main() {
    typedef unsigned char byte;
    volatile byte* portb=(byte*)0x1004;
    volatile byte* portc=(byte*)0x1003;
    if ((*portc&0x08)==0x00) {
        *portb=0x80;
    }
    else {
        *portb=0x01;
    }
    return 0;
}

De regel:
    if ((*portc&0x08)==0x00) {
kun je ook verkorten tot:
    if (!(*portc&0x08)) {
of tot:
    if (~*portc&0x08) {

De extra haakjes in de tweede if instructie zijn noodzakelijk omdat de bitwise-and operator & een lagere prioriteit heeft dan de logical-not operator !.

De expressie *portc&0x08 geeft namelijk als resultaat 0x00 als schakelaar PC3 0 is en 0x08 als schakelaar PC3 1 is. 0x00 is gelijk aan nul en wordt dus gezien als de logische waarde false en 0x08 is ongelijk aan nul en wordt dus gezien als de logische waarde true. Als je deze logische waarde met een logical-not operator inverteert krijg je de waarde true als bit PC3 0 is en false als PC3 1 is.

Je kunt ook eerst een bitwise-not uitvoeren op de van PORTC gelezen waarde. Alle bitjes (dus ook bitje PC3) worden dan geinverteerd.

Meerdere bitjes testen.

Je kunt vaak meerdere bitjes met 1 bewerking testen door meerdere bitjes te isoleren.

In het onderstaande voorbeeld wordt PB7 1 als PC5 1 is en PC3 1 is. Als dit niet het geval is wordt PB0 1.

int main() {
    typedef unsigned char byte;
    volatile byte* portb=(byte*)0x1004;
    volatile byte* portc=(byte*)0x1003;
    if ((*portc&0x28)==0x28) {
        *portb=0x80;
    }
    else {
        *portb=0x01;
    }
    return 0;
}

Let op! De regel:
    if ((*portc&0x28)==0x28) {
kun je nu niet verkorten!

In het onderstaande voorbeeld wordt PB7 1 als PC5 1 is of PC3 1 is (of beide 1 zijn). Als dit niet het geval is wordt PB0 1.

int main() {
    typedef unsigned char byte;
    volatile byte* portb=(byte*)0x1004;
    volatile byte* portc=(byte*)0x1003;
    if (*portc&0x28) {
        *portb=0x80;
    }
    else {
        *portb=0x01;
    }
    return 0;
}

Schuiven met bitjes.

In C zijn ook operatoren gedefinieerd waarmee je een bitpatroon kunt schuiven. Deze operatoren worden shift-operators genoemd en het zijn binaire operatoren (er zijn 2 operanden). De operator << schuift naar links en de operator >> naar rechts. Aan de linkerkant van de shift-operator staat het patroon dat verschoven moet worden en aan de rechterkant staat het aantal plaatsen wat geschoven moet worden, de zogenaamde shift-count. In C++ is de << operator overloaded voor stream output en is de >> operator overloaded voor stream input. Maar dit zit de schuifbewerkingen niet in de weg.

In het onderstaande voorbeeld wordt het bitpatroon van de schakelaars ingelezen en 2 plaatsen naar links geschoven naar de leds gestuurd. Als op de schakelaars 0xB2 staat zal op de leds dus 0xC8 verschijnen. Zet de getallen om naar het binaire talstelsel als je het niet meteen ziet.

int main() {
    typedef unsigned char byte;
    volatile byte* portb=(byte*)0x1004;
    volatile byte* portc=(byte*)0x1003;
    *portb=*portc<<2;
    return 0;
}

Er bestaat ook een <<= en een operator >>= waarmee schuiven en assignment gecombineerd kunnen worden. In het volgende programma wordt de waarde die op de led staat drie plaatsen naar rechts geschoven:

int main() {
    typedef unsigned char byte;
    volatile byte* portb=(byte*)0x1004;
    *portb>>=3;
    return 0;
}

Bij het schuiven naar links worden er altijd nullen ingeschoven. Schuiven van x plaatsen naar links komt overeen met vermenigvuldigen met 2x. Het onderstaande programma levert dus exact hetzelfde resultaat als het bovenstaande programma waarin 2 plaatsen naar links wordt geschoven:

int main() {
    typedef unsigned char byte;
    volatile byte* portb=(byte*)0x1004;
    volatile byte* portc=(byte*)0x1003;
    *portb=*portc*4;
    return 0;
}

Bij schuiven naar rechts is het wat ingewikkelder.

Als het patroon unsigned is worden er ook nullen ingeschoven. Dus als we in het bovenstaande programma de << operator vervangen door een >> operator verschijnt de waarde 0x2C op de leds. Bij unsigned getallen komt x plaatsen schuiven naar rechts overeen met delen door 2x. De twee onderstaande programma's leveren dus exact hetzelfde resultaat:

int main() {
    typedef unsigned char byte;
    volatile byte* portb=(byte*)0x1004;
    volatile byte* portc=(byte*)0x1003;
    *portb=*portc>>2;
    return 0;
}
int main() {
    typedef unsigned char byte;
    volatile byte* portb=(byte*)0x1004;
    volatile byte* portc=(byte*)0x1003;
    *portb=*portc/4;
    return 0;
}

Als het patroon signed is wordt bij het inschuiven bit7 gekopieerd. In het onderstaande voorbeeld wordt het bitpatroon van de schakelaars als een signed getal ingelezen en 2 plaatsen naar links geschoven naar de leds gestuurd. Als op de schakelaars 0xB2 staat zal op de leds dus 0xEC verschijnen. Zet de getallen om naar het binaire talstelsel als je het niet meteen ziet.

int main() {
    typedef signed char sbyte;
    volatile sbyte* portb=(sbyte*)0x1004;
    volatile sbyte* portc=(sbyte*)0x1003;
    *portb=*portc>>2;
    return 0;
}

Bij negatieve signed getallen komt x plaatsen schuiven naar rechts ook overeen met delen door 2x maar is het resultaat vreemd genoeg niet hetzelfde als het resultaat van de / operator. Als je in het bovenstaande programma de schuifbewerking vervangt door een deling en de schakelaars op 0xB2 zet dan zal op de leds de waarde 0xED verschijnen.

int main() {
    typedef signed char sbyte;
    volatile sbyte* portb=(sbyte*)0x1004;
    volatile sbyte* portc=(sbyte*)0x1003;
    *portb=*portc/4;
    return 0;
}

Bij signed schuiven naar rechts is de rest (wat er wordt uitgeschoven) altijd positief bij signed delen is de rest negatief als het deeltal negatief is.

Bij delen met behulp van de >> operator: 0xB2 gedeeld door 4 = 0xEC rest 0x02 (rest is wat er wordt uitgeschoven). In het signed two's complement talstelsel is dit dus decimaal: -78 gedeeld door 4 = -20 rest 2.

Bij delen met behulp van de / operator: 0xB2 gedeeld door 4 = 0xED rest 0xFE (rest kun je bepalen met de % operator). In het signed two's complement talstelsel is dit dus decimaal: -78 gedeeld door 4 = -19 rest -2.

Beide antwoorden zijn wiskundig correct. Want -20*4+2 = -78 en -19*4-2 = -78.