Archivo de la etiqueta: Programas Arduino

Arduino: Comunicación inalámbrica con NRF24L01

La función de este artículo es ofrecer una explicación sobre cómo establecer una comunicación inalámbrica entre dos placas Arduino usando el módulo transceptor NRF24L01.

La primera comunicación inalámbrica que explicaré será básica: enviar un simple mensaje de texto, tipo «Hola Mundo», de un Arduino a otro.

A continuación podremos extender este ejemplo de comunicación al envío de comandos de texto —usando el mismo circuito y estructura de programa— para controlar un sistema cualquiera ubicado en el extremo receptor.

Más adelante ofreceré un artículo con un ejemplo de comunicación bidireccional entre placas Arduino. Un Arduino tiene conectado un potenciómetro con el que se controla un servo en el segundo Arduino. Y en el otro sentido, unos interruptores en el segundo Arduino servirán para controlar LEDs en el primero.

Módulo transceptor NRF24L01

En la imagen se observa el módulo transceptor NRF24L01. Utiliza la banda de 2,4 GHz y puede operar con velocidades de transmisión de 250 kbps hasta 2 Mbps. Si se usa en espacios abiertos y, con menor velocidad de transmisión, su alcance puede llegar hasta los 100 metros. Para mayores distancias, hasta 1000 metros, existen módulos provistos con una antena externa en lugar de una antena trazada sobre la misma placa, como se observa en la imagen.

El módulo puede usar 125 canales diferentes, lo que da la posibilidad de tener una red de 125 módems que funcionen con independencia uno de otro en un solo lugar. Cada canal puede tener hasta 6 direcciones, es decir, cada unidad puede comunicarse con hasta otras 6 unidades al mismo tiempo.

El consumo de este módulo es de alrededor de 12 mA durante la transmisión, un valor menor al de un LED encendido. El voltaje de operación del módulo es de 1,9 a 3,6V, pero lo bueno es que los demás pines toleran la lógica de 5V, por lo que podemos conectarlo sin problemas a un Arduino sin necesidad de un convertidor de niveles lógicos.

Tres de estos pines son para la comunicación SPI: MOSI, MISO y SCK, que deben conectarse a los pines de la interfaz SPI del Arduino. Se debe tener en cuenta que diferentes placas Arduino pueden tener los pines del interfaz SPI en diferentes posiciones. Los pines CSN y CE se pueden conectar a cualquier pin digital de la placa Arduino, y su función es configurar el módulo en modo de espera o activo, así como para alternar entre modo de transmisión o de comando. El último pin, IRQ, es un pin de interrupción que no es necesario utilizar.

Una vez conectados los módulos NRF24L01 a las placas Arduino, llegará el momento de escribir los programas para el transmisor, y para el receptor.

Conexionado para utilizar Arduinos Uno R3 en ambos lados de la comunicación:

Código para los Arduino

Primero debemos descargar e instalar la librería RF24.

Aquí están los dos códigos para la comunicación inalámbrica y abajo, en los archivos, hay amplias explicaciones. He puesto la mayor parte de las explicaciones dentro del programa, como comentarios.

Código del Transmisor

Código del receptor

Una vez cargados ambos programas, podemos abrir el monitor serie en el Arduino receptor y observaremos la aparición del mensaje «Hola Mundo» cada 1 segundo.



CONTROL INALÁMBRICO A DISTANCIA

Esta prueba consiste en tener un Arduino (con el programa TRANSMISOR listado más abajo) conectado a la PC a través de USB / Conexión Serie, con el Monitor Serie del IDE de Arduino abierto (Herramientas > Monitor Serie) y asignado en el COM que corresponde, y transmitiendo en forma inalámbrica los caracteres que son tipeados dentro de este Monitor.




Nota: Un defecto que tiene el Monitor Serie del IDE del Arduino es que para que se transmita un caracter, o una serie de caracteres, hay que tipearlo y pulsar ENTER (ENTRAR). Eso es incómodo para controlar un equipo remoto pulsando teclas. Por eso recomiendo utilizar un programa simple y gratuito de terminal. Quizás usted ya tenga uno. En mi caso utilizo el programa Parallax Serial Terminal (PST.EXE), que se puede bajar de aquí (Página de manual de uso).

El otro Arduino lleva grabado el programa RECEPTOR, y en ese Arduino se ha agregado un pequeño servo al diagrama estándar, como muestra la figura más abajo.

