Les 4
Bits, registers en volatile
Memory-mapped I/O, bitmaskers op registers, waarom volatile nodig is en de typen uit stdint.h.
🎯 Na deze les kun je
- uitleggen wat memory-mapped I/O is
- bits in een register zetten, wissen, omdraaien en testen
- uitleggen wanneer een variabele volatile moet zijn
- typen met vaste breedte kiezen uit stdint.h
In les 3 schreef je DDRB |= (1 << DDB5); en ging er een LED aan. Dat ziet eruit als een gewone toekenning aan een variabele. Het is ook een gewone toekenning, maar dan aan een heel bijzonder geheugenadres: een register dat rechtstreeks met de hardware verbonden is. Deze les legt uit hoe dat werkt. Het is de basis voor alles wat volgt.
Je hebt bitoperatoren nodig. Ken je les 31 van Een beetje C & C++ (bitmanipulatie), dan is dit herhaling met een hardware-sausje.
Memory-mapped I/O
De ATmega328P heeft één adresruimte voor data. Een deel daarvan is SRAM, maar een ander deel is gekoppeld aan peripherals:
| Adres | Wat |
|---|---|
0x0000–0x001F |
32 werkregisters van de CPU |
0x0020–0x00FF |
I/O-registers: GPIO, timers, UART, ADC, … |
0x0100–0x08FF |
2 KB SRAM |
PORTB staat op adres 0x25. Schrijf je daar een byte, dan bepaalt elk bit of een pin van poort B hoog (5 V) of laag (0 V) is. Dat heet memory-mapped I/O: hardware bedienen door naar geheugenadressen te schrijven.
In <avr/io.h> is PORTB ongeveer zo gedefinieerd:
#include <stdint.h>
#define MIJN_PORTB (*(volatile uint8_t *)0x25)
Lees dat van binnen naar buiten: 0x25 is een adres, (volatile uint8_t *) maakt er een pointer naar een byte van, en * volgt die pointer. Elke keer dat je PORTB schrijft, wordt er dus naar adres 0x25 geschreven. Pointers uit les 6 van de C-cursus, nu met echte hardware aan de andere kant.
Bitmaskers op registers
Een register bevat 8 losse instellingen. Je wilt er meestal één veranderen zonder de rest aan te raken. De namen van de bits (DDB5, PORTB5, TOIE0, …) zijn in avr-libc gewoon getallen: het bitnummer.
#include <avr/io.h>
int main(void) {
DDRB |= (1 << DDB5); /* bit 5 zetten: PB5 is uitgang */
PORTB |= (1 << PORTB5); /* bit 5 zetten: PB5 hoog (LED aan) */
PORTB &= (uint8_t)~(1 << PORTB5); /* bit 5 wissen: PB5 laag */
PORTB ^= (1 << PORTB5); /* bit 5 omdraaien */
if (PINB & (1 << PINB0)) { /* bit 0 testen: is PB0 hoog? */
PORTB |= (1 << PORTB5);
}
/* meerdere bits tegelijk */
DDRD |= (1 << DDD6) | (1 << DDD7);
for (;;) {
}
}
| Doel | Patroon |
|---|---|
| zetten | REG \|= (1 << BIT); |
| wissen | REG &= ~(1 << BIT); |
| omdraaien | REG ^= (1 << BIT); |
| testen | if (REG & (1 << BIT)) |
avr-libc heeft ook de macro _BV(bit), die precies (1 << bit) is. Je ziet beide in voorbeeldcode.
Voorspel: PORTB is 0b00100001. Wat is hij na PORTB &= ~(1 << 5);? Antwoord: 0b00000001.
PORTB = (1 << PORTB5); (zonder |) zet bit 5 aan maar alle andere bits uit. Heel soms is dat bedoeld, meestal is het een bug die een andere pin uitschakelt.
Een veld van meerdere bits
Sommige instellingen beslaan meerdere bits. De prescaler van timer 0 zit in de bits CS02:CS00 van TCCR0B. Een veld schrijf je in twee stappen: eerst wissen, dan zetten.
#include <avr/io.h>
int main(void) {
/* prescaler 64: CS02..CS00 = 011 */
TCCR0B = (uint8_t)((TCCR0B & ~((1 << CS02) | (1 << CS01) | (1 << CS00)))
| (1 << CS01) | (1 << CS00));
for (;;) {
}
}
volatile
Bekijk deze lus, die wacht tot een knop op PB0 hoog wordt:
while (!(PINB & (1 << PINB0))) {
/* wachten */
}
Een optimaliserende compiler denkt normaal: "niemand in deze lus verandert de waarde, dus ik lees hem één keer en hergebruik het resultaat". Dat maakt van de lus een oneindige lus. Maar PINB verandert wél: door de hardware. volatile vertelt de compiler: deze waarde kan buiten je programma om veranderen, lees hem elke keer opnieuw en schrijf elke keer echt.
Alle registers in <avr/io.h> zijn al volatile. Jij hebt volatile nodig voor je eigen variabelen die gedeeld worden met een interrupt (les 7):
#include <stdint.h>
volatile uint8_t knop_ingedrukt = 0; /* wordt in een interrupt gezet */
volatile maakt een variabele niet thread-safe of atomair. Het zegt alleen: niet wegoptimaliseren. Daar komen we in les 7 op terug.
Typen met vaste breedte
Op een AVR is een int 16 bits, niet 32 zoals op je laptop. Code die ervan uitgaat dat int tot 2 miljard gaat, faalt hier al bij 32767. Gebruik daarom in embedded code de typen uit <stdint.h>:
| Type | Bereik | Gebruik |
|---|---|---|
uint8_t |
0–255 | registers, bytes, kleine tellers |
int8_t |
−128–127 | |
uint16_t |
0–65535 | ADC-waarden, timers |
int16_t |
−32768–32767 | temperaturen in tienden van graden |
uint32_t |
0–4,29 miljard | milliseconden sinds start |
Kies het kleinste type dat zeker past. Een 8-bit processor rekent met uint8_t in één instructie, met uint32_t in vier of meer.
#include <stdint.h>
uint32_t ms_naar_ticks(uint16_t ms) {
return (uint32_t)ms * 250u; /* zonder cast: 16-bit vermenigvuldiging, overflow */
}
Zonder de cast wordt ms * 250 als 16-bit gerekend (beide operanden passen in een 16-bit int) en loopt het boven 65535 / 250 = 262 ms over.
Typische fouten
=in plaats van|=bij het zetten van één bit.- Bitnummer verwarren met masker:
PORTB |= PORTB5;zet bit 0 en 2 (wantPORTB5is 5 =0b101). Het moet(1 << PORTB5)zijn. volatilevergeten bij een variabele die een interrupt wijzigt: de hoofdlus ziet de wijziging nooit, maar alleen met optimalisatie aan. Klassiek "het werkt in debug, niet in release".
Oefeningen
DDRDis0b00000000. Schrijf één regel die PD2, PD3 en PD4 uitgang maakt.- Wat doet
PORTB |= PORTB5;precies? - Wat is de waarde van
(uint16_t)300 * 300op een AVR, en waarom?
Modelantwoord
DDRD |= (1 << DDD2) | (1 << DDD3) | (1 << DDD4);ofDDRD |= 0x1C;.PORTB5is het getal 5 =0b00000101. Dus worden bit 0 en bit 2 gezet, niet bit 5.300 * 300 = 90000past niet in 16 bits. Eenuint16_twordt naarint(16-bit, signed) gepromoveerd; de vermenigvuldiging loopt over, wat bij signed ongedefinieerd gedrag is. Cast een operand naaruint32_t.
Lab: een registerwaarde ontleden
- Schrijf in
ontleed.ceen functievoid beschrijf_tccr0b(uint8_t waarde, char *uit, size_t n)die de prescaler-bits uitleest en als tekst teruggeeft (bijvoorbeeld"prescaler 64"). - Schrijf
uint8_t zet_veld(uint8_t reg, uint8_t masker, uint8_t positie, uint8_t waarde)die een veld van meerdere bits vervangt zonder andere bits te raken. - De check test beide functies op je pc (gewone gcc) met een reeks waarden.
beetje check embedded-c-les4
Samenvatting
- Registers zijn geheugenadressen die met hardware verbonden zijn (memory-mapped I/O).
- Zetten
|=, wissen&= ~, omdraaien^=, testen&, altijd met(1 << BIT). volatile: de waarde kan buiten je code om veranderen; nodig voor variabelen die je met interrupts deelt.- Een AVR-
intis 16 bits; gebruikuint8_t,uint16_tenuint32_t.
Volgende les: GPIO, en eindelijk zelf een LED laten knipperen.
Lab
Een registerwaarde ontleden
💻 Dit lab doe je op je eigen computer met Docker. Start de lab-container met (geeft de opdracht een eigen startcommando, gebruik dan dat):
$ docker run -it --rm registry.wizg.it/webhosting/beetje-labs:embedded-c-les4
Klaar? Controleer je werk met:
$ beetje check embedded-c-les4