Control de un motor con el PIC 16F876A, 16 bits de resolución, por Interrupción.

Últimamente no suelo utilizar la lectura del encoder para controlar motores usando la interrupción externa (RB0/Int) porque en este método no permite multiplicar la resolución por 4 (depende del número de entradas de interrupciones que tenga el PIC). Pero hay casos que sí se hace interesante utilizarla, y sucede, por ejemplo, cuando además de controlar el motor queremos hacer otras cosas.

El control del motor es muy simple. Se trata de comparar la posición actual (es la posición que está en ese instante el motor) con la posición final. Entonces podemos tener tres resultados: Puede ser mayor, menor o igual. Si la posición actual es mayor que la posición final el motor ha de girar en un sentido; si fuera menor ha de girar en sentido contrario; y si es igual el motor ha de parar. Para el control del motor sólo vamos a usar dos bits, el que indica "menor que" (RA2) y "mayor que" (RA3). El bit de "igual" (RA4) de momento le vamos a poner un LED para indicar que el motor ya ha llegado a la posición final.

Observa el programa:

Haz clic aquí para ver el código en versión Proton IDE.

Código fuente en CCS C (Primera versión):

The translation could modify the code. Use the code without translating.
#include <16F876A.h>
#FUSES NOWDT, XT, NOPUT, NOPROTECT, NODEBUG, NOBROWNOUT, NOLVP, NOCPD, NOWRT
#use delay(clock=4000000)
#byte porta = 0x05     // Asignamos PortA.
#byte portb = 0x06     // Asignamos PortB.
#byte portc = 0x07     // Asignamos PortC.
//----------------- Variables globales. ----------------------
signed int32 p=0;      // Declaramos el valor de P como DWord (32 bits), posición final.
signed int32 x=0;      // Declaramos el valor de X como DWord (32 bits), posición actual.
#bit  p0 = p.0         // Primer bit de la variable P.
#bit  p1 = p.1         // Segundo bit de la variable P.
#bit  a0 = porta.0     // Bit RA0 del puerto A.
#bit  a1 = porta.1     // Bit RA1 del puerto A.
// ---------- Interrupción externa ----------
#INT_EXT 
void IntRB0() 
{
   if (bit_test(portb, 0))     // Si RB0 se ha puesto a 1 (flanco de subida),
   {  
      ext_int_edge(H_TO_L);    // entonces activar la siguiente interrupción por flanco de bajada.
      if (bit_test(portb, 1))  // Si RB1 está 1,
      {
         x++;                  // entonces incrementar el contador X.
      }
   }
   else                        // Si RB0 se ha puesto a 0 (flanco de bajada),
   {  
      ext_int_edge(L_TO_H);    // entonces activar la siguiente interrupción por flanco de subida.
      if (bit_test(portb, 1))  // Si RB1 está 1,
      {
         x--;                  // entonces decrementar el contador X.
      }
   }
}
// ---------- Programa Principal ----------
void main()
{
   port_b_pullups(false);         // Configuramos todo digital, etc. 
   setup_adc_ports(NO_ANALOGS);
   setup_comparator(NC_NC_NC_NC);
   setup_vref(FALSE);
   
   enable_interrupts(int_ext);    // Activar Interrupcion Externa.
   ext_int_edge(L_TO_H);          // Inicialmente Interrupción por Flaco de Subida. 
   enable_interrupts(GLOBAL);     // Interrupciones Generales Activadas. 
   set_tris_a(0b100011);          // Configuración E/S del puerto A.
   set_tris_b(0b11111111);        // Configuración E/S del puerto B.
   set_tris_c(0b11111111);        // Configuración E/S del puerto C.
   
   //---- Fin de la configuración del 16F876A ----
   
   output_low(PIN_A2);            // Motor parado.
   output_low(PIN_A3);
   output_low(PIN_A4);            // Apaga el LED.
   
   while(true)                    // Bucle infinito.
   {
      // ---------------- Comparación para el control del Motor. ----------------
 
      if(x == p)
      {
         output_low(PIN_A2);       // Motor Parado.
         output_elow(PIN_A3);
         output_low(PIN_A4);       // Apaga el LED.
          
         p=make16(portc, portb);   // Carga PortC como byte alto y PortB como byte bajo en la variable P.
          
         p0=a0;   // Carga el valor de RA0 en el primer  bit de la variable P.
         p1=a1;   // Carga el valor de RA1 en el segundo bit de la variable P.
      }
 
      output_high(PIN_A4);      // Enciende el LED.
 
      if(x > p)                 // Si x>p, motor hacia atrás.
      {      
         output_high(PIN_A2);   // Motor hacia atrás.
         output_low (PIN_A3);
      }
  
      if(x < p)                 // Si x<p, motor hacia adelante.
      {      
         output_low (PIN_A2);   // Motor hacia adelante.
         output_high(PIN_A3);
      }   
   }
}

