Il C per i microcontrollori: registri, operazioni sui bit e interruzioni
Registri delle periferiche, maschere e operazioni sui bit, accesso con volatile, interruzioni e routine di servizio, temporizzazione senza bloccare, regole del firmware robusto.
Per un ingegnere elettronico il C non serve solo a fare calcoli: serve a far lavorare un microcontrollore, cioè accendere un LED, leggere un pulsante, generare un segnale PWM, comunicare con un sensore. Questo tipo di programmazione, a contatto diretto con l’hardware, si chiama embedded. In questa lezione mettiamo insieme quello che abbiamo visto (tipi a larghezza fissa, operatori sui bit, puntatori, cicli) per capire come il C controlla le periferiche attraverso i registri, e vediamo le interruzioni e le regole del firmware robusto.
I registri delle periferiche
In un microcontrollore ogni periferica (porte di ingresso e uscita, timer, convertitore analogico digitale, interfacce seriali) si controlla scrivendo e leggendo i suoi registri: celle di memoria a indirizzi fissi, descritti nel datasheet, in cui ogni bit o gruppo di bit ha un significato. Per esempio, in una porta di uscita a 8 bit, scrivere 1 nel bit 3 porta a livello alto il piedino 3.

Le operazioni sui bit
Per cambiare un bit senza toccare gli altri si usano le maschere: (1u << n) è un numero con solo il bit n a 1.
#include <stdint.h>
#define LED_PIN 3
PORTB |= (1u << LED_PIN); /* imposta il bit 3: LED acceso */
PORTB &= ~(1u << LED_PIN); /* azzera il bit 3: LED spento */
PORTB ^= (1u << LED_PIN); /* inverte il bit 3: il LED cambia stato */
if (PINB & (1u << 2)) { /* legge il bit 2: pulsante premuto? */
/* ... */
}
Per i campi di più bit si usano maschera e scorrimento insieme. Se i bit da 4 a 6 di un registro di configurazione scelgono la velocità di un’interfaccia (8 valori possibili):
reg = (reg & ~(0x7u << 4)) | ((velocita & 0x7u) << 4); /* scrive 3 bit senza toccare gli altri */
uint8_t v = (reg >> 4) & 0x7u; /* li rilegge */
È esattamente ciò che fanno le funzioni delle librerie dei produttori (HAL), che conviene comunque saper leggere: quando qualcosa non funziona, si va a guardare il registro.
Come si accede a un registro: volatile
I registri stanno a indirizzi fissi. Nei file di intestazione dei produttori sono definiti più o meno così:
#define GPIOA_ODR (*(volatile uint32_t *)0x40020014u)
GPIOA_ODR |= (1u << 5); /* scrive direttamente all'indirizzo 0x40020014 */
Si converte l’indirizzo in un puntatore e lo si dereferenzia. La parola chiave volatile dice al compilatore che il valore può cambiare in qualsiasi momento per cause esterne al programma (l’hardware, o un’interruzione) e che ogni lettura e scrittura va eseguita davvero, nell’ordine scritto. Senza volatile un compilatore che ottimizza potrebbe leggere un registro una sola volta e riusare il valore, o eliminare una scrittura che “sembra” inutile: il codice funziona senza ottimizzazioni e si rompe con -O2, uno dei misteri più classici per chi comincia.
Le interruzioni
Controllare continuamente un pulsante in un ciclo (polling) spreca tempo e può perdere eventi brevi. Con le interruzioni è l’hardware ad avvisare il processore: quando arriva un evento (un fronte su un piedino, un byte sulla seriale, lo scadere di un timer) il processore sospende quello che sta facendo, esegue una funzione apposita, la routine di servizio (ISR), e poi riprende.
#include <stdint.h>
#include <stdbool.h>
volatile bool dato_pronto = false; /* condivisa tra ISR e main: volatile */
volatile uint16_t ultima_lettura;
void ADC_IRQHandler(void) /* chiamata dall'hardware a fine conversione */
{
ultima_lettura = ADC_DATA; /* registro del convertitore */
dato_pronto = true;
}
int main(void)
{
inizializza_adc();
for (;;) {
if (dato_pronto) {
dato_pronto = false;
elabora(ultima_lettura);
}
/* qui si può anche mettere il processore in basso consumo fino alla prossima interruzione */
}
}
Le regole per le ISR: devono essere brevi (niente printf, niente attese), le variabili condivise con il programma principale vanno dichiarate volatile, e se sono più grandi di quanto il processore legge in un colpo solo (un uint32_t su un processore a 8 bit) l’accesso dal programma principale va protetto disattivando per un attimo le interruzioni, altrimenti si può leggere un valore mezzo vecchio e mezzo nuovo.
Il tempo senza bloccare
Una funzione delay(500) blocca il processore per mezzo secondo: in quel tempo non legge i pulsanti e non risponde alla seriale. Il firmware ben scritto usa un contatore di millisecondi aggiornato da un timer e confronta i tempi:
static uint32_t ultimo = 0;
if ((uint32_t)(millis() - ultimo) >= 500u) { /* la sottrazione senza segno regge anche il ritorno a zero */
ultimo += 500u;
lampeggia_led();
}
Grazie all’aritmetica modulo
degli interi senza segno (lezione sulla rappresentazione dei dati), la differenza è corretta anche quando il contatore riparte da zero dopo 49,7 giorni.
Le regole del firmware robusto
- Usare i tipi a larghezza fissa (
uint8_t,int32_t) e controllare i casi limite di overflow. - Evitare
mallocdopo l’inizializzazione e la ricorsione profonda: la memoria è poca e un errore non si vede subito. - Attivare il watchdog, un timer hardware che riavvia il microcontrollore se il programma si blocca e smette di “dargli da mangiare”.
- Leggere sempre il datasheet e l’errata del componente: le periferiche hanno sequenze di inizializzazione precise e, a volte, difetti documentati.
- Compilare con tutti gli avvisi, e nei progetti professionali seguire uno standard come MISRA C.
Il percorso del corso si chiude qui: dall’architettura del calcolatore ai registri di un microcontrollore, il C resta il linguaggio con cui il software parla direttamente con l’elettronica.