El funcionamiento será básico: pulsando los números de 1 a 0 ubicados en la hilera superior del teclado, o si lo desea y lo tiene, en el teclado numérico a la derecha. Para cada uno de estos números el servo se ubicará en 10 posiciones diferentes, separadas 20º. Desde 0 a 180º, que es el rango que tiene todo servo, y más aún estos pequeños. Los servos mayores se pueden mover 270º. La selección de acciones que corresponden a cada comando se realiza por medio de comparaciones IF en el primer programa ejemplo de RECEPTOR, y por medio de una estructura SWICH-CASE en el segundo programa ejemplo. El primero es así porque puede resultar más comprensible para los recién iniciados, y el segundo ejemplo es más simple, elegante y profesional. Puse enlaces en los nombres de las estructuras de programa que apuntan a la sección de REFERENCIA del sitio oficial de Arduino.

La elección de cuál programa RECEPTOR desea usar queda a su gusto. En realidad, es conveniente probar ambos. En lo funcional se comportarán igual.

DIAGRAMA

TRANSMISOR

RECEPTOR – Basado en elección de comandos por IF

RECEPTOR – Basado en elección de comandos por SWITCH-CASE


AQUÍ LOS ARDUINOS ARMADOS




Arduino: Entradas y salidas – Manipulación de puertos

NOTA: Para ver el uso de los pines del ATmega328P en Arduino recomiendo leer el artículo Arduino UNO R3 – Conectándolo al mundo exterior.

Programación avanzada de puertos

En principio, es importante recordar que los puertos de un microcontrolador de 8 bits tienen esa misma cantidad de entradas/salidas, o sea ocho líneas. Esto nos haría pensar que el ATmega328P, que posee tres puertos (B, C y D), dispone de 3 x 8 = 24 líneas de entrada/salida disponibles. Sin embargo, utilizado en un Arduino no es así, como veremos.

La mayoría de los bits de los puertos de los microcontroladores son de uso múltiple, es decir que se comportan de una forma u otra de acuerdo con su configuración. Varias líneas de puertos cumplen funciones vitales en la operación de un Arduino, funciones que no son líneas de entrada/salida de uso general.

El PORTB (puerto B) tiene ocupadas dos líneas de entrada/salida que se utilizan para conectar el cristal oscilador. Estos pines, el PORTB bit-6 y PORTB bit-7, pueden quedar libres si se configura al chip para utilizar el oscilador interno, pero esta opción no podemos utilizarla en el Arduino debido a que ya tiene su sistema basado en la velocidad de cristal de 16 MHz, además de que el cristal está soldado a esos pines en el circuito de la placa.

El PORTC tiene dos bits que no están disponibles, uno de ellos, el PORTC bit-6 se utiliza como entrada de reinicio (RESET), y el otro bit (7) no está cableado hacia el exterior del ATmega328P con cápsula PDIP que viene enchufado en el zócalo del Arduino Uno R3, porque no posee suficientes líneas disponibles en su encapsulado de 28 patas. Y cuando se trata de un chip con encapsulado de montaje superficial TQFP de 32 pines (como en el Arduino Nano y en algunos clones de Arduino Uno), las dos líneas faltantes están dedicadas al convertidor analógico digital (ADC6 y ADC7) y no son pines de entrada/salida digital.

Dos bits del PORTD, el PORTD bit-0 y el PORTD bit-1, se utilizan durante la programación del Arduino, ya que están conectados a la interfaz USB, además de ser los pines TX y RX utilizados para la comunicación serie. Estos pines se pueden utilizar para comunicación serie asincrónica hacia el exterior, y también como entradas o salidas cuando no se está grabando un programa. Pero no deben tener conexiones instaladas mientras se programa el Arduino.

En consecuencia, no se llega a disponer de la cantidad de 24 entradas/salidas que ofrecerían tres puertos de 8 bits.

El ATmega328P, como cualquier otro microcontrolador, tiene registros para cada puerto con los cuales se define si cada bit del puerto será usado como entrada o como salida, y en varios casos otra función. El ATmega328P tiene tres puertos: PORTB, PORTC y PORTD, por lo cual hay tres bancos de registros de configuración, uno para cada puerto.

Bancos y puertos de ATmega 328p
Banco 27 26 25 24 23 22 21 20
PORTB     Digital 13 Digital 12 Digital 11 Digital 10 Digital 9 Digital 8
PORTC     A5 A4 A3 A2 A1 A0
PORTD Digital 7 Digital 6 Digital 5 Digital 4 Digital 3 Digital 2 Digital 1 Digital 0
Valor 128 64 32 16 8 4 2 1

Registros

