Controlando una LCD de caracteres 16x2 con I2C y Arduino
Actualmente estoy trabajando en un pequeño proyecto que requiere de una simple pantalla para mostrar información.
Una de las formas más comunes de mostrar información es utilizar una simple pantalla de caracteres, siendo las más comunes las que pueden mostrar 16 caracteres en dos líneas, o las famosas "16x2".
Es muy común encontrar este tipo de pantallas controladas por el driver HD44780 de interfaz paralela. Sin embargo el problema de lo anterior es que terminamos utilizando muchas líneas de nuestro Arduino que podríamos utilizar para hacer cosas más interesantes.
En esta entrada vamos a utilizar un sencillo ATmega328 para controlar una de estas LCDs, pero lo más interesante es que lo vamos a configurar como "esclavo" en la interfaz I2C para que podamos utilizar este bus sin gastar los preciados pines de E/S digital de nuestro Arduino.
¡Comencemos!
¡Comencemos!
Hablando un poco de I2C
El I2C o 2-Wire o simplemente "TWI" es el nombre de un bus de comunicación tipo serie utilizado para permitir a micro-controladores o dispositivos interconectarse entre sí o con otros dispositivos o sensores.
La característica interesante de este bus es que utiliza únicamente dos líneas: Una línea de reloj (SCL) y una segunda para envío y recepción de datos (SDA). Estas líneas se mantienen normalmente en un valor lógico alto a través de un par de resistencias pull-up. Los dispositivos conectados al bus, envían los datos modificando el valor lógico de dichas líneas enviándolas a tierra. Utilizando esta sencilla forma de conexión se puede interconectar varios dispositivos sobre el mismo bus.
En el I2C, se identifican dos roles de dispositivos: los "maestros" quienes se encargan de generar señales de reloj y quienes solicitan o escriben datos a los dispositivos y los "esclavos" que usualmente se mantienen "escuchando" el bus y responden a los dispositivos maestro.
Dentro del bus I2C, cada dispositivo esclavo tiene una dirección (que debería ser única dentro del bus) de 7 o 10 bits, así que en teoría podríamos conectar hasta 128 o 1024 dispositivos esclavo dependiendo de la implementación en particular.
A pesar de ser muy utilizado, el I2C no es "tan estándar" y la implementación específica depende de cada fabricante de dispositivo. Sin embargo, podemos listar algunas ventajas de utilizar este bus en Arduino:
- Podemos tener hasta 2^7 (128) o 2^10 (1024) dispositivos TWI sobre el bus I2C.
- Solo necesitamos dos líneas (A4 y A5) por lo que nos ahorramos valiosas líneas de E/S en nuestro Arduino.
- Disponemos de una biblioteca (Wire) que implementa la mayor parte del "trabajo sucio" por nosotros.
- Múltiples máster, a diferencia de otros buses podemos tener varios controladores del mismo sobre el bus permitiendo que dos micro-controladores diferentes consulten el mismo sensor (en tanto no lo hagan al mismo tiempo).
Una de las preguntas que me han hecho sobre el I2C (y en general sobre estos buses de comunicación inter-chip) es: ¿Qué tanta distancia entre dispositivos puedo utilizar con este bus? La respuesta lastimosamente es: no demasiada.
Hay que recordar que estos buses de comunicación "entre-chip" están diseñados para comunicar dispositivos que se encuentran muy posiblemente sobre la misma PCB a centímetros o incluso milímetros de distancia, este tipo de bus no posee ningún mecanismo para reducir el efecto de interferencias externas o verificar la integridad de los datos transmitidos a través de el. A causa de lo anterior, si comenzamos a incrementar la distancia del dispositivo al micro-controlador cada vez será más difícil que obtengamos una comunicación estable. Muy posiblemente, un par de metros sea la máxima distancia que podamos extender los cables de conexión antes de comenzar a tener serios dolores de cabeza.
Diseñando el sistema
Los que leen mi blog saben que hago una combinación muy extraña a la hora de diseñar estos pequeños proyectos, a veces comenzando desde lo específico a lo general o viceversa.
Para esta entrada vamos a comenzar desde lo general hasta lo específico. El siguiente diagrama de bloques muestra el funcionamiento general del dispositivo que queremos construir.
Respecto al diagrama, realmente no vamos a crear un "controlador de display" con el ATmega328, sino más bien vamos a hacer una "interfaz" I2C para controlar el HD44780.
Así que teniendo la idea de lo que queremos hacer, es hora de ponernos a trabajar.
Si no fuera por el bootloader, necesitaríamos un programador de chips AVR para guardar nuestros programas cada vez que los los quisieramos modificar.
El ATmega328 usualmente trabaja a 16MHz gracias a un cristal oscilador externo, pero en esta ocasión queremos mantener la circuitería al mínimo. Afortunadamente existe un bootloader llamado
La mala noticia es que para cambiar el bootloader necesitamos un programador ISP AVR, la buena noticia es que podemos cargar un Sketch en nuestro Arduino UNO que nos servirá para convertirlo en un programador de chips AVR.
Para reemplazar el bootloader solo debes de revisar una de mis entradas anteriores.
Una vez tenemos el bootloader actualizado vamos a conectar la LCD como se muestra en el siguiente diagrama:
Para probar que todo esté bien, abrimos el IDE Arduino y cargamos el Sketch "HelloWorld" del menú "Archivo > Ejemplos > LiquidCrystal". Y lo cargamos al ATmega328 utilizando la tarjeta "Atmega328 on a breadboard (8Mhz internal oscilator)".
Si todo funciona de manera correcta deberíamos obtener algo como lo siguiente:
Una vez lo hayas descargado colocalo en el directorio "lib" de Arduino, deberá de aparecerte una nueva opción "LiquidCrystalI2C" en tu menú de ejemplos del IDE Arduino.
Vamos a crear nuestro controlador pensando en que lo conectaremos al bus I2C como esclavo. La idea es que lo podamos controlarlo haciendo uso de una biblioteca similar a la LiquidCrystal, pero en vez de especificar las líneas donde está conectada la LCD vamos a especificar la dirección del controlador.
Esta vez vamos a definir primero cómo queremos comunicarnos con el controlador y luego vamos a revisar el código. Para mantener la biblioteca simple solo nos vamos a encargar de enviar vía I2C las siguientes funciones:
Las conexiones serie envían datos de forma continua un bit tras de otro sobre el canal de comunicación. En I2C los datos se envían en bloques o paquetes de bytes como se muestra en el siguiente diagrama:
El encabezado contiene la dirección (de 7 bits en este caso) con la que el dispositivo maestro desea comunicarse el byte 1 al byte N son los datos que se envían con cada paquete. Dependiendo del valor del bit R/W será el maestro o el esclavo el que se encargará de cambiar el valor lógico de la línea SDA para enviar la información.
Sabiendo esto vamos a definir nuestro "protocolo" de funcionamiento utilizando nuestro propio paquete personalizado de la siguiente manera:
Aquí es donde vamos a "serializar" es decir los datos que recibimos de forma paralela en nuestra función van a ser transmitidos en serie a través del bus I2C. Mientras que el dispositivo se encargará de "de-serializar" y con los datos recibidos llamar al comando correspondiente.
Los comandos que vamos a enviar tendrán entonces el siguiente formato:
Vamos a dar un vistazo al archivo "LiquidCrystalI2C.h" que acabamos de descargar para encontrar las definiciones de cada comando:
Dentro del archivo "LiquidCrystalI2C.cpp" encontraremos la definición de cada una de las funciones de "serialización" de los comandos:
Cada comando de la clase se encarga de utilizar la biblioteca Wire. La función beginTransmission se encarga de establecer la dirección a donde queremos enviar los datos, luego enviamos el comando correspondiente y los datos que nos interesa transmitir para cada función. Por último finalizamos con la función endTransmission que se encarga de finalizar la transmisión de datos.
Ahora que tenemos claro como enviar nuestros comandos desde el "Master" hasta el "Esclavo" vamos a crear un sketch que se encargará de de-serializar los comandos recibidos por I2C.
Abrimos el sketch de Ejemplo WireSlave para analizar un poco el código.
El código de inicialización no muy es diferente al ejemplo "HelloWorld" de la biblioteca de LiquidCrystal de arduino. La única diferencia es que utilizamos Wire.begin(85); para indicar que queremos iniciar el I2C como esclavo con la dirección 85. La siguiente función que debe llamarnos la atención es Wire.onReceive(consumeData); Esta función se encarga de establecer que parte del código se encargará de procesar los datos recibidos por I2C.
Ahora examinemos el código dentro de "consumeData":
El funcionamiento del código es bastante sencillo, primero utilizamos Wire.read(); para leer el primer dato recibido por I2C, luego utilizamos un switch para ejecutar el código correspondiente dependiendo del comando recibido. En los casos que recibamos diferentes parámetros los almacenamos en variables temporales.
Los datos recibidos los procesamos de la siguiente manera:
Ya estamos casi listos para probar nuestro controlador. Ahora vamos a conectarlo de la siguiente manera:
¿Simple no? Recuerda siempre unir los tierra entre circuitos para asegurarte que ambos trabajen bajo la misma referencia de voltaje. Notarás que hemos agregado dos resistencias de 10K a 5V, estas no son necesarias ya que la biblioteca Wire de Arduino se encarga de configurar las salidas A4 y A5 con una resistencia pull-up interno. Para este ejemplo no son estrictamente necesarias, pero dependiendo de la aplicación y la hoja técnica del fabricante puede que te recomiende los valores más adecuados para utilizar en cada aplicación.
En el Arduino carga el sketch "WireMaster" que encontrarás en "Archivo > Ejemplos > LiquidCrystalI2C".
El código del Sketch es sumamente sencillo:
La biblioteca LiquidCrystalI2C utiliza la biblioteca Wire así que si la vamos a utilizar debemos asegurarnos de llamarlas juntas. Luego de que hemos inicializado I2C con Wire.begin(); inicializamos la LCD con LiquidCrystalI2C(85); Si queremos cambiar a otra dirección debemos de modificar el Sketch WireSlave que cargamos en el ATmega328.
Una vez conectado todo, debemos obtener algo como lo siguiente:
¿Genial no?
¿Recuerdan que al principio les explicábamos que el bus I2C puede tener múltiples dispositivos maestros y esclavos?
Ahora que el LCD funciona de manera independiente podemos tener distintos dispositivos controlandolo, en tanto no ocupen la misma sección de la pantalla (por razones obvias).
Modifiquemos un poco el Sketch de ejemplo de tal manera que el loop de la siguiente manera:
Ahora tomemos un segundo Arduino y modifiquemos el loop como en el siguiente ejemplo:
Conecta el segundo Arduino de la siguiente manera:
Recuerda Une todos los A4 con todos los A4 y todos los A5 con todos los A5, no olvides unir los GND entre sí.
Si cargaste bien los Sketchs el resultado deberá ser similar al siguiente:
En funcionamiento:
¿Por qué funciona? Una de las ventajas del protocolo I2C es que se encarga de auto-negociar que dispositivo hace del bus, así que no tenemos que preocuparnos en implementar a través de código un protocolo de detección de otros dispositivos. El controlador en cambio simplemente se encarga de ejecutar el código que le indica el dispositivo maestro.
Tal vez la única desventaja es que el dispositivo esclavo no sabe cuál de los dispositivos maestros está enviándole información. Si quisiéramos saber de donde vienen los datos deberíamos considerar alguna tipo de byte de identificación que se envíe desde el master al momento de enviar la información al esclavo.
Si bien la conexión no parece tan sencilla, el código si resulta considerablemente simple y el utilizar I2C te permitirá conectar una gran cantidad de dispositivos y sensores entre sí. La mejor forma de saber como funciona un dispositivo I2C es hacer revisión de la hoja técnica.
Antes de terminar solo quisiera recordarte que tengas cuidado con los voltajes. Muchos dispositivos I2C funcionan en 3.3V así que si deseas conectar alguno de ellos asegurate de agregar un convertidor de voltaje para evitar dañar los componentes.
No habiendo nada más que decir por ahora...
Respecto al diagrama, realmente no vamos a crear un "controlador de display" con el ATmega328, sino más bien vamos a hacer una "interfaz" I2C para controlar el HD44780.
Así que teniendo la idea de lo que queremos hacer, es hora de ponernos a trabajar.
Cambiando el bootloader del ATmega328.
El bootloader es una pequeñísima pieza de código que guardamos en el micro-controlador que se encarga de inicializar algunas configuraciones básicas dentro del chip y nos permite cambiar el programa de ejecución "en el aire". De esta manera podemos utilizar luego el IDE Arduino para modificar el programa de nuestro microcontrolador con un solo click.Si no fuera por el bootloader, necesitaríamos un programador de chips AVR para guardar nuestros programas cada vez que los los quisieramos modificar.
El ATmega328 usualmente trabaja a 16MHz gracias a un cristal oscilador externo, pero en esta ocasión queremos mantener la circuitería al mínimo. Afortunadamente existe un bootloader llamado
La mala noticia es que para cambiar el bootloader necesitamos un programador ISP AVR, la buena noticia es que podemos cargar un Sketch en nuestro Arduino UNO que nos servirá para convertirlo en un programador de chips AVR.
Para reemplazar el bootloader solo debes de revisar una de mis entradas anteriores.
Conectando la LCD
Una vez tenemos el bootloader actualizado vamos a conectar la LCD como se muestra en el siguiente diagrama:
Para probar que todo esté bien, abrimos el IDE Arduino y cargamos el Sketch "HelloWorld" del menú "Archivo > Ejemplos > LiquidCrystal". Y lo cargamos al ATmega328 utilizando la tarjeta "Atmega328 on a breadboard (8Mhz internal oscilator)".
Si todo funciona de manera correcta deberíamos obtener algo como lo siguiente:
¡Una pausa!
Dentro de poco comenzaremos a revisar el código de la aplicación, descarga el código del controlador LCD I2C con el ATmega328 de la siguiente dirección y tenlo a la mano para cuando lo necesitemos:Una vez lo hayas descargado colocalo en el directorio "lib" de Arduino, deberá de aparecerte una nueva opción "LiquidCrystalI2C" en tu menú de ejemplos del IDE Arduino.
El protocolo de comunicación
Vamos a crear nuestro controlador pensando en que lo conectaremos al bus I2C como esclavo. La idea es que lo podamos controlarlo haciendo uso de una biblioteca similar a la LiquidCrystal, pero en vez de especificar las líneas donde está conectada la LCD vamos a especificar la dirección del controlador.
Esta vez vamos a definir primero cómo queremos comunicarnos con el controlador y luego vamos a revisar el código. Para mantener la biblioteca simple solo nos vamos a encargar de enviar vía I2C las siguientes funciones:
- LiquidCrystalI2C(direccion): Inicializa nuestro controlador LCD conectado al bus I2C con la dirección especificada.
- LiquidCrystalI2C.clear(): Borra la pantalla.
- LiquidCrystalI2C.home(): Regresa el cursor hasta la posición 0,0.
- LiquidCrystalI2C.setCursor(columna,fila): Coloca el cursor en la posición especificada.
- LiquidCrystalI2C.print(cadena): Imprime la cadena de caracteres especificada.
Serializando y de-serializando
Las conexiones serie envían datos de forma continua un bit tras de otro sobre el canal de comunicación. En I2C los datos se envían en bloques o paquetes de bytes como se muestra en el siguiente diagrama:
El encabezado contiene la dirección (de 7 bits en este caso) con la que el dispositivo maestro desea comunicarse el byte 1 al byte N son los datos que se envían con cada paquete. Dependiendo del valor del bit R/W será el maestro o el esclavo el que se encargará de cambiar el valor lógico de la línea SDA para enviar la información.
Serializando
Sabiendo esto vamos a definir nuestro "protocolo" de funcionamiento utilizando nuestro propio paquete personalizado de la siguiente manera:
Aquí es donde vamos a "serializar" es decir los datos que recibimos de forma paralela en nuestra función van a ser transmitidos en serie a través del bus I2C. Mientras que el dispositivo se encargará de "de-serializar" y con los datos recibidos llamar al comando correspondiente.
Los comandos que vamos a enviar tendrán entonces el siguiente formato:
Vamos a dar un vistazo al archivo "LiquidCrystalI2C.h" que acabamos de descargar para encontrar las definiciones de cada comando:
// Commands to be decoded over I2C #define LCDI2C_PRINT 0x01 #define LCDI2C_CLEAR 0x02 #define LCDI2C_HOME 0x03 #define LCDI2C_SETCURSOR 0x04 #define LCDI2C_DEBUG 0x99
Dentro del archivo "LiquidCrystalI2C.cpp" encontraremos la definición de cada una de las funciones de "serialización" de los comandos:
void LiquidCrystalI2C::print(char *str) { Wire.beginTransmission(internal_address); Wire.write(LCDI2C_PRINT); Wire.write(str); Wire.endTransmission(); } void LiquidCrystalI2C::clear() { Wire.beginTransmission(internal_address); Wire.write(LCDI2C_CLEAR); Wire.endTransmission(); } void LiquidCrystalI2C::home() { Wire.beginTransmission(internal_address); Wire.write(LCDI2C_HOME); Wire.endTransmission(); } void LiquidCrystalI2C::setCursor(uint8_t col, uint8_t row) { Wire.beginTransmission(internal_address); Wire.write(LCDI2C_SETCURSOR); Wire.write(col); Wire.write(row); Wire.endTransmission(); }
Cada comando de la clase se encarga de utilizar la biblioteca Wire. La función beginTransmission se encarga de establecer la dirección a donde queremos enviar los datos, luego enviamos el comando correspondiente y los datos que nos interesa transmitir para cada función. Por último finalizamos con la función endTransmission que se encarga de finalizar la transmisión de datos.
De-Serializando
Ahora que tenemos claro como enviar nuestros comandos desde el "Master" hasta el "Esclavo" vamos a crear un sketch que se encargará de de-serializar los comandos recibidos por I2C.
Abrimos el sketch de Ejemplo WireSlave para analizar un poco el código.
#include#include #include #include void consumeData(int); LiquidCrystal lcd(12, 11, 5, 4, 3, 2); void setup() { Wire.begin(85); Wire.onReceive(consumeData); // set up the LCD's number of columns and rows: lcd.begin(16,2); Serial.begin(9600); Serial.println("Serial monitoring started"); }
El código de inicialización no muy es diferente al ejemplo "HelloWorld" de la biblioteca de LiquidCrystal de arduino. La única diferencia es que utilizamos Wire.begin(85); para indicar que queremos iniciar el I2C como esclavo con la dirección 85. La siguiente función que debe llamarnos la atención es Wire.onReceive(consumeData); Esta función se encarga de establecer que parte del código se encargará de procesar los datos recibidos por I2C.
Ahora examinemos el código dentro de "consumeData":
void consumeData(int dataSize) { if(!Wire.available()) { return; // Error detected. No data received } int i2c_command = Wire.read(); uint8_t param1, param2, i; char *buffer; switch(i2c_command) { case(LCDI2C_PRINT): buffer = (char*)malloc(dataSize); // create buffer i = 0; while(Wire.available()) { buffer[i++] = (char)Wire.read(); } buffer[dataSize-1] = 0; lcd.print(buffer); free(buffer); // We don't want memory leaks break; case(LCDI2C_CLEAR): lcd.clear(); break; case(LCDI2C_HOME): lcd.home(); break; case(LCDI2C_SETCURSOR): if(dataSize<3) return; // wrong format, end inmediatly param1 = Wire.read(); param2 = Wire.read(); lcd.setCursor(param1,param2); break; } }
El funcionamiento del código es bastante sencillo, primero utilizamos Wire.read(); para leer el primer dato recibido por I2C, luego utilizamos un switch para ejecutar el código correspondiente dependiendo del comando recibido. En los casos que recibamos diferentes parámetros los almacenamos en variables temporales.
Los datos recibidos los procesamos de la siguiente manera:
- LCDI2C_PRINT: Creamos un arreglo de caracteres con ayuda de malloc en base al total de datos a recibir, leemos los datos y luego llamamos a la función print de la biblioteca LiquidCrystal.
- LCDI2C_HOME: Llamamos a la función home de LiquidCrystal.
- LCDI2C_CLEAR: Llamamos a la función clear de LiquidCrystal.
- LCDI2C_SETCURSOR: Guardamos los datos de posición en variables temporales y cuando tenemos todos los datos llamamos a la función setCursor de la biblioteca LiquidCrystal.
Conectando nuestro Arduino
Ya estamos casi listos para probar nuestro controlador. Ahora vamos a conectarlo de la siguiente manera:
¿Simple no? Recuerda siempre unir los tierra entre circuitos para asegurarte que ambos trabajen bajo la misma referencia de voltaje. Notarás que hemos agregado dos resistencias de 10K a 5V, estas no son necesarias ya que la biblioteca Wire de Arduino se encarga de configurar las salidas A4 y A5 con una resistencia pull-up interno. Para este ejemplo no son estrictamente necesarias, pero dependiendo de la aplicación y la hoja técnica del fabricante puede que te recomiende los valores más adecuados para utilizar en cada aplicación.
En el Arduino carga el sketch "WireMaster" que encontrarás en "Archivo > Ejemplos > LiquidCrystalI2C".
El código del Sketch es sumamente sencillo:
#include#include "LiquidCrystalI2C.h" LiquidCrystalI2C lcdI2C; void setup() { Wire.begin(); lcdI2C = LiquidCrystalI2C(85); lcdI2C.clear(); } int count = 0; char buffer[16]; void loop() { lcdI2C.home(); lcdI2C.print("Hola TeUbi.co!"); lcdI2C.setCursor(0,1); itoa(count++,buffer,10); lcdI2C.print(buffer); delay(1000); }
La biblioteca LiquidCrystalI2C utiliza la biblioteca Wire así que si la vamos a utilizar debemos asegurarnos de llamarlas juntas. Luego de que hemos inicializado I2C con Wire.begin(); inicializamos la LCD con LiquidCrystalI2C(85); Si queremos cambiar a otra dirección debemos de modificar el Sketch WireSlave que cargamos en el ATmega328.
Una vez conectado todo, debemos obtener algo como lo siguiente:
¿Genial no?
Múltiples master I2C sobre el mismo bus
¿Recuerdan que al principio les explicábamos que el bus I2C puede tener múltiples dispositivos maestros y esclavos?
Ahora que el LCD funciona de manera independiente podemos tener distintos dispositivos controlandolo, en tanto no ocupen la misma sección de la pantalla (por razones obvias).
Modifiquemos un poco el Sketch de ejemplo de tal manera que el loop de la siguiente manera:
void loop() { lcdI2C.home(); lcdI2C.setCursor(0,0); lcdI2C.print("Arduino 1: "); itoa(count++,buffer,10); lcdI2C.print(buffer); delay(1000); }
Ahora tomemos un segundo Arduino y modifiquemos el loop como en el siguiente ejemplo:
void loop() { lcdI2C.home(); lcdI2C.setCursor(0,1); lcdI2C.print("Arduino 2: "); itoa(count++,buffer,10); lcdI2C.print(buffer); delay(1000); }
Conecta el segundo Arduino de la siguiente manera:
Recuerda Une todos los A4 con todos los A4 y todos los A5 con todos los A5, no olvides unir los GND entre sí.
Si cargaste bien los Sketchs el resultado deberá ser similar al siguiente:
En funcionamiento:
¿Por qué funciona? Una de las ventajas del protocolo I2C es que se encarga de auto-negociar que dispositivo hace del bus, así que no tenemos que preocuparnos en implementar a través de código un protocolo de detección de otros dispositivos. El controlador en cambio simplemente se encarga de ejecutar el código que le indica el dispositivo maestro.
Tal vez la única desventaja es que el dispositivo esclavo no sabe cuál de los dispositivos maestros está enviándole información. Si quisiéramos saber de donde vienen los datos deberíamos considerar alguna tipo de byte de identificación que se envíe desde el master al momento de enviar la información al esclavo.
Concluyendo
Si bien la conexión no parece tan sencilla, el código si resulta considerablemente simple y el utilizar I2C te permitirá conectar una gran cantidad de dispositivos y sensores entre sí. La mejor forma de saber como funciona un dispositivo I2C es hacer revisión de la hoja técnica.
Antes de terminar solo quisiera recordarte que tengas cuidado con los voltajes. Muchos dispositivos I2C funcionan en 3.3V así que si deseas conectar alguno de ellos asegurate de agregar un convertidor de voltaje para evitar dañar los componentes.
No habiendo nada más que decir por ahora...
¡Hasta la próxima!
Comentarios
Ya he intentado modificarlo pero no entiendo que pasa.. espero puedas apoyarme.
De antemano Gracias