Cinque modi per far lampeggiare un diodo LED (lezione 12)
Introduzione
Far lampeggiare un diodo LED può essere considerato dalla stragrande maggioranza delle persone un’operazione semplice, ma ci sono diversi modi per implementare questa funzione, in quest’articolo descriviamo i cinque metodi più utilizzati.
Il primo approccio che di solito un neofita ha con un sistema embedded è sicuramente quello di far lampeggiare un diodo LED, molti lo paragonano al famoso “Hello World” della programmazione.
Ma questa semplice implementazione, può essere realizzata in molteplici modi e ognuno di essi può essere un ottimo esercizio didattico che può insegnarci diverse caratteristiche di un microcontrollore, per esempio come gestire il timer.
Di seguito riporto l’elenco dei modi descritti in questo articolo per far lampeggiare un diodo LED:
- Delays
- Contatore
- Funzione Millis
- Watchdog Timer Interrupt
- Timer Interrupt
Delays
Il più semplice modo per far lampeggiare un diodo LED è quello di usare la funzione “delay”, ed è il primo esempio che normalmente viene proposto a chi si sta avvicinando al mondo della programmazione dei sistemi a microcontrollore.
/**************************************************************** Data 21-05-22 scritto da Gennaro Vitiello codice per far lampeggiare diodo LED usando la funzione delay Maggiori informazioni sul sito www.quattrodispositivi.it ******************************************************************/ #define LED 13 // Associamo un nome mnemonico al numero 13 void setup() { pinMode(LED, OUTPUT); //il pin dove e' connesso il dido LED impostato come uscita } void loop() { digitalWrite(LED, HIGH); // Accendiamo il diodo LED con un valore di tensione 5V sull'uscita delay(1000); // aspettiamo per un secondo digitalWrite(LED, LOW); // spegniamo il diodo LED con un valore di 0V sull'uscita delay(1000); // aspettiamo per un secondo }
Di seguito descriviamo i dettagli del codice.
#define LED 13 // Associamo un nome mnemonico al numero 13
Definiamo il pin dove è connesso il diodo LED.
void setup() { pinMode(LED, OUTPUT); //il pin dove e' connesso il dido LED impostato come uscita }
Impostiamo il pin 13 come un output, poichè esso deve pilotare il diodo LED.
void loop() { digitalWrite(LED, HIGH); // Accendiamo il diodo LED con un valore di tensione 5V sull'uscita delay(1000); // aspettiamo per un secondo digitalWrite(LED, LOW); // spegniamo il diodo LED con un valore di 0V sull'uscita delay(1000); // aspettiamo per un secondo }
Questa è la parte di codice che si ripete a ciclo continuo.
Tra le parentesi tonde della funzione “delay” è espresso il tempo in millisecondi del ritardo introdotto(1000mS = 1S) .
Il diodo LED alterna lo stato di accensione e spegnimento con un periodo di 1 secondo.
Questo tipo di approccio ha un grosso vantaggio, l’implementazione è molto semplice, basta usare la funzione “delay()” aggiungere tra le parentesi il ritardo desiderato ed il gioco è fatto!
La funzione “delay()” è poco efficiente, poiché quando essa entra in esecuzione, il microcontrollore non esegue nessuna istruzione e attende che trascorra l’intervallo temporale impostato.
Quando il codice è molto complesso e ci sono processi che devono controllare in real-time delle periferiche, lasciare il microcontrollore in una sorta di stallo funzionale è inaccettabile!
Contatore
Questo metodo è altrettanto semplice come quello descritto nel paragrafo precedente, ma il microcontrollore non verra’ mai bloccato, analizziamo i dettagli.
/* Data 21-05-22 scritto da Gennaro Vitiello codice per far lampeggiare diodo LED usando un contatore Maggiori informazioni sul sito www.quattrodispositivi.it */ #define LED 13 // Associamo un nome mnemonico al numero 13 long contatore = 0; // definamo la variabile contatore e azzerata void setup() { pinMode(LED, OUTPUT); // il pin dove e' connesso il dido LED impostato come uscita } void loop() { contatore++; // incrementiamo di uno la variabile contattore if (contatore == 100000) // effettuiamo un controllo sulla variabile contatore se e' inferiore a 100000 { digitalWrite(LED, HIGH); // Accendiamo il diodo LED con un valore di tensione 5V sull'uscita } else if (contatore == 200000) // effettuiamo un controllo sulla variabile contatore se e' inferiore a 200000 { contatore = 0; //azzeriamo la variabile contatore digitalWrite(LED, LOW); // spegniamo il diodo LED con un valore di 0V sull'uscita } }
Di seguito descriviamo i dettagli del codice.
#define LED 13 // Associamo un nome mnemonico al numero 13 long contatore = 0; // definamo la variabile contatore e azzerata
Associamo il numero 13 con la “define” LED.
La variabile del contatore è stata azzerata.
void setup() { pinMode(LED, OUTPUT); // il pin dove e' connesso il dido LED impostato come uscita }
Il pin 13, definito LED nell’istruzione precedente, è associato come una uscita per pilotare il diodo LED.
void loop() { contatore++; // incrementiamo di uno la variabile contattore if (contatore == 100000) // effettuiamo un controllo sulla variabile contatore se e' inferiore a 100000 { digitalWrite(LED, HIGH); // Accendiamo il diodo LED con un valore di tensione 5V sull'uscita } else if (contatore == 200000) // effettuiamo un controllo sulla variabile contatore se e' inferiore a 200000 { contatore = 0; //azzeriamo la variabile contatore digitalWrite(LED, LOW); // spegniamo il diodo LED con un valore di 0V sull'uscita } }
Ad ogni esecuzione delle istruzioni contenute nel loop, la variabile “contatore” sarà incrementata di una unità , al raggiungimento del valore 100.000, il diodo LED verrà acceso, e quando il contatore raggiunge 200.000 il diodo LED verrà spento e azzerata la variabile “contatore” per avviare un nuovo ciclo di esecuzione
Tra un istruzione di controllo e l’altra può essere inserito del codice che il microcontrollore potra’ eseguire.
Il maggiore svantaggio di questo metodo è quello di essere poco preciso, poiché la velocità con cui è incrementato il contatore dipende dalla complessità del programma e da quante istruzioni condizionali sono presenti e quindi il conteggio durante l’esecuzione può essere variabile.
In modo empirico si può selezionare il valore che più si avvicina all’intervallo temporale desiderato, ma il problema della scarsa costanza è difficilmente risolvibile, soprattutto quando il codice è molto complesso.
Funzione Millis
Anche questo esempio non blocca l’esecuzione delle istruzione del microcontrollore, è più precisa del contatore.
Si usa la funzione “millis()” integrata nella libreria di Arduino.
Questa funzione usa il contatore interno del microcontrollore e misura il tempo trascorso dall’accensione della board, in ms.
Il contatore va in overflow dopo circa 50 giorni.
/**************************************************************** Data 21-05-22 scritto da Gennaro Vitiello codice per far lampeggiare diodo LED usando la funzione mills() Maggiori informazioni sul sito www.quattrodispositivi.it ******************************************************************/ #define LED 13 // Associamo un nome mnemonico al numero 13 unsigned long millsPrecedente; // variabile per il confronto con il dato riportato dalla funzione millis() bool statoLED = false; // Stato del diodo LED void setup() { pinMode(LED, OUTPUT); // il pin dove e' connesso il dido LED impostato come uscita } void loop() { if (millis() - millsPrecedente >= 1000) // controlliamo se sono passati 1000 ms { if (statoLED == false) { // facciamo un toggle della variabile di stato del diodo LED statoLED = true; } else { statoLED = false; } digitalWrite(LED, statoLED); // impostiamo l'uscita in funzione dello stato del diodo LED millsPrecedente = millis(); // impostiamo la variabile millsPrecedente con il valore corrente riportato dalla funzione millis() } }
#define LED 13 // Associamo un nome mnemonico al numero 13 unsigned long millsPrecedente; // variabile per il confronto la funzione millis() bool statoLED = false; // Stato del diodo LED
In un determinato istante, nella variabile millisPrecedente verrà memorizzato il tempo riportato dalla funzione “millis()”, in questo modo abbiamo un riferimento con il quale monitorare il tempo trascorso da quando essa è stata aggiornata.
Il valore riportato dalla funzione “millis()” è incrementato con il trascorrere del tempo, esso verrà confrontato con il valore di riferimento memorizzato nella variabile millisPrecedente.
void loop() { if (millis() - millsPrecedente >= 1000) // controlliamo se sono passati 1000 ms { if (statoLED == false) { // facciamo un toggle della variabile di stato del diodo LED statoLED = true; } else { statoLED = false; } digitalWrite(LED, statoLED);// impostiamo l'uscita in funzione dello stato del diodo LED millsPrecedente = millis();// impostiamo la variabile millsPrecedente la funzione millis() } }
Nella prima istruzione, facciamo la differenza tra il valore riportato dalla funzione “millis ()” e la variabile “millisPrecedente”, se il risultato è maggiore o uguale a 1000 significa che è trascorso un tempo maggiore o uguale ad un secondo.
Se la condizione è verificata si esegue il toggle del valore della variabile booleana “statoLED” e si riaggiorna la variabile “millisPrecedente” e questo ciclo si ripete a loop.
Questo metodo è molto più accurato rispetto all’esempio del contatore ma esso richiede una maggiore quantità di memoria.
Watchdog Timer Interrupt
In questo esempio usiamo il Watchdog Timer Interrupt, integrato all’interno di quasi tutti i microcontrollori, questa periferica hardware, non è generalmente usata per questo tipo di applicazioni, ma per un uso didattico può essere molto istruttivo.
/**************************************************************** Data 21-05-22 scritto da Gennaro Vitiello codice per far lampeggiare diodo LED usando il watchdog Maggiori informazioni sul sito www.quattrodispositivi.it ******************************************************************/ #include <avr/wdt.h> //includiamo la libreria wdt AVR Watchdog #define LED 13 //Associamo un nome mnemonico al numero 13 boolean ledState = false; //Stato del diodo LED void setup() { pinMode(LED, OUTPUT); //il pin dove e' connesso il dido LED impostato come uscita cli(); //Disabilitiamo tutti gli interrupt WDTCSR = (1 << WDCE) | (1 << WDE); //abilitiamo Watchdog interrupt /********************************************************************************************/ // //WDTCSR = (1 << WDIE); //16ms //WDTCSR = (1 << WDIE) | (1 << WDP0); //32ms //WDTCSR = (1 << WDIE) | (1 << WDP1); //64ms //WDTCSR = (1 << WDIE) | (1 << WDP1) | (1 << WDP0); //0.125s //WDTCSR = (1 << WDIE) | (1 << WDP2); //0.25s //WDTCSR = (1 << WDIE) | (1 << WDP2) | (1 << WDP0); //0.5s WDTCSR = (1 << WDIE) | (1 << WDP2) | (1 << WDP1); //1s //WDTCSR = (1<<WDIE)| (1<<WDP2) | (1<<WDP1) | (1 << WDP0); // 2s //WDTCSR = (1<<WDIE)| (1<<WDP3); //4s //WDTCSR = (1 << WDIE) | (1 << WDP3) | (1 << WDP0); //8s /*******************************************************************************************/ sei(); //abilitiamo gli interrupts } void loop() { } ISR(WDT_vect) { // funzione per la gestione dei watchdog interrupt if (ledState) // { ledState = false; } else { ledState = true; } digitalWrite(LED, ledState); //impostiamo l'uscita in funzione dello stato del diodo LED }
#include <avr/wdt.h> //includiamo la libreria wdt AVR Watchdog
Aggiungiamo la libreria per gestire il Watchdog Timer, verranno associati gli indirizzi fisici dei registri a dei nomi mnemonici, riportati sul datasheet del microcontrollore.
cli(); //Disabilitiamo tutti gli interrupt WDTCSR = (1 << WDCE) | (1 << WDE); //abilitiamo Watchdog interrupt
Cli() disabilita il Watchdog Timer, questa istruzione può anche essere omessa, ma è una buona regola usarla prima della configurazione del Watchdog Timer.
L’istruzione (wdtcsr = (1 << wdce) | (1 << wde), imposta i bits WDCE e WDE ad “1” nel registro WDTCSR per abilitare il timer del watchdog in modalità di interrupt.
/********************************************************************************************/ // //WDTCSR = (1 << WDIE); //16ms //WDTCSR = (1 << WDIE) | (1 << WDP0); //32ms //WDTCSR = (1 << WDIE) | (1 << WDP1); //64ms //WDTCSR = (1 << WDIE) | (1 << WDP1) | (1 << WDP0); //0.125s //WDTCSR = (1 << WDIE) | (1 << WDP2); //0.25s //WDTCSR = (1 << WDIE) | (1 << WDP2) | (1 << WDP0); //0.5s WDTCSR = (1 << WDIE) | (1 << WDP2) | (1 << WDP1); //1s //WDTCSR = (1<<WDIE)| (1<<WDP2) | (1<<WDP1) | (1 << WDP0); // 2s //WDTCSR = (1<<WDIE)| (1<<WDP3); //4s //WDTCSR = (1 << WDIE) | (1 << WDP3) | (1 << WDP0); //8s /*******************************************************************************************/ sei(); //abilitiamo gli interrupts
La prossima istruzione è quella di impostare l’intervallo di tempo di attivazione dell’interrupt, in questo caso è stato selezionato 1 secondo, come da indicazione riportate nel datasheet del microcontrollore.
Ed in fine è stato riattivato il watchdog.
ISR(WDT_vect) { // funzione per la gestione dei watchdog interrupt if (ledState) // { ledState = false; } else { ledState = true; } digitalWrite(LED, ledState); //impostiamo l'uscita in funzione dello stato del diodo LED }
La funzione ISR (WDT_vect) verrà chiamata ogni volta che il timer del watchdog fa scattare l’interrupt.
Anche questo metodo non blocca l’esecuzione del codice.
Timer Interrupt
Questa è l’ultima soluzione che analizzeremo, Arduino Uno, integra tre timer timer0, timer1 e timer2, il timer0 è usato dalla funzione millis(), già analizzato in uno dei paragrafi precedenti.
In questo esempio usiamo il timer1.
/**************************************************************** Data 21-05-22 scritto da Gennaro Vitiello codice per far lampeggiare diodo LED usando il Timer Maggiori informazioni sul sito www.quattrodispositivi.it ******************************************************************/ #define LED 13 // Associamo un nome mnemonico al numero 13 boolean statoLED = false; //Stato del diodo LED void setup() { pinMode(LED, OUTPUT); //il pin dove e' connesso il dido LED impostato come uscita TCCR1A = 0; //Resettiamo il Timer1 control del Registro A bitClear(TCCR1B, WGM13); //impostiamo il contatore con il modo CTC bitSet(TCCR1B, WGM12); bitSet(TCCR1B, CS12); //impostiamo il prescale a 1024 bitClear(TCCR1B, CS11); bitSet(TCCR1B, CS10); //Resettiamo il Timer1 TCNT1 = 0; //impostiamo il valore di comparazione //massimo valore (16bit Timer) = 65535 /******************************************* per calcolare il valore di comparazione OCR1A = (time(s) * clock Freq.)/prescaler OCR1A = (1*16*10^6)/1024 ********************************************/ //OCR1A = 3906; //per 0.25sec //OCR1A = 7812; //per 0.5sec OCR1A = 15625; //per 1sec //OCR1A = 31250; //per 2sec bitSet(TIMSK1, OCIE1A); // abilitiamo l'interrupt di comparazione Timer1 sei(); // abilitiamo global interrupts } void loop() { } ISR(TIMER1_COMPA_vect) // funzione per la gestione degli interrupt { if (statoLED) { statoLED = false; } else { statoLED = true; } digitalWrite(LED, statoLED); //impostiamo l'uscita in funzione dello stato del diodo LED }
bitClear(TCCR1B, WGM13); //impostaimo il contatore con il modo CTC bitSet(TCCR1B, WGM12);
Settiamo i registri WGM12 e WGM13, per attivare la modalità CTC, come indicato nel datasheet del microcontrollore ATmega328P.
La modalità CTC (Clear Timer on Compare Match) resetta il contatore quando ha raggiunto il valore inserito nel registro OCR1A e il suo comportamento e indicato nella figura 4
bitSet(TCCR1B, CS12); //impostiamo il prescale a 1024 bitClear(TCCR1B, CS11); bitSet(TCCR1B, CS10);
Impostiamo il valore del prescaler come indicato nel datasheet:
Abbiamo selezionato la configurazione evidenziata in rosso, quindi nel nostro caso abbiamo un prescaler di 1024, cioè il contatore usa un clock con un periodo di 1024 volte più grande del clock di sistema, su Arduino Uno è di 16 Mhz.
Per determinare il valore da caricare nel registro e impostare un interrupt timer ogni secondo, dobbiamo usare la seguente equazione:
OCR1A = (1*16*10^6)/1024 = 15625
//impostiamo il valore di comparazione //massimo valore (16bit Timer) = 65535 /******************************************* per calcolare il valore di comparazione OCR1A = (time(s) * clock Freq.)/prescaler OCR1A = (1*16*10^6)/1024 ********************************************/ //OCR1A = 3906; //per 0.25sec //OCR1A = 7812; //per 0.5sec OCR1A = 15625; //per 1sec //OCR1A = 31250; //per 2sec
Infatti nel nostro codice abbiamo caricato il valore di 15625.
ISR(TIMER1_COMPA_vect) // funzione per la gestione degli interrupt { if (statoLED) { statoLED = false; } else { statoLED = true; } digitalWrite(LED, statoLED); //impostiamo l'uscita in funzione dello stato del diodo LED }
Questa è la parte di codice che si esegue a loop. Aggiungiamo l’ISR con la funzione IMER1_COMPA_vect, scatta l’interrupt ogni volta che il timer raggiunge il valore impostato nel registro OCR1A.
Spero che questo articolo l’abbiate trovato interessante e se aveste dei quesiti non esitate a scrivere nei commenti di seguito.