El ATmega328P tiene tres registros de 8 bits con los que administra estos tres puertos:

  • DDRx (donde x es B, C o D en este caso) determina si un bit es entrada (fijándolo en 0) o salida (fijándolo en 1)
  • PORTx controla si el pin está en nivel ALTO (HIGH) o BAJO (LOW). También define la existencia o no de un resistor de polarización a Vcc (pull-up, en inglés) si es una entrada.
  • PINx permite leer el estado de los pines de un puerto (solo es para lectura)

Antes de entrar de lleno a explicar el uso de los registros, veamos primero para qué podría servir este esfuerzo.

Ventajas de usar registros:

  • Cada instrucción de máquina necesita un ciclo de reloj a 16 MHz. Las funciones digitalRead() y digitalWrite() se componen cada una de ellas de varias instrucciones de máquina, lo que puede influir negativamente en aplicaciones muy dependientes del tiempo. El uso de los registros PORTx puede hacer el mismo trabajo en muchos menos ciclos de reloj.
  • Si es necesario cambiar de estado varios pines simultáneamente en lugar de ir haciéndolo de a uno con un ciclo for, que tomaría mucho tiempo, es posibles escribir directamente al registro y establecer los valores de varios pines de una sola vez.
  • Si el código está llegando al límite de memoria de programa disponible (memoria Flash), es posible usar este método para hacer que el código use menos bytes de programa.




Desventajas de usar registros:

  • El código no es fácil de entender para novatos.
  • Es mucho mas fácil cometer errores de difícil depuración. Por ejemplo con DDRD = B11111111 se pone a todos los pines, D0 a D7 como salida, inclyendo el pin D0 (RX), lo que causará que el puerto serie deje de funcionar.

En las librerías es muy recomendable usar la manipulación directa de registros, de modo de hacerlas mucho más eficientes.

Registro DDRx – Definición de pines como entrada o salida

Al utilizar los registros DDR tenemos la ventaja de que con solo una instrucción podemos declarar el pin como entrada o salida, en cambio, utilizando pinMode() necesitaríamos 8 instrucciones.

Veamos un ejemplo con el registro DDRB del PORTB:

El ejemplo de la imagen define los 4 bits bajos o menos significativos (0 a 3) como entradas, y los 4 bits altos o más significativos (4 a 7) como salidas. Veamos cómo se verían este y otros ejemplos en el programa del IDE de Arduino.

Mediante estos registros también podemos controlar las resistencias internas de pull-up que se usan, básicamente, para no dejar los pines de entrada al aire, ya que esto genera ruido eléctrico. Se puede resolver el problema de dos maneras: poner una resistencia externa de 10K a Vcc (+5V) o usar los pull-up internos del microcomputador, que polarizan de la misma forma las entradas y hacen más simple el circuito.

Para habilitar las resistencias pull-up, primero tenemos que configurar como entrada el puerto (con el bit 0), mediante registro DDRx, y luego escribir un 1 en el registro PORTx.

Es el equivalente de usar varias veces las funciones digitalRead() y digitalWrite() de Arduino. Sin embargo, con acceso directo al puerto se puede ahorrar espacio en la memoria flash, porque cada operación con funciones de leer estados de un puerto ocupa varios bytes de instrucciones, y también se puede ganar mucha velocidad, porque las funciones Arduino puede tomar más de 40 ciclos de reloj para leer o escribir un solo bit en un puerto.

Nada mejor que un ejemplo concreto para entender las instrucciones de programa. Veamos un ejemplo con Leds.

Diagrama:

En este ejemplo, durante dos segundos todos los leds encienden, durante otros dos segundos se encienden los impares, luego los pares, y durante dos más se apagan todos.

Abrimos Arduino IDE y escribimos el siguiente código:

Un segundo ejemplo que puede resultar más divertido. Enciende un led en secuencia hacia a un lado y hacia el otro. No hice más sofisticado el programa para claridad y comprensión. Se puede hacer dentro de una estructura for, por ejemplo, tomando el dato a poner el puerto de una matriz. O usando otros recursos más complicados, como desplazar el dato sobre un registro antes de ponerlo al puerto. Pero prefiero que sea didáctico y comprensible para todos.

La secuencia suelen llamarla «El auto fantástico». Hasta se vende un dispositivo con ese efecto de luces para adornar los autos.

El programa es:

@nbsp;

Un chip de la liga mayor:

Sólo por completar la información, y dar una idea de la importancia de este tipo de ahorro de código en otros microcontroladores, voy a mostrar algunos datos de los puertos del ATMega2560, el núcleo del Arduino Mega 2560 R3 (esquemático). Un tremendo chip con nada menos que 100 pines y ONCE puertos, que van desde el PORTA al PORTL. ¡Imagínense el banco de registros que hay para manejar!

