Servos de modelismo o RC
Control de un servo - Prueba 02: Control con interrupciones
por Eduardo J. Carletti


Servo utilizado: Hitec HS-311
Esta es la segunda etapa de un grupo de pruebas de laboratorio en las que implementamos el control de servos de modelismo por medio de un microcontrolador PIC. Si usted ha llegado directamente a este artículo, sepa que para tener una idea completa de cómo venimos trabajando con este desarrollo debería ver antes la primera parte de la serie, Control de un servo - Prueba 01: Control básico.

Si bien el programa de la primera parte funciona y es simple, y permite ver con claridad la manera de generar el pulso con los tiempos correctos, la verdad es que esa manera de solucionar el control de servos tiene el defecto de que mientras se producen los retardos, el microcontrolador está en un lazo cerrado, dedicado exclusivamente a esperar que se cumplan los tiempos.

¿Cómo se soluciona esto? Haciendo que los retardos se produzcan mientras el microcontrolador está haciendo otra cosa. Y para hacerlo se debe utilizar una de las herramientas más potentes en un sistema que debe ser sensible a sucesos producidos en el mundo real: las interrupciones.

El circuito que vamos a utilizar sigue siendo el mismo de la primera parte:

Segunda prueba de control de un servo RC

La foto que sigue nos permite apreciar la simplicidad del circuito. Podemos ver un integrado de 18 patas, que es el PIC16F628A (sin cristal, porque funciona con su generador interno de reloj de 4 MHz); el enchufe estándar de tres patas para el servo; y tres pequeños pulsadores. Los resistores son para polarizar las entradas del PIC que se utilizan para "leer" los pulsadores a un valor positivo, o valor lógico 1. Los pulsadores llevan este nivel a lógico 0, o tierra, cuando se los presiona.

Montaje del circuito básico de control de un servo RC


Al servo le hemos colocado un círculo de cartulina con una línea que facilita la visualización de su posición. Los tres cuadrados negros pequeños son los pulsadores, que corresponden, de izquierda a derecha, a "pulsador 1", "pulsador 2" y "pulsador 3" en el circuito. El pulsador 1 servirá para llevar al servo a su posición central, o "neutra". Con el pulsador 2 se hará avanzar al servo en sentido antihorario, y con el pulsador 3 se lo hará girar en sentido horario.

Programa de control de servos RC con interrupciones


;**********************************************************************
; MANEJO DE SERVOS - Programa Básico 2 - Manejo con interrupciones
; Por Eduardo J. Carletti, Robots Argentina, 2007
;**********************************************************************

	list      p=16F628A	; definir procesador

#include <p16F628A.inc>		; definiciones de variables específicas del procesador

	ERRORLEVEL 1;-302	; para evitar los mensajes de cambio de
				; banco en el resultado del compilador

	__CONFIG   _CP_OFF & _WDT_OFF & _LVP_OFF & _PWRTE_ON & _INTRC_OSC_NOCLKOUT & _MCLRE_OFF

;***** DEFINICIÓN DEL NOMBRE DE ENTRADAS Y SALIDAS

#define SERVO1	 	PORTA,0		; puerto de salida de servo 1
#define PULSADOR1 	PORTA,1		; puerto de entrada de pulsador 1
#define PULSADOR2 	PORTA,2		; puerto de entrada de pulsador 2
#define PULSADOR3 	PORTA,3		; puerto de entrada de pulsador 3

;***** VARIABLES

	CBLOCK	0x20
	acum_A		; variable momentánea
	Posic		; posición servo
	fase		; fase de programa para interrupción
	Guardar_STATUS	; Guardar Status
	Guardar_W	; Guardar W
	ENDC

;***********************************************************************************

	org	0x000
	goto	principal
	org	0x004
	goto	interrupcion

;***********************************************************************************
; Principal
;***********************************************************************************

principal
		movlw b'00000111' 	; deshabilita comparadores. Esto es
		movwf CMCON		; algo importante en el PIC 16F628A

		clrf PORTA		; inicia ports
		clrf PORTB		; inicia ports

		bsf STATUS,RP0		; Apunta a banco 1

		movlw b'00001110' 	; PORTA 
		movwf	TRISA		; salidas menos 1, 2 y 3, entradas

		MOVLW b'00000000' 	; PORTB 
		movwf	TRISB		; salidas

		movlw	b'00000010'	; Configuración para TMR0
		movwf	OPTION_REG	; preescaler 2, 1:8 con CLK interno (que es
					; de 1 MHz si el oscilador es de 4 MHz).
					; El contador cuenta cada 8 useg

	        bcf STATUS,RP0          ; Apunta a banco 0

		clrf	TMR0		; inicia registro de timer en 0

		movlw	d'133'		; inicia valor de posición de servo (centro)
		movwf	Posic

		clrf	fase		; inicia para que comience con la fase 0

		movlw	b'10100000'	; Interrupciones habilitadas
					; GIE (global), T0IE (timer 0)
		movwf	INTCON		; Interrupción General, Interrupción Timer 0


