Beetje Coderen

Les 4

Bits, registers en volatile

Memory-mapped I/O, bitmaskers op registers, waarom volatile nodig is en de typen uit stdint.h.

⏱ 50 minuten leestijd + oefenen

🎯 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 (want PORTB5 is 5 = 0b101). Het moet (1 << PORTB5) zijn.
  • volatile vergeten 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

  1. DDRD is 0b00000000. Schrijf één regel die PD2, PD3 en PD4 uitgang maakt.
  2. Wat doet PORTB |= PORTB5; precies?
  3. Wat is de waarde van (uint16_t)300 * 300 op een AVR, en waarom?
Modelantwoord
  1. DDRD |= (1 << DDD2) | (1 << DDD3) | (1 << DDD4); of DDRD |= 0x1C;.
  2. PORTB5 is het getal 5 = 0b00000101. Dus worden bit 0 en bit 2 gezet, niet bit 5.
  3. 300 * 300 = 90000 past niet in 16 bits. Een uint16_t wordt naar int (16-bit, signed) gepromoveerd; de vermenigvuldiging loopt over, wat bij signed ongedefinieerd gedrag is. Cast een operand naar uint32_t.

Lab: een registerwaarde ontleden

  1. Schrijf in ontleed.c een functie void beschrijf_tccr0b(uint8_t waarde, char *uit, size_t n) die de prescaler-bits uitleest en als tekst teruggeeft (bijvoorbeeld "prescaler 64").
  2. 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.
  3. De check test beide functies op je pc (gewone gcc) met een reeks waarden.
  4. 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-int is 16 bits; gebruik uint8_t, uint16_t en uint32_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

Volg daarna de stappen van de opdracht hierboven. Nog geen Docker? Zie les 14 van Een beetje Linux & Docker. Voor dit lab is er geen live lab in de browser.

Klaar? Controleer je werk met:

$ beetje check embedded-c-les4

📝 Quiz: bits, registers en volatile

5 vragen · 70% nodig om te slagen

1. Wat is memory-mapped I/O?
2. Hoe zet je bit 3 van PORTD zonder andere bits te veranderen?
3. Waarom moet een variabele die in een interrupt verandert volatile zijn?
4. Hoe groot is een int op een AVR?
5. Wat doet `PORTB |= PORTB5;`?