Microcontrolador ATmega2560-16AU

Placa Arduino Mega 2560 R3

Encapsulado del ATmega2560-16AU utilizado en el Mega 2560




Arduino: cómo hacer que los HEX queden en una carpeta conocida

Varias veces me han preguntado cómo se obtiene el archivo final de la compilación en «idioma de máquina» o hexadecimal (NombrePrograma.HEX) de un programa creado con el IDE de Arduino (también llamado Sketch por algunos).

Este archivo es útil para varias operaciones, como por ejemplo «descompilarlo» para ver el assembler, y también porque sería posible grabar el programa usando un programador de chips en un lote de microcontroladores que se necesite para un proyecto, sin usar bootloader ni una placa Arduino cuando vamos a producir una equis cantidad de plaquetas para uso personal, o para un cliente.

La secuencia para tenerlos en una carpeta accesible es:

1) Abrir el IDE de Arduino

2) Abrir el menú «Archivos» y allí elegir «Preferencias»

3) En el panel de Preferencias, abajo de todo, ingresar a la capeta de «preferences.txt» por medio del enlace que está debajo de un texto que dice «Más preferencias pueden se editadas [etc]…»

4) Una vez abierta la carpeta donde esta el archivo de preferencias, cerrar el IDE de Arduino (esto es muy importante)

5) Hacer una copia del archivo de preferencias para tenerlo de reserva por cualquier problema que se presente.

6) Abrir el TXT preferences.txt con un editor de texto simple, como el Bloc de Notas o Notepad

7) Hay una extensa lista allí. Buscar en el archivo de preferencias la zona donde está esta opción:

sketchbook.path=C:\Users\toshiba pc\Documents\Arduino

Obvio que la dirección dentro del disco C: que va luego del signo «=» será propia de cada máquina

8) Puede estar o no definida la opción build.path (es la que le indica al compilador dónde debe guardar el archivo compilado). Allí se puede crear o editarla. En mi caso, yo debí insertarla porque no existía y escribí:

build.path=C:\Users\toshiba pc\Documents\Arduino\HEX

Nota: En la publicación fuente de esta aplicación dice que es importante que este código agregado quede ubicado después de la opción export.application, pero sin embargo esto sería en una versión anterior, ya que ahora cada vez que se abre el IDE de Arduino estas opciones quedan ordenadas alfabéticamente.

9) Ahora sólo queda guardar este archivo editado y correr el IDE




10) Para comprobar que funciona, compilar cualquier programa y revisar si ha aparecido la carpeta HEX, y dentro de ella el compilado .HEX. Por ejemplo, si se compila el famoso ejemplo Blink.ino encontraremos el archivo Blink.ino.hex. Pero también encontrarán un Blink.ino.with_bootloader.hex. Es importante, ya que en este caso obtendrán un HEX con el que se podrán programar chips que además posean el habitual bootloader de la placa Arduino.

11) Luego uno puede ir distribuyendo estos .HEX en las carpetas de cada proyecto, ya que sólo estarán allí hasta que se haga la compilación de un nuevo programa. El IDE de la versión actual los borra antes de escribir un nuevo HEX.



Arduino: Usando la función millis() en lugar de delay()

El código que sigue es un típico ejemplo de escritura a través de la comunicación serie de Arduino. Escribe “Hola” a través del puerto COM serie (visible con el Monitor Serie de Arduino, en la pestaña Herramientas), y espera durante 1000 milisegundos (1 segundo) al final de cada iteración del bucle.

La función delay() es muy fácil de usar para crear esperas, pero tiene un inconveniente: deja al microcontrolador «atrapado» dentro de la ejecución de esta función durante el tiempo que se ha indicado. Si hubiese un cambio en un pin que debería detectar, o si llegase información a través de cualquiera de las comunicaciones posibles (serie, I2C o SPI) el microcontrolador sólo se enteraría luego de completarse el retardo.

Una solución es crear un retardo que no deje insensible al sistema durante un tiempo tan extenso.

La función millis()

millis() devuelve el número de milisegundos desde que la placa Arduino empezó a ejecutar, luego de un reinicio o el encendido. Este número se desbordará (volverá a cero), después de aproximadamente 50 días.

Retorna la cantidad de milisegundos en un valor long sin signo (unsigned long).

Nota: Tenga en cuenta que como el valor de retorno para millis() es un long sin signo (unsigned long), pueden producirse errores lógicos si un programador intenta hacer operaciones aritméticas con tipos de datos más pequeños, como de tipo int. Incluso con los long con signo se pueden producir errores ya que su valor máximo es la mitad que la de su contraparte sin signo.




