Varie · 31 marzo 2013 1

Manipoliamo direttamente le porte logiche di una MCU

Se vogliamo accendere un LED collegato ad un pin dell’Arduino sappiamo che dobbiamo, nell’ordine:

1) impostare il pin come OUTPUT usando il comando pinMode;
2) “scrivere” su quel pin il valore HIGH usando il comando digitalWrite.

Sembra semplice ma se andiamo ad analizzare il codice del core di Arduino ci accorgiamo che non lo è. La semplicità dell’IDE di Arduino permette sì di fare quest’operazione con 2 istruzioni ma nel “retrobottega” le operazioni che vengono eseguite sono diverse, perché il core di Arduino deve effettuare una serie di calcoli per trasformare il numero di pin nel corrispondente piedino fisico e poi ancora controllare se quel pin è collegato ad un timer, nel caso disattivare l’eventuale segnale PWM che ci può essere collegato. Solo allora il pin viene impostato come si vuole.

Se non si ricerca una grande velocità tutti questi passaggi, che vengono eseguiti tutte le volte che invochiamo tali istruzioni, possono anche non disturbare, ma se vogliamo fare le cose in modo più rapido e diretto la soluzione è solo una: bisogna manipolare direttamente le porte logiche del microcontrollore.

Un microcontrollore è un dispositivo elettronico e come tale non esistono dei meccanismi che vengono azionati per impostare i piedini ma tutto è fatto a livello digitale. Esistono dei particolari registri mappati in memoria che contengono lo stato di ogni singola linea di input/output (I/O) del microcontrollore: “mappati in memoria” significa che questi registri sono accessibili tramite delle particolari locazioni di memoria poste in una zona di SRAM non utilizzata per il salvataggio dei dati del programma dell’utente. Ogni registro rappresenta lo stato di una porta logica: una porta logica è l’insieme dei bit collegati ad un gruppo di piedini fisici della MCU. Leggendo o scrivendo in questi registri si manipola in maniera diretta lo stato dei pin.

piedinaturaAtmega328pLe porte logiche degli Atmel della serie AVR ATtiny e ATmega sono ad 8 bit, per cui ogni porta logica contiene lo stato di 8 pin del microcontrollore. A lato vedete la piedinatura dell’ATmega328P montato sull’Arduino UNO: ogni pin è identificato da una sigla come PD0, PC1 o PB2. Ad esempio, PD0 identifica il pin 0 della porta logica “D”. L’ATmega328P ha 23 linee di I/O raggruppati in 3 porte logiche da 8 bit l’una denominate B, C e D.

Ogni porta è gestita da 3 registri. Prendendo come esempio la porta D, questi registri sono:

  • DDRD
    questo registro è di lettura/scrittura e contiene la direzione dei pin ad esso collegati. Un bit a 0 rappresenta un pin impostato come INPUT; un bit ad 1 rappresenta un pin impostato come OUTPUT;
  • PORTD
    questo registro è di lettura/scrittura e contiene lo stato dei pin. Questo stato cambia a seconda della direzione del pin:
    – se il pin è impostato come INPUT, un bit ad 1 attiva la resistenza di PULL-UP, mentre un bit a 0 la disattiva;
    – se il pin è impostato come OUTPUT, un bit ad 1 indica uno stato HIGH sul relativo pin, mentre un bit a 0 indica la presenza dello stato LOW sullo stesso pin.
  • PIND
    questo registro è di sola lettura e contiene, nel caso di un pin impostato come INPUT, la lettura del segnale collegato al pin: 1 per un segnale alto (HIGH); 0 per un segnale basso (LOW).

Da ciò si capisce subito come per impostare un pin come OUTPUT con un livello HIGH basta impostare la direzione con il registro DDRx ed il suo stato con il registro PORTx. Non preoccupiamoci degli indirizzi di questi registri: sarà il compilatore avr-gcc che trasformerà le istruzioni su questi registri nelle giuste istruzioni in linguaggio macchina necessarie a scrivere in quelle locazioni di memoria.

Facciamo un esempio pratico. Vogliamo impostare il pin PD2 come OUTPUT con livello in uscita HIGH. L’immagine seguente rappresenta la coppia di registri che andremo a manipolare, con evidenziato il bit interessato, il bit n° 2 (3a posizione):portd2

Nel registro DDRD scriveremo “1” nel bit DDR2 per impostare il pin PD2 come OUTPUT, mentre nel registro PORTD imposteremo ad “1” il bit PORTD2 per attivare il segnale HIGH.

La soluzione che viene subito in mente è la seguente:

DDRD = 0b00000100;
PORTD = 0b00000100;