;***********************************************************************************
; Lazo principal
;***********************************************************************************

lazo
		movf	fase,f
		bz	fase0
		goto	lazo

;***********************************************************************************
; Rutina de apoyo
;***********************************************************************************

; Fase 0: Los servos actúan con un pulso de control que va entre 0,5 ms y 2,5 ms.
; En el bloque que sigue se inicia en 1 el pulso de control y se espera un 
; retardo fijo de aproximadamente 0,5 ms, que es el valor mínimo del pulso.
; Con este valor de longitud de pulso y una longitud cero en la parte variable, 
; el servo está parado al máximo hacia la izquierda (sentido antihorario).

fase0		bsf	SERVO1		; pone la señal de servo en 1
		bcf	INTCON,T0IF	; borra el flag de timer
		movlw	d'192'		; (256-192 = 64) 64 * 8 us = 0,512 ms
		movwf	TMR0		; valor al registro de timer
		incf	fase,f		; pasa a fase 1
		goto	lazo

;***********************************************************************************
; Manejo interrupciones
;***********************************************************************************

interrupcion

; Salva todo
		bcf	INTCON,T0IE	; deshabilita interrupción de timer
		movwf	Guardar_W
		swapf	STATUS, w
		movwf	Guardar_STATUS

; distribuidor
		movf	fase,w
		xorlw	0x01		; ¿fase 1?
		bz	fase1		; sí
		movf	fase,w		 
		xorlw	0x02		; ¿fase 2?
		bz	fase2		; sí
		movf	fase,w
		xorlw	0x03		; ¿fase 3?
		bz	fase3		; sí

		goto	salidaint	; ninguna de ellas, sale

; Fase 1: Aquí comienza el tiempo variable del pulso. Esto permite que
; el recorrido completo, de 180 grados (valores de pulso entre 0,5 ms
; y 2,5 ms), se divida en 256 segmentos (256 * 8 us = 2,048 ms).
; La parte variable del pulso de control varía entre 0 y 2 ms

fase1		movf	Posic,w		; 256-nn x 8 uS = 1 ms, 1,5 ms, 2,5 ms
		movwf	TMR0		; valor al registro de timer
		incf	fase,f		; pasa a fase 2
		goto	salidaint

; Fase 2: Retardo de 20 ms, que es el tiempo estándar que debe separar los pulsos 
; de control para los servos comunes de RC

fase2		bcf	SERVO1		; pone la señal de servo en 0
		bsf	STATUS,RP0	; Apunta a banco 1
		movlw	b'00000111'	; Configuración para TMR0
		movwf	OPTION_REG	; preescaler 7, 1:256 con CLK interno (que es
					; de 1 MHz si el oscilador es de 4 MHz).
					; El contador cuenta cada 256 useg
	        bcf STATUS,RP0          ; Apunta a banco 0
		movlw	d'249'		; (256-249 = 7) 7 * 256 us = 17,92 ms + 2 ms pulso
		movwf	TMR0		; valor al registro de timer
		incf	fase,f		; pasa a fase 3
		goto	salidaint

; Fase 3: Como en el bloque de la fase 2 cambió el prescaler del timer a 7, ahora 
; debe reponerlo al valor de un conteo cada 8 useg, y volver al estado de fase = 0, 
; con lo cual el ciclo se reinicia. También hemos insertado en esta fase, que se 
; ejecuta cada 20 ms aproximadamente, la lectura de los pulsadores.

fase3		
		bsf STATUS,RP0		; Apunta a banco 1
		movlw	b'00000010'	; Configuración para TMR0
		movwf	OPTION_REG	; preescaler 2, 1:8 con CLK interno (que es
					; de 1 MHz si el oscilador es de 4 MHz).
					; El contador cuenta cada 8 useg
	        bcf	STATUS,RP0	; Apunta a banco 0
		clrf	fase		; pasa a fase 0

pulsadores	; lee órdenes a través de pulsadores
		btfss	PULSADOR1
		goto	iniciaPos	; servo en posición central
		btfss	PULSADOR2
		goto	incPos		; incrementa Posic
		btfss	PULSADOR3
		goto	decPos		; decrementa Posic
		goto	salidaint

;***********************************************************************************
; Rutinas de apoyo
;***********************************************************************************


iniciaPos	movlw d'133'		; 256-133 = 123, 123 * 8 us = 984 us + 512 us
		movwf Posic		; posición central en esta disposición
		goto salidaint		; y sale

decPos		movf Posic,f		; se fija si Posic ya es cero
		bz sale1		; si es cero no decrementa
		decf Posic,f		; si no es cero, decrementa
