Multiple PWM sin CCP

Una forma de utilizar varios canales PWM de forma dinámica y simultánea es con los módulos CCP del MCU, pero como podrán ver, en la mayoría de los MCU de microchip de baja gama no encontraremos más de 2 o 3 módulos. 
Que pasa si usamos un MCU que solo posee 1 CCP o si quisiéramos por ejemplo 8 PWM. Aquí entran muchas maneras de realizar una virtualizacion del PWM una que se suele usar es el multiplexado del PWM. 

NOTA: Como sabrán los módulos ADC del MCU multiplexan sus entradas, por ejemplo en un MCU que posee 8 canales ADC, no es que internatemnte tenga 8 ADC, sino que tiene solo uno pero multiplexa las entradas, por eso siempre hay que esperar un tiempo de conversión y demás. 

De esta misma forma podemos hacer un multiplexado de salidas PWM, por ejemplo con un multiplexor de 8 canales (ej. 4051) podemos selecciona el canal deseado mediante un direccionamiento de 3bit que podremos manejarlo directamente con cualquier puerto, y luego con un solo PWM podemos sincronizar el valor que deseamos del ciclo de trabajo con el canal habilitado en el multiplexor. Esto tiene una demora y una respuesta en frecuencia que respetar, aparte de que necesitamos también de hardware externo.

Otra forma muy utilizada es una virtualizacion netamente por software.
En este post les mostrare esta última opción. Utilizaremos una técnica muy sencilla, dividermos el programa en dos partes.


1) Sección que se encargara de modificar dinámicamente el valor del ciclo de trabajo de cada canal PWM. (Por ejemplo, canal 1: 40%, canal 2: 30%, canal..... canal 8: 75%) esto lo realizaremos de forma muy sencilla, pasaremos cada valor a una función, y cada vez que modifiquemos estos valores se actualizaran los ciclos de trabajo del PWM.


2) La parte mas importante ocurrirá dentro de una interrupción por desbordamiento del timer0 (se puede usar el timer que ustedes quieran, yo he utilizado este porque fue lo primero que se me ocurrió).
Generamos una interrupción interna cada un determinado valor de tiempo (que podrán ajustar ustedes mismos, en mi caso lo tengo al rededor de los 3ms que serán algo asi como 300Hz).
Cada vez que se repita el ciclo dentro de la interrupción se sumara una unidad al contador (cont++) y moviendo los valores de ciclo a la función se comparara con el valor que tiene en ese momento el contador.


Por ejemplo tomemos un canal, como podemos ver tenemos que se pondrá a 1 la salida PWM si el valor del contador se encuentra entre 0 y el valor que enviamos por la función, si enviamos el valor 70, la salida comenzara encendida ya que se encuentra entre 0 y 70, una vez que el ciclo se repite 70 veces a la vez 71 saldrá del if y pondrá en 0 la salida PWM, de esta forma estaremos obteniendo un ciclo de trabajo de 70%, esto mismo lo calcula en todos los if, testeara cada uno de los if, si el valor que trae de la función es menor al del contador y de esa forma encenderá y apagar la salida en cuestión.


Por ejemplo para dos valores, si tenemos valor 1: 50%, y valor 2: 60%, tendremos dos if, uno que preguntara si el valor del contador se encuentra entre 0 y 50, y otro if que preguntara si se encuentra entre 0 y 60, de esta forma el primer if que se cumplirá es de 0 y 50 ya que cuando el contador tenga 51, pondrá a 0 el pin PWM del valor 1, y luego seguirá contando y cuando llegue a 61 pondrá a 0 el pin PWM del valor 2. 


Esta rutina no solo nos sirve para dos o tres canales sino para los canales que queramos.
Como se puede ver la resolución sera de 100 pasos. Se hace así para que sea lineal al ciclo de trabajo... Ej. Valor 10 = 10%, y también serán menos pasadas del timer por ende mayor frecuencia de salida en cada PWM.


Aqui mostrae el código fuente para 3 canales PWM, por ejemplo para manejar un led RGB.

