Di tanto in tanto capita che qualche utente del forum richieda un circuito per variare in modo lineare o PWM l' intensità di LED. L' ultima discussione che ho letto si chiama "breathing LED" in cui si richiedeva un circuito in grado di far variare la luminosità di un LED con un andamento ascendente e discendente in modo da dare l' impressione che il LED respiri. La prima cosa che ho pensato è stata "Questo è un lavoro che con un micro si risolve in pochissimo tempo!". Infatti l' ho preso come spunto per implementarlo sul PIC18F47J53 cogliendo allo stesso tempo l' occasione per illustrare come le cose si possono fare con poco (spirito da vecchio microcontrollista eh eh eh) e come si utilizzano le fasi di funzionamento all' interno di un programma oltre che, ovviamente, implementare un PWM multicanale. Per la realizzazione pratica utilizzerò la scheda PIERIN PIC18.
Indice |
La tecnica PWM da implementare
Non sarò di certo io quello che farà il solito pistolotto sulla tecnica di regolazione PWM (Pulse Width Modulation) perché sia ElectroYou che la rete ne sono pieni e la sua spiegazione esula dallo scopo di questo articolo. Quello che invece si deve sapere è che con questo programma si implementerà un controllo PWM a 32 livelli che verrà utilizzato per far variare la luminosità dei due LED che si trovano montati sul PIERIN PIC18 in modo lineare dando l' impressione che questi "respirino". In questo programma vengono comandati solo due LED ma, come si potrà ben vedere e capire, se ne potrebbero pilotare molti di più. La cosa che penso sia interessante è che non verrà utilizzata nessun' altra periferica che non sia stata utilizzata fino a questo articolo. In pratica utilizzerò solamente il Timer 2 che è già stato utilizzato nel programma di Demo e quindi conosciuto.
E' vero che il PIC18F47J53 ha una ricca dotazione di timer con i quali si può fare qualsiasi cosa ma in questo caso se ne possono fare a meno. Abbiamo un micro potente e veloce ed un timer con comparatore. Non ci serve altro e tanto meno ci andremo a complicare la vita studiando altri tipi di timer ... almeno per ora
Descrizione del programma
L' idea alla base del programma è realizzare, all' interno di un' interrupt ciclica, il controllo PWM. La frequenza deve essere sufficientemente alta da non fare apparire il LED sfarfallanti ma che, allo stesso tempo, sia gestibile con facilità dal microcontrollore. Nel programma di demo l' interrupt ciclica era realizzata con il timer 2 che generava una chiamata ogni millisecondo. Questa volta il timer 2 verrà inizializzato in modo da generare una interrupt ogni 100 us. , dieci volte di più, con la particolarità che gestirà ugualmente i timer software (sempre con risoluzione 1 ms.) e contemporaneamente la larghezza dell' impulso delle due uscite collegate ai LED con una frequenza di 312,5 Hz (un periodo di 3,2 ms) sufficientemente alta da dare un buon effetto visivo ed evitare lo sfarfallamento.
Gestione dei timer software
Posto che si vuole mantenere il funzionamento dei timer software come nel programma demo (decremento ogni millisecondo) e sapendo che l' interrupt viene chiamata ogni 100 us si utilizza una variabile che funge da divisore. In pratica la si fa contare da 0 a 10 e, quando è arrivata a 10 la si azzera e si aggiornano i timer software. Il risultato è avere dei timer software che si decrementano ogni millisecondo esattamente come nel programma demo.
Gestione del PWM
Anche la gestione del PWM utilizza una variabile che conta da 0 a 32. Se il valore della variabile (che contiene il livello di luminosità del canale) è superiore al valore del contatore l' uscita corrispondente verrà tenuta alta mentre se il suo valore è inferiore l' uscita sarà tenuta a 0. Avremo così ottenuto in modo semplice la parzializzazione del periodo in 32 passi che possono essere tutti disattivati (nel caso che il valore valga 0) oppure tutti attivati del il valore vale 32.
Il programma
Il progetto è stato creato partendo dal progetto di base e si è lavorato esclusivamente su file main.c . La prima cosa che salta all' occhio è la dichiarazione delle variabili locali. Questa volta ne abbiamo 4
volatile unsigned short timer_delay_1; // Timer software 1 volatile unsigned short timer_delay_2; // Timer software 2
Che sono i due timer software ognuno utilizzato dalla sua funzione autonoma che gestisce l' effetto luminoso del suo led e
volatile char pwm_level_1; // Livello luminosità LED1 (0-32) volatile char pwm_level_2; // Livello luminosità LED2 (0-32)
Che sono le due variabili che contengono i livelli di luminosità dei due LED.
Troviamo anche due prototipi di funzione in più che sono
void gestione_LED1(void); void gestione_LED2(void);
Si è deciso di fare due funzioni distinte ognuna per la gestione autonoma di ognuno dei due LED ma non solo per questo motivo. Questa volta prossima vedere come si gestiscono due processi contemporaneamente a patto di evitare alcune cose e seguire un certo rigore nella scrittura delle stesse.
La funzione di servizio dell' interrupt
Anche in questo caso il timer è stato inizializzato per generare una interrupt con priorità bassa mantenendo così le cose come sono nel programma di demo. Troviamo inizialmente la dichiarazione delle due variabili che fungono da divisore di base tempi e da contatore per il PWM. Sono dichiarate come statiche perché devono mantenere il loro valore anche al di fuori della funzione (se fossero dichiarate come variabili locali sarebbero azzerate ogni volta che viene chiamata la funzione) e, allo stesso tempo, non sono visibili al resto del programma. In pratica si comportano come variabili globali ma si possono usare solo all' interno della funzione. A loro viene assegnato un valore iniziale di 0.
void lowPriorityInterrupt() { // Verifica quale flag ha causato l' interrupt // Esegui la parte di codice di servizio dell' interrupt // Azzera il flag che ha causato l' interrupt // ... static char time_base = 0; static char pwm_counter = 0; // Gestione dell' interrupt del timer 2 if(PIR1bits.TMR2IF) { // Gestione del PWM del LED 1 if (pwm_level_1 < pwm_counter) LATDbits.LATD6 = 1; else LATDbits.LATD6 = 0; // Gestione del PWM del LED 2 if (pwm_level_2 < pwm_counter) LATDbits.LATD7 = 1; else LATDbits.LATD7 = 0; pwm_counter++; if (PWM_LEVELS == pwm_counter) pwm_counter = 0; // gestione del timer software. I timer software devono decrementarsi // fino ad arrivare a 0. Una volta arrivati a 0 restano fermi a 0. // La gestione avviene solo quando la variabile time_base raggiunge il // valore TIME_BASE_COUNTS in modo da aggiornare i timers ogni ms. time_base++; if (TIME_BASE_COUNTS == time_base) { time_base = 0; if (timer_delay_1) timer_delay_1--; if (timer_delay_2) timer_delay_2--; } // Resetta il flag che ha generato l' interrupt PIR1bits.TMR2IF = 0; } }
La gestione delle uscite dei LED è semplicissima. Se il valore contenuto nella variabile che rappresenta la luminosità è inferiore al valore del contatore l' uscita viene mandata alta, altrimenti rimane bassa. Così, se il valore della variabile vale 0 l' uscita sarà sempre a 0, ma se il valore vale 32 (valore che il contatore non raggiunge mai) l' uscita sarà fissa ad 1. Notare infatti che il contatore viene incrementato solo dopo la gestione delle uscite e se vale 32 viene riportato a 0. In pratica varia da 0 a 31.
La gestione dei timer software differisce da quella del programma demo solo proprio perché i timer vengono decrementati ogni 10 incrementi della variabile time_base ottenendo così un decremento ogni millisecondo.
Le funzioni che gestiscono i due LED
Sono due funzioni sostanzialmente uguali e quindi ne analizziamo una sola.
Notare la dichiarazione di una variabile (statica) di tipo enum. Questa variabile può assumere solo due valori: fase_up e fase_down. Il perché è comprensibile: se dobbiamo fare vedere i LED che "respirano" ci sarà una prima fase in cui il LED aumenta la luminosità (fase_up) ed una in cui la diminuisce (fase_down).
//------------------------------------------------------------------------------ // Funzione per la gestione del LED1 // La luminosità sale lentamente e scende velocemente // Note: // utilizza il timer software soft_timer_1 e controlla la // luminosita del LED tramite la variabile pwm_level_1 void gestione_LED1(void) { static enum // variabile che contiene lo stato di funzionamento { fase_up, fase_down } fase_ciclo = fase_up; // Aggiorna la luminosità quando il timer_1 è arrivato a 0 if (!timer_delay_1) { switch(fase_ciclo) { // Salita lenta case fase_up: timer_delay_1 = 80; if (PWM_LEVELS == pwm_level_1) fase_ciclo = fase_down; else pwm_level_1++; break; // Discesa veloce case fase_down: timer_delay_1 = 20; if (0 == pwm_level_1) fase_ciclo = fase_up; else pwm_level_1--; break; } //switch(fase_ciclo) } // if (!timer_delay_1) }
Quindi quando la funzione sta eseguendo la fase ascendente verrà solo eseguita la parte di programma che sta sotto il case fase_up: e l'ì altra non verrà eseguita. Quando la fase ascendente sarà terminata, cioè quando il valore di luminosità avrà rangiunto il valore massimo la variabile fase_ciclo cambierà di valore assumendo il valore fase_down come indicato in questo disegno.
Forse per due soli stati di funzionamento questo può sembrare eccessivo dichiarare una variabile enum ma gli stati di funzionamento potrebbero essere molti. In questo modo viene solo eseguita la parte relativa allo stato di funzionamento realizzando così una macchina a stati. Se non si introducono cicli che possono fermare l' elaborazione (come ad esempio i ritardi fissi o le attese di tasti premuti) ma si usa la macchina a stati si possono eseguire più compiti contemporanenamente. Una sorta di multitasking cooperativo (di bassa macelleria) ma che funziona ... e pure bene.
Il main
Nel main, dopo le opportune inizializzazioni che non starò nuovamente ad analizzare (non vorrei essere ripetitivo) troviamo la parte operativa vera e propria.
// Inizializza i livelli dei due LED ed azzera i due timer pwm_level_1 = 0; pwm_level_2 = 0; timer_delay_1 = 0; timer_delay_2 = 0; // -------- Ciclo infinito di funzionamento -------- for(;;) { // Inserire il programma qui. gestione_LED1(); gestione_LED2(); } //for(;;)
E' effettivamente semplice: azzeramento delle variabili ed esecuzione continua delle due funzioni. Notare che in questo programma sono solo due perché ci sono da gestire solo gli effetti visivi di 2 LED ma ce ne potrebbero essere molte di più. Magari ci potrebbe essere una funzione che testa i pulsanti per farci un qualcosa, un' altra funzione che realizza una rete logica con altri ingressi ed uscite, un po' di tutto. L' importante è che si tenga presente che ogni funzione (task) non deve tenere il controllo del programma per più di un tempo stabilito a priori e, tanto meno, deve piantare il programma all' interno di una attesa! Se non si seguono queste regole allora niente funziona ma se si seguono le potenzialità di un programma diventano notevoli.
Suggerimenti per la sperimentazione
Il risultato visivo di questo programma è quello mostrato in questo video. I due LED non "resipirano" sincronizzati. E' un effetto voluto proprio per far vedere che due funzioni sono completamente autonome. Il programma esegue in buona sostanza due task in parallelo.
Per poterlo vedere funzionare anche senza compilare tutto il programma con MPLAB si può programmare il file .hex chiamato easy_pwm.hex direttamente dentro la scheda.
Se poi si incomincia a variare i valori con cui sono caricati i timer software si variano le velocità di lampeggio. Si può anche leggere da un ingresso analogico, magari collegato ad un potenziometro, scalarne il valore in modo che vada da 0 a 32 e comandare manualmente la luminosità di un LED oltre che, naturalmente, aggiungere altri canali PWM. Con un po' di buona volontà il divertimento e gli esperimenti sono assicurati.
Cio detto, e scusando di eventuali errori ed imprecisioni, non mi resta che augurare
BUONA SPERIMENTAZIONE!.
Il sorgente completo
// File di definizione dei registri del micro. #include "p18f47j53.h" // File di libreria contenete le funzioni di ritardo #include "delays.h" // File di configurazione dei fuses #include "configurazione.h" // Mappatura delle interrupt #include "mappa_int.h" // Header del main #include "main.h" //------------------------------------------------------------------------------ // Defines del programma //------------------------------------------------------------------------------ // Numero di interrupts per base tempi 1ms. #define TIME_BASE_COUNTS 10 // Nmumero di livelli per le uscite PWM #define PWM_LEVELS 32 //------------------------------------------------------------------------------ // Variabili globali //------------------------------------------------------------------------------ #pragma udata volatile unsigned short timer_delay_1; // Timer software 1 volatile unsigned short timer_delay_2; // Timer software 2 volatile char pwm_level_1; // Livello luminosità LED1 (0-32) volatile char pwm_level_2; // Livello luminosità LED2 (0-32) //------------------------------------------------------------------------------ // Funzione di servizio delle interrupt ad ALTA priorità //------------------------------------------------------------------------------ #pragma code #pragma interrupt highPriorityInterrupt void highPriorityInterrupt() { // Verifica quale flag ha causato l' interrupt // Esegui la parte di codice di servizio dell' interrupt // Azzera il flag che ha causato l' interrupt // ... } //------------------------------------------------------------------------------ // Funzione di servizio delle interrupt a BASSA priorità //------------------------------------------------------------------------------ #pragma interruptlow lowPriorityInterrupt void lowPriorityInterrupt() { // Verifica quale flag ha causato l' interrupt // Esegui la parte di codice di servizio dell' interrupt // Azzera il flag che ha causato l' interrupt // ... static char time_base = 0; static char pwm_counter = 0; // Gestione dell' interrupt del timer 2 if(PIR1bits.TMR2IF) { // Gestione del PWM del LED 1 if (pwm_level_1 < pwm_counter) LATDbits.LATD6 = 1; else LATDbits.LATD6 = 0; // Gestione del PWM del LED 2 if (pwm_level_2 < pwm_counter) LATDbits.LATD7 = 1; else LATDbits.LATD7 = 0; pwm_counter++; if (PWM_LEVELS == pwm_counter) pwm_counter = 0; // gestione del timer software. I timer software devono decrementarsi // fino ad arrivare a 0. Una volta arrivati a 0 restano fermi a 0. // La gestione avviene solo quando la variabile time_base raggiunge il // valore TIME_BASE_COUNTS in modo da aggiornare i timers ogni ms. time_base++; if (TIME_BASE_COUNTS == time_base) { time_base = 0; if (timer_delay_1) timer_delay_1--; if (timer_delay_2) timer_delay_2--; } // Resetta il flag che ha generato l' interrupt PIR1bits.TMR2IF = 0; } } //------------------------------------------------------------------------------ // Prototipi delle funzioni //------------------------------------------------------------------------------ #pragma code void timer2_deInit(void); void gestione_LED1(void); void gestione_LED2(void); //------------------------------------------------------------------------------ // Funzioni //------------------------------------------------------------------------------ #pragma code //------------------------------------------------------------------------------ // De-inizializza il timer 2 e lo porta nello stato in cui si trovava // subito dopo il RESET void timer2_deInit(void) { T2CON = 0; // Resetta il timer 2 control register TMR2 = 0; // Azzera il contatore interno PR2 = 0; // Azzera il registro comparatore PIE1bits.TMR2IE = 0; // Disabilita l' interrupt IPR1bits.TMR2IP = 0; // Resetta il bit di priorità dell' interrupt PIR1bits.TMR2IF = 0; // Azzera il flag di interrupt } //------------------------------------------------------------------------------ // Funzione per la gestione del LED1 // La luminosità sale lentamente e scende velocemente // Note: // utilizza il timer software soft_timer_1 e controlla la // luminosita del LED tramite la variabile pwm_level_1 void gestione_LED1(void) { static enum // variabile che contiene lo stato di funzionamento { fase_up, fase_down } fase_ciclo = fase_up; // Aggiorna la luminosità quando il timer_1 è arrivato a 0 if (!timer_delay_1) { switch(fase_ciclo) { // Salita lenta case fase_up: timer_delay_1 = 80; if (PWM_LEVELS == pwm_level_1) fase_ciclo = fase_down; else pwm_level_1++; break; // Discesa veloce case fase_down: timer_delay_1 = 20; if (0 == pwm_level_1) fase_ciclo = fase_up; else pwm_level_1--; break; } //switch(fase_ciclo) } // if (!timer_delay_1) } //------------------------------------------------------------------------------ // Funzione per la gestione del LED2 // La luminosità sale velocemente e scende velocemente in modo simmetrico // Note: // utilizza il timer software soft_timer_2 e controlla la // luminosita del LED tramite la variabile pwm_level_2 void gestione_LED2(void) { static enum // variabile che contiene lo stato di funzionamento { fase_up, fase_down } fase_ciclo = fase_up; // Aggiorna la luminosità quando il timer_2 è arrivato a 0 if (!timer_delay_2) { switch(fase_ciclo) { // Salita veloce case fase_up: timer_delay_2 = 15; if (PWM_LEVELS == pwm_level_2) fase_ciclo = fase_down; else pwm_level_2++; break; // Discesa veloce case fase_down: timer_delay_2 = 15; if (0 == pwm_level_2) fase_ciclo = fase_up; else pwm_level_2--; break; } //switch(fase_ciclo) } // if (!timer_delay_2) } //------------------------------------------------------------------------------ // MAIN FUNCTION //------------------------------------------------------------------------------ void main(void) { // Fa partire il PLL. // Anche se viene selezionato tramite i bit di configurazione // il suo funzionamento non è automatico. Ha bisogno di un comando. OSCTUNEbits.PLLEN = 1; // Attende abbastanza tempo per far stabilizzare il PLL Delay1KTCYx(10); // Da ora in poi abbiamo il PLL funzionante ed il micro con il turbo. // -------- Inizializzazione delle periferiche -------- // Inizializza la PORTD // bit 4 input pulsante PL0 // " 5 input pulsante PL1 // " 6 output LED LED1 // " 7 output LED LED2 TRISD = 0x3F; // Mette a 0 tutte le uscite LATD = 0; // De-inizializza il timer2. Non sarebbe necessario perché il micro esce // allo stato di RESET ma è comunque buona pratica de-inizializzare sempre // le periferiche per non tralasciare nessun bit. timer2_deInit(); // Inizializza il timer 2 per interrupt ogni 100 microsecondi. // prescaler divide per 16 T2CONbits.T2CKPS = 2; // Postscaler divide per 5 T2CONbits.T2OUTPS = 4; // Imposta il valore comparatore a 150 PR2 = 15; // Imposta l' interrupt del Timer 2 a priorita' bassa IPR1bits.TMR2IP = 0; // abilita interrupt del timer PIE1bits.TMR2IE = 1; // -------- Selezione ed abilitazione delle interrupt -------- // Ora che si sono inizializzate tutte le periferiche si possono abilitare // Oppurtunamente le interrupt // abilita le interrupt a bassa priorita' RCONbits.IPEN = 1; // abilta tutte le interrupt a priorità bassa INTCONbits.GIEL = 1; // Abilita tutte le interrupt in generale INTCONbits.GIEH = 1; // -------- Attivazione delle periferiche -------- // Con le interrupt abilitate possiamo ora far partire il timer 2 // Accende il timer T2CONbits.TMR2ON = 1; // Inizializza i livelli dei due LED ed azzera i due timer pwm_level_1 = 0; pwm_level_2 = 0; timer_delay_1 = 0; timer_delay_2 = 0; // -------- Ciclo infinito di funzionamento -------- for(;;) { // Inserire il programma qui. gestione_LED1(); gestione_LED2(); } //for(;;) }