Arduino registri e bit manipulation: guida pratica per ATmega328P
Impariamo a gestire input/output agendo direttamente sui bit dei registri DDRx e PORTx

Premessa: agire direttamente sui registri delle porte (port manipulation) rende il codice molto più veloce di pinMode() e digitalWrite() e permette di cambiare più pin nello stesso istante, ma si perde la portabilità su schede con hardware diverso da quello di partenza: ad esempio il codice di questa guida non funziona su Arduino UNO R4, basato sul microcontrollore Renesas RA4M1, che non ha i registri DDRx/PORTx/PINx.
Porte e pin dell’ATmega328P
Per prima cosa è necessario fare chiarezza su come sono raggruppati i pin del microcontrollore e per semplicità ci riferiremo ad un ATMEGA328 (ovvero il cuore della scheda ArduinoUNO). Come viene mostrato dalla seguente immagine i pin sono raggruppati in gruppi facilmente distinguibili:
PD0, PD1, PD2, PD3, PD4, PD5, PD6 e PD7 –> input/output digitali
PB0, PB1, PB2, PB3, PB4, PB5, PB6 e PB7 –> input/output digitali
PC0, PC1, PC2, PC3, PC4 e PC5 –> ingressi analogici A0–A5, utilizzabili anche come input/output digitali
PC6 –> pin di RESET (non utilizzabile come I/O su Arduino UNO)