#include <16F628A.h>             //MCU a utilizar
#FUSES NOWDT, HS, NOMCLR         //Fueses
#use delay(clock=20000000)       //Oscilador Externo
#int_TIMER0                      //LLamado a interrupcion
void TIMER0_isr(int pwm0,        //Funcion Interrupcion
                int pwm1,        //Variables de link
                int pwm2){       //PWM0 PWM1 PWM2
   int cont;                     //Variable de Contador
   if(cont>99)                   //Si cont 100 se inicializa
      cont=0;                    //Cont = 0
   else                          //Caso contrario
      cont++;                    //Incrementa en una unidad cont
   if(cont>0&&cont<pwm0)         //Si 0<Cont<PWM0
      output_high(PIN_B0);       //Salida PWM0 a nivel alto
   else                          //Caso contrario
      output_low(PIN_B0);        //Salida PWM0 a nivel bajo
   if(cont>0&&cont<pwm1)         //Si 0<Cont<PWM1
      output_high(PIN_B1);       //Salida PWM1 a nivel alto
   else                          //Caso contrario
      output_low(PIN_B1);        //Salida PWM1 a nivel bajo
   if(cont>0&&cont<pwm2)         //Si 0<Cont<PWM2
      output_high(PIN_B2);       //Salida PWM2 a nivel alto
   else                          //Caso contrario
      output_low(PIN_B2);        //Salida PWM2 a nivel bajo
   set_timer0(255);              //Seteo de interrupcion
}
void main(){                     //Funcion principal
   setup_timer_0(RTCC_INTERNAL|RTCC_DIV_2);  //Seteo timer0
   enable_interrupts(INT_TIMER0);            //Seteo timer0
   enable_interrupts(GLOBAL);                //Seteo timer0
   while(true){                  //Loop infinito
      TIMER0_isr(10,45,73);      //Se movera a la funcion
   }                             //PWM0=10%, PWM1=45%, PWM2=73%
}



Ahora realizaremos la actualizacion de este programa para 8 canales.

#include <16F883.h>
#FUSES NOWDT, HS, NOMCLR
#use delay(clock=20000000)
#int_TIMER0
void TIMER0_isr(int pwm0, int pwm1, int pwm2,
                int pwm3, int pwm4, int pwm5,
                int pwm6, int pwm7){
   int cont;
   if(cont>99)
      cont=0;
   else  
      cont++;
   if(cont>0&&cont<pwm0)
      output_high(PIN_B0);
   else
      output_low(PIN_B0);
   if(cont>0&&cont<pwm1)
      output_high(PIN_B1);
   else
      output_low(PIN_B1);
   if(cont>0&&cont<pwm2)
      output_high(PIN_B2);
   else
      output_low(PIN_B2);
   if(cont>0&&cont<pwm3)
      output_high(PIN_B3);
   else
      output_low(PIN_B3);
   if(cont>0&&cont<pwm4)
      output_high(PIN_B4);
   else
      output_low(PIN_B4);
   if(cont>0&&cont<pwm5)
      output_high(PIN_B5);
   else
      output_low(PIN_B5);     
   if(cont>0&&cont<pwm6)
      output_high(PIN_B6);
   else
      output_low(PIN_B6);
   if(cont>0&&cont<pwm7)
      output_high(PIN_B7);
   else
      output_low(PIN_B7);      
   set_timer0(255);
}
void main(){
   setup_timer_0(RTCC_INTERNAL|RTCC_DIV_2);
   enable_interrupts(INT_TIMER0);
   enable_interrupts(GLOBAL);
   while(true){
      TIMER0_isr(10,20,30,40,50,60,70,80);
   }
}




Timer 0 - Como Temporizador en CCS

Como podríamos saber, los microcontroladores poseen en su mayoría varios temporizadores, pero en este caso hablare del Timer 0 ya que este se encuentra en los microcontroladores mas económicos y antiguos. Este puede funcionar como Contador o como Temporizador.
Si lo configuramos como Contador este debe adquirir pulsos desde el exterior y si lo configuramos como Temporizador utilizara el mismo oscilador del microcontrolador. 
Mostrare un ejemplo de utilización simple como temporizador, esta función sera útil cuando necesitamos realizar una tarea que corra en paralelo a nuestro programa principal, por ejemplo, el multiplexado de displays para visualizar algún valor. En este ejemplo que comentaba debemos atender el multiplexado de los display de forma independiente al programa principal, esto nos resulta útil porque por ejemplo en un reloj tenemos el segundero que cambia cada 1 segundo pero el muestreo de los displays se debe realizar a una frecuencia mucho mayor a 1 segundo para lograr una persistencia en la visión.
Si configuramos este timer en lenguaje ensamblador tendremos que manipular directamente los registros TMR0 (Banco 0) y OPTION (Banco 1), dentro del registro OPTION se encuentran los bit PS2, PS1 y PS0, estos serán los que configuran el Prescaler del Oscilador del microcontrolador. En otras palabras, como antes mencionaba cuando el Timer 0 funciona como temporizador utilizara los pulsos de reloj del mismo microcontrolador pero por ejemplo para un cristal de 4MHz (donde el bus del microcontrolador funcionara a 1MHz) debemos dividir esta frecuencia ya que es demasiado rápido para realizar un timer, entonces con el prescaler seteando los tres bit PS2 al PS0 logramos setear el factor de división que les mostrare en la siguiente tabla:



