© Harry Broeders.
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.
De onderstaande voorbeelden veranderen een bitje in het PORTB
register van de AVR ATmega328P. Op het display shield van de Hogeschool
Rotterdam is de pin 3 van poort B (PB3) verbonden met de rode led van een RGB
led zodat we meteen het resultaat van de bewerking kunnen zien. Het
DDRB
register moet dan wel geladen worden met
0b00001110
om pin 3, pin 2 en pin1 van poort B op output te zetten
(met deze pinnen sturen we de 3 ledjes in de RGB led aan) .
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 pin PB3 één te maken moeten we PORTB
or-en met
het binaire getal: 00001000.
#include <avr/io.h>
int main(void) {
DDRB = 0b00001110;
/* PB3, PB2 en PB1 op output */
PORTB = PORTB | 0b00001000; /* alleen pin PB3 1 maken, de overige pinnen van poort B worden niet gewijzigd */
while (1);
return 0;
}
De regel:
PORTB = PORTB | 0b00001000;
kun je ook verkorten tot:
PORTB |= 0b00001000;
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.
0b00101100|0b00001001
is dus gelijk aan 0b00101101
.
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). 0b00101100||0b00001001
is dus gelijk aan
0b00000001
. Als in het bovenstaande programma de |
operator vervangen wordt door de ||
operator wordt pin PB3 niet
geset! 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 beiden 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 beiden
aangeroepen).
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 pin PB3 nul te maken moeten we PORTB
and-en
met het binaire getal: 11110111.
#include <avr/io.h>
int main(void) {
DDRB = 0b00001110;
/* PB3, PB2 en PB1 op output */
PORTB = PORTB & 0b11110111; /* alleen pin PB3 0 maken, de overige pinnen van poort B worden niet gewijzigd */
while (1);
return 0;
}
De regel:
PORTB = PORTB & 0b11110111;
kun je ook verkorten tot:
PORTB &= 0b11110111;
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 &= ~0b00001000;
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. 0b00101100&0b00001001
is dus gelijk aan
0b00001000
. 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).
0b00101100&&0b00001001
is dus gelijk aan
0b00000001
. Als in het bovenstaande programma de
&
operator vervangen wordt door de &&
operator dan wordt pin PB3 niet geset! Begrijp je dat?
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 beiden 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 beiden 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.
~0b00101100
is dus gelijk aan 0b11010011
. 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). !0b00101100
is dus gelijk aan 0b00000000
.
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 pin PB3 te inverteren moeten we
PORTB
exor-en met het binaire getal: 00001000.
#include <avr/io.h>
int main(void) {
DDRB = 0b00001110;
/* PB3, PB2 en PB1 op output */
PORTB = PORTB ^ 0b00001000; /* alleen pin PB3 inverteren, de overige pinnen van poort B worden niet gewijzigd */
while (1);
return 0;
}
De regel:
PORTB = PORTB ^ 0b00001000;
kun je ook verkorten tot:
PORTB ^= 0b00001000;
Er bestaat in C vreemd genoeg geen logical-exor operator.
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:
#include <avr/io.h>
int main(void) {
DDRB = 0b11111111;
/* alle pinnen poort B op output */
PORTB = 0b01011010;
/* willekeurige test waarde op poort B */
PORTB |= 0b00010100; /* alleen PB4 en PB2 1 maken */
PORTB &= ~0b00100010; /* alleen PB5 en PB1 0 maken */
PORTB ^= 0b11000001; /* alleen PB7, PB6 en PB0 inverteren */
while (1);
return 0;
}
De onderstaande voorbeelden testen een bitje in het PINB
register van de AVR ATmega328P. Op het display shield van de Hogeschool
Rotterdam kan pin 4 van poort B (PB4) verbonden worden met de voedingsspanning
via een drukknopje dat SW3 is genoemd. We kunnen dus met SW3 de waarde op pin
PB4 aanpassen. Als SW3 wordt ingedrukt staat er een logische 1 op ingang PB4 en
als SW2 niet wordt ingedrukt dan is ingang PB4 via een weerstand verbonden met
de ground en staat er dus een logische 0. In de onderstaande programma's
gebruiken we PB4 zodat we het gedrag van het programma met SW3 kunnen testen.
Als de test true oplevert wordt pin PB3 hoog gemaakt (het rode ledje wordt
aangezet) en als de test false oplevert wordt pin PB3 laag gemaakt (het rode
ledje wordt uitgezet) zodat we meteen het resultaat van de test kunnen zien.
Het DDRB
register moet dan wel geladen worden met
0b00001110
om de pinnen van poort B die met de RGB led zijn
verbonden op output te zetten en de overige pinnen van poort B, waaronder
degene die verbonden is met SW3, op input te zetten..
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 SW3 ingedrukt is (pin PB4 is dan 1) alleen het rode ledje laten branden (PB3 wordt hoog gemaakt) en anders (SW3 niet ingedrukt) het rode ledje niet laten branden (PB3 wordt laag gemaakt):
#include <avr/io.h>
int main(void) {
DDRB = 0b00001110;
/* PB3, PB2 en PB1 op output */
PORTB = 0b00000000;
/* Alle outputs op 0 en pull-up weerstanden uitzetten op alle ingangen */
while (1) {
if ((PINB & 0b00010000) == 0b00010000) {
PORTB |= 0b00001000;
}
else {
PORTB &= 0b11110111;
}
}
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((PINB & 0b00010000) == 0b00010000) {
kun je ook verkorten tot:
if(PINB & 0b00010000) {
De expressie (PINB &
0b00010000)
geeft namelijk als resultaat 0b00010000
als pin PB4 één is en
0b00000000
als pin PB4 nul is. 0b00010000
is ongelijk
aan nul en wordt dus gezien als de logische waarde true en
0b00000000
is gelijk aan nul en wordt dus gezien als de logische
waarde false.
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 SW3 niet ingedrukt is (PB4 is dan 0) alleen het rode ledje laten branden (PB3 wordt hoog gemaakt) en anders (SW3 niet ingedrukt) het rode ledje niet laten branden (PB3 wordt laag gemaakt):
#include <avr/io.h>
int main(void) {
DDRB = 0b00001110;
/* PB3, PB2 en PB1 op output */
PORTB = 0b00000000;
/* Alle outputs op 0 en pull-up weerstanden uitzetten op alle ingangen */
while (1) {
if ((PINB & 0b00010000) == 0b00000000) {
PORTB |= 0b00001000;
}
else {
PORTB &= 0b11110111;
}
}
return 0;
}
De regel:
if ((PINB &
0b00010000) == 0b00000000) {
kun je ook verkorten tot:
if (!(PINB &
0b00010000)) {
of tot:
if (~PINB &
0b00010000) {
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 (PINB &
0b00010000)
geeft namelijk als resultaat 0b00000000
als pin PB4 nul is en 0b00010000
als pin PB4 één is.
0b00000000
is gelijk aan nul en wordt dus gezien als de logische
waarde false en 0b00010000
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 PB4 nul
is en false als PB4 één is.
Je kunt ook eerst een bitwise-not uitvoeren op de uit
PINB
gelezen waarde. Alle bitjes (dus ook bitje PB4) worden dan
geinverteerd. Hierna kan je dan op de hierboven beschreven manier testen of
bitje 4 in de geïnverteerde waarde van PINB
één is (~PINA & 0b00001000)
. Er zijn daarbij geen
extra haakjes nodig omdat de bitwise-not operator een hogere prioriteit heeft
dan de bitwise-and operator.
Je kunt vaak meerdere bitjes met één bewerking testen door meerdere bitjes te isoleren.
In het onderstaande voorbeeld wordt alleen het rode ledje aangezet als PB4 één is en PB5 één is (dus als SW3 ingedrukt is en SW2 ingedrukt is). Als dit niet het geval is (SW3 of SW2 is niet ingedrukt of beiden zijn niet ingedrukt) dan wordt het rode ledje niet uitgezet.
#include <avr/io.h>
int main(void) {
DDRB = 0b00001110;
/* PB3, PB2 en PB1 op output */
PORTB = 0b00000000;
/* Alle outputs op 0 en pull-up weerstanden uitzetten op alle ingangen */
while (1) {
if ((PINB & 0b00110000) == 0b00110000) {
PORTB |= 0b00001000;
}
else {
PORTB &= 0b11110111;
}
}
return 0;
}
De onderstaande waarheidstabel kan helpen bij het doorgronden van de werking van het bovenstaande programma:
SW2 | SW3 | PB5 | PB4 | PINB & 0b00110000 |
(PINB & 0b00110000) == 0b00110000 |
PORTB |
PB3 | Rode ledje |
---|---|---|---|---|---|---|---|---|
niet ingedrukt | niet ingedrukt | 0 | 0 | 0b00000000 |
false | 0b00000000 |
0 | uit |
niet ingedrukt | wel ingedrukt | 0 | 1 | 0b00010000 |
false | 0b00000000 |
0 | uit |
wel ingedrukt | niet ingedrukt | 1 | 0 | 0b00100000 |
false | 0b00000000 |
0 | uit |
wel ingedrukt | wel ingedrukt | 1 | 1 | 0b00110000 |
true | 0b00001000 |
1 | aan |
Let op! De regel:
if((PINB & 0b00110000) == 0b00110000) {
kun je nu niet verkorten!
De regel:
if(PINB & 0b00110000) {
geeft namelijk een heel ander resultaat. De expressie (PINB & 0b00110000)
levert namelijk ook true
op als alleen pin PB5 één is of alleen PB4 één is!
In het onderstaande voorbeeld wordt het rode ledje aangezet als PB5 één is of PB4 één is (dus als SW2 ingedrukt is of SW3 ingedrukt is). Met of wordt hier een inclusieve of bedoeld. Dus het rode ledje wordt ook aangezet als PB5 één is en PB4 één is.
#include <avr/io.h>
int main(void) {
DDRB = 0b00001110;
/* PB3, PB2 en PB1 op output */
PORTB = 0b00000000;
/* Alle outputs op 0 en pull-up weerstanden uitzetten op alle ingangen */
while (1) {
if ((PINB & 0b00110000) != 0b00000000) {
PORTB |= 0b00001000;
}
else {
PORTB &= 0b11110111;
}
}
return 0;
}
De onderstaande waarheidstabel kan helpen bij het doorgronden van de werking van het bovenstaande programma:
SW2 | SW3 | PB5 | PB4 | PINB & 0b00110000 |
(PINB & 0b00110000) != 0b00000000 |
PORTB |
PB3 | Rode ledje |
---|---|---|---|---|---|---|---|---|
niet ingedrukt | niet ingedrukt | 0 | 0 | 0b00000000 |
false | 0b00000000 |
0 | uit |
niet ingedrukt | wel ingedrukt | 0 | 1 | 0b00010000 |
true | 0b00001000 |
1 | aan |
wel ingedrukt | niet ingedrukt | 1 | 0 | 0b00100000 |
true | 0b00001000 |
1 | aan |
wel ingedrukt | wel ingedrukt | 1 | 1 | 0b00110000 |
true | 0b00001000 |
1 | aan |
De regel:
if((PINB & 0b00110000) != 0b00000000) {
kun je verkorten tot:
if(PINB & 0b00110000) {
De expressie (PINB &
0b00110000)
levert namelijk ook true op als alleen pin PB5 één
is of alleen PB4 één is.
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 het onderstaande voorbeeld wordt het bitpatroon van de schakelaars op PB5
en PB4 ingelezen en 3 plaatsen naar rechts geschoven naar de leds gestuurd. Als
op de schakelaars 0b00010000
staat (SW3 is ingedrukt) zal op de
leds dus 0b00000010
verschijnen (het blauwe ledje brandt).
#include <avr/io.h>
int main(void) {
DDRB = 0b00001110;
/* PB3, PB2 en PB1 op output */
PORTB = 0b00000000;
/* Alle outputs op 0 en pull-up weerstanden uitzetten op alle ingangen */
while (1) {
PORTB = PINB >> 3;
}
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 schakelaars staat
eerst ingelezen in een variabele en daarna drie plaatsen naar rechts
geschoven:
#include <avr/io.h>
int main(void) {
DDRB = 0b00001110;
/* PB3, PB2 en PB1 op output */
PORTB = 0b00000000;
/* Alle outputs op 0 en pull-up weerstanden uitzetten op alle ingangen */
while (1) {
uint8_t b;
b = PINB;
b >>= 3;
PORTB = b;
}
return 0;
}
Bij het schuiven naar links worden er altijd nullen ingeschoven. Schuiven van x plaatsen naar links komt overeen met vermenigvuldigen met 2x. De schuifoperatie:
a = b << 2;
geeft exact hetzelfde resultaat als de vermenigvuldiging:
a = b * 4;
Bij schuiven naar rechts is het wat ingewikkelder.
Als het patroon unsigned is worden er ook nullen ingeschoven. Als de
unsigned 8 bits waarde 0b10111101
twee plaatsen naar rechts wordt
geschoven, dan levert dat de waarde 0b00101111
op. Bij
unsigned getallen komt x plaatsen schuiven naar rechts overeen
met delen door 2x. Als gegeven is dat de variabelen
a
en b
gedefinieerd zijn van het type
uint8_t
, dan levert de schuifoperatie:
a = b >> 2;
exact hetzelfde resultaat als de deling:
a = b / 4;
Als het patroon signed is wordt bij het inschuiven de tekenbit (bit7)
gekopieerd. Als de 8 bits signed waarde 0b10111101
twee plaatsen
naar rechts wordt geschoven, dan levert dat de waarde 0b11101111
op.
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 gegeven is dat de variabelen a
en b
gedefinieerd zijn van het type int8_t
, dan levert de
schuifoperatie:
a = b >> 2;
niet hetzelfde resultaat als de deling:
a = b / 4;
Als b
de waarde 0b10111101
heeft dan krijgt
a
na de schuifoperator de waarde 0b11101111
. Als
b
de waarde 0b10111101
heeft dan krijgt
a
na de schuifoperator de waarde 0b11110000
.
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 door vier met behulp van de >>
operator:
0b10111101 >> 2
= 0b11101111
rest
0b01
(rest is wat er wordt uitgeschoven). In het signed two's
complement talstelsel is dit dus decimaal: -67 gedeeld door 4 = -17 rest 1.
Deze vorm van delen wordt "Euclidean division" genoemd: http://en.wikipedia.org/wiki/Euclidean_division
.
Bij delen met behulp van de /
operator: 0b10111101 /
4
= 0b11110000
rest 0b11111101
(de rest kun je
bepalen met de %
operator). In het signed two's complement
talstelsel is dit dus decimaal: -67 gedeeld door 4 = -16 rest -3. Deze manier
van delen wordt "truncated division" genoemd: http://en.wikipedia.org/wiki/Modulo_operation.
Beide antwoorden zijn wiskundig correct. Want -17 * 4 + 1 = -67 en -16 * 4 + -3 = -67. Zie eventueel http://en.wikipedia.org/wiki/Remainder#The_case_of_general_integers .
Bij het manipuleren en testen van afzonderlijke bits wordt vaak gebruik
gemaakt van bitpatronen of maskers waarin op slecht één positie een 1
voorkomt. Om bijvoorbeeld pin PB3 één te maken moeten we PORTB
or-en met het binaire patroon: 00001000.
#include <avr/io.h>
int main(void) {
DDRB = 0b00001110;
PORTB |= 0b00001000;
while (1);
return 0;
}
Je kunt het benodigde patroon ook uit laten rekenen door de compiler door de constante 1 drie plaatsen naar links te schuiven:
#include <avr/io.h>
int main(void) {
DDRB = 0b00001110;
PORTB |= 1<<3;
while (1);
return 0;
}
De expressie 1<<3
wordt door de compiler uitgerekend en
levert de waarde 0b00001000
op zodat beide programma's exact
dezelfde machinecode opleveren. De meeste mensen vinden het tweede programma
beter leesbaar omdat je meteen ziet dat bit 3 van PORTB
geset
wordt.
Als in het benodigde patroon meer dan 1 bit geset moeten worden dan kan dit
door verschillende schuifexpressies met een bitwise-or met elkaar te
combineren. In het onderstaande programma worden de bits 3, 2 en 1 van DDRB
één gemaakt d.m.v. <<
en |
operatoren:
#include <avr/io.h>
int main(void) {
DDRB = 1<<3 | 1<<2 | 1<<1;
PORTB |= 1<<3;
while (1);
return 0;
}
De regel:
DDRB = 1<<3 | 1<<2 | 1<<1;
kan natuurlijk ook vervangen worden door:
DDRB = 0x0E;
Dit is misschien minder duidelijk maar wel minder typewerk ;-).
In de headerfile avr/io.h
zijn alle namen van de verschillende
bitjes in de I/O registers van de AVR met #define
gekoppeld aan hun bitnummer. Op
deze manier kun je dus met behulp van een schuifoperatie bitjes manipuleren en
testen zonder dat je het bitnummer hoeft te weten (je moet dan natuurlijk wel
de naam van het bitje weten).
Als je bijvoorbeeld wilt wachten tot het TOV0 bitje (Timer OVerflow 0 flag)
in het TIFR
register (TImer Flag Register) 1 wordt dan kan dit met
de volgende C instructie:
while ((TIFR & 1<<TOV0) == 0); /* wacht tot TOV0 is geset */
Je hoeft dan dus niet te weten welk bitnummer het TOV0 bitje heeft.
In de file avr/io.h
wordt gekeken naar het ingestelde type AVR
microcontroller om te bepalen welke bitnamen aan welke bitnummers moeten worden
gekoppeld. Het is dus van groot belang om bij de project opties het juiste AVR
type, in ons geval de ATmega328P, te selecteren.