sale1		goto salidaint		; y sale

incPos		movlw 0xFF		; se fija si Posic no es 0xFF
		xorwf Posic,w		; que es el valor máximo
		bz sale2		; si es el valor máximo, no incrementa
		incf Posic,f		; si no es el valor máx, incrementa
sale2		goto salidaint		; y sale

; Regresa todo
salidaint
		bcf	INTCON,T0IF	; borra el flag de timer
		swapf	Guardar_STATUS, w
		movwf	STATUS
		swapf	Guardar_W, f
		swapf	Guardar_W, w
		bsf	INTCON,T0IE	; habilita interrupción de timer
		retfie			; retorna

		END

Bajar el programa en formato ASM (puede usar el botón derecho de su mouse)
Bajar el programa en formato HEX (puede usar el botón derecho de su mouse)
Por las dudas, el archivo incluido P16f628a.inc (puede usar el botón derecho de su mouse)

Para que el lector no deba buscar con lupa los cambios (aunque algunos son muy evidentes), listaremos qué es lo que debimos cambiar para que el PIC funcione con interrupciones.

  • En primer lugar, se observa el agregado de tres variables: "fase", que nos servirá para que cuando entremos a la rutina de manejo de interrupción podamos saber en qué parte del pulso estamos, y las variables "Guardar_W" y "Guardar_STATUS", en las que nuestra rutina de interrupción reservará estos dos importantes valores (registro STATUS y registro W, o acumulador), para que cuando regrese al sitio de programa donde se produjo la interrupción las cosas estén tal cual estaban al salir.
  • En segundo lugar notaremos la declaración "org 0x004", seguida de la instrucción "goto interrupcion". Con este valor se define (para el funcionamiento interno del PIC) dónde está la rutina que se ejecutará cuando aparezca una interrupción. Nótese que en la parte final del programa la palabra "interrupcion" es la etiqueta que señala la rutina de manejo de las interrupciones.
  • En tercer lugar encontramos, hacia el final de la parte de inicialización de sistema, puertos y variables (justo antes de entrar al lazo principal), la habilitación del funcionamiento en base a interrupciones, que se logra al definir valores del registro de control de interrupciones, INTCON. El valor que definimos tiene el bit 7 en 1, que significa que se habilita el funcionamiento de interrupción a nivel gobal (el bit es GIE, Global Interrupt Enable); y el bit 5 en 1, que significa que habilitamos la interrupción por sobrepaso (overflow) del timer 0 (cero). Esto significa una interrupción cada vez que el registro del timer 0 cuente hasta su valor máximo, que es 255 ó hexadecimal 0xFF, y pase de nuevo a cero. Nosotros usaremos esta característica para colocar distintos valores en el registro, que avanza de valor a un ritmo conocido por nosotros, de manera de lograr retardos controlados y exactos. Como se ve, con dos bits solamente hemos cambiado del todo el funcionamiento de nuestro microcontrolador.

El siguiente cambio en el programa, éste sí muy evidente, es en la rutina principal o lazo principal. Como se ve, consta de apenas tres instrucciones. Es obvio que así el PIC también está desaprovechado, pero está claro que en este lugar ubicaremos, en el futuro, programas más importantes.

A continuación de este lazo vemos una parte del programa anterior, la que determina el encendido del pulso de control del servo a valor 1, y define el primer retardo, básico, de unos 0,5 ms. Lo que se debe observar es que el retardo no se produce en esta rutina, sino que sólo se "lanza" al timer, programando su registro o contador con el valor necesario. Aquí no hay un lazo cerrado de espera.

¿Cómo se produce entonces el retardo? El retardo ha comenzado ya, y transcurre durante todo el tiempo en que el programa se encuenta en fase 1. Este pequeño bloque de programa, que inició el pulso y el retardo, también ha puesto el programa en esta fase, poniendo a valor 1 la variable "fase".

¿Qué ocurre ahora? El PIC sigue con sus tareas habituales (en este programa, bien, el PIC prácticamente no hace nada, pero podría estar comandando un robot). El contador del timer 0 va contando, cada 8 microsegundos, hasta que llega a sobrepasar su cuenta, rebasando su valor máximo, que es 0xFF (ó 255 en decimal) y llegando de nuevo al valor 0x00. Aquí se produce la interrupción.

¿Qué significa esto? Significa que la lógica interna del PIC registra el lugar exacto en el que está el programa (para luego volver a él), se fija en ese vector 0x004 que hemos programado para ver dónde debe ir a ejecutar su rutina de manejo de la interrupción, y allí va...