Hay otros bit's obviamente en los registros del Timer pero en este caso tratare de hacer lo mas sencillo posible el funcionamiento del mismo como temporizador y dejare esos bit's extra para su investigación.
Dentro del datasheet podemos encontrar la formula para calcular el tiempo con el que se creara la interrupción del timer.
El timer funciona como un contador de 8 bits, (del 0 al 255) cuando este llega al máximo produce un overflow (desbordamiento) y cada vez que se produce se genera una interrupción que reinicia el timer y comenzara de nuevo.
Para calcular el tiempo debemos, aparte del divisor de frecuencia de reloj, setear una variable que modificara el valor de cuenta (0 a 255) para que desborde y produzca la interrupción antes o después del tiempo que divide el prescaler.


Ti: Tiempo de interrupción
fosc: Frecuencia del oscilador
Vs: Valor de seteo del timer
P: Prescaler

Si nosotros no usamos el valor de seteo del timer, la formula quedara:



Ejemplos [con prescaler en 4]:
Sin valor de seteo:

Con valor de seteo:

Podemos concluir que al no usar valor de seteo del timer, la interrupción sera de 1,024ms y con el valor de seteo sera de 1ms, entonces aquí podemos ver la importancia de esta variable cuando uno requiere una interrupción a un periodo mas exacta (si quisiéramos que sea a 1ms).
NOTA: este calculo sera eficiente cuando trabajemos en lenguaje ensamblador, lamentablemente al trabajar en C perdemos algo de precisión y este calculo tendrá un error de aproximadamente 3%, por eso debemos medir nuestra salida con un frecuencimetro u osciloscopio para saber exactamente si la interrupción es correcta, solo tendremos que ajusta el valor de seteo.

Cuando utilicemos este timer debemos tener en cuenta que el código que se ejecutara en este temporizador debe encontrarse dentro de la funciona timer0(); donde esta tendra el vector de interrupción inmediatamente antes de la función con la nomenclatura #int_timer0.
Dentro de esta función se pondrá el código que queramos, al final de nuestro código (Dentro de esta función pondremos el valor de seteo del timer set_timer0(valor); y luego cerramos la llave de la función.

Dentro de la función principal, tendremos que configurar el timer y las interrupciones.
1) setup_timer_0(RTCC_INTERNAL|RTCC_DIV_4); aquí decimos que el timer funcionara como temporizador con el reloj del microcontrolador "RTCC_INTERNAL" y también que el prescaler sera dividiendo por 4 "RTCC_DIV_4".
2) set_timer0(6); este es el valor de seteo del timer, valor decimal 6.
3) enable_interrupts(INT_TIMER0); y enable_interrupts(GLOBAL); tenemos la habilitación de las interrupciones.

Ahora veremos el ejemplo codificado en C.
Podremos analizar que dentro de la funcion timer0() tenemos output_toggle(PIN_A0); y como había mencionado antes al final tenemos el seteo del timer set_timer0(6);
Con este valor la función se interrumpirá cada 1ms, al hacer toggle o swap al bit 0 del puerto y A, cambiara de 0 a 1 y de 1 a 0 cada 1ms, esto podremos verlo en el osciloscopio donde veremos una señal cuadrada de 2ms (1ms tON + 1ms tOFF) con un ciclo de trabajo del 50% y una frecuencia de 500Hz.  
NOTA: estos son valores teóricos, debemos tener en cuenta el 3% de error del calculo en el lenguaje C.

#include <16f628a .h="">
#FUSES NOWDT 
#FUSES XT 
#FUSES MCLR 
#use delay(clock=4000000) 
#int_timer0 
void timer0(){ 
   output_toggle(PIN_A0); 
   set_timer0(6); 

void main(){ 
   setup_timer_0(RTCC_INTERNAL|RTCC_DIV_4); 
   set_timer0(6); 
   enable_interrupts(INT_TIMER0); 
   enable_interrupts(GLOBAL); 
   while(TRUE){
   } 
}