Cos'è ElectroYou | Login Iscriviti

ElectroYou - la comunità dei professionisti del mondo elettrico

11
voti

PIC18F47J53 come fare un PWM facile e multicanale

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(;;)
}
3

Commenti e note

Inserisci un commento

di ,

Hai capito bene. E sono contento che questo articolo sia, in qualche modo, servito a qualcosa. :)

Rispondi

di ,

E' veramente interessante: se ho ben capito sei partito nella creazione del programma pensando alle fasi più brevi, più veloci, ossia alla frazione del periodo della modulazione PWM in cui bisogna prendere la decisione di impostare l'uscita alta o bassa. Poi sfruttando i timer software, cioè contando quante volte accadono gli eventi più veloci, sei riuscito a portarti a un livello temporale più alto in cui vai a gestire la luminosità dei led e le fasi di aumento o diminuzione di essa. Una delle difficoltà che io trovo nella stesura del programma è che so che verrà eseguito sequenzialmente e non riesco a collocare le varie parti del programma una dopo l'altra e mi blocco: questo articolo mi ha un po' chiarito le idee dandomi una nuova prospettiva, anche se lo devo ancora assimilare per bene. Per ora mi accontento di essere riuscito a leggerlo e a capirlo, scorrendo in su per rileggere i passaggi che venivano man mano richiamati.

Rispondi

di ,

Bello, lo proverò sicuramente ciao Ivo

Rispondi

Inserisci un commento

Per inserire commenti è necessario iscriversi ad ElectroYou. Se sei già iscritto, effettua il login.