viernes, 16 de noviembre de 2018

TMR1 Control de Servomotores


Múltiples Servos con el PIC16F




Un gran saludo para todos los visitantes  que comparten interés en este mundo de la electrónica y en especial para aquello aficionados a la programación de microcontroladores PIC, Este vez quiero mostrarles como hacer uso de varios servomotores utilizando el modulo TMR1.

Si no tienes claro de como funciona el modulo TMR1 del microcontrolador PIC16F, te recomiendo revises esta entrada en la que se dedica ejemplos de uso para este recurso.

El objetivo de esta sección es poder utilizar hasta ocho servomotores con el TMR1 del PIC16F887, cada servo podrá trabajar dentro de todo su rango de movimiento, es decir de 0 hasta los 180 grados, que pueden ser controlados linealmente utilizando potenciómetros o en posiciones fijas mediante pulsadores.

La programación se realizara utilizando el compilador XC8 v2.0 y el entorno de desarrollo MPLABX v5.20 disponibles en la pagina de microchip de forma gratuita, puede descargar aqui todo el proyecto generado que incluye los procedimientos y funciones básicas para el PIC16F887.


Paso 1. Sobre los Servomotores.

Un servo esta compuesto por un motor DC, una caja reductora y un circuito electrónico de control que nos permite ubicar un eje central de accionamiento en cualquier posición dentro del rango de operación para el cual fue diseñado, en general de 0 a 180° grados. 

Suele contar con tres lineas de conexión, dos corresponden a la alimentación de energía y una para el control de posición por la cual se envía una señal digital modulada a una frecuencia de 50 Hertz, esto representa un periodo de 20 mili-segundos. Observe la siguiente figura que muestra la relación entre el pulso digital y la posición del servo.



Notara que la señal de control es prácticamente una modulación de ancho de pulso conocida con el termino PWM, por lo que nuestra primera idea al implementar un servo en nuestro PIC16F pasaría por utilizar el modulo CCP(Captura-Compare-PWM) y de hecho es algo viable pero debemos considerar los siguiente:
  • En general los Micro-controladores salvo algunos del tipo DSP poseen una cantidad de pines PWM limitadas, por ejemplo el PIC16F88x solo cuenta con 2 que comparten funcionalidad y podrían no ajustarse a ciertas necesidades.
  • Un modulo PWM esta diseñado para generar una señal con un ciclo de trabajo 0 - 100%, en cambio un servo solo requiere un ciclo de 5-10%, con lo cual podría no adecuarse a requerimientos en la resolución.
  • El modulo CCP de un PIC16F no esta diseñado para trabajar a bajas frecuencias. Para que el modulo pueda ajustarse a 50Hz la frecuencia del oscilador no debería superar los 1MHz, esto mas que seguro limitara las prestaciones del sistema.

Por esta razón una mejor alternativa al uso modulo del CCP, es utilizar un modulo TMR en conjunto con su interrupción para generar esta señal de control y de esta manera poder controlar en teoría hasta 10 servos, aunque en la practica son solo 8 por razones que las discutiremos mas adelante.


Paso 2. Generar la señal de control PWM.

Dada la explicación en el punto anterior del porque no es conveniente usar el modulo CCP, nos centraremos en utilizar el TIMER1 para generar esta señal de control a un servo.

Si observamos la siguiente imagen que nos muestra el pulso S1 que debemos enviar mediante un pin digital para controlar un servo, el pulso que tiene una duración entre 1-2ms debe repetirse cada 20ms.


Para implementar esta señal en nuestro programa realizaremos las siguiente definiciones y cálculos:


  • svnum. Representa la cantidad de servos que serán utilizados
  • slot. Es la fracción de tiempo o ranura dentro del periodo en la cual debe enviarse el pulso de control de un determinado servo, su valor representa los micro-segundos necesarios para el desbordamiento del registros TMR1, su calculo obedece a la siguiente ecuación.


  • sv1val. Este valor corresponde al tiempo de duración del pulso S1, como valores reverenciales se tiene derecha = 1000, centro = 1500, izquierda = 2000
  • El TMR1 deberá ajustarse para que el registro contador se incremente cada 1uS, obteniendo como valor máximo aproximado de 65ms(65535), aquí sera necesario considerar el uso de un cristal de 4MHz o múltiplos porque la relación entre la frecuencia de oscilación y el ciclo de instrucción con el que se incrementa el registro contador es: 