La variable 'P' (posición final) está declarada con 32 bits pero sólo se usan los primeros 16 bits. La entrada de datos la hacemos de forma paralela a través de dos puertos: RB y RC, cada uno es de 8 bits y suman los 16 bits que necesitamos. Pero sucede que RB0 y RB1 se usan para decodificar el encoder, entonces P0 y P1 son falsos. Si observas el esquema del circuito verás que los dos primeros bits de entrada de datos se dirigen hacia RA0 y RA1. Una vez que hemos cargado RB y RC, hemos de sobre-escribir los dos primeros bits de la variable 'P' con RA0 y RA1 porque contienen los dos bits verdaderos.

Esto es un truco para poder usar el puerto RB como entrada de datos y a la vez poder usar la interrupción (RB0/Int) junto con RB1 que se encargan de decodificar el encoder.

Eliminar Movimiento Pendular:

Este programa utiliza la interrupción externa (RB0/Int) para el conteo de pulsos, por tanto necesitamos un acondicionador muy rápido, como el 74LS14 o los sensores Hall digitales que ya lo llevan integrado.

Utilizar este tipo de acondicionador conlleva una histéresis y en algunos casos puede suceder que el motor al querer quedarse parado no lo haga del todo y quede durante un tiempo con movimiento pendular entre las dos posiciones adyacentes. En la mayoría de los casos este movimiento desaparece por el propio rozamiento entre el piñón del motor con el primer engranaje de la reductora; de todas formas a través de programación podemos darle una solución si ese movimiento persistiese. Se trata de que cuando el motor llegue a la posición final, reducir la potencia del motor para eliminar esa inercia pendular. Para ello usaremos la técnica PWM (modulación por anchura de pulso). A través de programación hago el PWM y esto es así porque las patillas con función PWM del propio PIC ya están ocupadas.

En el segundo programa verás que sólo se activa el PWM cuando el motor ya ha pasado por la posición de -igual-, de esto se encarga la variable 'Flag'. Es decir, el motor tiene toda su potencia y sólo cuando ya ha llegado a la posición final y trata de pendular es entonces cuando se activa el PWM.

Observa el programa:

Haz clic aquí para ver el código en versión Proton IDE.

Código fuente en CCS C (Eliminar movimiento pendular):

