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:

  1. El mínimo valor para la frecuencia del pulso está dado por los parámetros mecánicos del sistema, una frecuencia demasiado baja produce un movimiento de mala calidad, ya que los pulsos pueden ser apreciados a simple vista en el movimiento del robot.

    Este "Ripple mecánico" es bastante difícil de calcular analíticamente, ya que intervienen muchas variables y complicadas ecuaciones de retroalimentación y control.

    Sin embargo, el método que yo utilizo es algo más simple: probemos una frecuencia razonable, digamos 1kHz. Si el robot da pequeños saltitos en lugar de avanzar suavemente, incrementamos la frecuencia hasta que el movimiento sea suave, y listo! (si se tratase de un robot de verdad, para la Toshiba o algo parecido, tendremos que hacer los cálculos de retroalimentación, o nos arriesgamos a un fracaso total :)
    `
  2. La frecuencia máxima posible está dada, obviamente, por la frecuencia del clock del robot. Para encender y apagar los motores hacen falta varias instrucciones (unas 20 para encenderlo y hacer los cálculos correspondientes a la temporización, actualización del estado, etc., y otras tantas para apagar los motores durante el Toff). Además nos tiene que quedar algún tiempo para el hilo principal del programa, ya que de otra forma la aplicación no tendría sentido...

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

OTRAS NOTAS

Contador