Translate

viernes, 22 de junio de 2012

Decodificador de un mando a distancia


Vamos a escribir una aplicación que junta los tres temas sobre los que hemos hablado hasta ahora: timers, interrupciones y UART.  El objetivo es detectar los pulsos y medir sus tiempos de un mando a distancia por infrarrojos, obteniendo un volcado por el puerto serie similar a este:


Esta información la podremos usar para determinar qué protocolo se está usando. Posteriormente, una vez que sabemos que protocolo usa nuestro mando será simple escribir otro programa que decodifique o emule los pulsos. Esto puede ser muy útil en muchos proyectos para usar un mando por infrarrojos para enviar órdenes a nuestro sistema.

Codigo asociado a esta entrada: IR_timing.c
------------------------------------------------------------------------------------------------------------

Hardware:

Un mando a distancia manda una serie de pulsos con una modulación AM con una portadora de alrededor de unos 40 KHz. Usaremos un módulo TSOP38238 (Vishay) para demodular la señal. 

Este módulo (y otros similares) eliminan la modulación de la portadora (en este caso está sintonizado a 38 KHz) y a la salida nos presentan los pulsos enviados por el mando. Normalmente son del tipo "active low" por lo que la salida está por defecto en un nivel alto y cuando baja a 0 es que se está recibiendo un pulso. El TSOP38238 se alimenta a 5V por lo que su interfaz con el PIC es inmediata. La resistencia y el condensador en la alimentación se recomiendan en el datasheet. La salida del módulo está conectada al pin RB0. 



Adicionalmente usaremos el puerto serie para enviar la información recolectada al PC. Como siempre, si vuestro sistema de desarrollo no cuenta con un conversor de niveles (MAX232 o similar) tendréis que implementarlo por vuestra cuenta o adquirir alguno en eBay por no más de 5 euros:


Software:

Sería sencillo hacer un bucle monitorizando el pin RB0 y usar un timer para medir los tiempos involucrados en los pulsos ON (abajo) y OFF (arriba). El problema, como siempre, es que con ese enfoque el PIC no podría hacer otra cosa. Lo que haremos es usar la interrupción INT0 (asociada a cambios del pin RB0) para detectar los pulsos. De esta forma mantenemos el PIC libre para otras funciones.

El aspecto general de un paquete enviado por el mando puede verse en la siguiente imagen:




Por defecto, la señal esta en nivel alto (inactivo). El paquete consiste en una serie de pulsos ON (bajo) y OFF (alto). Obviamente tras bajar/subir varias veces terminaremos arriba. Normalmente suele haber un pulso ON inicial más largo como indicación de START, pero eso depende del protocolo. La sucesión de ON/OFF codifica los bits del mensaje. Hay muchas posibilidades (codificación Manchester, codificación por ancho de pulsos, etc).  Es este proyecto no estamos interesados en decodificar, sólo en ver la información sobre la duración de los pulsos ON y OFF. Con estos datos tendríamos una idea del protocolo que usa el mando y podremos modificar el programa para obtener los códigos. Considerando estos objetivos, la idea general a aplicar en nuestro software se basa en:

  • Empezamos activando la interrupción INT0 en modo HIGH to LOW para detectar el inicio de un paquete. Al mismo tiempo se pone a cero el timer 0 y se arranca para medir tiempos.
  • Cada vez que detectemos un cambio en RB0 guardaremos valor del timer 0 y volveremos a resetearlo para medir el siguiente pulso.
  • Con lo anterior mediríamos los tiempos entre bajada y bajada. Como interesa discriminar entre tiempo abajo (ON) y arriba (OFF) lo que haremos es que cada vez que entre la interrupción INT0 cambiaremos el sentido de la detección. De esta forma si entramos con una bajada, pondremos la detección a subida y así medimos el tiempo abajo.
  • El timer 0 se resetea a cada cambio. Usaremos la interrupción del TMR0 como indicador de que hace mucho que no hay cambios y que por lo tanto el pulso ha terminado. En ese caso se deja de monitorizar la línea y simplemente volcamos los tiempos (convertidos a milisegundos) de todas las transiciones.
  • Para volcar los tiempos usaremos el puerto serie. Podríamos usar el enfoque de interrupciones explicado antes, pero como no es crítico usaremos en principio las funciones bloqueantes del compilador.


