Arduino

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)

Pinout ATmega328P con le porte B, C e D
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.

PortaPin ATmega328PPin Arduino UNONote
DPD0–PD7D0–D7PD0 = RX e PD1 = TX della seriale (USB)
BPB0–PB5D8–D13PB5 = D13, LED integrato
BPB6–PB7—collegati al quarzo da 16 MHz, non disponibili
CPC0–PC5A0–A5 (D14–D19)ingressi analogici, utilizzabili anche come I/O digitali
CPC6RESETnon 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);
}

Articoli collegati

Admin | MST-Tutorial

Appassionato di elettronica, elettrotecnica e programmazione. Fondatore di MST-Tutorial, condivido con entusiasmo conoscenze, esperienze e progetti legati all’innovazione tecnologica, con l’obiettivo di rendere accessibili concetti tecnici complessi attraverso tutorial chiari e pratici.

Articoli Correlati

Pulsante per tornare all'inizio