Aplicando la formula para un cristal de 4MHZ se consigue un incremento del registro contador TMR1 cada 1uS, y si el cristal fuera de 8MHz o 16MHz tendríamos que utilizar una pre-escala 1:2 o 1:4 respectivamente para mantener el tiempo de incremento 1uS.


Examinemos ahora con más detalle la programación de control para un servo, la misma se desglosara en tres etapas:
 
1. Encabezado

#include <xc.h>
#define _XTAL_FREQ 4000000
#define svnum 1 //Define la cantidad de servos a controlar
#define svslot (65536UL - (2000UL/svnum))//Calculo del slot a cada servo
#define sv1pin PORTAbits.RA0 //Pin del servo1
volatile unsigned int svtime, sv1val = 1000;
char svon = 0;

Notara que por definición se calcula el valor del tiemplo o slot asignado al TMR1, se define el pin RA0 por el cual se enviara el pulso y su valor inicial de control a 0° (sv1val=1000).

2. Programa principal

void main(void) //Función principal
{
    ANSEL = 0;  //Configura los pines AN0-AN7 en modo digital
    ANSELH = 0; //Configura los pines AN8-AN15 en modo digital
    TRISA = 0;  //Configura como salida todos los pines del PORTA
    PORTA = 0;  //Coloca en 0 lógico los pines del PORTA
    T1CONbits.TMR1CS = 0; //Ajusta el modo Temporizador   
    T1CONbits.T1CKPS = 0b00; //Sin pre-escala o 1:1  
    TMR1H = 0;
    TMR1L = 0;
    PIR1bits.TMR1IF = 0; //Limpia la bandera
    INTCONbits.PEIE = 1; //Cada pulso capturado es de 1uS (4*Tosc)
    PIE1bits.TMR1IE = 1; //Activa la interrupción del TMR1
    INTCONbits.GIE = 1; //Habilitador Global
    T1CONbits.TMR1ON = 1;
    TMR1Start(); //Arranca el modulo TMR1

    while(1)
    {
      __delay_ms(2000);
      sv1val = 1500; //Moverá el servo 90°
      __delay_ms(2000);
      sv1val = 2000; //Moverá el servo 180°
      __delay_ms(2000);

      sv1val = 1000; //Moverá el servo 0°
    }
}

Básicamente el programa principal configura en primera instancia el TMR1 y las interrupciones necesarias, luego arranca el temporizador y entran en el bucle indefinido para cambiar las posiciones del servo manteniendo una pausa de dos segundos.

2. Rutina de Interrupción.

