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.
Buenas tardes, esta muy interesante la aplicación.
ResponderEliminarUna duda... en que compilador se realizo el código de programación?
C18 como el resto de los ejemplos de este blog
Eliminar