In questa guida trattiamo la gestione per bit delle porte B, C e D usate come input/output digitali. La tabella seguente riporta la corrispondenza tra le porte e i pin di Arduino UNO.
| Porta | Pin ATmega328P | Pin Arduino UNO | Note |
|---|---|---|---|
| D | PD0–PD7 | D0–D7 | PD0 = RX e PD1 = TX della seriale (USB) |
| B | PB0–PB5 | D8–D13 | PB5 = D13, LED integrato |
| B | PB6–PB7 | — | collegati al quarzo da 16 MHz, non disponibili |
| C | PC0–PC5 | A0–A5 (D14–D19) | ingressi analogici, utilizzabili anche come I/O digitali |
| C | PC6 | RESET | non utilizzabile come I/O |
Attenzione: scrivere l’intero byte di DDRD o PORTD (ad esempio DDRD = 0b11111111) modifica anche PD0 e PD1 e interferisce con la comunicazione seriale (Serial e caricamento degli sketch); su Arduino UNO, inoltre, PB6 e PB7 sono collegati al quarzo e non sono disponibili. In questi casi conviene agire solo sui bit interessati, con le operazioni descritte più avanti.
I registri DDRx e PORTx
Il microcontrollore lavora su tre registri per ogni porta: quello che definisce la direzione del pin (DDRx), quello riguardante lo stato logico (PORTx) e quello che permette di leggere lo stato degli ingressi (PINx), descritto più avanti.
Individuiamo con DDRx (dove x sarà B, C o D) il registro di direzione del pin. Ci permetterà di settare un determinato terminale come input o come output digitale. Il registro è formato da 1 byte, ovvero 8 bit, uno per ciascun pin della porta. Se il bit ha valore 1 significa che il terminale è un pin di output, se ha valore 0 si tratta di input.
Ad esempio dichiarare DDRD = 0b11111111 significa dichiarare tutti i terminali PDx ( PD0, PD1, PD2, PD3, PD4, PD5, PD6 e PD7 ) come output. La dicitura DDRD = 0b00000000 significa invece che tutti i pin PDx sono input. Il prefisso 0b è la notazione binaria standard del C/C++; la vecchia forma B11111111, specifica di Arduino, è deprecata.
NB. il bit più a sinistra corrisponde a PD7, mentre quello più a destra corrisponde a PD0.
Il secondo registro che ci serve è PORTx (dove x sarà B, C o D) e permette di impostare lo stato logico del terminale. Se il pin è stato precedentemente battezzato come input (nel registro DDRx) allora nel registro PORTx il valore 0 corrisponde a ingresso semplice mentre il valore 1 attiva la resistenza di PULLUP.
Se il pin è stato precedentemente battezzato come output (nel registro DDRx) allora nel registro PORTx il valore 0 corrisponde a stato logico low, mentre il valore 1 a stato logico high.
Per esempio abbiamo le due seguenti istruzioni:
DDRB = 0b00100111; PORTB = 0b00011101;
si deduce che:
PB7 e PB6 (bit 7 e 6) sono lasciati a 0: su Arduino UNO sono collegati al quarzo e non vanno usati,
PB5 (D13) è un output con stato logico low,
PB4 (D12) è un input con resistenza di pullup attiva,
PB3 (D11) è un input con resistenza di pullup attiva,
PB2 (D10) è un output con stato logico high,
PB1 (D9) è un output con stato logico low,
PB0 (D8) è un output con stato logico high.
Impostare e azzerare un singolo bit
Scrivere il byte intero del registro è il metodo più rapido (una sola istruzione macchina, 62,5 ns a 16 MHz, contro alcuni microsecondi di una chiamata a digitalWrite()) e cambia più pin nello stesso istante, ma modifica tutti i pin della porta. Per agire su un solo pin senza alterare gli altri si usano gli operatori logici OR e AND; con un pin e una posizione costanti il compilatore li traduce nelle istruzioni SBI/CBI, che richiedono 2 cicli di clock (125 ns a 16 MHz). Quindi i casi si riducono a due: la necessità di mettere un determinato bit a valore 0 o a valore 1.
La dicitura seguente prende il byte del registro desiderato e semplicemente mette 1 alla posizione x del byte (la posizione più a sinistra è 7, quella più a destra è 0).
REGISTRO |= (1 << x);
Quindi, avendo PORTD = 0b00000000, ed eseguendo la seguente direttiva, vado a mettere 1 nel bit 6 ottenendo quindi PORTD = 0b01000000
PORTD |= (1 << 6);
Se invece volessimo mettere un bit a 0 dovremo utilizzare il comando (dove x è la posizione del bit all’interno del byte).
REGISTRO &= ~(1 << x);
quindi, se abbiamo per esempio PORTD = 0b11111111 e sfruttiamo il seguente comando porteremo uno zero nel bit 6 ottenendo PORTD = 0b10111111.
PORTD &= ~(1 << 6);
Per scrivere più pin contemporaneamente senza toccare gli altri si usa una maschera. Ad esempio, per scrivere un valore a 6 bit su PD2–PD7 lasciando invariati PD0 e PD1 (seriale):
PORTD = (PORTD & 0x03) | (valore << 2); // valore da 0 a 63
Nota sull’atomicità: un’operazione come PORTD |= maschera che non viene tradotta in una singola SBI/CBI (maschera non costante o più bit insieme) è una sequenza leggi-modifica-scrivi: se un interrupt modifica la stessa porta nel mezzo, la sua modifica va persa. In questi casi si disabilitano gli interrupt durante l’operazione con cli()/sei() oppure con ATOMIC_BLOCK(ATOMIC_RESTORESTATE) di <util/atomic.h>.
Leggere gli ingressi: il registro PINx
Il terzo registro di ogni porta è PINx (dove x sarà B, C o D): contiene lo stato logico effettivamente presente sui pin e si usa per leggere gli ingressi. Per verificare un singolo bit si usa l’operatore AND con una maschera:
// PD2 (pin digitale 2) come ingresso con resistenza di pull-up
DDRD &= ~(1 << PD2);
PORTD |= (1 << PD2);
if (PIND & (1 << PD2)) {
// ingresso alto: pulsante rilasciato
} else {
// ingresso basso: pulsante premuto verso GND
}
Sull’ATmega328P scrivere 1 in un bit di PINx inverte lo stato del corrispondente bit di PORTx: è il modo più rapido per fare il toggle di un’uscita. Lo sketch seguente fa lampeggiare il LED integrato di Arduino UNO (D13 = PB5) usando solo i registri:
void setup() {
DDRB |= (1 << PB5); // PB5 (D13) come output
}
void loop() {
PINB = (1 << PB5); // inverte lo stato di PB5
delay(500);
}




