MODULACIÓN POR ANCHO DE PULSO en el PIC 16F84
Por Marcelo Valdéz, Nov 2002
Una de las formas más interesantes de obtener una tensión analógica en el F84, es usando una señal PWM (Pulse Width Modulation), y un filtro pasabajos a la salida.
Como ejemplo de PWM, voy a transcribir el programa que codifiqué para el microbot MONTY. El filtro pasabajos no está presente (aunque el bobinado de los motores suaviza un poco la onda rectangular), sin embargo, desde el punto de vista mecánico, el movimiento del robot es totalmente suave, debido a que la masa de todo el conjunto actúa de pasabajos.
Un punto a considerar es la frecuencia elegida para la onda PWM, ya
que no puede escogerse cualquier valor:
El valor medio de la tensión obtenida será de Vcc Volts multiplicado por el "Duty Cycle" de la onda PWM, o también como la integral de la onda abarcando un periodo completo, dividida por T:
Duty Cycle = Ton / (Ton + Toff) = Ton / T
Vav = 1/T S|ta, ta+T|(PWM(t))
Aquí va el programa principal:
NOTA: Este código es para el MPLAB for Windows/16 versión 5.11.0
;+---------------------------------------------------------------------+ ;| FUNCIONES Y VARIABLES GLOBALES | ;+---------------------------------------------------------------------+
GLOBAL _W, _STATUS, DLAY ; P/ salvar variables de entorno. GLOBAL TMP1, TMP2 ; Necesarios para los Delays. GLOBAL MotorReg ; Registro que contiene el ; estado de los motores durante ; el periodo Ton de la PWM. ;+---------------------------------------------------------------------+ ;| USES | ;+---------------------------------------------------------------------+ EXTERN StandBy, Pinza, StopMotor, Forward, Backward EXTERN TL, TLwf, TLwb EXTERN TR, TRwf, TRwb ; movimientos y giros simples EXTERN Delay200, Delay1s ;+---------------------------------------------------------------------+ ;| RAM | ;+---------------------------------------------------------------------+ INT UDATA _W RES 1 _STATUS RES 1 PWM UDATA MotorReg RES 1 PWMOn RES 1 ; Tiempo en ON (Ton) del periodo PWM PWMflag RES 1 ; flag de estado de la PWM (Ton o Toff) #DEFINE PWM PWMflag, 0 TEMPVAR UDATA_OVR DLAY RES 3 ; para compartir con rutinas de retardo CNT RES 1 ; externas Flags RES 1 TMP1 RES 1 TMP2 RES 1 ;+---------------------------------------------------------------------+ ;| Entry Point | ;+---------------------------------------------------------------------+ RVECTOR CODE ; la sección RVECTOR apunta al 0x00 goto INICIO ; de la página 0 (sigue abajo...
La Rutina de Tratamiento de Interrupciones es la encargada de llevar el hilo a la Modulación por Ancho de pulsos. De esta manera, el programa principal trata a los motores como si la PWM no existiera.
Para ello, interrumpimos al hilo principal cada Tcy microsegundos, actualizamos el valor de la onda, y regresamos. La onda está "corriendo" en todo momento, y hacemos que los motores de MONTY se "enganchen" con la onda PWM cada vez que los encendemos.
El hilo principal debe encargarse de encender y apagar los motores, y determinar el sentido de giro. También es en el hilo principal donde establecemos el nivel de potencia aplicada a los motores, pero aquí lo hago simplemente seteando un registro, que es una de las dos variables de comunicación entre el hilo principal y la rutina de tratamiento de interrupción.
La otra variable que utiliza el hilo principal para comunicarse con la ISR, es el registro de control del motor, donde le avisamos si los motores están detenidos, avanzando, girando, etc.
En resumen:
Obviamente, es fundamental contar estrictamente el tiempo instrucción por instrucción, y computar todas las posibilidades. Para ello, he creado una nomenclatura que utilizo siempre en mis programas cuando la temporización es crítica:
Nomenclatura de la temporización:
[2] ==> indica tiempo en uS requerido para ejecutar la instrucción {8} ==> indica tiempo total acumulado hasta esta instrucción {2*} ==> indica instrucción de comienzo del conteo de tiempo {2#} ==> indica instrucción de fin de conteo del tiempo [2/1] ==> el primer valor, es el tiempo de instrucción si la salida es por VERDADERO, el segundo valor indica el tiempo si sale por FALSO.
;+---------------------------------------------------------------------+ ;| Rutina de Servicio a las Interrupciones | ;+---------------------------------------------------------------------+ PROG0 CODE 0004 ; Vector de interrupción ; [2]{2*} por la llamada movwf _W ; [1] Salvar entorno... movf STATUS, w ; [1] movwf _STATUS ; [1]{5} btfsc PWM ; [2/1] PWM = OFF ? (motor detenido?) goto PWM_ON ; [0/2] NO: cambiar a OFF (detener) nop ; [1/0] SI: cambiar a ON (arrancar) ; ----- ; [3/3]{8/8} ==> ecualizado OK
Una aclaración: como esta ISR es larga, he definido la sección PROG0 desde la posición 0x0004 hasta el final, es decir, hasta la 0x03FF, no protegida. El Script original que trae el MPLAB define la sección PROG desde la 0x05, por lo tanto tienen que redefinirla como les digo para que este código se linkedite bien:
Concretamente, hay que definir una página así:
CODEPAGE NAME=page0 START=0x4
END=0x3FF
y luego defino una única sección PROG0 para esta
página:
SECTION NAME=PROG0 ROM=page0 // ROM code space
Para el vector de Reset, defino una página desde 0x0000 hasta
la 0x0003 para la sección RVECTOR:
CODEPAGE NAME=rvector START=0x0 END=0x3 PROTECTED
FILL=0x00
Sigo con mi programa:
PWM_OFF: bsf PWM ; [1]{9} cambiar a ON para discriminar en la proxima entrada ; PERIODO T = 1ms = 4 * 250 us, Prescaler a 1:4 ; Hay que cargar TMR0 con -TOn, descontando el tiempo ; de proceso de la interrupción antes de setear TMR0, ; pero teniendo en cuenta que se incrementa cada 4 ; instrucciones, luego hay que dividir por 4 el número ; de instrucciones desde la entrada a la ISR hasta el ; seteo de TMR0 ; W := - (PWMOn - 4) = -PWMOn + 4 ; hay 16 periodos de instrucción desde ; la llamada a la ISR hasta el seteo del ; temporizador. (16/4 = 4 incrementos de TMR0) movf PWMOn, w ; [1]{10} w = PWMOn sublw 4 ; [1]{11} w = 4 - PWMOn goto PWM_DONE ; [2]{13} TMR0 := -(PWMOn') ; Aquí comienza la alternativa 2 para el cómputo del tiempo: esta sección se ejecuta ; cuando es momento de entrar a Toff (1ms - PWMOn) PWM_ON: bcf PWM ; *[1]{9} cambiar a OFF movf PWMOn, w ; *[1]{10} W := -(PWMOff - 4) addlw 4 - .250 ; *[1]{11} = -[(D250 - PWMOn) - 4] ; = PWMOn + (4 - D250) goto PWM_DONE ; *[2]{13} Para ecualizar ciclos. Hay que poner ; esta instrucción aunque el destino sea ; la instrucción que sigue! ; A partir de este punto, las instrucciones son comunes a todas las entradas a la ISR: PWM_DONE: nop ; [1]{14} para alcanzar los 16 ciclos exactos movwf TMR0 ; [2]{16#} 2 instrucciones (8 ciclos) porque ; el TMR0 arranca 1 ciclo despues ; \____________________________________/ ; Hasta aquí se cuentan las instrucciones. ;------------------------------------------------------------------------------------- btfss PWM ; estamos en Ton ? goto PulsoOff ; NO: parar motores ==> Toff ; SI: encender motores ==> Ton PulsoOn movf MotorReg, w movwf PORTA ; aquí se encienden los motores que goto OkMotor ; corresponden, en el sentido fijado en ; el thread principal PulsoOff clrf PORTA ; aquí es donde se apagan los motores ; momentaneamente OkMotor bcf INTCON, T0IF ; hay que resetear la flag para no provocar una ; nueva interrupción apenas se setee GIE en retfie movf _STATUS, w movwf STATUS swapf _W, f swapf _W, W retfie ; acá se setea GIE
Ahora viene la entrada al programa principal. Utilizo la macro Movfl para mover "literal a registro", y la macro Bank0 o Bank1 para cambiar de banco.
Además hay que hacer las siguientes definiciones:
#DEFINE _T0CS_INT
(0FF^(1<<5))
; para seleccionar reloj interno para el TMR0, equivale a B'1101 1111'
#DEFINE _PSA_T0_4
0F1 ; para asignar el prescaler al
TMR0 con una relacion 1:4, equivale a B'1111 0001'
#DEFINE _GIE
(1<<7) ; equivale al binario B'1000
0000' [General Interrupt Enable]
#DEFINE _T0IE
(1<<5) ; equivale al binario B'0010
0000' [Habilitar Interrupción por TMR0]
Ahora sigo con el código:
;+---------------------------------------------------------------------+ ;| Entrada Principal del Programa | ;+---------------------------------------------------------------------+ INICIO CODE INICIO: Bank1 Movfl TRISA^80, b'10000' Movfl TRISB^80, b'01111111' Movfl OPTION_REG^80, _T0CS_INT & _PSA_T0_4 Bank0 Movfl INTCON, _GIE + _T0IE ; habilitar interrupcion por TMR0 movlw .125 ; Setear PWM al 50% movwf PWMOn call StopMotor ; las rutinas de movimiento como ; StopMotor, Forward, etc. setean ; el registro MotorReg, el cual será ; más tarde copiado al PORTA para ; controlar los motores call StandBy ; espera a que se presione un Bumper. ; Acá se están produciendo interrupciones ; por PWM, pero los motores se mantienen ; apagados, ya que el MotorReg se controla ; desde este thread.
Al salir del StandBy, presionando algún Bumper, se ingresa al LOOP principal, que lista los movimientos que debe hacer MONTY. Observar que para cambiar la potencia aplicada a los motores, simplemente se cambia el valor de un registro (PWMOn), y para definir el tipo de movimiento, se actualiza el registro MotorReg. La interrupción se encarga de todo:
LOOP bcf INTCON, T0IE ; Desactivar PWM para marcha al 100% call Pasito ; Hace un paso adelante y uno atrás Movfl PWMOn, .200 ; Marcha al 80% del máximo bsf INTCON, T0IE ; Activar PWM call Pasito Movfl PWMOn, .150 ; Marcha al 60% del máximo call Pasito Movfl PWMOn, .100 ; Marcha al 40% del máximo call Pasito Movfl PWMOn, .83 ; Marcha al 33% del máximo call Pasito goto LOOP ;+---------------------------------------------------------------------+ ;| Sub-rutinas | ;+---------------------------------------------------------------------+ Pasito call Forward call Delay1s call Delay1s call Backward call Delay1s call Delay1s return END
NOTA: Así queda StopMotor para usar con este programa:
StopMotor ; PARAR clrf MotorReg ; La ISR se encarga de ir encendiendo y apagando los motores. clrf PORTA ; Para cuando desactivamos T0IE, para potencia al 100%. return
Y así se ve StandBy:
StandBy btfsc BUMI ; Chequear Bumper izquierdo: (¿Apagado?) return ; NO: Bumper apretado, volver! btfsc BUMD ; SI: Bumper izquierdo no apretado, ; Probar Bumper derecho... Apagado? return ; NO: Bumper apretado, volver!
goto StandBy ; SI: Ningun bumper apretado, ; probar nuevamente izquierdo...
Así se vería, más o menos, en un osciloscopio la señal que controla los motores de MONTY:
+--------+ +--------+ +--------+ +--------+ | | | | | | | | DCy = 50% -------+ +--------+ +--------+ +--------+ + +-------------+ +-------------+ +-------------+ +--------- | | | | | | | DCy = 75% -------+ +---+ +---+ +---+ +-+ +-+ +-+ +-+ | | | | | | | | DCy = 15% -------+ +---------------+ +---------------+ +---------------+ +------- | | | | | >|-|< | |<------------->| Ton = PWMOn | Toff | | |<--------------->| T = 1000 us
Marcelo Valdéz - marzzelo@yahoo.com