Sembra giusto, vero? Invece è sbagliato! In questo modo infatti modifico TUTTI i bit dei 2 registri, alterando tutte le linee di I/O della porta D, non solo quella che ci serve. Quindi come possiamo fare?

In nostro aiuto vengono le operazioni di algebra booleana OR ed AND. La prima è detta disgiunzione logica mentre la seconda congiunzione logica. Facciamo un veloce ripasso delle tabelle della verità di entrambe:

tables_and_or

Analizziamo l’OR logico. Basta che uno dei due valori sia pari ad 1 affinché anche il risultato dell’operazione lo sia. L’unico caso in cui il risultato di un OR è uguale a 0 è quando entrambi i valori A e B sono pari a 0. Nell’AND logico, invece, la situazione è opposta: basta che uno solo dei due valori A e B sia pari a 0 che il risultato sarà anch’esso 0. L’unico modo per avere un risultato uguale ad 1 è quello in cui entrambi i valori A e B sono pari ad 1.

L’operatore OR è ciò che ci serve per “accendere” un bit, ossia impostarlo ad 1: basta infatti fare l’OR fra il valore del bit ed 1 per esser certi che il risultato sia 1. Per completare il nostro compito ci serve un altro operatore, quello di scorrimento <<. La sintassi di questo operatore binario (che opera cioè sui bit) è la seguente:

VALORE << SPOSTAMENTO

Esso cioè dice al compilatore: prendi “VALORE” e spostalo a sinistra di tanti posti (bit) quanti sono quelli indicati da “SPOSTAMENTO”. Alcuni esempi numerici:

1<< 2 = 100
101<<1 = 1010
1111<4 = 11110000

Adesso riprendiamo il nostro esempio: vogliamo impostare il pin PD2 come OUTPUT con livello HIGH. Per prima cosa dobbiamo impostare questo pin come OUTPUT ma, come detto, non vogliamo alterare il resto dei pin di quella porta:

DDRD = DDRD OR DDD2

DDD2 è il bit in posizione 2 del registro, per cui in codice C scriveremo così:

DDRD |= (1<<2)

Spieghiamo brevemente. Analizziamo prima la parte fra parentesi: diciamo al compilatore di prendere il bit 1 e di spostarlo in posizione 2 (2 spostamenti a sinistra). Poi diciamo al compilatore di prendere il valore di DDRD e di fare un OR logico con il risultato della precedente operazione e di rimettere il risultato in DDRD.

DDRD = DDRD | 0b00000100

In questo modo modificheremo solo il bit 2 lasciando inalterati i restanti bit. Analogamente, per mettere  il pin a livello logico HIGH faremo:

PORTD |= (1<<2)

che diventa

PORTD = PORTD | 0b00000100

Per “spengere” il bit, invece, usiamo l’operatore AND, perché basta fare un AND logico fra il valore 0 ed il bit che ci interessa per ottenere 0 come risultato dell’operazione. L’AND logico è però critico da usare perché può alterare facilmente gli altri bit visto che solo nel caso in cui entrambi i valori A e B sono pari ad 1 si ottiene come risultato ancora 1. In nostro aiuto viene il segno di inversione, ~. Questo operatore inverte il valore di un bit, o di tutti i bit di un byte.

Alcuni esempi:

~1 = 0
~0b0001000 = 0b11110111
~0b11001100 = 0b00110011

Riprendiamo l’esempio qui sopra e spengiamo il bit n° 2 della porta PORTD, ossia attiviamo un segnale logico LOW su PD2. Per fare ciò dobbiamo usare insieme l’operatore logico e l’operatore inverso.

PORTD = PORTD AND ~PD2

da cui si ottiene

PORTD &= ~(1<<2)

Cosa fa questo codice? Intanto iniziamo a svolgere l’operazione fra parentesi, che dà come risultato 0b00000100. Adesso entra in gioco l’operatore ~ che inverte il valore di tutti i bit di questo byte:

~0b00000100 = 0b11111011

Come ultima operazione si ha l’AND logico fra il valore del registro PORTD ed il byte 0b11111011. Siccome sappiamo che l’AND con 1 dà come risultato 1 nel caso il bit sia ad 1 e restituisce invece 0 nel caso il bit sia a 0, possiamo dire che l’unico bit che cambia di segno è proprio il nostro bit, dato che gli altri manterranno il loro valore originale. Ad esempio, se PORTD valesse 0b11001111, l’operazione sarebbe:

PORTD &= ~(1<<2)

da cui

PORTD = 0b11001111 & 0b11111011

con PORTD che assumerebbe il valore di

PORTD = 0b11001011

Tutti i bit manterranno il loro valore tranne il bit in posizione 2 (il 3°).