Uscita digitale dell’ATmega328P
I terminali di ingresso e uscita dell’ATmega328P sono programmabili. Prima di utilizzare un pin, il programma deve stabilire se deve funzionare come ingresso oppure come uscita.
Quando un pin viene configurato come uscita digitale, il microcontrollore può portarlo in uno dei due stati disponibili:
HIGH: 5 V;LOW: 0 V.
Lo stato dell’uscita rimane invariato finché il programma non lo modifica. Se il programma imposta HIGH, il pin rimane a 5 V; se successivamente imposta LOW, il pin passa a 0 V e rimane in tale condizione fino a un nuovo comando.
Un’uscita digitale non genera valori intermedi. Non può essere impostata direttamente, per esempio, a 2 V oppure a 3,5 V: può assumere soltanto lo stato alto o lo stato basso.
La commutazione può avvenire una sola volta oppure essere ripetuta nel tempo. Alternando periodicamente HIGH e LOW si produce una sequenza come questa:
5 V → 0 V → 5 V → 0 V
La durata di ciascuno stato viene determinata dal programma. Se il livello alto dura un secondo e quello basso dura un secondo, l’intera sequenza si ripete ogni due secondi.
Questo principio verrà utilizzato prima per accendere e spegnere un LED e successivamente per comandare lo stadio elettronico collegato alla bobina di un relè.
Corrente dei pin e limiti elettrici
Un pin dell’ATmega328P può fornire o assorbire soltanto una corrente limitata.
Con alimentazione a 5 V, il datasheet specifica le caratteristiche dei livelli logici di uscita a una corrente di 20 mA. Questo valore è quindi un riferimento operativo, non il limite assoluto del pin.
Tra gli Absolute Maximum Ratings del microcontrollore sono indicati:
- 40 mA per ciascun pin di I/O;
- 200 mA complessivi attraverso i pin VCC e GND.
Questi valori sono limiti assoluti di stress e non devono essere utilizzati come normali valori di progetto. Il datasheet specifica inoltre limiti cumulativi per gruppi di pin, che devono essere rispettati quando più uscite erogano o assorbono corrente contemporaneamente.
In pratica è opportuno mantenere la corrente di ciascuna uscita ben al di sotto dei limiti assoluti. Un LED può essere pilotato direttamente attraverso una resistenza dimensionata correttamente; la bobina di un relè richiede invece uno stadio di pilotaggio, per esempio con transistor.
LED esterno con resistenza
Un LED non deve essere collegato direttamente a un’uscita dell’ATmega328P. Senza una resistenza, la corrente sarebbe limitata quasi soltanto dalle caratteristiche interne del LED e del pin, con rischio di danneggiare entrambi.
Il collegamento è composto da:
- uscita digitale dell’ATmega328P;
- resistenza in serie;
- anodo del LED;
- catodo del LED collegato a GND.
L’anodo è il terminale positivo del LED; il catodo è il terminale negativo. Nei LED tradizionali, il catodo è normalmente riconoscibile dal terminale più corto e dal lato piatto dell’involucro.
Quando l’uscita è HIGH, il pin assume un livello di tensione prossimo a VCC, il cui valore effettivo dipende anche dalla corrente erogata; il LED si accende. Quando l’uscita è LOW, il pin assume un livello prossimo a 0 V e il LED si spegne.
Il valore della resistenza si calcola con:
R = (Vuscita − VLED) / ILED
Per esempio, considerando:
- tensione dell’uscita: 5 V;
- caduta di tensione di un LED rosso: circa 2 V;
- corrente desiderata: 10 mA, cioè 0,01 A;
si ottiene:
R = (5 − 2) / 0,01 = 300 Ω
Si sceglie il valore commerciale immediatamente superiore:
R = 330 Ω
Con una resistenza da 330 Ω, la corrente è circa:
I = (5 − 2) / 330 ≈ 9 mA
Questo valore è sufficiente per ottenere una buona luminosità e rimane inferiore alla corrente di 20 mA alla quale il datasheet specifica le caratteristiche del livello logico di uscita con alimentazione a 5 V.
La resistenza può essere collegata prima oppure dopo il LED, purché sia sempre in serie. Una resistenza da ¼ W è più che sufficiente per questo circuito.
Variabili e nomi utilizzati nel programma
Una variabile è uno spazio di memoria al quale viene assegnato un nome. Il programma utilizza quel nome per leggere, modificare e confrontare il valore conservato.
Una dichiarazione contiene almeno:
- il tipo della variabile;
- il nome della variabile.
Può inoltre contenere un valore iniziale.
Per esempio:
uint32_t millisecondi = 0;
In questa dichiarazione:
uint32_tè il tipo;millisecondiè il nome;0è il valore iniziale.
Il tipo stabilisce quali valori possono essere memorizzati e quanta memoria viene occupata. Nel programma verranno utilizzati principalmente:
uint8_t: numero intero senza segno da 8 bit;uint32_t: numero intero senza segno da 32 bit.
Il nome può essere scelto dal programmatore, rispettando alcune regole:
- può contenere lettere, numeri e il carattere
_; - non può contenere spazi;
- non può iniziare con un numero;
- non può coincidere con una parola riservata del linguaggio, come
int,while,iforeturn; - distingue le lettere maiuscole dalle minuscole.
Questi sono nomi validi:
millisecondi
ultimo_cambio_led
led_acceso
valore1
Questi nomi non sono validi:
ultimo cambio
1valore
int
stato-led
led_acceso, Led_acceso e LED_ACCESO sono tre nomi differenti.
Il nome dovrebbe descrivere chiaramente il contenuto o la funzione della variabile. Per esempio:
uint8_t led_acceso = 0;
indica immediatamente che la variabile rappresenta lo stato del LED.
Sarebbe sintatticamente valido scrivere:
uint8_t pippo = 0;
Un vezzo diffuso tra i programmatori italiani consiste infatti nell’utilizzare pippo come nome generico durante prove, esempi o verifiche rapide. In ambito internazionale vengono spesso usati allo stesso modo nomi come foo e bar.
pippo non spiega però che cosa contiene la variabile. In un programma reale è quindi preferibile utilizzare:
uint8_t led_acceso = 0;
anziché:
uint8_t pippo = 0;
Lo stesso criterio vale per tutte le variabili:
uint32_t ultimo_cambio_led = 0;
uint32_t tempo_attuale = 0;
uint8_t led_acceso = 0;
I nomi rendono riconoscibile il significato dei valori senza dover ricostruire ogni volta il funzionamento del programma.
Le definizioni introdotte con #define, invece, non sono variabili:
#define LED_BIT PB0
#define INTERVALLO_LED_MS 500UL
Sono sostituzioni definite prima della compilazione. Nel programma:
LED_BITidentifica il bit utilizzato per il LED;INTERVALLO_LED_MSrappresenta l’intervallo di 500 millisecondi.
Anche per queste definizioni si utilizzano nomi descrittivi. La scrittura in lettere maiuscole permette di distinguerle visivamente dalle variabili.
Timer e gestione del tempo
Un timer è un circuito interno del microcontrollore che conta gli impulsi prodotti dall’orologio di sistema.
L’ATmega328P lavora, nel caso considerato, a una frequenza di:
16 MHz
Ciò significa che l’orologio genera:
16 000 000
impulsi ogni secondo.
Contare direttamente tutti questi impulsi non sarebbe pratico. Per questo il timer può utilizzare un prescaler, cioè un divisore della frequenza.
Con un prescaler pari a 64:
16 000 000 / 64 = 250 000
Il timer riceve quindi 250.000 impulsi al secondo.
La durata di ogni impulso ricevuto dal timer è:
1 / 250 000 = 0,000004 s
cioè:
4 µs
Per ottenere un intervallo di un millisecondo servono 250 impulsi:
250 × 4 µs = 1 ms
Il contatore parte da zero. Per eseguire 250 conteggi assume i valori da 0 a 249. Per questo il valore di confronto viene impostato a:
OCR1A = 249;
I timer dell’ATmega328P
L’ATmega328P dispone di tre timer:
- Timer0, a 8 bit;
- Timer1, a 16 bit;
- Timer2, a 8 bit.
Un timer a 8 bit può contare da 0 a 255.
Un timer a 16 bit può contare da 0 a 65.535.
Nel programma viene utilizzato Timer1, perché è un timer a 16 bit e permette di gestire con maggiore flessibilità il conteggio e i valori di confronto.
Il contatore del timer
Timer1 possiede un registro chiamato:
TCNT1
Questo registro contiene il valore corrente del conteggio.
Il valore aumenta automaticamente ogni volta che il timer riceve un impulso. Il programma non deve incrementarlo manualmente.
Per iniziare il conteggio da zero si scrive:
TCNT1 = 0;
Il valore di confronto
Il registro:
OCR1A
contiene il valore con il quale viene confrontato il contatore.
Nel programma:
OCR1A = 249;
Quando TCNT1 raggiunge 249, si verifica un evento di confronto.
Il timer viene configurato in modalità CTC, abbreviazione di Clear Timer on Compare Match. In questa modalità, quando il contatore raggiunge il valore di confronto:
- viene generato l’evento di confronto;
- il contatore viene riportato automaticamente a zero;
- inizia un nuovo intervallo.
La sequenza è quindi:
0 → 1 → 2 → ... → 248 → 249 → 0
Questa sequenza dura un millisecondo e si ripete continuamente.
L’interruzione del timer
Quando Timer1 raggiunge il valore contenuto in OCR1A, può richiedere l’esecuzione immediata di una funzione chiamata routine di interruzione.
Nel programma la routine è:
ISR(TIMER1_COMPA_vect)
{
millisecondi++;
}
TIMER1_COMPA_vect identifica l’interruzione prodotta dal confronto A di Timer1.
Ogni volta che trascorre un millisecondo, la routine aumenta di uno la variabile:
millisecondi
La variabile può quindi assumere valori come:
0
1
2
3
...
500
...
1000
Il valore rappresenta i millisecondi trascorsi dall’avvio del programma.
Dopo 500 millisecondi:
millisecondi = 500
Dopo un secondo:
millisecondi = 1000
Dopo cinque secondi:
millisecondi = 5000
Variabili utilizzate per controllare il tempo
Per far cambiare stato al LED ogni 500 millisecondi servono tre valori.
Il tempo corrente:
uint32_t tempo_attuale;
Il momento dell’ultimo cambiamento:
uint32_t ultimo_cambio_led;
L’intervallo richiesto:
#define INTERVALLO_LED_MS 500UL
I nomi descrivono direttamente il contenuto:
tempo_attuale: tempo trascorso dall’avvio;ultimo_cambio_led: momento dell’ultima variazione del LED;INTERVALLO_LED_MS: durata richiesta tra due variazioni.
Il tempo trascorso dall’ultima variazione viene calcolato con:
tempo_attuale - ultimo_cambio_led
Per esempio:
tempo_attuale = 1300 ms
ultimo_cambio_led = 1000 ms
Il tempo trascorso è:
1300 ms − 1000 ms = 300 ms
L’intervallo di 500 millisecondi non è ancora terminato.
Successivamente:
tempo_attuale = 1500 ms
ultimo_cambio_led = 1000 ms
Il tempo trascorso diventa:
1500 ms − 1000 ms = 500 ms
La condizione è quindi soddisfatta e il LED può cambiare stato.
Nel programma il controllo è:
if (tempo_attuale - ultimo_cambio_led >= INTERVALLO_LED_MS)
{
ultimo_cambio_led = tempo_attuale;
/* cambiamento dello stato del LED */
}
Dopo il cambiamento, la variabile:
ultimo_cambio_led
viene aggiornata con il tempo corrente. Inizia così il conteggio del nuovo intervallo.
Più intervalli con lo stesso timer
Un solo timer può fornire il riferimento temporale per più funzioni.
Per esempio:
uint32_t ultimo_cambio_led = 0;
uint32_t ultimo_cambio_rele = 0;
#define INTERVALLO_LED_MS 500UL
#define INTERVALLO_RELE_MS 5000UL
Il LED utilizza un intervallo di 500 millisecondi:
if (tempo_attuale - ultimo_cambio_led >= INTERVALLO_LED_MS)
{
ultimo_cambio_led = tempo_attuale;
/* cambiamento dello stato del LED */
}
Il relè utilizza un intervallo di 5000 millisecondi:
if (tempo_attuale - ultimo_cambio_rele >= INTERVALLO_RELE_MS)
{
ultimo_cambio_rele = tempo_attuale;
/* cambiamento dello stato del relè */
}
Le due funzioni utilizzano lo stesso valore:
tempo_attuale
ma conservano separatamente il momento dell’ultima variazione.
In questo modo:
- il LED cambia stato ogni 500 millisecondi;
- il relè cambia stato ogni 5000 millisecondi;
- le due temporizzazioni rimangono indipendenti.
Programma in C
Nel programma il LED esterno è collegato all’uscita PB0 attraverso la resistenza da 330 Ω descritta nel pezzo precedente.
Il codice è scritto in C e utilizza direttamente i registri dell’ATmega328P. Il timer interno genera un riferimento temporale di un millisecondo; il LED cambia stato ogni 500 millisecondi.
#define F_CPU 16000000UL
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/atomic.h>
#include <stdint.h>
#define LED_BIT PB0
#define INTERVALLO_LED_MS 500UL
volatile uint32_t millisecondi = 0;
ISR(TIMER1_COMPA_vect)
{
millisecondi++;
}
uint32_t leggi_millisecondi(void)
{
uint32_t valore;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
{
valore = millisecondi;
}
return valore;
}
void inizializza_timer1(void)
{
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;
OCR1A = 249;
TCCR1B |= (1 << WGM12);
TCCR1B |= (1 << CS11) | (1 << CS10);
TIMSK1 |= (1 << OCIE1A);
}
int main(void)
{
uint32_t ultimo_cambio_led = 0;
uint8_t led_acceso = 0;
PORTB &= ~(1 << LED_BIT);
DDRB |= (1 << LED_BIT);
inizializza_timer1();
sei();
while (1)
{
uint32_t tempo_attuale = leggi_millisecondi();
if (tempo_attuale - ultimo_cambio_led >= INTERVALLO_LED_MS)
{
ultimo_cambio_led = tempo_attuale;
if (led_acceso == 0)
{
PORTB |= (1 << LED_BIT);
led_acceso = 1;
}
else
{
PORTB &= ~(1 << LED_BIT);
led_acceso = 0;
}
}
}
}
Spiegazione completa del codice C
La prima istruzione definisce la frequenza di lavoro del microcontrollore:
#define F_CPU 16000000UL
F_CPU rappresenta la frequenza della CPU, pari a 16 MHz. Il suffisso UL indica che il valore è un intero senza segno di tipo unsigned long.
Le direttive successive includono le definizioni necessarie:
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/atomic.h>
#include <stdint.h>
avr/io.h contiene i nomi dei registri e dei bit dell’ATmega328P, come DDRB, PORTB, TCCR1B e PB0.
avr/interrupt.h fornisce gli strumenti per gestire le interruzioni, compresi ISR() e sei().
util/atomic.h permette di leggere in modo sicuro una variabile condivisa tra il programma principale e una routine di interruzione.
stdint.h definisce tipi con dimensione precisa, come uint8_t e uint32_t.
Le due costanti successive definiscono il pin utilizzato e l’intervallo di lampeggio:
#define LED_BIT PB0
#define INTERVALLO_LED_MS 500UL
LED_BIT indica il bit PB0 della porta B.
INTERVALLO_LED_MS stabilisce che il LED cambi stato ogni 500 millisecondi. Rimane quindi acceso per mezzo secondo e spento per mezzo secondo.
La variabile:
volatile uint32_t millisecondi = 0;
contiene il numero di millisecondi trascorsi dall’avvio del programma.
uint32_t è un intero senza segno da 32 bit.
volatile informa il compilatore che il valore può cambiare in qualsiasi momento, perché viene modificato dalla routine di interruzione. Senza volatile, il compilatore potrebbe conservare una copia del valore e non rilevare correttamente gli aggiornamenti.
La routine:
ISR(TIMER1_COMPA_vect)
{
millisecondi++;
}
viene eseguita automaticamente ogni volta che Timer1 raggiunge il valore impostato nel registro OCR1A.
TIMER1_COMPA_vect è il vettore associato all’interruzione di confronto A di Timer1.
A ogni interruzione:
millisecondi++;
incrementa di uno il contatore del tempo. Poiché l’interruzione avviene ogni millisecondo, il valore della variabile corrisponde al tempo trascorso espresso in millisecondi.
La funzione:
uint32_t leggi_millisecondi(void)
restituisce una copia sicura del contatore.
La dichiarazione:
uint32_t valore;
crea una variabile locale nella quale verrà copiato il contenuto di millisecondi.
La copia viene eseguita all’interno di:
ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
{
valore = millisecondi;
}
L’ATmega328P è un microcontrollore a 8 bit e non può leggere una variabile da 32 bit con una sola operazione. Durante la lettura dei quattro byte potrebbe verificarsi un’interruzione e modificare il valore prima che la copia sia completa.
Il blocco atomico impedisce temporaneamente l’esecuzione delle interruzioni, copia il valore e ripristina automaticamente lo stato precedente.
La funzione termina con:
return valore;
che restituisce la copia al punto del programma che ha chiamato la funzione.
La funzione:
void inizializza_timer1(void)
configura Timer1.
Le prime istruzioni azzerano i registri principali:
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;
TCCR1A e TCCR1B sono i registri di controllo di Timer1.
TCNT1 contiene il valore corrente del conteggio. Impostandolo a zero, il timer parte dall’inizio.
Il registro:
OCR1A = 249;
contiene il valore con il quale viene confrontato il contatore.
La CPU lavora a 16 MHz. Con un prescaler pari a 64, Timer1 riceve:
16 000 000 / 64 = 250 000 impulsi al secondo
Ogni incremento del timer dura quindi:
1 / 250 000 = 0,000004 secondi
cioè 4 microsecondi.
Il timer conta da 0 a 249, quindi esegue 250 conteggi:
250 × 4 µs = 1 ms
La modalità CTC viene attivata con:
TCCR1B |= (1 << WGM12);
WGM12 è il bit che seleziona la modalità Clear Timer on Compare Match. Quando il contatore raggiunge OCR1A, viene automaticamente riportato a zero.
L’espressione:
1 << WGM12
crea un valore nel quale è impostato soltanto il bit corrispondente a WGM12.
L’operatore:
|=
imposta quel bit senza modificare gli altri bit del registro.
Il prescaler viene impostato a 64 con:
TCCR1B |= (1 << CS11) | (1 << CS10);
I bit CS11 e CS10 selezionano il divisore della frequenza del timer.
L’interruzione di confronto viene abilitata con:
TIMSK1 |= (1 << OCIE1A);
TIMSK1 è il registro che abilita le interruzioni di Timer1.
OCIE1A abilita specificamente l’interruzione associata al confronto tra TCNT1 e OCR1A.
Il programma principale inizia con:
int main(void)
main() è la funzione dalla quale parte l’esecuzione del programma.
All’interno vengono dichiarate due variabili:
uint32_t ultimo_cambio_led = 0;
uint8_t led_acceso = 0;
ultimo_cambio_led memorizza il momento dell’ultima variazione del LED.
led_acceso memorizza lo stato del LED:
0: LED spento;1: LED acceso.
uint8_t è un intero senza segno da 8 bit ed è sufficiente per rappresentare i due stati.
Prima di configurare il pin come uscita viene impostato il livello basso:
PORTB &= ~(1 << LED_BIT);
1 << LED_BIT crea un valore con il solo bit PB0 impostato a uno.
L’operatore ~ inverte tutti i bit, ottenendo una maschera nella quale il bit PB0 vale zero e gli altri valgono uno.
L’operazione:
PORTB &=
azzera soltanto il bit PB0, lasciando invariati gli altri bit di PORTB.
Il pin viene quindi configurato come uscita:
DDRB |= (1 << LED_BIT);
DDRB è il registro che stabilisce la direzione dei pin della porta B.
Impostando a uno il bit corrispondente a PB0, il pin viene configurato come uscita.
Successivamente viene inizializzato Timer1:
inizializza_timer1();
Le interruzioni globali vengono abilitate con:
sei();
Senza questa istruzione, Timer1 potrebbe raggiungere il valore di confronto, ma la routine ISR() non verrebbe eseguita.
Il ciclo principale è:
while (1)
Il valore 1 rende la condizione sempre vera, quindi il ciclo continua per tutta la durata del funzionamento del microcontrollore.
All’inizio di ogni ripetizione viene letto il tempo corrente:
uint32_t tempo_attuale = leggi_millisecondi();
tempo_attuale contiene una copia del numero di millisecondi trascorsi.
La condizione:
if (tempo_attuale - ultimo_cambio_led >= INTERVALLO_LED_MS)
calcola il tempo trascorso dall’ultima variazione del LED.
Quando la differenza raggiunge almeno 500 millisecondi, il codice contenuto nel blocco viene eseguito.
L’istante della variazione viene aggiornato:
ultimo_cambio_led = tempo_attuale;
Successivamente viene controllato lo stato del LED:
if (led_acceso == 0)
Se il LED è spento, il bit PB0 viene portato a uno:
PORTB |= (1 << LED_BIT);
Il pin passa a HIGH e il LED si accende.
La variabile di stato viene aggiornata:
led_acceso = 1;
Se invece led_acceso non vale zero, viene eseguito il blocco else:
PORTB &= ~(1 << LED_BIT);
led_acceso = 0;
Il pin passa a LOW, il LED si spegne e la variabile torna a zero.
La sottrazione tra tempo_attuale e ultimo_cambio_led utilizza valori senza segno. In questo modo il controllo continua a funzionare correttamente anche quando il contatore da 32 bit raggiunge il proprio valore massimo e riparte da zero.
Il ciclo principale non rimane fermo durante i 500 millisecondi. Continua a essere eseguito e controlla ripetutamente se è trascorso l’intervallo stabilito.
Programma equivalente in C++
Il programma seguente produce lo stesso lampeggio del codice C: il LED esterno collegato a PB0 cambia stato ogni 500 millisecondi. Anche questa versione utilizza direttamente i registri dell’ATmega328P e Timer1.
#define F_CPU 16000000UL
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/atomic.h>
#include <stdint.h>
constexpr uint8_t LED_BIT = PB0;
constexpr uint32_t INTERVALLO_LED_MS = 500UL;
volatile uint32_t millisecondi = 0;
ISR(TIMER1_COMPA_vect)
{
++millisecondi;
}
uint32_t leggi_millisecondi()
{
uint32_t valore;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
{
valore = millisecondi;
}
return valore;
}
void inizializza_timer1()
{
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;
OCR1A = 249;
TCCR1B |= (1 << WGM12);
TCCR1B |= (1 << CS11) | (1 << CS10);
TIMSK1 |= (1 << OCIE1A);
}
void imposta_led(bool acceso)
{
if (acceso)
{
PORTB |= (1 << LED_BIT);
}
else
{
PORTB &= ~(1 << LED_BIT);
}
}
int main()
{
uint32_t ultimo_cambio_led = 0;
bool led_acceso = false;
imposta_led(false);
DDRB |= (1 << LED_BIT);
inizializza_timer1();
sei();
while (true)
{
const uint32_t tempo_attuale = leggi_millisecondi();
if (tempo_attuale - ultimo_cambio_led >= INTERVALLO_LED_MS)
{
ultimo_cambio_led = tempo_attuale;
led_acceso = !led_acceso;
imposta_led(led_acceso);
}
}
}
Programma equivalente con framework Wiring
Nel framework Wiring, il collegamento corrispondente a PB0 sulla UNO R3 viene indicato con il numero 8.
Il LED esterno cambia stato ogni 500 millisecondi.
const int pin_led = 8;
const unsigned long intervallo_ms = 500;
unsigned long ultimo_cambio_ms = 0;
bool led_acceso = false;
void setup()
{
pinMode(pin_led, OUTPUT);
digitalWrite(pin_led, LOW);
}
void loop()
{
unsigned long tempo_attuale_ms = millis();
if (tempo_attuale_ms - ultimo_cambio_ms >= intervallo_ms)
{
ultimo_cambio_ms = tempo_attuale_ms;
if (led_acceso == false)
{
digitalWrite(pin_led, HIGH);
led_acceso = true;
}
else
{
digitalWrite(pin_led, LOW);
led_acceso = false;
}
}
}
Spiegazione di pinMode(), digitalWrite(), HIGH, LOW e millis()
La dichiarazione:
const int pin_led = 8;
assegna il nome pin_led al collegamento digitale 8 della scheda. Nel programma si utilizza quindi pin_led al posto del numero 8.
const indica che il valore non deve cambiare durante l’esecuzione.
La dichiarazione:
const unsigned long intervallo_ms = 500;
stabilisce che il LED deve cambiare stato ogni 500 millisecondi.
unsigned long è un tipo intero senza segno adatto a contenere i valori temporali restituiti da millis().
La variabile:
unsigned long ultimo_cambio_ms = 0;
memorizza il momento nel quale il LED ha cambiato stato l’ultima volta.
La variabile:
bool led_acceso = false;
memorizza lo stato del LED:
false: LED spento;true: LED acceso.
pinMode()
Nel setup() viene eseguita:
pinMode(pin_led, OUTPUT);
pinMode() stabilisce il funzionamento del collegamento indicato.
Il primo argomento è il pin da configurare:
pin_led
Il secondo argomento è la modalità:
OUTPUT
OUTPUT configura il pin come uscita digitale. Dopo questa istruzione, il programma può portarlo a 5 V oppure a 0 V.
digitalWrite()
L’istruzione:
digitalWrite(pin_led, LOW);
porta inizialmente l’uscita a 0 V, mantenendo il LED spento.
digitalWrite() riceve due argomenti:
digitalWrite(pin, stato);
Il primo indica quale uscita deve essere modificata. Il secondo indica il livello da applicare.
Nel programma vengono utilizzate queste due forme:
digitalWrite(pin_led, HIGH);
e:
digitalWrite(pin_led, LOW);
HIGH e LOW
HIGH indica il livello logico alto dell’uscita. Con alimentazione a 5 V, la tensione del pin è prossima a VCC ma dipende dalla corrente erogata e dalle caratteristiche elettriche dell’uscita.
Con il collegamento utilizzato, il LED si accende.
LOW indica il livello logico basso dell’uscita. La tensione del pin è prossima a 0 V, ma il valore effettivo dipende dalla corrente assorbita dal pin.
Il LED si spegne.
HIGH e LOW non sono variabili create nel programma. Sono costanti definite dal framework Wiring.
millis()
All’interno di loop() viene eseguita:
unsigned long tempo_attuale_ms = millis();
millis() restituisce il numero di millisecondi trascorsi dall’avvio del programma.
Per esempio:
- dopo un secondo restituisce circa
1000; - dopo cinque secondi restituisce circa
5000; - dopo un minuto restituisce circa
60000.
Il valore viene salvato nella variabile:
tempo_attuale_ms
Il programma confronta il tempo attuale con il momento dell’ultimo cambiamento:
if (tempo_attuale_ms - ultimo_cambio_ms >= intervallo_ms)
La sottrazione:
tempo_attuale_ms - ultimo_cambio_ms
calcola quanti millisecondi sono trascorsi dall’ultima variazione del LED.
Quando il risultato raggiunge almeno 500:
>= intervallo_ms
la condizione diventa vera e il LED cambia stato.
Subito dopo viene aggiornato il riferimento temporale:
ultimo_cambio_ms = tempo_attuale_ms;
Da quel momento inizia il conteggio dei successivi 500 millisecondi.
Cambiamento dello stato del LED
Il programma controlla la variabile:
if (led_acceso == false)
Se il LED risulta spento, esegue:
digitalWrite(pin_led, HIGH);
led_acceso = true;
L’uscita passa a 5 V e la variabile registra che il LED è acceso.
Se invece il LED è già acceso, viene eseguito il blocco else:
digitalWrite(pin_led, LOW);
led_acceso = false;
L’uscita passa a 0 V e la variabile registra che il LED è spento.
La sequenza si ripete ogni 500 millisecondi:
HIGH per 500 ms
LOW per 500 ms
HIGH per 500 ms
LOW per 500 ms
Il periodo completo del lampeggio è quindi di un secondo.
Relè e corrente della bobina
Un relè elettromeccanico contiene una bobina e uno o più contatti.
Quando la bobina viene alimentata, genera un campo magnetico che sposta il meccanismo interno e modifica la posizione dei contatti. Quando la bobina non è alimentata, il meccanismo ritorna nella posizione di riposo.
La tensione e la corrente della bobina non devono essere confuse con la tensione e la corrente che possono attraversare i contatti.
Per esempio, un relè può riportare:
- bobina: 5 V;
- corrente della bobina: 70 mA;
- contatti: 10 A a 250 V AC.
I 10 A indicano la corrente massima commutabile dai contatti nelle condizioni specificate. Non indicano la corrente necessaria per eccitare la bobina.
La corrente richiesta dalla bobina deve essere ricavata dal datasheet del relè oppure calcolata conoscendone la resistenza:
I = V / R
Se una bobina da 5 V presenta una resistenza di 71 Ω:
I = 5 / 71 ≈ 0,070 A
La bobina assorbe quindi circa:
70 mA
Questa corrente è molto superiore ai 20 mA usati dal datasheet per specificare le caratteristiche del livello logico di uscita a 5 V e supera anche il limite assoluto di 40 mA per singolo pin. La bobina non può quindi essere collegata direttamente al pin.
L’uscita del microcontrollore deve fornire soltanto il segnale di comando. La corrente richiesta dalla bobina viene gestita da un transistor BJT NPN.
Pilotaggio del relè con BJT NPN
Per comandare la bobina del relè si utilizza come esempio un transistor BJT NPN come interruttore elettronico.
Il transistor dispone di tre terminali:
- base;
- collettore;
- emettitore.
Nel circuito:
- l’emettitore è collegato a GND;
- il collettore è collegato a un terminale della bobina;
- l’altro terminale della bobina è collegato a +5 V;
- la base è comandata dall’uscita dell’ATmega328P attraverso una resistenza;
- la massa dell’alimentazione del relè e quella del microcontrollore devono essere collegate insieme.
Quando l’uscita del microcontrollore è LOW, sulla base non circola corrente sufficiente. Il transistor è interdetto, la corrente non attraversa la bobina e il relè rimane diseccitato.
Quando l’uscita passa a HIGH, una piccola corrente entra nella base. Il transistor entra in saturazione e permette alla corrente della bobina di scorrere dal positivo dell’alimentazione verso massa:
+5 V → bobina → collettore → emettitore → GND
Il relè si eccita e i contatti cambiano posizione.
Il pin dell’ATmega328P fornisce soltanto la corrente di base. La corrente di 70 mA richiesta dalla bobina viene fornita dall’alimentazione esterna e attraversa il collettore e l’emettitore del transistor.
Il BJT viene quindi utilizzato in due soli stati:
- interdizione: transistor spento, relè diseccitato;
- saturazione: transistor acceso, relè eccitato.
La regione intermedia non è utile in questo circuito, perché il relè deve essere completamente eccitato oppure completamente diseccitato.
Resistenza di base, caratteristiche e calcoli del BC337
Per pilotare la bobina, come esempio, si utilizza un transistor BJT NPN BC337.
Per evitare di mescolare valori appartenenti a produttori o versioni differenti, nell’esempio si assumono le caratteristiche del BC337-25 prodotto da onsemi, in contenitore TO-92:
- transistor bipolare NPN;
- tensione massima collettore-emettitore
VCEO: 45 V; - corrente continua massima di collettore
IC: 800 mA; - potenza massima dissipabile: 625 mW, nelle condizioni termiche indicate dal produttore;
- guadagno in corrente continuo minimo
hFE: 160 per la versioneBC337-25; - tensione massima emettitore-base
VEBO: 5 V.
Il suffisso identifica la classe di guadagno. Nella famiglia onsemi:
BC337-16:hFEminimo 100;BC337-25:hFEminimo 160;BC337-40:hFEminimo 250.
Per il pilotaggio della bobina sono particolarmente importanti:
- la corrente massima di collettore, che deve essere superiore alla corrente della bobina;
- la tensione massima collettore-emettitore;
- la potenza dissipabile;
- la corrente di base necessaria per portare il transistor in saturazione.
Questi valori sono ricavati dalla documentazione tecnica ufficiale del produttore. Il calcolo della resistenza di base, invece, deriva dalla legge di Ohm e dall’impiego di un guadagno forzato prudenziale. Non viene utilizzato direttamente il valore nominale di hFE, perché varia con la corrente, la temperatura e il singolo esemplare.
Nel caso considerato, la bobina del relè funziona a 5 V e assorbe:
IC = 70 mA
Il BC337 viene utilizzato come interruttore e deve lavorare in saturazione. Per dimensionare la corrente di base si assume un guadagno forzato pari a 10:
IB = IC / 10
IB = 70 mA / 10
IB = 7 mA
Considerando:
- uscita dell’ATmega328P a
HIGH: 5 V; - caduta base-emettitore del BC337 in saturazione: circa 0,8 V;
- corrente di base richiesta: 7 mA;
la resistenza di base teorica è:
RB = (5 − 0,8) / 0,007
RB = 600 Ω
Si sceglie il valore commerciale da:
RB = 560 Ω
La corrente di base effettiva diventa:
IB = (5 − 0,8) / 560
IB = 0,0075 A
IB = 7,5 mA
La corrente richiesta al pin dell’ATmega328P, pari a circa 7,5 mA, rimane ben al di sotto dei 20 mA usati dal datasheet per specificare le caratteristiche del livello logico di uscita a 5 V.
Con una corrente di collettore di 70 mA e una tensione collettore-emettitore in saturazione assunta pari a 0,2 V, la potenza dissipata dal BC337 è:
P = VCE(sat) × IC
P = 0,2 × 0,070
P = 0,014 W
Il transistor dissipa quindi circa 14 mW, valore molto inferiore alla potenza massima prevista per il componente nelle condizioni termiche indicate dal produttore.
Diodo di ricircolo
La bobina del relè è un componente induttivo. Quando il BC337 si spegne, la corrente nella bobina non può interrompersi immediatamente e viene generata una sovratensione che potrebbe danneggiare il transistor.
Per proteggere il BC337 si collega un diodo 1N4007 in parallelo alla bobina:
- il catodo, riconoscibile dalla fascetta stampata sul corpo del diodo, deve essere collegato al terminale della bobina connesso a +5 V;
- l’anodo, sul lato opposto alla fascetta, deve essere collegato al terminale della bobina connesso al collettore del BC337.
La fascetta del diodo deve quindi essere rivolta verso il +5 V.
Durante il normale funzionamento il diodo non conduce. Quando il BC337 si spegne, il diodo permette alla corrente residua della bobina di circolare fino alla dissipazione dell’energia immagazzinata.
Se il diodo viene montato al contrario, conduce quando il transistor si accende e provoca un collegamento diretto tra +5 V e massa.
Commutazione periodica del relè
Il relè viene collegato a una seconda uscita dell’ATmega328P attraverso il BC337 descritto nel pezzo precedente.
Si utilizza:
PB0per il LED;PB1per la base del BC337 attraverso la resistenza da 560 Ω.
Nel framework Wiring, sulla UNO R3:
PB0corrisponde al collegamento digitale8;PB1corrisponde al collegamento digitale9.
Il funzionamento è:
- uscita
PB1aHIGH: il BC337 entra in saturazione e il relè si eccita; - uscita
PB1aLOW: il BC337 si interdice e il relè si diseccita.
Il relè viene fatto cambiare stato ogni 5 secondi. Il LED continua invece a cambiare stato ogni 500 millisecondi. Le due temporizzazioni sono indipendenti.
La commutazione periodica del relè serve a verificare il funzionamento del circuito di pilotaggio. La prova deve essere eseguita per un tempo limitato, perché il relè contiene parti meccaniche soggette a usura.
Aggiunta al programma in C
Si aggiungono le costanti:
#define RELE_BIT PB1
#define INTERVALLO_RELE_MS 5000UL
All’interno di main() si aggiungono:
uint32_t ultimo_cambio_rele = 0;
uint8_t rele_eccitato = 0;
Prima di configurare il pin come uscita, viene imposto il livello basso:
PORTB &= ~(1 << RELE_BIT);
Il pin PB1 viene configurato come uscita insieme a PB0:
DDRB |= (1 << LED_BIT) | (1 << RELE_BIT);
Nel ciclo while (1) si aggiunge:
if (tempo_attuale - ultimo_cambio_rele >= INTERVALLO_RELE_MS)
{
ultimo_cambio_rele = tempo_attuale;
if (rele_eccitato == 0)
{
PORTB |= (1 << RELE_BIT);
rele_eccitato = 1;
}
else
{
PORTB &= ~(1 << RELE_BIT);
rele_eccitato = 0;
}
}
Il controllo del LED e quello del relè utilizzano lo stesso contatore dei millisecondi, ma intervalli differenti.
Aggiunta al programma in C++
Si aggiungono:
constexpr uint8_t RELE_BIT = PB1;
constexpr uint32_t INTERVALLO_RELE_MS = 5000UL;
La funzione che controlla il relè è:
void imposta_rele(bool eccitato)
{
if (eccitato)
{
PORTB |= (1 << RELE_BIT);
}
else
{
PORTB &= ~(1 << RELE_BIT);
}
}
All’interno di main() si aggiungono:
uint32_t ultimo_cambio_rele = 0;
bool rele_eccitato = false;
Lo stato iniziale viene impostato prima della configurazione del pin:
imposta_rele(false);
DDRB |= (1 << LED_BIT) | (1 << RELE_BIT);
Nel ciclo principale si aggiunge:
if (tempo_attuale - ultimo_cambio_rele >= INTERVALLO_RELE_MS)
{
ultimo_cambio_rele = tempo_attuale;
rele_eccitato = !rele_eccitato;
imposta_rele(rele_eccitato);
}
Aggiunta al programma con framework Wiring
Si aggiungono le dichiarazioni:
const int pin_rele = 9;
const unsigned long intervallo_rele_ms = 5000;
unsigned long ultimo_cambio_rele_ms = 0;
bool rele_eccitato = false;
Nel setup():
pinMode(pin_rele, OUTPUT);
digitalWrite(pin_rele, LOW);
Nel loop(), dopo il controllo del LED:
if (tempo_attuale_ms - ultimo_cambio_rele_ms >= intervallo_rele_ms)
{
ultimo_cambio_rele_ms = tempo_attuale_ms;
if (rele_eccitato == false)
{
digitalWrite(pin_rele, HIGH);
rele_eccitato = true;
}
else
{
digitalWrite(pin_rele, LOW);
rele_eccitato = false;
}
}
La sequenza risultante è:
- il LED cambia stato ogni 500 ms;
- il relè cambia stato ogni 5 s;
- quando il relè è eccitato,
rele_eccitatovaletrue; - quando il relè è diseccitato,
rele_eccitatovalefalse; - il cambiamento dello stato del LED non modifica la temporizzazione del relè;
- il cambiamento dello stato del relè non modifica la temporizzazione del LED.
Schemi elettrici
L’articolo deve contenere due schemi elettrici separati, realizzati con simboli elettronici standard e non con rappresentazioni testuali.
Circuito del LED
Il primo schema mostra:
- uscita
PB0dell’ATmega328P; - resistenza da 330 Ω collegata in serie;
- anodo del LED collegato alla resistenza;
- catodo del LED collegato a GND.
Il percorso della corrente è quindi:
PB0 → resistenza da 330 Ω → LED → GND
Quando PB0 è HIGH, il LED si accende. Quando PB0 è LOW, il LED si spegne.
Circuito del relè
Il secondo schema mostra:
- uscita
PB1dell’ATmega328P; - resistenza da 560 Ω tra
PB1e la base del BC337; - emettitore del BC337 collegato a GND;
- collettore del BC337 collegato a un terminale della bobina;
- altro terminale della bobina collegato a +5 V;
- diodo
1N4007collegato in parallelo alla bobina; - catodo del diodo, riconoscibile dalla fascetta, collegato a +5 V;
- anodo del diodo collegato al collettore del BC337;
- massa dell’alimentazione del relè collegata alla massa dell’ATmega328P.
Lo schema deve riportare chiaramente i nomi dei terminali del transistor:
B: base;C: collettore;E: emettitore.
Deve inoltre essere indicata la fascetta del diodo sul lato collegato al positivo della bobina.
Quando PB1 è HIGH, il BC337 entra in saturazione e alimenta la bobina. Quando PB1 è LOW, il transistor si interdice e il relè si diseccita.
Collegamenti completi e programma Wiring
Pin utilizzati
Nel programma con framework Wiring si utilizzano:
D8, corrispondente aPB0, per il LED;D9, corrispondente aPB1, per il comando del relè tramite BC337.
Collegamento del LED
Il LED esterno deve essere collegato in questo modo:
D8collegato a una resistenza da 330 Ω;- altro terminale della resistenza collegato all’anodo del LED;
- catodo del LED collegato a
GND.
La resistenza e il LED sono collegati in serie.
Quando D8 è HIGH, il LED si accende. Quando D8 è LOW, il LED si spegne.
Collegamento del relè
Il relè non deve essere collegato direttamente a D9.
I collegamenti sono:
D9collegato a una resistenza da 560 Ω;- altro terminale della resistenza collegato alla base del
BC337; - emettitore del
BC337collegato aGND; - collettore del
BC337collegato a un terminale della bobina del relè; - altro terminale della bobina collegato a
+5 V; - massa dell’alimentazione del relè collegata al
GNDdella UNO R3.
I terminali base, collettore ed emettitore devono essere identificati consultando il datasheet del BC337 effettivamente utilizzato. Non bisogna ricavare la piedinatura soltanto dalla forma del contenitore.
Il diodo 1N4007 deve essere collegato in parallelo alla bobina:
- catodo, riconoscibile dalla fascetta, collegato al terminale della bobina connesso a
+5 V; - anodo collegato al terminale della bobina connesso al collettore del BC337.
La fascetta del diodo deve quindi essere rivolta verso il positivo.
I contatti COM, NO e NC del relè sono elettricamente separati dalla bobina:
- con relè diseccitato,
COMè collegato aNC; - con relè eccitato,
COMè collegato aNO.
Per la prova del circuito i contatti possono essere lasciati scollegati oppure controllati con un multimetro in modalità continuità.
Programma completo con framework Wiring
const int pin_led = 8;
const int pin_rele = 9;
const unsigned long intervallo_led_ms = 500;
const unsigned long intervallo_rele_ms = 5000;
unsigned long ultimo_cambio_led_ms = 0;
unsigned long ultimo_cambio_rele_ms = 0;
bool led_acceso = false;
bool rele_eccitato = false;
void setup()
{
pinMode(pin_led, OUTPUT);
pinMode(pin_rele, OUTPUT);
digitalWrite(pin_led, LOW);
digitalWrite(pin_rele, LOW);
}
void loop()
{
unsigned long tempo_attuale_ms = millis();
if (tempo_attuale_ms - ultimo_cambio_led_ms >= intervallo_led_ms)
{
ultimo_cambio_led_ms = tempo_attuale_ms;
if (led_acceso == false)
{
digitalWrite(pin_led, HIGH);
led_acceso = true;
}
else
{
digitalWrite(pin_led, LOW);
led_acceso = false;
}
}
if (tempo_attuale_ms - ultimo_cambio_rele_ms >= intervallo_rele_ms)
{
ultimo_cambio_rele_ms = tempo_attuale_ms;
if (rele_eccitato == false)
{
digitalWrite(pin_rele, HIGH);
rele_eccitato = true;
}
else
{
digitalWrite(pin_rele, LOW);
rele_eccitato = false;
}
}
}
Il LED cambia stato ogni 500 millisecondi. Rimane quindi acceso per mezzo secondo e spento per mezzo secondo.
Il relè cambia stato ogni 5 secondi. Rimane quindi eccitato per cinque secondi e diseccitato per cinque secondi.
Le due temporizzazioni utilizzano lo stesso valore restituito da millis(), ma conservano separatamente il momento dell’ultima commutazione. Il lampeggio del LED e la commutazione del relè rimangono quindi indipendenti.
Schede relè già pronte e logica HIGH/LOW
In commercio sono disponibili schede relè già assemblate, con uno o più relè e il relativo circuito di pilotaggio.
Una tipica scheda può comprendere:
- relè elettromeccanico;
- transistor di pilotaggio;
- resistenza di base;
- diodo di ricircolo;
- LED di segnalazione;
- morsetti
COM,NOeNC; - connettori di alimentazione e comando;
- eventuale optoisolatore.
I collegamenti di ingresso sono normalmente indicati con:
VCC: alimentazione del circuito;GND: massa;IN: ingresso di comando.
Nelle schede con più relè sono presenti ingressi come IN1, IN2, IN3 e IN4.
Alcuni moduli presentano anche i terminali:
JD-VCC: alimentazione delle bobine;VCC: alimentazione della parte logica;GND: riferimento comune.
Un ponticello può collegare VCC e JD-VCC. La disposizione cambia tra i diversi moduli e deve essere verificata leggendo le indicazioni stampate sulla scheda.
Moduli attivi con livello alto
In un modulo attivo HIGH il relè si eccita quando l’ingresso viene portato a livello alto:
IN = HIGH → relè eccitato
IN = LOW → relè diseccitato
Il programma può utilizzare direttamente:
digitalWrite(pin_rele, HIGH);
per eccitare il relè e:
digitalWrite(pin_rele, LOW);
per diseccitarlo.
Moduli attivi con livello basso
Molte schede relè già pronte sono invece attive LOW.
In questo caso il funzionamento è invertito:
IN = LOW → relè eccitato
IN = HIGH → relè diseccitato
Il livello basso attiva il transistor o l’optoisolatore presente sulla scheda. Di conseguenza, un programma scritto per il circuito con BC337 descritto in precedenza farebbe funzionare il relè al contrario.
Con un modulo attivo LOW, il comando corretto è:
digitalWrite(pin_rele, LOW);
per eccitare il relè e:
digitalWrite(pin_rele, HIGH);
per diseccitarlo.
Anche lo stato iniziale deve essere modificato. Nel setup() si deve scrivere:
pinMode(pin_rele, OUTPUT);
digitalWrite(pin_rele, HIGH);
In questo modo il relè rimane diseccitato all’avvio.
Programma adattato a un modulo attivo LOW
La funzione di comando può essere scritta in modo esplicito:
void imposta_rele(bool eccitato)
{
if (eccitato)
{
digitalWrite(pin_rele, LOW);
}
else
{
digitalWrite(pin_rele, HIGH);
}
}
Il resto del programma può continuare a utilizzare una variabile descrittiva:
bool rele_eccitato = false;
Quando la variabile vale true, la funzione applica LOW al pin. Quando vale false, applica HIGH.
Il programma continua quindi a ragionare sullo stato fisico del relè, senza dover ricordare ogni volta che la logica elettrica del modulo è invertita.
Come verificare la logica del modulo
Prima di utilizzare una scheda relè occorre verificare se l’ingresso è attivo HIGH oppure attivo LOW.
La prova può essere eseguita senza collegare alcun carico ai contatti:
- alimentare correttamente il modulo;
- collegare
GNDdel modulo alGNDdella scheda; - impostare l’ingresso a
HIGH; - osservare se il relè si eccita;
- impostare l’ingresso a
LOW; - osservare nuovamente lo stato del relè.
Il LED presente sul modulo può aiutare, ma non sempre indica direttamente lo stato meccanico dei contatti. La verifica più sicura consiste nell’ascoltare lo scatto del relè oppure misurare la continuità tra COM, NO e NC.
La logica non deve essere dedotta soltanto dal colore del modulo o dal numero dei componenti. Due schede esteticamente simili possono utilizzare circuiti di ingresso differenti.
Compatibilità della tensione di comando
Una scheda con relè da 5 V non è automaticamente compatibile con qualsiasi segnale logico.
Occorre distinguere:
- tensione della bobina;
- tensione di alimentazione del modulo;
- tensione riconosciuta dall’ingresso
IN.
Un modulo alimentato a 5 V può accettare correttamente un comando a 5 V, ma potrebbe non riconoscere in modo affidabile un livello alto di 3,3 V. Altri moduli funzionano invece correttamente sia a 3,3 V sia a 5 V.
La compatibilità deve essere verificata prima del collegamento. Il fatto che il relè riporti la scritta “5 V” indica normalmente la tensione della bobina, non necessariamente la tensione minima riconosciuta dall’ingresso di comando.
Fonti principali
Microchip Technology — ATmega328P: 8-bit AVR Microcontroller with 32K Bytes In-System Programmable Flash, datasheet.
Arduino — Arduino UNO R3: scheda tecnica, schema elettrico e corrispondenza dei collegamenti con l’ATmega328P.
onsemi — BC337: NPN Bipolar Transistor, datasheet.
Vishay — 1N4001–1N4007: General Purpose Plastic Rectifier, datasheet.