Una forma muy sencilla de usar Arduino en domótica es utilizando un mando a distancia por infrarrojos para controlarlo. Seguro que tienes en algún cajón el de una TV que ya has reciclado o un mando universal que puede dar más de sí. Además, puedes usarlo para controlar varias cosas a la vez; suponiendo, lógicamente, que la luz infrarroja llegue tanto al dispositivo original como al receptor que instales en Arduino.
Es posible recibir datos desde un mando a distancia por infrarrojos utilizando un fotodiodo o un fototransistor, pero será necesario filtrar la señal resultante, amplificarla y lo que es más delicado, demodularla; ya que los mandos a distancia utilizan una onda portadora sobre la que montan la información que se envía. Aunque es viable hacer un montaje sencillo e incluso demodular por software, lo más común es utilizar un dispositivo integrado que contenga los componentes necesarios para encargarse de todo el proceso y que generalmente ahorrará mucho espacio en el montaje (y en su caso en la aplicación) sin un sobre-coste apreciable o hasta ahorrando en el circuito final.
En la imagen de abajo se puede comparar el tamaño entre varios módulos de recepción de señal de mandos de infrarrojos (a la izquierda de la foto) y un fotodiodo y un fototransistor (a la derecha de la foto)
Cuando se opta por añadir al hardware o al software la demodulación, es decir, hacer «a mano» el trabajo de separar la portadora de la onda recibida, usando circuitería o en el programa, hay que conocer la frecuencia de la portadora. Cada protocolo utiliza su propia frecuencia portadora; por ejemplo a Sony le gusta la de 40 KHz, a Philips 36 KHz (aunque también tiene un protocolo a 38 KHz) y en el resto de fabricantes la más habitual es de 38 KHz
El esquema de la imagen de abajo representa la señal realmente emitida, con la portadora, en color claro y en color oscuro la señal que contiene la información que se interpreta una vez retirada la portadora.
Si decides utilizar un módulo receptor de infrarrojos, uno de más populares es el 1838 (AX-1838HS, en mi caso) que incluye además del fotodiodo receptor varias etapas de amplificación y filtrado para devolver una señal digital a un nivel que puede leerse directamente desde el microcontrolador. El 38 del nombre hace referencia a que trabaja con una señal modulada a 38 KHz. Aunque el componente probablemente funcionará sin más que alimentarlo y conectarlo a una entrada digital, el fabricante recomienda un circuito mínimo de aplicación principalmente para evitar rebotes de la señal y filtrar posibles ruidos producidos por la alimentación. En mis pruebas, conectándolo directamente con un cable de algo menos de medio metro y una alimentación sin ruidos funciona correctamente incluso sin los componentes pasivos que indica el fabricante en la hoja de datos del dispositivo.
Seguramente esperas poder utilizar un «mando a distancia universal» y que con estudiar ese tipo de señal ya esté todo resuelto. Malas noticias: no existen los mandos a distancia universales, por más que el nombre sí exista. Cuando compras un mando universal puede ser uno que utilice un formato concreto, nada universal y bastante habitual, uno que permite elegir el protocolo manualmente de entre una base de datos interna o un poco más avanzado, escaneando el dispositivo que se controla a partir de unas condiciones predeterminadas y el cuarto, el más avanzado, que es capaz de aprender cómo funciona cualquier mando, almacenar las órdenes en su memoria y reproducirlas después para controlar el dispositivo.
En la imagen de abajo puedes ver un ejemplo de los dos últimos tipos: el más pequeño es capaz de controlar la mayoría de los equipos de aire acondicionado detectando (probando) qué tipo de señal los hace funcionar; el más grande tiene un receptor, además del emisor, con el que puede «aprender» la señal de otro mando y reproducirla después. El último método es el más completo pero aún así puede que no funcione siempre ya que necesita el mando original para copiar las órdenes o detectar el protocolo así que suele incluir también la opción de prueba con los protocolos estándar (o, al menos, conocidos)
Buscando entre los mandos que tengo sin usar he encontrado que la mayoría trabajan con el protocolo NEC de mando a distancia por infrarrojos. Como es además el que usan los mandos que vienen incluidos en muchos módulos de recepción de infrarrojos para Arduino, usaré a lo largo del artículo ese protocolo, aunque lo que describo es fácilmente extrapolable a los demás con la documentación correspondiente, que puedes encontrar en la web de San Bergmans sobre los mandos a distancia por infrarrojos que mencionaba más arriba.
Así que el siguiente paso, una vez resuelta la parte electrónica de la recepción de la señal infrarroja, es determinar cómo es a nivel lógico esta señal, es decir, determinar el protocolo que utiliza para enviar las órdenes al dispositivo controlado. Seguramente, la forma ortodoxa consiste en estudiar el manual de instrucciones del mando a distancia o, al menos, el del protocolo con el que se comunica. Si no está claro qué protocolo utiliza pueden aplicarse principalmente dos métodos para determinarlo: el primer método es utilizar hardware y/o software a tal efecto (un detector de protocolo infrarrojo) y el segundo método consiste en estudiar la señal con un analizador lógico genérico y compararla con los estándares de transmisión de señal infrarroja para mandos a distancia, que puedes encontrar, por ejemplo en el excelente trabajo de la web de San Bergmans sobre los mandos a distancia por infrarrojos.
Por supuesto, para determinar que mis mandos utilizan el protocolo NEC, he seguido el segundo método, más largo pero mucho más instructivo. Como puede verse en la imagen de arriba, la conexión al hardware del analizador lógico es muy sencilla; se alimenta el receptor y se conecta GND al analizador y la salida del módulo AX-1838HS a uno de los canales del analizador; el cero, en mi caso.
Para realizar el análisis he utilizado PulseView, una excelente aplicación libre y multiplataforma, licenciada con la tercera versión de la GPL, que trabaja tanto con osciloscopios como con analizadores lógicos.
Para analizar la señal basta con elegir:
- El dispositivo con el que se realiza el análisis; en mi caso un clon del USBee AX Pro.
- Los canales que se analizan; el cero, en mi caso.
- Las muestras que se van a tomar. He elegido 2 M para que sobre, ya que a priori no sé qué me voy a encontrar.
- La frecuencia; 2 KHz, por la misma razón que en el punto anterior
Cuando todo está listo se pulsa sobre «run» en la aplicación y sobre un botón cualquiera en el mando a distancia para que envíe una señal infrarroja al receptor. En la ventana de PulseView aparecerá algo como lo que puede verse en la imagen de abajo.
Dejando pulsado el mismo botón se repite el patrón del grupo de la derecha así que lo primero que se aprecia es que existe un código de repetición que se usa en lugar de enviar una y otra vez la misma orden.
Ampliando la representación de la señal y midiéndola con los cursores de la aplicación se puede apreciar que siempre tiene los mismos valores y que son diferentes de los enviados al representar una orden.
En la captura de pantalla de abajo he ampliado el primer patrón que es presumiblemente (y se puede confirmar consultando la documentación) el código de la orden.
Se pulse sobre el botón que se pulse, al principio del código de cada orden aparece la misma señal que además es muy diferente del resto del código que representa la orden. Se trata de una cabecera que sirve para indicar el comienzo y sincronizar la información ignorando otros pobres pulsos que se hubieran detectado antes.
Con esta información, que se puede obtener analizando la señal, ya se puede programar un control por infrarrojos que sería totalmente operativo pero contando con la documentación del protocolo NEC se puede sacar ventaja de las siguientes cuestiones:
-
Cada bit de la codificación está formado por un nivel bajo y otro alto, siendo más largo el segundo cuando se transmite un valor uno que cuando se transmite un valor cero. En mi caso, las gráficas del analizador lógico parecen indicar lo contrario, esto es debido a que, en el módulo de infrarrojos que he utilizado, la amplificación de la señal la invierte. Aunque invertir la señal es habitual en estos dispositivos, es conveniente verificarlo antes de empezar a programar.
-
Antes de enviar la orden se envía la dirección del dispositivo, lo que permite controlar varios del mismo tipo, por ejemplo, varios aparatos de TV, sin más que usar una dirección diferente para cada uno de ellos y el mando que los controla.
-
Tanto la dirección como la orden se envían dos veces, la primera sin modificar y la segunda con el valor negado (una operación lógica NOT a nivel de bits) para verificar que se leen correctamente.
-
Se utilizan ocho bits para expresar la dirección y otros ocho para la orden, como además ambos se envían también negados, se usan en total 32 bits para codificar cada operación conforme a este protocolo de comunicaciones por infrarrojos.
En primer lugar, vamos a ver si un Arduino Uno es capaz de recibir los datos a la velocidad que llegan y que desviación tiene la lectura con respecto a los valores «reales» de la documentación. Esta información será importante para usar esos tiempos a modo de calibración en el programa definitivo.
Para poder utilizar la primera interrupción (la número cero) de Arduino, se conecta el receptor de infrarrojos, además de la alimentación, al pin correspondiente a esta interrupción, PIN_IR: el número 2 en el caso de un Arduino Uno o al 3 de un Arduino Leonardo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
// La interrupción 0 en una placa Leonardo corresponde al pin 3 y al 2 en las otras #define NUMERO_INTERRUPCION 0 #if defined(__AVR_ATmega32U4__) #define PIN_IR 3 #define ESPERAR_LEONARDO while(!Serial){;} #else #define PIN_IR 2 #define ESPERAR_LEONARDO // Si no es una placa Leonardo no hay que esperar al puerto serie #endif volatile boolean nuevo_dato_ir=false; volatile unsigned long cronometro; volatile unsigned long tiempo_transcurrido; void setup() { Serial.begin(115200); ESPERAR_LEONARDO pinMode(PIN_IR,INPUT); cronometro=micros(); attachInterrupt(NUMERO_INTERRUPCION,recibir_ir,CHANGE); } void loop() { if(nuevo_dato_ir) // Si se ha recibido un nuevo dato… { Serial.println(tiempo_transcurrido,DEC); // …informar del tiempo transcurrido… nuevo_dato_ir=false; // …y tomar nota de que ya se ha informado. } } void recibir_ir() { detachInterrupt(NUMERO_INTERRUPCION); // Por seguridad, desactivar la interrupción (en este caso funcionará también si no lo haces) tiempo_transcurrido=micros()-cronometro; // Calcular el tiempo transcurrido cronometro=micros(); // Volver a iniciar el cronómetro nuevo_dato_ir=true; // Tomar nota de que no se ha avisado de este nuevo dato attachInterrupt(NUMERO_INTERRUPCION,recibir_ir,CHANGE); // Si se ha desactivado la interrupción volver a activarla } |
Una vez monitorizados los tiempos de los pulsos de la señal se puede probar a interpretar la información para verificar que corresponde con lo observado utilizando el analizador lógico y/o la información de la documentación del protocolo de comunicaciones del mando a distancia por infrarrojos.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
// La interrupción 0 en una placa Leonardo corresponde al pin 3 y al 2 en las otras #define NUMERO_INTERRUPCION 0 #if defined(__AVR_ATmega32U4__) #define PIN_IR 3 #define ESPERAR_LEONARDO while(!Serial){;} #else #define PIN_IR 2 #define ESPERAR_LEONARDO // Si no es una placa Leonardo no hay que esperar al puerto serie #endif #define TIEMPO_CORTO 500 // 560=560*1 (560) #define TIEMPO_LARGO 1500 // 1680=560*3 (1690) #define TIEMPO_REPETICION 2000 // 2240=560*4 (2250) #define TIEMPO_ORDEN 4000 // 4480=560*8 (4500) #define TIEMPO_CABECERA 8000 // 8960=560*16 (9000) #define TIEMPO_INACTIVO 9100 // >9000 volatile boolean nuevo_dato_ir=false; volatile unsigned long cronometro; volatile unsigned long tiempo_transcurrido; void setup() { Serial.begin(115200); ESPERAR_LEONARDO pinMode(PIN_IR,INPUT); cronometro=micros(); attachInterrupt(NUMERO_INTERRUPCION,recibir_ir,CHANGE); } void loop() { if(nuevo_dato_ir) { //Serial.println(tiempo_transcurrido,DEC); if(tiempo_transcurrido>TIEMPO_INACTIVO) { Serial.println("~"); // Para representar inactivo (Añadiendo una nueva línea delante se puede cambiar print por println en los códigos de operación para hacerlo más legible) } else if(tiempo_transcurrido>TIEMPO_CABECERA) { Serial.println("C"); // Cabecera } else if(tiempo_transcurrido>TIEMPO_ORDEN) { Serial.println("O"); // Orden } else if(tiempo_transcurrido>TIEMPO_REPETICION) { Serial.println("R"); // Repetición } else if(tiempo_transcurrido>TIEMPO_LARGO) { Serial.println("_"); // Para representar un pulso largo (usando print en lugar de println lo hace más legible pero necesita un salto de línea antes de un nuevo código, por ejemplo al detectar inactividad) } else if(tiempo_transcurrido>TIEMPO_CORTO) { Serial.println("."); // Para representar un pulso corto (usando print en lugar de println lo hace más legible pero necesita un salto de línea antes de un nuevo código, por ejemplo al detectar inactividad) } else // Tiempo demasiado corto { Serial.println("X"); // Para representar error } nuevo_dato_ir=false; } } void recibir_ir() { detachInterrupt(NUMERO_INTERRUPCION); // Por seguridad, desactivar la interrupción (en este caso funcionará también si no lo haces) tiempo_transcurrido=micros()-cronometro; // Calcular el tiempo transcurrido cronometro=micros(); // Volver a iniciar el cronómetro nuevo_dato_ir=true; // Tomar nota de que no se ha avisado de este nuevo dato attachInterrupt(NUMERO_INTERRUPCION,recibir_ir,CHANGE); // Si se ha desactivado la interrupción volver a activarla } |
El programa anterior mostrará un código (~) para indicar que se ha recibido el final de un periodo de inactividad del mando a distancia, otro (C) para indicar una cabecera, un tercero (O) para una orden y un cuarto (R) cuando se recibe una repetición. Si se reciben datos se codifican los bit representándolos con punto o guión según sean cero o uno. También he incluido un código (X) para indicar que la información recibida no respeta la duración de pulsos esperada.
En las constantes que representan los tiempos puede verse lo que he medido con el primer programa. Es posible que sea necesario que calcules tus propios valores dependiendo del microcontrolador que tenga la placa concreta que utilices e incluso haya que corregirlos si hay mucho código que modifique el tiempo de ejecución.
El siguiente programa calcula el código de la orden recibido. Es un número de 32 bits en el que están incluidos tanto la dirección como el código de la orden en sentido estricto así como sus versiones negadas de verificación. Esta aplicación ya es funcional, se pueden detectar las teclas pulsadas como diferentes y establecer una opción diferente para cada una de ellas.
Para calcular el código recibido se utiliza un contador de bits que los va rotando desde el último al primero para reconstruir su orden lógico. De esta forma, el primer bit recibido quedará en la posición más significativa y en la menos significativa el último. Para evitar números negativos, el contador se decrementa antes de empezar a operar.
Como se sabe por la documentación que se usa como marca en cada bit un pulso corto, sólo se contabilizan bits cuando el nivel del pin de entrada es bajo; debería haber sido al contrario pero hay que recordar que, como se apreciaba con el analizador lógico, la señal está invertida por la amplificación.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
// La interrupción 0 en una placa Leonardo corresponde al pin 3 y al 2 en las otras #define NUMERO_INTERRUPCION 0 #if defined(__AVR_ATmega32U4__) #define PIN_IR 3 #define ESPERAR_LEONARDO while(!Serial){;} #else #define PIN_IR 2 #define ESPERAR_LEONARDO // Si no es una placa Leonardo no hay que esperar al puerto serie #endif #define TIEMPO_CORTO 500 // 560=560*1 (560) #define TIEMPO_LARGO 1500 // 1680=560*3 (1690) #define TIEMPO_REPETICION 2000 // 2240=560*4 (2250) #define TIEMPO_ORDEN 4000 // 4480=560*8 (4500) #define TIEMPO_CABECERA 8000 // 8960=560*16 (9000) #define TIEMPO_INACTIVO 9100 // >9000 #define ESPERA_INACTIVO 0 #define ESPERA_CABECERA 1 #define ESPERA_DESCRIPTOR 2 #define ESPERA_OPERACION 3 #define ANCHO_CODIGO 32 // 8 bits de dirección, 8 bits de dirección negada, 8 bits de orden y 8 bits de orden negada byte estado=ESPERA_INACTIVO; volatile boolean nuevo_dato_ir=false; volatile unsigned long cronometro; volatile unsigned long tiempo_transcurrido; volatile unsigned long codigo_operacion; volatile byte contador_bit; void setup() { Serial.begin(115200); ESPERAR_LEONARDO pinMode(PIN_IR,INPUT); cronometro=micros(); attachInterrupt(NUMERO_INTERRUPCION,recibir_ir,CHANGE); } void loop() { if(nuevo_dato_ir) { if(tiempo_transcurrido>TIEMPO_INACTIVO) { estado=ESPERA_CABECERA; } else if(tiempo_transcurrido>TIEMPO_CABECERA) { if(estado==ESPERA_CABECERA) { estado=ESPERA_DESCRIPTOR; } else // Error en el código; volver a empezar { estado=ESPERA_INACTIVO; } } else if(tiempo_transcurrido>TIEMPO_ORDEN) { if(estado==ESPERA_DESCRIPTOR) { codigo_operacion=0; contador_bit=ANCHO_CODIGO; estado=ESPERA_OPERACION; } else // Error en el código; volver a empezar { estado=ESPERA_INACTIVO; } } else if(tiempo_transcurrido>TIEMPO_REPETICION) { if(estado==ESPERA_DESCRIPTOR) { estado=ESPERA_CABECERA; Serial.println("R"); } else // Error en el código; volver a empezar { estado=ESPERA_INACTIVO; } } else if(tiempo_transcurrido>TIEMPO_LARGO) { if(estado!=ESPERA_OPERACION) // Error en el código; volver a empezar { estado=ESPERA_INACTIVO; } } else if(tiempo_transcurrido>TIEMPO_CORTO) { if(estado!=ESPERA_OPERACION) // Error en el código; volver a empezar { estado=ESPERA_INACTIVO; } } else // Tiempo demasiado corto { estado=ESPERA_INACTIVO; } nuevo_dato_ir=false; } } void recibir_ir() { detachInterrupt(NUMERO_INTERRUPCION); tiempo_transcurrido=micros()-cronometro; cronometro=micros(); if(estado==ESPERA_OPERACION&&!digitalRead(PIN_IR)) // Si se está esperando el código de una orden y el nivel es bajo (el pulso está invertido por la amplificación) { if(contador_bit>0) // El contador debe recorrer desde 31 (establecido al iniciar el estado ESPERA_OPERACION) a 0… { contador_bit--; // Como el contador empieza en 32, antes de operar hay que decrementarlo if(tiempo_transcurrido>TIEMPO_LARGO) // Si el bit es 1 { codigo_operacion|=(unsigned long)1<<contador_bit; // …rotarlo a la posición correspondiente } } else // Cuando se hayan leído todos los bits… { Serial.println(codigo_operacion,DEC); // …mostrar el código resultante… estado=ESPERA_INACTIVO; // …y volver al modo de espera. } } nuevo_dato_ir=true; attachInterrupt(NUMERO_INTERRUPCION,recibir_ir,CHANGE); } |
Como decía más arriba, el programa anterior es perfectamente funcional pero no está muy optimizado. Por una parte no aprovecha la posibilidad de discriminar direcciones de órdenes y por otra la verificación del código con su versión negada. Además requeriría comparar números muy grandes lo que requeriría más tiempo y sobre todo más memoria para almacenar los códigos.
En la versión de abajo se almacenan por separado las cuatro partes del código para poder compararlas y devolver el código sólo si es coherente, discriminando además dirección de operación. Para hacerlo se usa un contador de grupos de bits (palabras) además del contador de bits que es el que determina el incremento del primero cuando termina su decremento (empieza en el MSB y termina en el LSB) En esta versión he utilizado un límite negativo para el contador de forma que se ilustran las dos opciones cada una en el contexto de programa más ventajoso.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
// La interrupción 0 en una placa Leonardo corresponde al pin 3 y al 2 en las otras #define NUMERO_INTERRUPCION 0 #if defined(__AVR_ATmega32U4__) #define PIN_IR 3 #define ESPERAR_LEONARDO while(!Serial){;} #else #define PIN_IR 2 #define ESPERAR_LEONARDO // Si no es una placa Leonardo no hay que esperar al puerto serie #endif #define TIEMPO_CORTO 500 // 560=560*1 (560) #define TIEMPO_LARGO 1500 // 1680=560*3 (1690) #define TIEMPO_REPETICION 2000 // 2240=560*4 (2250) #define TIEMPO_ORDEN 4000 // 4480=560*8 (4500) #define TIEMPO_CABECERA 8000 // 8960=560*16 (9000) #define TIEMPO_INACTIVO 9100 // >9000 #define ESPERA_INACTIVO 0 #define ESPERA_CABECERA 1 #define ESPERA_DESCRIPTOR 2 #define ESPERA_OPERACION 3 #define PALABRAS_CODIGO 4 #define BITS_PALABRA_CODIGO 8 byte estado=ESPERA_INACTIVO; volatile boolean nuevo_dato_ir=false; volatile unsigned long cronometro; volatile unsigned long tiempo_transcurrido; volatile byte codigo_operacion[PALABRAS_CODIGO]; volatile char contador_bits; volatile char contador_palabras; void setup() { Serial.begin(115200); ESPERAR_LEONARDO pinMode(PIN_IR,INPUT); cronometro=micros(); attachInterrupt(NUMERO_INTERRUPCION,recibir_ir,CHANGE); } void loop() { if(nuevo_dato_ir) { if(tiempo_transcurrido>TIEMPO_INACTIVO) { estado=ESPERA_CABECERA; } else if(tiempo_transcurrido>TIEMPO_CABECERA) { if(estado==ESPERA_CABECERA) { estado=ESPERA_DESCRIPTOR; } else // Error en el código; volver a empezar { estado=ESPERA_INACTIVO; } } else if(tiempo_transcurrido>TIEMPO_ORDEN) { if(estado==ESPERA_DESCRIPTOR) { for(byte contador=0;contador<PALABRAS_CODIGO;contador++) { codigo_operacion[contador]=0; } contador_palabras=0; contador_bits=BITS_PALABRA_CODIGO-1; estado=ESPERA_OPERACION; } else // Error en el código; volver a empezar { estado=ESPERA_INACTIVO; } } else if(tiempo_transcurrido>TIEMPO_REPETICION) { if(estado==ESPERA_DESCRIPTOR) { estado=ESPERA_CABECERA; Serial.println("R"); } else // Error en el código; volver a empezar { estado=ESPERA_INACTIVO; } } else if(tiempo_transcurrido>TIEMPO_LARGO) { if(estado!=ESPERA_OPERACION) // Error en el código; volver a empezar { estado=ESPERA_INACTIVO; } } else if(tiempo_transcurrido>TIEMPO_CORTO) { if(estado!=ESPERA_OPERACION) // Error en el código; volver a empezar { estado=ESPERA_INACTIVO; } } else // Tiempo demasiado corto { estado=ESPERA_INACTIVO; } nuevo_dato_ir=false; } } void recibir_ir() { detachInterrupt(NUMERO_INTERRUPCION); tiempo_transcurrido=micros()-cronometro; cronometro=micros(); nuevo_dato_ir=true; if(estado==ESPERA_OPERACION&&!digitalRead(PIN_IR)) { if(contador_palabras<PALABRAS_CODIGO) // ¿Se han leído ya todas las partes (palabras) del código? { if(tiempo_transcurrido>TIEMPO_LARGO) // Si se ha recibido un bit con valor uno… { codigo_operacion[contador_palabras]|=(unsigned long)1<<contador_bits; // …moverlo a su posición } contador_bits--; if(contador_bits==-1) // Si se han leído todos los bits de la palabra… { contador_palabras++; // …pasar a la siguiente palabra… contador_bits=BITS_PALABRA_CODIGO-1; // …y empezar a contar bits de la siguiente palabra } } else // Si ya se han leído todas las partes (palabras) del código… { if(codigo_operacion[0]==(byte)~codigo_operacion[1]&&codigo_operacion[2]==(byte)~codigo_operacion[3]) // …y la verificación es correcta { Serial.println(String(codigo_operacion[0],HEX)+":"+String(codigo_operacion[2],HEX)); // …mostrar el código de la dirección y de la orden… } else { Serial.println("Error"); // …o informar de un error en caso contrario } contador_palabras=0; // Preparar la primera palabra para contar el próximo código estado=ESPERA_INACTIVO; // Pasar al modo de espera } } attachInterrupt(NUMERO_INTERRUPCION,recibir_ir,CHANGE); } |
El código ya está lo bastante refinado como para usarlo en producción en una aplicación pero sobre todo para generalizarlo, por ejemplo haciendo una librería, hay un inconveniente: se han usado interrupciones como forma de asegurarse que va a responder cuando llegue la señal y pero hay pocas y puede que no siempre estén disponibles en función de la aplicación. Ahora que no se usan interrupciones, puede cambiarse el valor de PIN_IR a otro diferente. El siguiente código es simplemente una adaptación que permite verificar si va a funcionar antes de programarlo como librería para usarlo en otros proyectos. En lugar de verificar la llegada de señal con la variable ahora se comprueba en el bucle principal si el nivel del pin conectado al módulo de infrarrojos ha cambiado de valor.
Para integrar el código del bucle principal también he cambiado la comprobación en cascada con
para sustituir la verificación de la duración de los pulsos de datos por la recepción y dentro de ella el orden de comprobación: primero verificar si ya se han recibido todas las palabras y sólo después si está en bajo (cero) el nivel del pin al que se conecta el receptor de infrarrojos para el mando a distancia.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
#if defined(__AVR_ATmega32U4__) #define ESPERAR_LEONARDO while(!Serial){;} #else #define ESPERAR_LEONARDO // Si no es una placa Leonardo no hay que esperar al puerto serie #endif #define PIN_IR 8 #define TIEMPO_CORTO 500 // 560=560*1 (560) #define TIEMPO_LARGO 1500 // 1680=560*3 (1690) #define TIEMPO_REPETICION 2000 // 2240=560*4 (2250) #define TIEMPO_ORDEN 4000 // 4480=560*8 (4500) #define TIEMPO_CABECERA 8000 // 8960=560*16 (9000) #define TIEMPO_INACTIVO 9100 // >9000 #define ESPERA_INACTIVO 0 #define ESPERA_CABECERA 1 #define ESPERA_DESCRIPTOR 2 #define ESPERA_OPERACION 3 #define PALABRAS_CODIGO 4 #define BITS_PALABRA_CODIGO 8 byte estado=ESPERA_INACTIVO; boolean valor_anterior_pin_ir; boolean valor_actual_pin_ir; boolean nuevo_dato_ir=false; unsigned long cronometro; unsigned long tiempo_transcurrido; byte codigo_operacion[PALABRAS_CODIGO]; int contador_bits; byte contador_palabras; void setup() { Serial.begin(115200); ESPERAR_LEONARDO pinMode(PIN_IR,INPUT); cronometro=micros(); valor_anterior_pin_ir=digitalRead(PIN_IR); } void loop() { valor_actual_pin_ir=digitalRead(PIN_IR); if(valor_actual_pin_ir!=valor_anterior_pin_ir) { valor_anterior_pin_ir=valor_actual_pin_ir; tiempo_transcurrido=micros()-cronometro; cronometro=micros(); if(tiempo_transcurrido>TIEMPO_INACTIVO) { estado=ESPERA_CABECERA; } else if(tiempo_transcurrido>TIEMPO_CABECERA) { if(estado==ESPERA_CABECERA) { estado=ESPERA_DESCRIPTOR; } else // Error en el código; volver a empezar { estado=ESPERA_INACTIVO; } } else if(tiempo_transcurrido>TIEMPO_ORDEN) { if(estado==ESPERA_DESCRIPTOR) { for(byte contador=0;contador<PALABRAS_CODIGO;contador++) { codigo_operacion[contador]=0; } contador_palabras=0; contador_bits=BITS_PALABRA_CODIGO-1; estado=ESPERA_OPERACION; } else // Error en el código; volver a empezar { estado=ESPERA_INACTIVO; } } else if(tiempo_transcurrido>TIEMPO_REPETICION) { if(estado==ESPERA_DESCRIPTOR) { estado=ESPERA_CABECERA; Serial.println("R"); } else // Error en el código; volver a empezar { estado=ESPERA_INACTIVO; } } else if(tiempo_transcurrido>TIEMPO_CORTO) { if(estado==ESPERA_OPERACION) { if(contador_palabras<PALABRAS_CODIGO) { if(!valor_actual_pin_ir) { if(tiempo_transcurrido>TIEMPO_LARGO) { codigo_operacion[contador_palabras]|=(unsigned long)1<<contador_bits; } contador_bits--; if(contador_bits==-1) { contador_bits=BITS_PALABRA_CODIGO-1; contador_palabras++; } } } else { if(codigo_operacion[0]==(byte)~codigo_operacion[1]&&codigo_operacion[2]==(byte)~codigo_operacion[3]) { Serial.println(String(codigo_operacion[0],HEX)+":"+String(codigo_operacion[2],HEX)); } else { Serial.println("Error"); } contador_palabras=0; estado=ESPERA_INACTIVO; } } else // Error en el código; volver a empezar { estado=ESPERA_INACTIVO; } } else // Tiempo demasiado corto { estado=ESPERA_INACTIVO; } } } |
Y para terminar, queda convertir en una librería para Arduino el código anterior. La primera caja de código de abajo es un ejemplo de cómo se usaría la librería para lectura de códigos de un mando a distancia por infrarrojos, las dos siguientes cajas corresponden al código de la librería.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#include "IRNEC1838.h" #if defined(__AVR_ATmega32U4__) #define ESPERAR_LEONARDO while(!Serial){;} #else #define ESPERAR_LEONARDO // Si no es una placa Leonardo no hay que esperar al puerto serie #endif #define PIN_MANDO_IR 8 IRNEC1838 mando(PIN_MANDO_IR); void setup() { Serial.begin(115200); ESPERAR_LEONARDO } void loop() { if(mando.leer_ir()&&mando.sin_error()) { if(mando.repetir()) { Serial.print("R "); } Serial.println(String(mando.direccion(),HEX)+":"+String(mando.orden(),HEX)); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
//IRNEC1838.h #if defined(ARDUINO) && ARDUINO>=100 #include "Arduino.h" #else #include "WProgram.h" #endif #define TIEMPO_CORTO 500 // 560=560*1 (560) #define TIEMPO_LARGO 1500 // 1680=560*3 (1690) #define TIEMPO_REPETICION 2000 // 2240=560*4 (2250) #define TIEMPO_ORDEN 4000 // 4480=560*8 (4500) #define TIEMPO_CABECERA 8000 // 8960=560*16 (9000) #define TIEMPO_INACTIVO 9100 // >9000 #define ESPERA_INACTIVO 0 #define ESPERA_CABECERA 1 #define ESPERA_DESCRIPTOR 2 #define ESPERA_OPERACION 3 #define PALABRAS_CODIGO 4 #define BITS_PALABRA_CODIGO 8 #define CODIGO_ERROR 255 class IRNEC1838 { private: byte pin_ir; byte estado=ESPERA_INACTIVO; boolean valor_anterior_pin_ir; boolean valor_actual_pin_ir; boolean nuevo_dato_ir; unsigned long cronometro; unsigned long tiempo_transcurrido; byte codigo_operacion[PALABRAS_CODIGO]; boolean repetir_codigo; char contador_bits; char contador_palabras; protected: public: IRNEC1838(byte pin_mando); ~IRNEC1838(); boolean leer_ir(); byte direccion(); byte orden(); boolean repetir(); boolean sin_error(); }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
//IRNEC1838.cpp #include "IRNEC1838.h" IRNEC1838::IRNEC1838(byte pin_mando) { pin_ir=pin_mando; nuevo_dato_ir=false; cronometro=micros(); valor_anterior_pin_ir=digitalRead(pin_ir); repetir_codigo=false; } IRNEC1838::~IRNEC1838() { } byte IRNEC1838::direccion() { return codigo_operacion[0]; } byte IRNEC1838::orden() { return codigo_operacion[2]; } boolean IRNEC1838::repetir() { return repetir_codigo; } boolean IRNEC1838::sin_error() { return (codigo_operacion[0]!=CODIGO_ERROR&&codigo_operacion[2]!=CODIGO_ERROR); } boolean IRNEC1838::leer_ir() { valor_actual_pin_ir=digitalRead(pin_ir); if(valor_actual_pin_ir!=valor_anterior_pin_ir) { valor_anterior_pin_ir=valor_actual_pin_ir; tiempo_transcurrido=micros()-cronometro; cronometro=micros(); repetir_codigo=false; if(tiempo_transcurrido>TIEMPO_INACTIVO) { estado=ESPERA_CABECERA; } else if(tiempo_transcurrido>TIEMPO_CABECERA) { if(estado==ESPERA_CABECERA) { estado=ESPERA_DESCRIPTOR; } else // Error en el código; volver a empezar { estado=ESPERA_INACTIVO; } } else if(tiempo_transcurrido>TIEMPO_ORDEN) { if(estado==ESPERA_DESCRIPTOR) { for(byte contador=0;contador<PALABRAS_CODIGO;contador++) { codigo_operacion[contador]=0; } contador_palabras=0; contador_bits=BITS_PALABRA_CODIGO-1; estado=ESPERA_OPERACION; } else // Error en el código; volver a empezar { estado=ESPERA_INACTIVO; } } else if(tiempo_transcurrido>TIEMPO_REPETICION) { if(estado==ESPERA_DESCRIPTOR) { estado=ESPERA_CABECERA; repetir_codigo=true; return true; // Avisa de que se ha leído un código aunque sea repetición. } else // Error en el código; volver a empezar { estado=ESPERA_INACTIVO; } } else if(tiempo_transcurrido>TIEMPO_CORTO) { if(estado==ESPERA_OPERACION) { if(contador_palabras<PALABRAS_CODIGO) { if(!valor_actual_pin_ir) { if(tiempo_transcurrido>TIEMPO_LARGO) { codigo_operacion[contador_palabras]|=(unsigned long)1<<contador_bits; } contador_bits--; if(contador_bits==-1) { contador_bits=BITS_PALABRA_CODIGO-1; contador_palabras++; } } } else { contador_palabras=0; estado=ESPERA_INACTIVO; if(codigo_operacion[0]==(byte)~codigo_operacion[1]&&codigo_operacion[2]==(byte)~codigo_operacion[3]) { return true; // Avisa de que se ha leído un código sólo si es correcto (o de repetición) } else { for(byte contador=0;contador<PALABRAS_CODIGO;contador++) { codigo_operacion[contador]=CODIGO_ERROR; } return false; // Avisa de que se ha leído un código sólo si es correcto (o de repetición) } } } else // Error en el código; volver a empezar { estado=ESPERA_INACTIVO; } } else // Tiempo demasiado corto { estado=ESPERA_INACTIVO; } } return false; // Avisa de que no se ha leído un código } |
Para usar esta librería, puedes copiar el código en tu IDE de Arduino o, si te resulta más cómo, puedes descargar la librería para controlar Arduino con un mando a distancia por infrarrojos desde el enlace y copiarlo en la carpeta libraries correspondiente. Recuerda que funciona con el protocolo NEC y con un módulo receptor 1838 que invierte la señal, por lo que puede que tengas que cambiar el código dependiendo del hardware en concreto que uses.
Ricardo Galindo
Saludos Victor , tu publicación me ha ayudado mucho a comprender el funcionamiento de los controles IR , como funciona el protocolo NEC , que es el mas comun en la mayoria de controles y sobre todo el como hacer tu propia libreria , aun no entiendo bien todo el codigo , mi idea es hacer una pequeña libreria para un attiny ya sea 85 o 45 , en vez del atmega 328 , ya que las que hay disponibles no las puedo hacer funcionar en ninguno de esos dos micros , y no hay mucha informacion disponible , con esto que publicas ya me puedo dar mas o menos una idea de por donde ir empezando y a pesar de que tengo muy poco en esto de arduino , espero poder lograrlo.
Víctor Ventura
Hola, Ricardo.
Estoy encantado de ser de utilidad y te deseo mucha suerte en tu proyecto ¡seguro que lo consigues!
Muchas gracias por visitar el blog.
DOMINGO ORTIZ
Saludos, quiero leer un codigo de un mando a distancia marca samsung pero tengo un arduino uno, la pregunta es seria el mismo codigo?
Víctor Ventura
Hola, Domingo.
Puedes ver cómo es el código de (casi) cada marca de mandos en la web de San Bergmans sobre los mandos a distancia por infrarrojos. En el menú de navegación (arriba a la derecha) puedes encontrar una lista de los protocolos sobre los que tiene información.
El código de ejemplo del artículo puedes usarlo en cualquier placa Arduino siempre que utilices el pin de la interrupción correspondiente.
¡Suerte con tu proyecto y gracias por visitar polaridad.es!
Yillmer
Existe otra opcion que no sea USBee X pro y como descargo el pulseview . Gracias por tu aporte.