Ejemplo de texto «Hola» utilizando la función millis()

Hacerlo de esta manera solo tiene sentido como ejemplo, ya que es evidente que en este caso directamente se puede usar una función delay(1000). Pero de todos modos la diferencia entre este ejemplo y el código que usa la función delay(1000) es que el ciclo del código que usa la función millis() se ejecutará una vez por segundo con la máxima precisión posible. El bucle en un código con delay(1000) se ejecutará en algo más de tiempo, ya que se produce un retardo al ejecutar Serial.println(«Hola»). Ocurrirá igual con cualquier otra serie de instrucciones que se incluyan dentro del bucle.

¿Por qué usar millis() en lugar de delay()?

Presentaré dos ventajas al utilizar millis() para crear retardos, en comparación con el uso habitual de delay().

1. Cronometraje preciso

La primera ventaja que discutiremos es la exactitud en el tiempo.

Con millis() podemos garantizar que el bucle se ejecute tantas veces como queramos dentro del retardo sin afectar su extensión, independientemente del tiempo de ejecución (obviamente, siempre que el tiempo de ejecución de todas las instrucciones sea menor al retardo deseado).

Con delay() esto no es posible, ya que no sabemos cuánto tiempo durará el tiempo de ejecución de todas las instrucciones de programa que están dentro del ciclo.

Una sincronización precisa como esta es muy útil cuando se muestrea a una cierta frecuencia, o cuando se utilizan filtros, entre otras cosas.

2. Sin bloqueo

La ventaja principal de la función millis() es que no nos impide ejecutar otro código mientras estamos «esperando».

Ejemplo: digamos que queremos imprimir «Hola» en el puerto serie una vez por segundo mientras hacemos otras cosas. Esto no es posible con delay(), ya que entrar a la función delay() pausa todo el código.

Aquí hay una manera de hacer esto:

Este fragmento de código es bastante similar al primer ejemplo que mostramos, excepto que este no bloquea el resto del programa mientras no se está imprimiendo hacia la línea serie.

3. Un simple secuenciador

Vamos a escribir un ejemplo simple en el que creamos un planificador que imprime ciertos trozos de texto a través de la línea serie a diferentes intervalos.

Así es como se ven los primeros 60 segundos en el Monitor Serie:

Esta es una manera agradable y fácil de sincronizar las ejecuciones en su código. También se puede ejecutar otras partes de código simultáneamente.

4. Una nueva función delay()

La función delay() estándar podemos reemplazarla por una implementada con la función millis(). Si bien parece que es lo mismo, ya veremos a continuación qué ventaja nos puede ofrecer su estructura de programa:

Además de esperar el tiempo indicado, esta función monitorea un pin de entrada del Arduino que haya sido cableado para detectar que ha sucedido algún evento externo. Si esa señal va a nivel BAJO (LOW), se interrumpe el retardo.

Para conectar varias entradas capaces de interrumpir el ciclo de retardo, se pueden agregar más elementos en la comparación de cierre del while, por ejemplo

while (Contador<=millis() && digitalRead(12)==HIGH) && digitalRead(11)==HIGH);

O se puede conectar a la entrada por el pin 12 (u otro pin elegido) un circuito como el que sigue, un AND realizado con lógica de diodos de señal (1N914 o 1N4148), que tiene la ventaja de que se puede ampliar indefinidamente. También se puede implementar con un circuito integrado compuerta AND.

Nota: quede claro que el circuito se representa con pulsadores, pero cada una de estas entradas puede ser un microswith, los contactos de un relé, o cualquier otro sensor que cierre circuito hacia GND (tierra o común).

Función micros() y desbordamiento

Al igual que delay() tiene una versión en microsegundos llamada delayMicroseconds(), la función millis() tiene como compañera para tiempos breves la función micros(). Si necesitamos una mejor resolución, micros() puede ser el camino a seguir. Sin embargo, tenga en cuenta que el valor devuelto por micros() se desbordará (volverá a cero) después de aproximadamente 70 minutos, en comparación con los 50 días de millis(). “Desbordamiento” significa que el conteo llega a su máximo, y entonces los valores de retorno de la función recomienzan desde cero.

Resumen

millis() y micros() son funciones realmente útiles para usar en tareas relacionadas con el tiempo. La opción inicial y típica de un programador de Arduino es usar delay(), que no siempre funcionará tan bien, principalmente cuando se programan muchas funciones que tienen que ver con el tiempo y existen eventos que no se pueden perder.