Explicaremos el programa (IR_timing.c) paso a paso. Comenzamos con los #include, #defines y #pragma config habituales, así como la función get_usart_speed:

#include <p18F4520.h>

#pragma config OSC=HS
#pragma config PWRT = OFF, BOREN = OFF
#pragma config WDT = OFF, WDTPS = 128
#pragma config PBADEN = OFF, LVP = OFF

#include "..\int_defs_C18.h"
#include "..\tipos.h"

#include <delays.h>
#include <usart.h>
#include <stdio.h>
#include <timers.h>

#define start_TMR0 T0CONbits.TMR0ON=1;
#define stop_TMR0 T0CONbits.TMR0ON=0;

#define send_CR putc((char)0x0D,stdout)  // send CR

uint8 get_usart_speed(uint32 baud, uint32 Fosc, uint8* spbrg)
// baud rate 1200,2400,4800,9600,19200, ..., 115200
// Fosc = Frequency osc in KHz -> 20MHz = 20000 
// Returns 0 (USART_BRGH_LOW), 1 (USART_BRGH_HIGH), or 2 (cannot get speed requested)
// It places in spbrg the value to be used as argument spbrg in OpenUSART
 {
  uint16 FF;
  uint8 brgh;
  uint8 shift;

  Fosc=Fosc*1000; 
  baud>>=1; Fosc=Fosc/baud+1; Fosc>>=1;  // computes round(Fosc/baud)

  FF=(uint16)Fosc;
 
  if ((FF>16320)||(FF<16)) return 2;   // Baud rate too low or too high
 
  brgh = (FF>4096)? 0:1;  // Decides between BRGH=0 o BRGH=1
  shift= (brgh==1)? 3:5; 
  FF>>=shift; FF--; FF>>=1; // Computes round(FF/64 -1) or round(FF/16-1)

  *spbrg = (uint8)FF;
  return brgh;
 }

A continuación tenemos el código de la interrupción INT0. Está declarada de alta prioridad par asegurarnos de que no perdemos nada. En un principio el programa arranca con un status=WAIT (esperando un paquete). Cuando se detecta el arranque del paquete ponemos en marcha TMR0 y habilitamos su interrupción para disponer de una base de tiempos y pasamos a status = SCANNING. Si estamos en modo SCANNING simplemente guardamos el tiempo transcurrido desde la última vez que estuvimos por aquí e incrementamos una variable que lleva la cuenta del número de transiciones. En ambos casos se pone a 0 el timer y se invierte el sentido de la detección de cambios:


#define WAIT 0
#define SCANNING 1

uint8 stat=WAIT;
uint8 n_changes=0;
uint8 timings[80];
uint8 ir_detected=0;

#pragma interrupt high_ISR // High priority interruption
void high_ISR (void)
{

 if (INT0_flag)   // INT0 int -> detected change in RB0
  {
   uint8 t;

   t=TMR0L; TMR0L=0;  // store time since last transitioon, reset TMR0L

   if (stat==WAIT) // Start scanning
     { start_TMR0; enable_TMR0_int; stat=SCANNING;}
   else
     timings[n_changes++]=t; // save t and increase counter

   INTCON2bits.INTEDG0=~INTCON2bits.INTEDG0; // Detect next change

   INT0_flag=0;
  }  
}

// Code @ 0x0008 -> Jump to ISR for high priority interruption
#pragma code high_vector = 0x0008
  void code_0x0008(void) {_asm goto high_ISR _endasm}
#pragma code

Ahora viene el código de la interrupción del TMR0, declarada como de baja prioridad. Como cada vez que detectamos un cambio se hace TMR0L=0, la interrupción no entra mientras haya actividad en la línea. Una vez que la línea se queda en OFF ya no hay cambios y nadie se ocupa de resetear el contador, por lo que terminará rebosando y se provocará la interrupción.

