Javier Valcarce's Homepage

Cómo acoplar un mando a distancia en Arduino

Resumen
Acoplar un mando a distancia (de infrarrojos, IR) a nuestros proyectos con microcontroladores (en este caso Arduino puede ser una opción interesante y de bajo coste para tener comunicación inhalámbrica. El siguiente proyecto muestra cómo se hace. El programa recibe las pulsaciones del mando a distancia de la XboxViene con el kit DVD de la consola que es un accesorio que se compra aparte normalmente e imprime el código de cada tecla en hexadecimal por el puerto serie.

Puedes leer primero Cómo funciona un mando a distancia

Hardware

Material:

  • Placa Arduino (ATmega8) y de prototipos
  • Chip [http://www.alldatasheet.com/datasheet-pdf/pdf/26102/TEMIC/TFMS5360.html TFMS5560] (Telefunken). Hay decenas de chips receptores de IR, no tiene por qué ser este en concreto, puede ser cualquier otro, eso sí, con filtro paso-banda en 56kHz a ser posible
  • Cables de conexión
Alimenta la placa de inserción usando las salidas +5V y GND de Arduino, luego el chip TFMS5660 y finalmente la salida V_{out} de este chip al pin 2 de Arduino (interrupción externa INT0) El montaje, como ves, tiene muy pocos cables, así que si no funciona... ¡asegúrate de que el mando tiene pilas!

Código de línea del telemando de la Xbox

LA SIGUIENTE INFORMACIÓN ES ESPECÍFICA PARA EL MANDO A DISTANCIA DE LA XBOX. SI TIENES UN MANDO DE OTRA MARCA O MODELO, LEE MÁS ABAJO. He elegido este mando porque me parece que es bastante popular y además su código de línea (modulación digital en banda base) es sencillo.No todos los telemandos IR se decodifican con la misma facilidad, los hay que envían códigos de línea bastante puñeteros, usan códigos especiales cuando una tecla permanece pulsada y otras mezquindades.... Además, es uno de los mandos que tenía en casa muertos de risa, algo había que hacer con él... [[Image:Ir_signal_xbox.png|600px|center|]] Cada vez que pulsamos una tecla del mando, el TFMS5560 procesa la señal recibida Lo que hace es bajar la señal a banda base, que esta doblemente mezclada: primero suprime la portadora óptica (la luz IR) mediante un fotodiodo PIN y luego la portadora eléctrica (señal cuadrada de frecuencia 56kHz) mediante un integrador y un filtro paso banda y entrega en el terminal V_{out} una secuencia de pulsos y espacios como la que aparece en la figuraEn realidad es la inversa de la que muestra la figura porque la salida V_{out} es activa con nivel bajo, con 13 flancos de subida y 13 de bajada, alternados, lógicamente. La distancia entre dos flancos consecutivos (que llamaremos ranura), determina si el bit es "0" o "1". * La primera ranura es el pulso inicial AGC (Automatic Gain Control) necesario para estabilizar el control automático de ganancia del TFMS5560 y no representa ningún bit de información. * A continuación llega el código de 24 bits de la tecla pulsada
MANDO A DISTANCIA DE LA XBOX (DVDKit)

Frecuencia de la portadora: 56 kHz
Duración del pulso inicial AGC: 4500 us
Duración de la ranura correspondiente al bit 0: 1500 us
Duración de la ranura correspondiente al bit 1: 2500 us
Número de bits/trama: 24
Código de cada tecla:

Tecla         Código de tecla
-----------------------------
DISPLAY       0x52AAD5
REVERSE       0x51DAE2
PLAY          0x515AEA
FORWARD       0x51CAE3
SKIP-         0x522ADD
STOP          0x51FAE0
PAUSE         0x519AE6
SKIP+         0x520ADF
TITLE         0x51AAE5
INFO          0x53CAC3
MENU          0x508AF7
BACK          0x527AD8
UP            0x559AA6
DOWN          0x558AA7
LEFT          0x556AA9
RIGHT         0x557AA8
SELECT        0x5F4A0B
1             0x531ACE
2             0x532ACD
3             0x533ACC
4             0x534ACB
5             0x535ACA
6             0x536AC9
7             0x537AC8
8             0x538AC7
9             0x539AC6
0             0x530ACF

Software

Parece que la forma más conveniente de demodular este código de línea (puedo estar equivocado) es usar la interrupción externa INT0 y la del temporizador TMR2. No parece factible hacer esto por sondeo ni usar bucles de espera activa porque consumiría buena parte de los recursos del micro (y se supone que el receptor de IR va a ser sólo una pequeña parte del proyecto total). '''La interrupción INT0'''
Salta cada vez que hay un flanco de bajada o de subida en el pin 2 de Arduino. Esta rutina de servicio a la interrupción es básicamente una máquina de estados cuya variable de estado (variable bit) es el número bit que estamos recibiendo (hay un pulso inicial y luego 24 bits). La rutina comprueba el número de bit que estamos recibiendo, mide la duración de la ranura y decide si es "0" ó "1". Cuando llegamos al bit 24, el código de tecla queda almacenado en la variable ir_cmd. '''La interrupción TMR2'''
La rutina anterior necesita poder medir el tiempo. Con una resolución de 16us es más que suficiente. Para esto lo mejor es usar la interrupción del temporizador TMR2 configurado en modo normal y sin divisor de reloj (preescaler = 1) para que dispare cada 16us e incremente la variable usec '''ALTERNATIVA'''
Otra posibilidad sería usar únicamente el temporizador TMR2, muestrear periódicamente la señal V_{out} de salida del chip y demodular. Así nos ahorramos la interrupción externa INT0. Aun así, he elegido la primera opción en lugar de esta porque me parece más clara, más pedagógica ;-) PREESCALER * TIMERCOUNT / F_CLK = 1*256/16000000 = 16us ir_remote_xbox.pde

#include 
#include 


// This is the INT0 Pin of the ATMega8
#define PIN_IRRX 2

// Xbox IR remote control codes
#define       IRCODE_DISPLAY       0x52AAD5
#define       IRCODE_REVERSE       0x51DAE2
#define       IRCODE_PLAY          0x515AEA
#define       IRCODE_FORWARD       0x51CAE3
#define       IRCODE_SKIPSUB       0x522ADD
#define       IRCODE_STOP          0x51FAE0
#define       IRCODE_PAUSE         0x519AE6
#define       IRCODE_SKIPADD       0x520ADF
#define       IRCODE_TITLE         0x51AAE5
#define       IRCODE_INFO          0x53CAC3
#define       IRCODE_MENU          0x508AF7
#define       IRCODE_BACK          0x527AD8
#define       IRCODE_UP            0x559AA6
#define       IRCODE_DOWN          0x558AA7
#define       IRCODE_LEFT          0x556AA9
#define       IRCODE_RIGHT         0x557AA8
#define       IRCODE_SELECT        0x5F4A0B
#define       IRCODE_1             0x531ACE
#define       IRCODE_2             0x532ACD
#define       IRCODE_3             0x533ACC
#define       IRCODE_4             0x534ACB
#define       IRCODE_5             0x535ACA
#define       IRCODE_6             0x536AC9
#define       IRCODE_7             0x537AC8
#define       IRCODE_8             0x538AC7
#define       IRCODE_9             0x539AC6
#define       IRCODE_0             0x530ACF
///////////////////////////////////////////////////////////////////////////////////////////////

volatile unsigned int   usec;
volatile int            ir_bit;
volatile unsigned long  ir_tmp;
volatile unsigned long  ir_cmd;
volatile boolean        ir_cmd_new;

// Decoder state (variable "ir_bit") -1, 0, 1, 2, 3... 24 and (25)

#define WAIT      -1    // Waiting for an IR signal
#define HEAD      0     // Receiving AGC pulse
//...

#define PERR      250   // Maximun absolute error permited in us

#define TIME_HEAD 4500  // AGC pulse lenght (in us)
#define TIME_BIT0 1500  // Bit 0 lenght (in us)
#define TIME_BIT1 2500  // Bit 1 lenght (in us)


// Aruino runs at 16 Mhz,
// Raise interrupt every 1 / ((16000000 / 1) / 256) = 1 / 62500 = 16us
///////////////////////////////////////////////////////////////////////////////////////////////
ISR(TIMER2_OVF_vect)
{
usec += 16;
if (usec == 6400)  // 6400 % 16 == 0
{
// Time over! Reset receiver's state machine
usec   = 0;
ir_bit = WAIT;
}
};


///////////////////////////////////////////////////////////////////////////////////////////////
#define VALID_TIME(v, a, b) ( ((v) > (a)-PERR) && ((v) < (b)+PERR) )

// INT0, arduino pin2
ISR(INT0_vect)
{
switch (ir_bit)
{
case WAIT:
// Waiting for an incoming signal
if (!ir_cmd_new) ir_bit++;
break;

case HEAD:
// Initial AGC pulse
if ( VALID_TIME(usec, TIME_HEAD) )
{
ir_tmp = 0;
ir_bit++;
}
else
{
ir_bit = WAIT;
}
break;

default:
// Data bits 01, 02, 03, ... 24
if ( VALID_TIME(usec, TIME_BIT0) )
{
ir_tmp = ir_tmp << 1;
ir_bit++;
}
else if ( VALID_TIME(usec, TIME_BIT1) )
{
ir_tmp = (ir_tmp << 1) | 0x00000001;
ir_bit++;
}
else
{
ir_bit = WAIT;
}

if (ir_bit == 25)
{
// Bit "25" means END OF TRANSMISION, new IR data is available
ir_cmd     = ir_tmp;
ir_cmd_new = true;
}

break;
}

usec = 0;
}



///////////////////////////////////////////////////////////////////////////////////////////////
void setup()
{

usec = 0;
ir_bit  = WAIT;
ir_cmd_new = false;

pinMode(PIN_IRRX, INPUT);

// Timer Setup, valid only for ATmega8
// Normal mode
TCCR2 &= ~(1 << WGM21);
TCCR2 &= ~(1 << WGM20);

// No Timer Prescaler
TCCR2 |=  (1 << CS20);
TCCR2 &= ~(1 << CS22);
TCCR2 &= ~(1 << CS21);

// Use internal clock
ASSR  &= ~(1 << AS2);

// TMR2 Overflow Interrupt
TIMSK |=  (1 << TOIE2);
TIMSK &= ~(1 << OCIE2);

// INT0 interrupt
GICR  |= (1 << INT0);
MCUCR |= (1 << ISC00);  // positive edge
MCUCR |= (1 << ISC01);  // negative edge

sei();

Serial.begin(9600);

// prints title with ending line break
Serial.println("");
Serial.println("Xbox IR remote control");
Serial.println("Waiting...");
// wait for the long strings to be sent to serial port
delay(100);
}

///////////////////////////////////////////////////////////////////////////////////////////////
void loop()
{
if (ir_cmd_new)
{
Serial.print("code - ");
Serial.println(ir_cmd, HEX);
ir_cmd_new = false;

delay(40);
}
}
///////////////////////////////////////////////////////////////////////////////////////////////

Mandos de otras marcas y modelos

Por desgracia, no hay ningún tipo de normalización y cada mando genera un código de línea (secuencia de pulsos y espacios) diferente. Puedes encontrar información sobre el formato de trama y códigos de distintos modelos en el proyecto [http://www.lirc.org/ LIRC (Linux Infrared Remote Control)].

f0 (kHz) IC
30 kHzTFMS5300
33 kHzTFMS5330
36 kHzTFMS5360
38 kHzTFMS5380
40 kHzTFMS5400
56 kHzTFMS5560

Si tienes un mando "rarito" que no aparece en LIRC tendrás que investigar tú mismo el código de línea que genera. Para ello puedes usar el programilla ir_remote_raw, que te comento a continuación. El hardware necesario para usarlo es el mismo, reemplazando el chip receptor por uno adecuado a la frecuencia de portadora del mando. Hay varios modelos del chip receptor de Telefunken (ver la tabla). Algunos mandos (muy pocos) usan incluso f_0=455kHz. Si no sabes cual es la f_0 de tu mando usa un chip de f_0=38kHz y seguro que aciertas. Aunque no uses el chip exacto lo único que ocurrirá es que disminuirá el alcance del mando. Prueba con un chip de 38kHz primero y si no funciona prueba con uno de 56kHz. Lo mejor es comprar un mando universal y configurarlo para que use el protocolo RC5 de Sony, que es el más popular. La especificación de dicho protocolo la tienes [http://www.atmel.com/dyn/resources/prod_documents/doc1473.pdf aquí] (con ejemplos en ensamblador para un micro AVR8)

ir_remote_raw

ir_remote_raw.pde

En este pequeño programa, la RSI de INT0 guarda en una tabla en memoria la duración de cada pulso y de cada espacio de la secuencia. Cuando la transmisión finaliza (lo decide un temporizador de inactividad al vencer), imprime por el puerto serie la lista de tiempos (''raw codes''). La lista presenta la duración de pulsos y espacios alternadamente (pulso-espacio-pulso-espacio-...) siendo el primer valor la duración del pulso inicial AGC. Puedes imprimir esta lista en un fichero de texto desde HyperTerminal de Windows o Minicom en Linux. Los tiempos medidos no siempre son exactamente igualesPuedes probar a pulsar la tecla varias veces, cargar estos ficheros de datos en vectores en Matlab y promediar. Y eso para cada tecla del mando. Como ya habrás sospechado, el trabajo es de chinos... cada vez que pulsas la tecla ni tampoco el número de pulsos recibidos, procura pulsar la tecla del mando rápidamente (un golpe seco) para que envíe sólo una secuencia, prueba varias veces. Ajusta la constante TIME_OVER si crees que el mando transmite más de una vez la misma secuencia en un corto espacio de tiempo. Puedes ver el cronograma de la señal usando la función [{{SERVER}}/{{DIR avr}}/irsignal.m irsignal.m] para Matlab.

A partir de esta lista de tiempos deberás fijarte bien e ''intentar'' inferir cómo se transmite el "1" y el "0". Se suelen usar 4 tipos de códigos de línea:

  • Anchura de pulso. La distancia entre 1 flancos de subida y el siguiente de bajada (en realidad es al revés porque el TFMS5XX0 es activo a nivel bajo) determina si el bit es "0" o "1"
  • Anchura de espacio. La distancia entre 1 flancos de bajada y el siguiente de subida (en realidad es al revés porque el TFMS5XX0 es activo a nivel bajo) determina si el bit es "0" o "1"
  • Anchura de pulso o espacio. La distancia entre dos flancos consecutivos (de bajada o de subida) determina si el bit es "0" o "1". Este es precisamente el código de línea que usa el mando a distancia de la Xbox
  • De la familia [http://es.wikipedia.org/wiki/Codificaci%C3%B3n_Manchester Manchester]. Un ejemplo de este código es el RC5 de Sony (bastante popular), para decodificarlo sólo es necesario que la rutina de servicio a la interrupción del temporizador muestree la señal en 1/4 y 3/4 del periodo de bit para saber si se trata de un "0" o "1"
Además puede que haya códigos especiales al dejar pulsada una tecla, ten ojo avizor.