Revisemos entonces la rutina de interrupción. A la entrada vemos que lo primero que hace es inhibir una nueva interrupción a causa del timer. Esto es importante y debe ser una práctica común a utilizar porque si se produce una nueva interrupción mientras estamos atendiendo ésta, es decir, mientras estamos dentro de la rutina de atención de interrupciones, algo fallará.

A continuación, la rutina se ocupa de guardar el estado (STATUS) y el valor del acumulador (W). Esto es necesario para no afectar el funcionamiento del programa cuando el PIC retorne al lugar donde se produjo la interrupción. Ya veremos que a la salida de la rutina de manejo de la interrupción los valores se reponen.


En la siguiente parte de la rutina de manejo de interrupción vemos que ésta debe hacer diferentes cosas según en qué fase de programa nos encontremos. A este punto del seguimiento estamos en fase 1, así que entrará a la rutina llamada "fase1".

fase1 es un fragmento del programa anterior, en el que definíamos el valor de ancho, o retardo, del resto del pulso: la parte variable. Sólo se ha quitado, una vez más, el lazo de espera sobre sí mismo (que es el que volvía esclavo al microcontrolador). Como se observa en las líneas de programa, lo único que debe hacer esta rutina es colocar un valor variable, la variable Posic, en el registro del timer, pasar el estado a fase 2, y salir.

En la próxima interrupción (que se producirá cuando se cumpla el tiempo de la segunda parte del pulso, o la parte variable del pulso de control), la rutina de manejo de interrupción entrará a la parte de la rutina con la etiqueta fase2.

fase2 también es una parte del programa anterior, y también quedó sin el lazo de espera. Aquí se pone a cero la señal de control del servo y se define la siguiente espera, que es la separación entre pulsos de control. Avanza el estado del programa a fase 3 y sale.

Si observamos el código, vemos que en esta parte estamos cambiando algo en la configuración del timer. ¿Qué es esto? A no asustarse. Debido a que en los retardos anteriores teníamos pulsos angostos, la velocidad de conteo del registro del timer la habíamos definido en un pulso (o conteo) cada 8 microsegundos. Esto nos daba una buena resolución para controlar el servo y nos permitía alcanzar los valores de retardo necesario con sólo un byte para definirlos (la variable Posic). El máximo es de 256 x 8 us = 2048 microsegundos, ó 2,048 milisegundos. Pero en esta parte necesitamos un retardo de 20 milisegundos. No nos alcanza. Por eso, cambiando el valor elegido de prescaler, cambiamos el ritmo del contador para que cuente una vez cada 256 microsegundos. Un valor de 7 en el contador (7 x 256 us) nos dará el valor deseado, de (aproximadamente) 18 milisegundos.

Cuando se cumpla este tiempo, se producirá una nueva interrupción y entraremos a la parte etiquetada fase3 de la rutina de manejo de interrupciones.

En fase3 debemos preparar todo para un nuevo ciclo. Se repone el prescaler anterior, que nos da un conteo cada 8 us, y se pone el estado del programa en fase 0. Esta parte de la rutina es muy sencilla, como se ve.

Dentro de fase3 hemos incluido la lectura de los pulsadores y, según su estado, la actualización de la posición del servo. Lo colocamos aquí porque a esta parte de las fases entra una vez cada más o menos 20 milisegundos, y así, al actuar un pulsador, el avance y dismimución de la posición que se produce en el movimiento del servo no es excesivamente veloz. Si dejásemos esta parte de la rutina en el lazo principal, los pulsadores serían leídos tantas veces, y a tanta velocidad, que el servo saltaría de un extremo al otro en lugar de moverse gradualmente. Se nota aquí cómo los tiempos están mejor aprovechados, ya que el movimiento del servo es más fluido.

En este punto regresamos a la fase 0. Al salir de la rutina de manejo de interrupciones volvemos al lazo principal de programa y desde allí volveremos a repetir, una vez más, el ciclo completo de fases.

Espero haber sido claro. Quizás sea necesario leer un par de veces este programa, imprimiendo la rutina y haciendo marcas en los lugares que nos resulte necesario.

Para facilitar el seguimiento, aquí van los diagramas de flujo:

Diagrama de flujo de la parte en el programa principal

Diagrama de flujo de la parte en la rutina de atención de las interrupciones

El próximo paso será manejar, con estos mismos conceptos, a más de un servo.


Muy pronto aportaremos la tercera parte de este trabajo: Control de múltiples servos.

Algunas cosas que observé en la parte 1 y 2

  • Importante: el temblor y el calentamiento del servo crecen mucho si se supera el retardo de 20 ms entre pulsos de control.

Datos adicionales:

Microcontrolador PIC16F628A

PIC16F628A.

Servo de modelismo o RC

Utilizamos el servo Hitec HS-311 porque era el más barato disponible en ese tipo al momento de encarar el proyecto.

Recomendamos ver, para completar esta información -> Servos, Características básicas