© Harry Broeders.
Deze pagina is bestemd voor studenten van de Haagse Hogeschool - Academie voor Technology, Innovation & Society Delft.
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 ATmega32. Op het STK500 practicumbordje zijn de pinnen
van poort B PB0 t/m PB7 verbonden met 8 ledjes (LED0 t/m LED7) zodat we meteen
het resultaat van de bewerking kunnen zien. Het DDRB
register
moet dan wel geladen worden met 0xFF
om alle pinnen van poort
B op output te zetten. Denk eraan dat de LEDjes op het STK500 bord worden
aangezet met een 0 en uitgezet met een 1.
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 = 0xFF;
/* alle pinnen poort B op output */
PORTB = 0x55; /* willekeurige test waarde op poort B */
PORTB = PORTB | 0x08; /* alleen pin PB3 1 maken, de overige pinnen van poort B worden niet gewijzigd */
while (1);
return 0;
}
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 wordt pin PB0 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 = 0xFF;
/* alle pinnen poort B op output */
PORTB = 0xAA; /* willekeurige test waarde op poort B */
PORTB = PORTB & 0xF7; /* alleen pin PB3 0 maken, de overige pinnen van poort B worden niet gewijzigd */
while (1);
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
. Als in het bovenstaande programma de
&
operator vervangen wordt door de
&&
operator dan wordt pin PB0 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.
~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
.
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 = 0xFF;
/* alle pinnen poort B op output */
PORTB = 0xAA;
/* willekeurige test waarde op poort B */
PORTB = PORTB ^ 0x08; /* alleen pin PB3 inverteren, de overige pinnen van poort B worden niet gewijzigd */
while (1);
return 0;
}
De regel:
PORTB = PORTB ^0x08;
kun je ook verkorten tot:
PORTB ^= 0x08;
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 = 0xFF;
/* alle pinnen poort B op output */
PORTB = 0x5A;
/* willekeurige test waarde op poort B */
PORTB |= 0x14; /* alleen PB4 en PB2 1 maken */
PORTB &= ~0x22; /* alleen PB5 en PB1 0 maken */
PORTB ^= 0xc1; /* alleen PB7, PB6 en PB0 inverteren */
while (1);
return 0;
}
De onderstaande voorbeelden testen een bitje in het PINA
register
van de AVR ATmega32. Op het STK500 practicumbordje zijn de pinnen van poort
A PA0 t/m PA7 verbonden met 8 drukschakelaars (SW0 t/m SW7) zodat we de
programma's eenvoudig kunnen testen. Als de test true oplevert worden de
pinnen PB0 t/m PB6 één en pin PB7 nul gemaakt (ledje LED7 wordt
als enige aangezet) en als de test false oplevert worden de pinnen PB1 t/m
PB7 één en pin PB0 nul gemaakt (ledje LED0 wordt als enige
aangezet) zodat we meteen het resultaat van de test kunnen zien. Het
DDRA
register moet dan wel geladen worden met 0x00
om alle pinnen van poort A op intput te zetten en het DDRB
register
moet geladen worden met 0xFF
om alle pinnen van poort B op output
te zetten. Denk eraan dat de drukschakelaars op het STK500 bord de pin 0
maken als de schakelaar wordt ingedrukt en de pin 1 maken als de schakelaar
niet wordt ingedrukt.
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 niet ingedrukt is (pin PA3 is dan 1) alleen LED7 laten branden (PB7 is als enige pin van poort B nul) en anders (SW3 wel ingedrukt) alleen LED0 laten branden:
#include <avr/io.h>
int main(void) {
DDRB = 0xFF;
DDRA = 0x00;
while (1) {
if ((PINA & 0x08) == 0x08) {
PORTB = 0x7F;
}
else {
PORTB = 0xFE;
}
}
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((PINA & 0x08) == 0x08) {
kun je ook verkorten tot:
if(PINA & 0x08) {
De expressie (PINA & 0x08)
geeft namelijk als resultaat
0x08
als pin PA3 één
is en 0x00
als pin PA3 nul 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.
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 ingedrukt is (PA3 is dan 0) alleen LED7 laten branden en anders alleen LED0 laten branden:
#include <avr/io.h>
int main(void) {
DDRB = 0xFF;
DDRA = 0x00;
while (1) {
if ((PINA & 0x08) == 0x00) {
PORTB = 0x7F;
}
else {
PORTB = 0xFE;
}
}
return 0;
}
De regel:
if
((PINA & 0x08) == 0x00) {
kun je ook verkorten tot:
if
(!(PINA & 0x08)) {
of tot:
if
(~PINA & 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 (PINA & 0x08)
geeft namelijk als resultaat 0x00
als schakelaar PA3 nul is
en 0x08
als schakelaar PA3 één 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 PA3 nul is en false als
PA3 één is.
Je kunt ook eerst een bitwise-not uitvoeren op de uit
PINA
gelezen waarde. Alle bitjes (dus ook bitje PA3) worden
dan geinverteerd. Hierna kan je dan op de hierboven beschreven manier testen
of bitje 3 in de geïnverteerde waarde van PINA
één
is (~PINA & 0x08)
. 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 LED7 aangezet als PB5 één is en PB3 één is (dus als SW5 niet ingedrukt is en SW3 niet ingedrukt is). Als dit niet het geval is (SW5 of SW3 is ingedrukt of beiden zijn ingedrukt) dan wordt alleen LED0 aangezet.
#include <avr/io.h>
int main(void) {
DDRB = 0xFF;
DDRA = 0x00;
while (1) {
if ((PINA & 0x28) == 0x28) {
PORTB = 0x7F;
}
else {
PORTB = 0xFE;
}
}
return 0;
}
De onderstaande waarheidstabel kan helpen bij het doorgronden van de werking van het bovenstaande programma:
SW5 | SW3 | PA5 | PA3 | PINA & 0x28 |
(PINA & 0x28) == 0x28 |
PORTB |
PB7 | PB0 | LED7 | LED0 |
---|---|---|---|---|---|---|---|---|---|---|
niet ingedrukt | niet ingedrukt | 1 | 1 | 0x28 |
true | 0x7F |
0 | 1 | aan | uit |
niet ingedrukt | wel ingedrukt | 1 | 0 | 0x20 |
false | 0xFE |
1 | 0 | uit | aan |
wel ingedrukt | niet ingedrukt | 0 | 1 | 0x08 |
false | 0xFE |
1 | 0 | uit | aan |
wel ingedrukt | wel ingedrukt | 0 | 0 | 0x00 |
false | 0xFE |
1 | 0 | uit | aan |
Let op! De regel:
if((PINA & 0x28) == 0x28) {
kun je nu niet verkorten!
De regel:
if(PINA & 0x28) {
geeft namelijk een heel ander resultaat. De expressie
(PINA & 0x28)
levert namelijk
ook true op als alleen pin PA5 één is of alleen PA3 één
is!
In het onderstaande voorbeeld wordt alleen LED7 aangezet als PA5 één is of PA3 één is (dus als SW5 niet ingedrukt is of SW3 niet ingedrukt is). Als dit niet het geval is (SW5 en SW5 zijn beiden ingedrukt) dan wordt alleen LED0 aangezet.
#include <avr/io.h>
int main(void) {
DDRB = 0xFF;
DDRA = 0x00;
while (1) {
if ((PINA & 0x28) != 0x00) {
PORTB = 0x7F;
}
else {
PORTB = 0xFE;
}
}
return 0;
}
De onderstaande waarheidstabel kan helpen bij het doorgronden van de werking van het bovenstaande programma:
SW5 | SW3 | PA5 | PA3 | PINA & 0x28 |
(PINA & 0x28) != 0x00 |
PORTB |
PB7 | PB0 | LED7 | LED0 |
---|---|---|---|---|---|---|---|---|---|---|
niet ingedrukt | niet ingedrukt | 1 | 1 | 0x28 |
true | 0x7F |
0 | 1 | aan | uit |
niet ingedrukt | wel ingedrukt | 1 | 0 | 0x20 |
true | 0x7F |
0 | 1 | aan | uit |
wel ingedrukt | niet ingedrukt | 0 | 1 | 0x08 |
true | 0x7F |
0 | 1 | aan | uit |
wel ingedrukt | wel ingedrukt | 0 | 0 | 0x00 |
false | 0xFE |
1 | 0 | uit | aan |
De regel:
if((PINA & 0x28) != 0x00) {
kun je verkorten tot:
if(PINA & 0x28) {
De expressie (PINA &
0x28)
levert namelijk ook true op als alleen pin PA5
één is of alleen PA3 éé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 ingelezen
en 2 plaatsen naar links geschoven naar de leds gestuurd. Als op de schakelaars
0xBD
staat (SW6 en SW1 zijn ingedrukt) zal op de leds dus
0xF4
verschijnen (LED0, LED1 en LED3 branden). Zet de getallen
om naar het binaire talstelsel als je het niet meteen ziet.
#include <avr/io.h>
int main(void) {
DDRB = 0xFF;
DDRA = 0x00;
while (1) {
PORTB = PINA << 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 schakelaars staat
eerst ingelezen in een variabele en daarna drie plaatsen naar rechts
geschoven:
#include <avr/io.h>
#include <stdint.h>
int main(void) {
DDRB = 0xFF;
DDRA = 0x00;
while (1) {
uint8_t b;
b = PINA;
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. Het onderstaande programma levert dus exact hetzelfde resultaat als het bovenstaande programma waarin 2 plaatsen naar links wordt geschoven:
#include <avr/io.h>
int main(void) {
DDRB = 0xFF;
DDRA = 0x00;
while (1) {
PORTB = PINA * 4;
}
return 0;
}
Bij schuiven naar rechts is het wat ingewikkelder.
Als het patroon unsigned is worden er ook nullen ingeschoven. Als
de unsigned 8 bits waarde 0xBD
twee plaatsen naar rechts wordt
geschoven dan levert dat de waarde 0x4F
op. Bij unsigned
getallen komt x plaatsen schuiven naar rechts overeen met delen door
2x. De twee onderstaande programma's leveren dus exact
hetzelfde resultaat:
#include <avr/io.h>
#include <stdint.h>
int main(void) {
uint8_t b = 0xBD;
b >>= 2;
DDRB = 0xFF;
PORTB = b;
while (1);
return 0;
}
#include <avr/io.h>
#include <stdint.h>
int main(void) {
uint8_t b = 0xBD;
b /= 4;
DDRB = 0xFF;
PORTB = b;
while (1);
return 0;
}
Als het patroon signed is wordt bij het inschuiven de tekenbit (bit7)
gekopieerd. In het onderstaande voorbeeld wordt de 8 bits signed waarde
0xBD
plaatsen naar rechts geschoven . Dit levert de waarde
0xEF
op. Zet de getallen om naar het binaire talstelsel als
je het niet meteen ziet.
#include <avr/io.h>
#include <stdint.h>
int main(void) {
int8_t b = 0xBD;
b >>= 2;
DDRB = 0xFF;
PORTB = b;
while (1);
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 dan wordt de variabele b na de deeloperatie gelijk aan
0xF0
.
#include <avr/io.h>
#include <stdint.h>
int main(void) {
int8_t b = 0xBD;
b /= 4;
DDRB = 0xFF;
PORTB = b;
while (1);
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: 0xBD
gedeeld door 4 = 0xEF
rest 0x01
(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: 0xBD
gedeeld
door 4 = 0xF0
rest 0xFD
(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 afzondere bits wordt vaak gebruik gemaakt
van bitpatronen of maskers waarin slecht één positie een 1
voorkomt. Om bijvoorbeeld pin PB6 één te maken moeten we
PORTB
or-en met het binaire patroon: 01000000.
#include <avr/io.h>
int main(void) {
DDRB = 0xFF;
PORTB |= 0x40;
while (1);
return 0;
}
Je kunt het benodigde patroon ook uit laten rekenen door de compiler door de constante 1 zes plaatsen naar links te schuiven:
#include <avr/io.h>
int main(void) {
DDRB = 0xFF;
PORTB |= 1<<6;
while (1);
return 0;
}
De expressie 1<<6
wordt door de compiler uitgerekend en
levert de waarde 0x40
op zodat beide programma's exact dezelfde
machinecode opleveren. De meeste mensen vinden het tweede programma beter
leesbaar omdat je meteen ziet dat bit 6 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 pinnen PB6, PB4, PB2 en PB0 één gemaakt:
#include <avr/io.h>
int main(void) {
DDRB = 0xFF;
PORTB |= 1<<6 | 1<<4 | 1<<2 | 1<<0;
while (1);
return 0;
}
De regel:
PORTB |= 1<<6 | 1<<4 | 1<<2 | 1<<0;
kan natuurlijk ook vervangen worden door:
PORTB |= 0x55;
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 bij opdracht 2a wilt wachten
tot het TOV1 bitje (Timer OVerflow 1 flag) in het TIFR
register
(TImer Flag Register) 1 wordt dan kan dit met de volgende C instructie:
while ((TIFR & 1<<TOV1) == 0); /* wacht tot TOV1 is set */
Je hoeft dan dus niet te weten welk bitnummer het TOV1 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 ATmega32, te selecteren.