The translation could modify the code. Use the code without translating.
#include <16F876A.h>
#FUSES NOWDT, XT, NOPUT, NOPROTECT, NODEBUG, NOBROWNOUT, NOLVP, NOCPD, NOWRT
#use delay(clock=4000000)
#byte porta = 0x05     // Asignamos PortA.
#byte portb = 0x06     // Asignamos PortB.
#byte portc = 0x07     // Asignamos PortC.
// ----------------- Variables globales. ----------------------
signed int32 p=0;      // Declaramos el valor de P como DWord (32 bits), posición final.
signed int32 x=0;      // Declaramos el valor de X como DWord (32 bits), posición actual.
signed int32 q=0;      // Q nos servirá para saber que ha llegado a la posición y no hay nueva posición.
int8 temp=0;           // Contador para temporización.
int1 flag=0;           // Bandera que avisa de nueva posición.
#bit p0 = p.0          // Primer  bit de la variable P.
#bit p1 = p.1          // Segundo bit de la variable P.
#bit a0 = porta.0      // Bit RA0 del puerto A.
#bit a1 = porta.1      // Bit RA1 del puerto A.
#bit a4 = porta.4      // Bit RA4 del puerto A.
// ---------- Interrupción externa. ----------
#INT_EXT 
void IntRB0() 
{
   if(bit_test(portb, 0))     // Si RB0 se ha puesto a 1 (flanco de subida),
   {  
      ext_int_edge(H_TO_L);   // entonces activar la siguiente interrupción por flanco de bajada.
      if(bit_test(portb, 1))  // Si RB1 está 1,
      {
         x++;                 // entonces incrementar el contador X.
      }
   }
   else                       // Si RB0 se ha puesto a 0 (flanco de bajada),
   {  
      ext_int_edge(L_TO_H);   // entonces activar la siguiente interrupción por flanco de subida.
      if(bit_test(portb, 1))  // Si RB1 está 1,
      {
         x--;                 // entonces decrementar el contador X.
      }
   }
}
// ---------- Programa Principial. ----------
void main()
{
   port_b_pullups(false);         // Configuramos todo digital. 
   setup_adc_ports(NO_ANALOGS);
   setup_comparator(NC_NC_NC_NC);
   setup_vref(FALSE);
   
   enable_interrupts(int_ext);    // Activar Interrupcion Externa.
   ext_int_edge(L_TO_H);          // Inicialmente Interrupción por Flaco de Subida. 
   enable_interrupts(GLOBAL);     // Interrupciones Generales Activadas. 
   set_tris_a(0b100011);          // Configuración E/S puerto A.
   set_tris_b(0b11111111);        // Configuración E/S puerto B.
   set_tris_c(0b11111111);        // Configuración E/S puerto C.
   
   output_low(PIN_A2);            // Motor parado.
   output_low(PIN_A3);
   output_low(PIN_A4);            // Apaga el LED.
   
   // ---------------- Comparación para el control del Motor. ----------------
   
   while(true)                     // Bucle infinito.
   {
      a4=flag;                     // Enciende o apaga el LED si el motor está en marcha o parado.
      
      if(flag==1)
      {
         q=p;                      // Igualamos para saber si luego se producen cambios.
         p=make16(portc, portb);   // Carga PortC como byte alto y PortB como byte bajo en la variable P.
          
         p0=a0;                    // Carga el valor de RA0 en el primer bit de la variable P.
         p1=a1;                    // Carga el valor de RA1 en el segundo bit de la variable P.
          
         if (p!=q) {flag=0;}       // Detectar nueva entrada de datos.
      }
 
      if(x == p)
      {
         output_low(PIN_A2);       // Motor Parado.
         output_low(PIN_A3);
         flag=1;
      }
      if(temp>8) {temp=0;}
      temp++;
 
      if(flag==1 && temp>5)
      {
         output_low(PIN_A2);        // Motor Parado si cumple condición de tiempo para el PWM.
         output_low(PIN_A3);
      }    
      else
      {
         if(x > p)                  // Si x>p, motor hacia atrás.
         {      
            output_high(PIN_A2);    // Motor hacia atrás.
            output_low (PIN_A3);
         }
  
         if(x < p)                  // Si x<p, motor hacia adelante.
         {      
            output_low (PIN_A2);    // Motor hacia adelante.
            output_high(PIN_A3);
         }
      }    
   }
}

En este segundo programa habrás observado que hay tres variables nuevas: Q, Flag y Temp. Q y Flag se encargan de detectar que el motor ya ha llegado a la posición final y cuando eso sucede ha de activar el PWM. La variable Temp es el PWM en sí mismo. A mi me sucede que cuando miro un programa que no es mío, me cuesta verle la lógica y paso mucho tiempo tratándolo de entender. Es posible que te suceda lo mismo ahora con este segundo programa. Pero ten en cuenta que no es sencillo de verle la lógica porque está ejecutándose cuatro procesos a la vez dentro de un mismo programa: Contar bidireccionalmente pulsos, cargar la posición final, controlar el sentido de giro del motor y detectar que ya ha llegado a la posición final para luego activar un PWM.

Importante: Este programa está pensado para ser usado con PICs a 4MHz, entonces has de dejar la condición "if(temp>8) {temp=0;}" tal como está. Pero para PICs que funcionen a frecuencias superiores habría que aumentar el 8 por otro número mayor, más adelante pongo un ejemplo. La otra condición que normalmente se ha de tocar es esta: "if(flag==1 && temp>5)" Ese '5' puede ser un número del 1 al 8; el 8 es el límite máximo que pusimos en la condición anterior. "5" es el tiempo que el motor está en ON y el resto del periodo, hasta llegar al 8, está en OFF. Si cambiamos el valor de ese 5 por otro número, variaremos la potencia del motor cuando éste se quiere quedar parado.

Por ejemplo, para un 18F4550 haciéndolo funcionar a 48MHz, tuve que cambiar los valores del PWM de esta manera: (Si no lo hacía parecía que el motor estaba 'suelto', como parado, aunque le moviese el eje.)

if (temp>254) {temp=0;}
temp++;
if (flag==1 && temp>200)  // Motor Parado si cumple condición de tiempo para el PWM.
{
   output_low(PIN_A2);
   output_low(PIN_A3);
}