En la interrupción lo primero que se hace es volver al status de esperar inicio de paquete (status=WAIT, parar TMR0 y esperar un cambio HIGH2LOW). A continuación se verifica que el número de transiciones no es muy bajo (para evitar "falsos positivos" por alguna transición espurea en la línea). Todos los protocolos tendrán un mínimo de 12-16 transiciones. Si hay un número suficiente de transiciones  se procede a desactivar la interrupción INT0 (esto es, dejamos de monitorizar la línea) y volcamos los datos por el puerto serie. Para evitar incluir un código complejo dentro de la interrupción (lo que no es aconsejable) lo que se hace es poner a 1 la bandera ir_detected. En el programa principal detectaremos dicha bandera y volcaremos los resultados.


#pragma interruptlow low_ISR  // Low priority interruption
void low_ISR (void)
{
 if (TMR0_flag) // Way too long since last transition
  {
   stat=WAIT; stop_TMR0; INT0_high2low;  // wait for next packet
   if (n_changes>8)  // At least 8 transitions  -> dumps timings
    {
     disable_INT0_int;  // Do not pay attention to RB0
     ir_detected=1;
    }
   else n_changes=0;

   TMR0_flag=0;
  }
}

// Code @ 0x0018 -> Jump to ISR for Low priority interruption
#pragma code low_vector = 0x0018
  void code_0x0018 (void){_asm goto low_ISR _endasm}
#pragma code


La siguiente función vuelca los resultados guardamos (los timings de los pulsos). Como obviamente la línea parte de OFF y vuelve a OFF tendremos un número impar de pulsos, empezando con un pulso de start (ON) y luego una sucesión de pares (OFF/ON). Usamos printf para volcar los resultados a la salida standard (puerto serie).

