Cómo acoplar un mando a distancia en Arduino
From JavierValcarce.Es
Acoplar un mando a distancia (de infrarrojos, IR) a nuestros proyectos con microcontroladores (en este caso Arduino) puede ser una opción muy 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 Xbox[1] e imprime el código de cada tecla en hexadecimal por el puerto serie.
Contents |
Aunque no es imprescindible, puedes leer primero Cómo funciona un mando a distancia, por si no lo sabes.
Hardware
Material
- Placa Arduino (ATmega8) y de prototipos
- Chip 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 Vout 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.[1]. Además, es uno de los mandos que tenía en casa muertos de risa, algo había que hacer con el...
Cada vez que pulsamos una tecla del mando, el TFMS5560 procesa la señal recibida [1] y entrega en el terminal Vout una secuencia de pulsos y espacios como la que aparece en la figura[1], 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 Vout 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
Fichero: ir_remote_xbox.pde
#include <avr/io.h> #include <avr/interrupt.h> // 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 LIRC (Linux Infrared Remote Control).
| f0 | IC |
|---|---|
| 30kHz | TFMS5300 |
| 33kHz | TFMS5330 |
| 36kHz | TFMS5360 |
| 38kHz | TFMS5380 |
| 40kHz | TFMS5400 |
| 56kHz | TFMS5560 |
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 f0=455kHz.
Si no sabes cual es la f0 de tu mando usa un chip de f0=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 aquí (con ejemplos en ensamblador para un micro AVR8)
ir_remote_raw
Fichero: 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 iguales[1] 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 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 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.