Un problema que me encontré cuando porté este programa (que es para el 16F876) a un 18F4550, es que las directivas #bit que están al comienzo del programa, daban problemas. Te aconsejo que si este es tu caso, uses el método de bucle infinito en vez de éste método.

Etapa de potencia para controlar el motor.

En la etapa de potencia uso un L293, pero puedes usar cualquier puente en H que se corresponda con la potencia del motor que vayas a usar. Lo único que has de tener presente es que cuando le entre un "11" tenga protección para evitar que se auto-cortocircuite. En puentes en H con protección, un "11" actúa como un "00", es decir, motor parado. El L293 tienen esta protección, al menos con la construcción que presento. Tiene dos puentes en H y uso los dos para un solo motor poniéndolos en paralelo, de esta forma soportará el doble de corriente.

Antes de ponerlo en marcha y otras cuestiones:

  • Cuando pongas en marcha el circuito, todas los bits de entrada de datos han de estar a cero.
  • Si al poner en marcha el circuito, el motor gira sin parar, significa que la polaridad del motor está invertida por tanto has de cambiar la polaridad del motor. Y si al alimentar el circuito el motor se pone en marcha sólo un tiempo y luego se detiene, es debido a que hay algún bit de entrada a uno. Al alimentar el circuito el motor siempre ha de estar parado si todos los bits de entrada de datos están a cero.
  • Los PICs son de tecnología CMOS, eso significa que la entrada de datos no pueden estar al aire porque crearían bits aleatorios (bits falsos). Si vas a probar este circuito poniendo a mano la información de posición con cables o pulsadores, has de añadir en la entrada de datos una red de resistencias para polarizarlas a positivo, como en la imagen de abajo. Puedes usar red de resistencias integradas para que quede todo más compacto. Observa que los pulsadores son "normalmente cerrados", es decir, que normalmente ha de haber un 0 en cada bit hasta que se pulse el (o los) pulsador(es) para poner a 1 el(o los) bit(s) correspondiente(s).
  • Cuando montamos el circuito en una protoboard es importante que tengas el motor fijo a una superficie. Si el motor está suelto, la propia inercia de moverse y pararse mueve los cables. En una protoboard hay que evitar esto a toda costa porque no hay soldaduras. Puedes usar plastilina para fijar el motor a la mesa. Cualquier movimiento de cables o malas conexiones puede crear ruido y convertirse en falso conteo.
  • Si por la razón que sea necesitas que el motor gire en sentido contrario a como lo hace normalmente, has de enrocar (intercambiar) las entradas del encoder, y también, la polaridad del motor.
  • En mis pruebas usé encoders de 2 a 12 pulsos por revolución del motor y funcionó perfectamente bien. Luego probé con un motor con encoder de 334 pulsos por revolución del motor y no se puede aplicar este método porque hay tanta resolución que el motor vibraría una vez que llegase a la posición donde ha de parar debido a que el espacio para hacerlo es ínfimo. Para encoders con mucha resolución es necesario cambiar de método utilizando un control PID.
  • Debido a que es un encoder incremental, en la práctica necesitarás que cuando se ponga en marcha el PIC lo primero que haga sea llevar el motor (con reductora) a la posición cero real (en este sentido sería como cuando controlas un motor paso a paso) porque, si no, considerará la posición cero allá donde esté en el momento de poner en marcha el circuito. Pero te aconsejo que antes de añadir esta parte primero hagas las pruebas sin este detalle (tal como lo presento). Cuando todo te funcione bien entonces le añades unas líneas de programación para que el PIC encuentre la posición cero real. Puedes aprovechar la patilla RA5 para poner el sensor de posición de cero real.
  • Habrás observado que las variables que intervienen para contar y posicionar (x y p) están declaradas con 32 bits en vez de 16. Esto es así para que el contador pueda contabilizar un poco más allá de 65535 (última posición posible con 16 bits) porque por inercia, el motor siempre se pasa unos pulsos de más y necesitamos que pueda corregirse para alcanzar esa última posición. Por otra parte, están declaradas con signo para que pueda parar en la posición cero.
  • El encoder funciona con un acondicionador de señal de tecnología TTL tipo 74LS14 ó 7414. También puedes hacerlo con sensores Hall digitales; si utilizas este tipo de sensor magnético no te harán falta los 74LS14 que hacen de acondicionador de las señales del encoder porque lo tienen integrado en su interior.