void interrupt isr()    //Rutina de servicio a la interrupción
{
    if(PIR1bits.TMR1IF == 1) //Condición de desbordamiento con tiempo variable
    {
        switch(svon)
        {
            case 0:
                sv1pin = !sv1pin; //Genera pulso de control servo1
                if(sv1pin == 1) si el pulso es alto
                    svtime = 65536 - sv1val; //Ajusta tiempo para fin de pulso
                else
                {
                    svtime = svslot + sv1val; //Ajusta el tiempo de descanso
                    svon ++;
                }
                break;
        }
        if(svon >= svnum) //Si el control supera al máximo de servos 
            svon = 0;
        T1CONbits.TMR1ON = 0; //Para el Modulo TMR1
        TMR1H = svtime >> 8;
        TMR1L = svtime;    //Actualiza el registro contador
        T1CONbits.TMR1ON = 1 //Arranca el contador
        PIR1bits.TMR1IF = 0; //Limpia la bandera
    }


Cuando ocurre la interrupción del TMR1, Si el pin de salida es bajo(no hay pulso), se cambiara el estado del pin RA0 para iniciar el pulso de control sv1pin = !sv1pin, aqui se debe ajustar el temporizador para un tiempo igual a la duración del pulso svtime = 65536 - sv1val.

La siguiente interrupción que indica el tiempo de duración del pulso de control, debe finalizar la señal cambiando el nivelo lógico del pin RA0. en este punto de descanso el temporizador debera ajustarse a un valor de 2000ms - duración del pulso, esto es: svtime = svslot + sv1val

La rutina puede ser fácilmente adecuada para manipular varios servos que se condicionan por la estructura de control selectivo switch y la variable svon

En las siguientes imágenes se ilustran los pulsos y tiempos necesarios para el control de varios servos, hasta ocho en la practica debido a retrasos que se originan por latencia de la interrupción.




 
Paso 3. Conectar servomotores al PIC16F.

Como mencionamos anteriormente un servo suele contar con tres lineas de conexión, dos corresponden a la alimentación de energía y una para el control de posición. La siguiente figura ilustra la disposición de los pines y colores utilizados por algunos fabricantes.

Es importante mencionar que existen diversos modelos con requerimientos específicos de corriente y voltaje que deberán ser considerados en el diseño de nuestro circuito electrónico.

Paso 4. Programación para control de 3 servos.

En este ejemplo utilizaremos tres servomotores controlados por tres pulsadores, donde cada uno  permitirá cambiar la posición izquierda o derecha.

#include <stdio.h>
#pragma config FOSC = INTRC_NOCLKOUT, WDTE = OFF, LVP = OFF
#include <xc.h>
#define _XTAL_FREQ 4000000
#define svnum 3 //Define la cantidad de servos a controlar
#define svslot (65536UL - (2000UL/svnum)) //Calculo del slot de tiempo a cada servo
#define sv1pin PORTAbits.RA0 //Pin del servo1
#define sv2pin PORTAbits.RA1 //Pin del servo2
#define sv3pin PORTAbits.RA2 //Pin del servo3
#define bt1pin PORTBbits.RB0 //Pulsador 1
#define bt2pin PORTBbits.RB1 //Pulsador 2
#define bt3pin PORTBbits.RB2 //Pulsador 3
#define left 900 //Definicion de tiempo para posicion 0° (900ms)
#define center 1400 //Definicion de tiempo para posicion 90° (1400ms)
#define right 1900 //Definicion de tiempo para posicion 180° (1900ms)

volatile unsigned int svtime, sv1val = center, sv2val = center, sv3val = center;
char svon = 0, ledcnt = 0;
char bt1val, bt2val, bt3val; //Variables de estado de pulsadores
char bt1cnt, bt2cnt, bt3cnt; //Contadores de tiempo para los pulsadores
char bt1ok = 0, bt2ok = 0, bt3ok = 0; //Banderas de confirmación de pulsadores
void interrupt isr()    //Rutina de servicio a la interrupción
{
    if(PIR1bits.TMR1IF == 1) //Condición de desbordamiento con tiempo variable
    {
        switch(svon)
        {
            case 0:
                sv1pin = !sv1pin; //Genera pulso de control servo1
                if(sv1pin)
                    svtime = 65536 - sv1val; //Tiempo para finalizar pulso
                else
                {
                    svtime = svslot - sv1val; //Tiempo para iniciar pulso
                    svon ++;
                }
                break;
            case 1:
                sv2pin = !sv2pin; //Genera pulso de control servo2
                if(sv2pin)
                    svtime = 65536 - sv2val; //Tiempo para finalizar pulso
                else
                {
                    svtime = svslot - sv2val; //Tiempo para iniciar pulso
                    svon ++;
                }
                break;
            case 2:
                sv3pin = !sv3pin; //Genera pulso de control servo2
                if(sv3pin)
                    svtime = 65536 - sv3val; //Tiempo para finalizar pulso
                else
                {
                    svtime = svslot - sv3val; //Tiempo para iniciar pulso
                    svon ++;
                }
                break;
        }
        if(svon >= svnum) //Si el control supera al máximo de servos 
            svon = 0;
        T1CONbits.TMR1ON = 0; //Para el Modulo TMR1
        TMR1H = svtime >> 8;
        TMR1L = svtime;    //Actualiza el registro contador
        T1CONbits.TMR1ON = 1 //Arranca el contador
        PIR1bits.TMR1IF = 0; //Limpia la bandera

    }
}
void main(void) //Funcion principal
{
    ANSEL = 0;  //Configura los pines AN0-AN7 en modo digital
    ANSELH = 0; //Configura los pines AN8-AN15 en modo digital
    TRISB = 0b00000111; //Pulsadores en RB0,RB1,RB2
    OPTION_REGbits.nRBPU = 0; //Acvita las resistencias PullUP
    TRISA = 0;  //Configura como salida todos los pines del PORTA
    PORTA = 0;  //Coloca en 0 lógico los pines del PORTA
    TRISEbits.TRISE2 = 0; //Configura el pin RE2 como salida
    T1CONbits.TMR1CS = 0; //Ajusta el modo Temporizador   
    T1CONbits.T1CKPS = 0b00; //Sin pre-escala o 1:1  
    TMR1H = 0;
    TMR1L = 0;
    PIR1bits.TMR1IF = 0; //Limpia la bandera
    INTCONbits.PEIE = 1; //Cada pulso capturado es de 1uS (4*Tosc)
    PIE1bits.TMR1IE = 1; //Activa la interrupción del TMR1
    INTCONbits.GIE = 1; //Habilitador Global
    T1CONbits.TMR1ON = 1; //Arranca el modulo TMR1
    while(1)
    {
        if(bt1pin != bt1val)
        {
            bt1cnt ++;
            if(bt1cnt > 200) //El pulsador1 debe estar presionado por lo 
            { //menos unos 200ms para confirmar 
                bt1cnt = 0;
                bt1val = !bt1val;
                bt1ok = 1; //Confirma que pulsador1 fue presionado
            }
        }
        else bt1cnt = 0; //Reinicia contador cuando el pulsador1 cambia
        if(bt2pin != bt2val)
        {
            bt2cnt ++;
            if(bt2cnt > 200)//El pulsador2 debe estar presionado por lo 
            { //menos unos 200ms para confirmar
                bt2cnt = 0;
                bt2val = !bt2val;
                bt2ok = 1; //Confirma que pulsador2 fue presionado
            }
        }
        else bt2cnt = 0; //Reinicia contador cuando el pulsador2 cambia
        
        if(bt3pin != bt3val)
        {
            bt3cnt ++;
            if(bt3cnt > 200)//El pulsador3 debe estar presionado por lo 
            { //menos unos 200ms para confirmar
                bt3cnt = 0;
                bt3val = !bt3val;
                bt3ok = 1; //Confirma que pulsador3 fue presionado
            }
        }
        else bt3cnt = 0; //Reinicia contador cuando el pulsador3 cambia
        if(bt1ok) //Verifica si el pulsador1 fue presionado y confirmado 
        {
            if(bt1val == 1)
                sv1val = left;
            else
                sv1val = right;
            bt1ok = 0;
        }
        if(bt2ok) //Verifica si el pulsador2 fue presionado y confirmado
        {
            if(bt2val == 1)
                sv2val = left;
            else
                sv2val = right;
            bt2ok = 0;
        }
        if(bt3ok) //Verifica si el pulsador3 fue presionado y confirmado
        {
            if(bt3val == 1)
                sv3val = left;
            else
                sv3val = right;
            bt3ok = 0;
        }
        if(ledcnt++ > 100) //Aproximado 100x1ms = 100ms
        {
            ledcnt = 0; //Reinicia contador de ciclos (1ms)
            PORTEbits.RE2 = !PORTEbits.RE2; //Destella LED conectado a RE2
        }
        __delay_ms(1);
    }

}


Paso 5. Prueba y Simulación.




Paso 6. Conclusiones y Recomendaciones.

Pudimos ver mediante un ejemplo como hacer uso del TMR1 para controlar hasta ocho servomotores utilizando eventos de interrupción, adicionalmente se podría tomar en cuenta lo siguientes:

  • Es posible utilizar los módulos TMR0 o TMR2  pero con una menor precisión debido que los registros contadores son de solo 8-bits.
  • Si bien se recomienda utilizar múltiplos de 4MHZ para la frecuencia del cristal, es posible emplear cualquier otros valor siendo conscientes de que los valores asignados para el control no siempre serán en unidades de uS.
  • La latencia en la interrupciones dependerá de la frecuencia del oscilador, es posible utilizar un valor offset para ajustar el tiempo del contador al momento de su carga, un valor nominal para la corrección sera de 20-50. Ej. svtime = 65536 - (sv3val + 25)
Descarga el código completo del proyecto TMR1Servo.X

Ya finalizando esta entrada, nuevamente agradecer por la visita a este blog, cualquier consulta, sugerencia o critica constructiva me hacen llegar a la dirección de correcto, Saludos y hasta pronto.

Pablo Zarate Arancibia
Ingeniero Electronico
pablinzte@gmail.com