Lo único más críptico es la conversión de los tiempos a microsegundos. Los tiempos guardados (uint8's) están en unidades de ticks de reloj. Con la configuración escogida para el TIMER0, cada paso del contador corresponde a 51.2 microsegundos. Una aproximación con aritmética entera y que evita dividir es multiplicar por 205 y dividir por 4 (>>2) lo que corresponde  a 205/4 = 51.25, lo suficientemente cerca para la aplicación en curso. Obviamente esta conversión será dependiente de los parámetros elegidos al configurar TMR0.
  

void dump_timings(void)
  {
   uint8 k;
   uint16 t1,t2;

   if (n_changes==0) return;

   t1=timings[0]*205; t1>>=2;
   printf("PACKET START ON %d (ms)\n",t1); send_CR;
   for (k=1;k<n_changes;k+=2)
     {
      t1=timings[k]*205; t1>>=2;
      t2=timings[k+1]*205; t2>>=2;
      printf("%2d) OFF/ON %4d %4d (ms)\n",(k+1)>>1,t1,t2); send_CR;
     }
   printf("-------------------------\n"); send_CR;

   n_changes=0;
  }

El programa principal abre el puerto serie (115200 bauds) y configura TMR0 (sin arrancarlo).  Se habilita la interrupción INT0 (que siempre es de alta prioridad) y la interrupción del TMR0 (baja prioridad).

La parte más delicada es escoger los parámetros del Timer1. Hemos escogido usar modo 8 bits porque es suficiente para los tiempos involucrados y nos ahorramos manejar contadores de 16 bits. Se ha escogido un divisor de 1:256 lo que corresponde a 256x0.2 = 51 usec. Está será la máxima resolución de los timing hallados (ya que se miden en multiplos de 51 usec). Es adecuada porque típicamente los pulsos más pequeños en los mandos a distancia son del orden de 400-600 usec, por lo que entran del orden de 10 pasos como mínimo.

La consecuencia inmediata de dicha elección es que el rebosamiento por falta de actividad sucederá al cabo de 256 x 51 = 12.5 msec. Esto determina que los pulsos más largos detectables serán de 12 msec. Si un pulso (ON/OFF) dura más de 12 msec el programa considerará que ya no va a haber más actividad y declarará (erróneamente) un fin de paquete.  

Tras habilitar las interrupciones, del grueso del trabajo se encargan ellas. El programa principal simplemente entra en un bucle donde se monitoriza la bandera ir_detected. Si se pone a 1 es señal de que hemos detectado un paquete y llamamos a la función que vuelca la información por el puerto. Tras terminar de volcar los resultados volvemos a habilitar la monitorización de la línea.

void main()
  {
   uint8 sp, brgh, brgh_config;
   uint16 temp;
  

   //Configuracion puerto serie.
   brgh=get_usart_speed(115200,20000,&sp);
   brgh_config = (brgh)? USART_BRGH_HIGH:USART_BRGH_LOW;
   OpenUSART(USART_ASYNCH_MODE & USART_EIGHT_BIT & brgh_config, sp);
   disable_TX_int;  // By default TX int is enable using AND masks

    
   // TIMER0 8bit,1:256, @20MHz -> dt=256*0.2=50.2 usec, overflows in 13 msec
   OpenTimer0(T0_8BIT&T0_SOURCE_INT&T0_PS_1_256);
   stop_TMR0; TMR0L=0;

     
   INT0_high2low; enable_INT0_int; // Enable INT0 int (always a high priority int)
   enable_TMR0_int; set_TMR0_low;  // Enable TMR0 int with low priority
  
   enable_priority_levels;
   enable_high_ints; enable_low_ints;
  
   puts("IR remote timings"); send_CR;

   while(1)
    {
     if (ir_detected==1)
      {
       ir_detected=0;
       dump_timings();
       enable_INT0_int;
      } 
    }
}


Veamos los resultados. Se adjuntan dos volcados del puerto serie al pulsar un par de teclas de un mando SONY:

          


Vemos que empezamos siempre con un START largo (2400 usec) y luego hay una serie de OFF/ON. La parte OFF es siempre constante (512 usec) pero la parte ON oscila entre 615 y 1230 usec. Claramente los 0/1 están codificados en esta duración. De hecho el protocolo usado por SONY establece un bit de START de 2400 usec, unos pulsos OFF de 600 usec y unos pulsos ON de 600 (0) o 1200 (1):



En el primer caso se decodificaría como 110010000100010 y el segundo como 101010000100010. El protocolo como vemos es de 15 bits (otras variantes del protocolo pueden tener más o menos bits, pero respetando la codificación basada en la duración del pulso ON con pulsos OFF constantes.

Otro pequeño detalle es que mientras que los tiempos del pulso ON se ajustan bastante bien a las especificaciones (dada la resolución que tenemos de 50 usec), los tiempos OFF son sistemáticamente más pequeños. La diferencia puede verse también en un osciloscopio, por lo que es posiblemente debida al comportamiento del módulo demodulador y no a nuestro programa

Probemos ahora con un remoto para disparar a distancia una cámara de fotos (Pentax). Este es el resultado:

PACKET START ON 12915 (ms)
 1) OFF/ON 2870 1025 (ms)
 2) OFF/ON  922 1076 (ms)
 3) OFF/ON  922 1025 (ms)
 4) OFF/ON  922 1025 (ms)
 5) OFF/ON  922 1025 (ms)
 6) OFF/ON  922 1025 (ms)
 7) OFF/ON  922 1025 (ms)


Vemos que arranca con un pulso ON muy largo (con 12-13 msec ha debido de estar a punto de saltar la interrupción  por inactividad). Luego sigue una zona OFF de 3 msec y termina con una sucesión de ON/OFF de 1 msec.

Para terminar un volcado del mando de una TV LG. Se aprecia que el inicio del paquete viene marcado por un par de pulsos muy largos, el primero el doble del otro:  ON = 9 msec, OFF = 4.4 msec 

A partir de entonces los pulsos ON son prácticamente constantes con un valor nominal de 600 msec (563-615). Al contrario de antes, los bits claramente vienen ahora codificados en los pulsos OFF con valores de 500-1500 usec.


Para dar una idea de la variedad de protocolos existentes en el fichero  volcado.txt se adjunta la salida obtenida con uno de esos mandos de TV universales en los que se pulsa la tecla MUTE y el mando va probando con un montón de códigos/protocolos hasta encontrar uno que tiene efecto sobre la TV.







2 comentarios:

  1. Buenas tardes, esta muy interesante la aplicación.

    Una duda... en que compilador se realizo el código de programación?

    ResponderEliminar
    Respuestas
    1. C18 como el resto de los ejemplos de este blog

      Eliminar