Icono del sitio Programar fácil con Arduino

Comunicación I2C con Arduino lo mejor de dos mundos

Comunicar dos Arduinos con I2C

El protocolo de comunicación I2C con Arduino es ampliamente utilizado en multitud de sensores y actuadores. Esto se debe principalmente a dos factores:

Estos dos factores han hecho que el protocolo de comunicación I2C haya resurgido de sus cenizas como el Ave Fénix.

Pero el protocolo de comunicación I2C es bastante antiguo. No tanto como la humedad pero lleva con nosotros desde los mundiales que se celebraron en España, 1982.

Desde entonces han sucedido muchas cosas entorno a la tecnología y ha tenido que venir Arduino para mostrarnos la utilidad real de este protocolo de comunicaciones.

En este artículo y tutorial descubrirás cómo utilizar el protocolo de comunicaciones I2C con Arduino de una forma eficiente. Además, aprenderás cómo funciona y los problemas que vas a encontrar al utilizarlo.

Y como siempre, tienes un caso práctico para que puedas practicar toda la parte de teoría.

Ahora prepárate una buena taza de café calentito (invierno) o una cerveza fría (verano) que empezamos con el protocolo I2C con Arduino.

¿Qué es y cómo funciona la comunicación I2C con Arduino?

I2C (por sus siglas en inglés Inter-Integrated Circuit) es un protocolo de comunicación serial desarrollado por Phillips Semiconductors, allá por la década de los 80s. En un inicio se creó para poder comunicar varios chips al mismo tiempo dentro de los televisores que fabricaba la compañía. Sin embargo, con el paso del tiempo otros fabricantes comenzaron a adoptarlo hasta convertirse en el estándar del mercado mundial que es hoy

I2C también es denotado como TWI (por sus siglas en inglés Two Wired Interface) o interfaz a dos hilos. Este nombre alternativo surge por motivos de licencia que impedían utilizar el término original.

El protocolo I2C funciona con una arquitectura maestro-esclavo (master-slave). En esta arquitectura existen dos tipos de dispositivos:

Para comprender mejor el funcionamiento de esta arquitectura veras un ejemplo.

Puedes pensar en un salón de clases, donde hay un profesor y varios estudiantes. Como es lógico, el profesor es el que coordina la clase, lo que significa que los estudiantes tienen que estar atentos a él

Cuando el profesor va ha hacer una pregunta se dirige a un estudiante por su nombre, luego realiza la pregunta y espera por la respuesta del alumno.

Una vez el alumno ha respondido el profesor da por terminada la conversación y puede interactuar con otro estudiante. 

En este caso, el profesor es el dispositivo maestro o controlador (cuidado, no es un juego de palabras) y los estudiantes juegan el papel de periféricos o esclavos. Por supuesto, los componentes no se comunican hablando como nosotros, pero eso ya lo veras más adelante.

En la siguiente figura puedes ver la arquitectura necesaria para conectar varios dispositivos usando el protocolo I2C.

Como puedes ver, solamente se precisa de dos pines para la comunicación, es decir, que todos los dispositivos se conectan a los mismos cables:

La verdad es que existe una tercera conexión que es la referencia de tierra (GND), pero como esta es común para todos los dispositivos de un circuito no se incluye en el esquema.

Es importante destacar las resistencia pull-up ubicados en ambos pines. Estas son resistencias comunes, iguales a las que te encuentras en los kits de Arduino. Sin embargo, su presencia es vital para que la comunicación funcione correctamente, ya que I2C es un bus de colector-abierto.

En el esquema se muestran varios dispositivos maestros. Esto se debe a que I2C permite un ambiente multi-maestro, como si en el salón de clases hubieran varios docentes. Esto es especialmente útil cuando tu proyecto utiliza varios microcontroladores y existen periféricos compartidos. Por ejemplo, imagina que tu proyecto involucra un Arduino UNO, un Arduino Nano y un sensor de temperatura que soporta I2C, por ejemplo, el HDC1080

Una alternativa para que ambos procesadores puedan acceder a la temperatura puede ser: conectar el sensor al Arduino Nano y comunicar de alguna forma el Arduino UNO con el Nano. Esquemáticamente sería algo así:

Este método presenta dos inconvenientes:

Una mejor variante es conectar directamente ambos procesadores al sensor utilizando I2C.

De esta forma cada dispositivo puede interactuar directamente con el sensor cuando lo requiera, sin afectar el funcionamiento del otro. 

¿Por qué usar I2C?

Seguro has oído mencionar varios protocolos de comunicación. Cada uno tiene sus puntos fuertes y falencias. Ya hablamos del RS-485 por ejemplo.

Para responder, es necesario conocer los problemas de algunas opciones de comunicación que están disponibles en las placas Arduino o ESP8266.

Problemas con la comunicación serial

Comencemos con los inconvenientes de los famosos puertos Serial.

Todas las placas Arduino cuentan con, al menos, un puerto serial. Estos puertos sólo ocupan dos pines del microcontrolador, lo cual es una ventaja obvia.

Sin embargo, tienen un inconveniente y es que solo pueden utilizarse para comunicarse con un único dispositivo. Es decir, que si necesitas comunicarte con varios sensores o actuadores utilizando este protocolo es necesario emplear una placa que posea un puerto Serial por cada dispositivo.

Como puedes ver, está en clara desventaja frente a I2C cuando se trata de comunicarse con varios dispositivos.

Otro factor que influye negativamente es la velocidad de transmisión, ya que, si bien no existe un límite de velocidad (teóricamente hablando) para los puertos serial, en la mayoría de los casos estos están limitados a valores inferiores a los 300.000 bits por segundo (300 Kbps). Mientras que en I2C es posible alcanzar velocidades de hasta 5 Mbps, más de 10 veces mayor que con un puerto serial estándar.

Problemas con la comunicación SPI

La desventaja más evidente de SPI frente a I2C es que consume demasiados pines del microcontrolador. Por ejemplo, para comunicar un maestro con un esclavo utilizando SPI son necesarios 4 pines.

Adicionalmente, cuando se utilizan varios esclavos, el maestro debe disponer de un pin adicional para cada uno, tal y como se muestra en la siguiente imagen.

Otra desventaja de SPI es que no permite un ambiente multi-maestro. Eso implica que en SPI solo puede existir un maestro. En I2C, por el contrario, no hay límite para el número de maestros o controladores.

En la siguiente tabla puedes ver un resumen de las características más importantes del protocolo I2C.

Número de vías o cables2
Velocidad máxima*Modo estándar (Sm) = 100kbps 
Modo rápido (Fm) = 400kbps 
Modo High Speed (Fm+) = 3.4Mbps 
Modo Ultra Fast (Hs-mode) = 5Mbps
Número máximo de maestrosIlimitado (teóricamente)
Número máximo de esclavos (7-bits)112
Número máximo de esclavos (10-bits) ** 1008

* Las placas Arduino basadas en AVR, como Arduino UNO y MEGA, solo soportan los dos primeros modos.

** La librería Wire de Arduino IDE solamente soporta I2C utilizando direcciones de 7 bits.

¿Dónde se usa la comunicación I2C?

Hoy te puedes encontrar todo tipo de dispositivos compatibles con I2C. Desde memorias EEPROM y sensores de temperatura, hasta lectores RFID y pantallas OLED. Además, la gran mayoría de microcontroladores y placas de desarrollo existentes en el mercado cuentan con este protocolo.

A continuación, te dejo un listado de algunos componentes y módulos que soportan comunicación I2C y pueden ser útiles en tus proyectos con Arduino:

Protocolo I2C con Arduino

Anteriormente comenté que la comunicación entre componentes, cuando se utiliza I2C, es similar a un salón de clases. Sin embargo, estos realmente no hablan, en su lugar se envían mensajes utilizando los pines SDA y SCL

Cada pulso en el pin SCL le indica al dispositivo receptor que lea el valor del pin SDA. De esta forma los bits son puestos en SDA uno por uno. Para que lo comprendas mejor en la siguiente figura se muestra una transferencia del valor 77.

Los mensajes están compuestos por varias tramas o secciones. Específicamente un mensaje está compuesto por una trama de dirección (que indica a cual esclavo va dirigida la información) y una o varias tramas de datos.

En la siguiente figura se muestra la estructura de un mensaje I2C.

Condición de inicio (Start condition)

Para enviar un mensaje un dispositivo controlador coloca el pin SDA en estado bajo manteniendo el pin SCL en estado alto. Esto hace que el resto de los dispositivos entiendan que un mensaje está a punto de ser enviado.

Trama de dirección (Address frame)

La trama de dirección contiene la dirección del esclavo con el cual se desea comunicar el maestro, seguida por el bit escritura/lectura (R/W) y el bit de reconocimiento (ACK/NACK).

La dirección del esclavo puede tener 7 o 10 bits y solo puede existir un esclavo con esa dirección en el bus. El bit R/W indica el tipo de operación a realizar. Es decir, que indica si el controlador va a enviar (R/W = 0) o recibir (R/W = 1) información desde el esclavo.

El bit de ACK/NACK o de reconocimiento es el último bit de cada trama (tanto de dirección como de datos). Este bit es enviado por el dispositivo que está recibiendo la trama. En el caso particular de la trama de dirección si el bit de reconocimiento es igual a cero indica que en el bus existe un esclavo con la dirección enviada.

Trama de datos (Data frame)

Después de enviada la trama de dirección comienza la transferencia de información entre el maestro y el esclavo anteriormente indicado.

Los datos pueden ser enviados por el esclavo o el maestro, depende del valor del bit R/W de la trama de dirección.

Toda trama de datos concluye con un bit de reconocimiento. Este bit tiene que ser puesto a cero por el dispositivo que está recibiendo la información para indicar que se recibió correctamente.

Condición de parada (Stop condition)

Una vez se han enviado todas las tramas de datos el dispositivo controlador pone ambos pines en estado alto, primero el pin SCL y después el pin SDA. A esto se le denomina condición de parada y sirve para indicarle al esclavo que la comunicación ha terminado.

En la siguiente imagen se muestra el mensaje completo para un maestro que envía el carácter ‘A’ al esclavo con dirección 0x23.  

Es importante destacar que las transferencias se realizan comenzando por el bit más significativo.

Conexión de un bus I2C con Arduino

Ya se ha visto bastante teoría sobre I2C. Ahora toca la parte practica. A continuación, verás cómo conectar varios Arduinos utilizando I2C

Lo primero es identificar los pines SDA y SCL de todos los dispositivos a conectar. En la siguiente tabla tienes un resumen de los pines correspondientes de las placas más significativas.

PlacaPines I2C
Arduino UNOA4 (SDA), A5 (SCL)
Arduino EthernetA4 (SDA), A5 (SCL)
Arduino NanoA4 (SDA), A5 (SCL)
Arduino Mega 256020 (SDA), 21 (SCL)
Arduino Leonardo2 (SDA), 3 (SCL)
Arduino Due*20 (SDA), 21 (SCL), SDA1, SCL1
Basadas en ESP8266 **4 (SDA) 5 (SCL)

* La placa Arduino Due cuenta con dos interfaces I2C.

** En las placas basadas en ESP8266 es posible especificar otros pines para la comunicación I2C.

En algunas placas, como el Arduino UNO y el Arduino MEGA, los pines I2C se encuentran además junto al pin AREF.

Una vez identificados los pines de I2C solo queda conectar todos los SDA y todos los SCL juntos. También es necesario colocar una resistencia pull-up entre cada línea y la alimentación.

Acontinuación, se muestra el esquema de conexiones necesario para conectar un Arduino Nano, un módulo de temperatura MCP9808 y un Arduino UNO utilizando I2C.

El circuito quedaría como se muestra en la siguiente figura.

La mayoría de los módulos de sensores I2C traen marcados los pines SDA y SCL.

Librería Wire para I2C

La librería Wire es la herramienta indispensable para interactuar con el bus I2C con arduino de forma simple e intuitiva. De hecho, es casi tan simple de utilizar como el clásico Serial que empleas para enviar datos al monitor serie, pero eso ya lo verás en los ejemplos.

Esta librería no necesita ser instalada, ya que viene de forma predeterminada con el software Arduino IDE. Eso significa que no es necesario utilizar el gestor de librerías ni nada por el estilo para empezar a crear tus códigos utilizando la librería.

Pero antes de llegar a eso, veamos las funciones más importantes que aporta.

Funciones principales

Wire es una clase estática, lo que quiere decir que no hace falta crear una instancia u objeto de la clase. Es similar a la librería Serial.

Directamente escribes la clase (Wire) y luego llamas a la función correspondiente. Veamos algunas de estas funciones.

Wire.begin()

Esta función es la encargada de inicializar la librería Wire y de hacer que la placa se una al bus I2C. Se puede llamar de dos maneras diferentes dependiendo del número de parámetros que admita

Una función que puede ser llamada de diferentes maneras dependiendo el número de parámetros, se dice que es una función o un método sobrecargado.

La primera sobrecarga del método no admite parámetros.

De esta forma la placa se vincula al bus I2C para actuar como un dispositivo maestro o controlador.

La segunda sobrecarga del método admite un único parámetro.

Donde:

Esta función vincula la placa al bus I2C para actuar como un dispositivo esclavo cuya dirección es especificada por el parámetro address.

Wire.requestFrom()

Esta función es utilizada por un dispositivo maestro para solicitar datos de un dispositivo esclavo. Al igual que la función Wire.begin(), este método cuenta con dos sobrecargas.

La primera sobrecarga de la función admite dos argumentos.

Donde:

Esta función solicita a un esclavo una cantidad específica de bytes y libera el bus I2C para que otro dispositivo lo pueda utilizar. Los bytes solicitados son leídos utilizando la función Wire.read().

La segunda sobrecarga de la función admite tres parámetros.

Donde:

Esta sobrecarga permite realizar la solicitud de lectura sin liberar el bus I2C.

Wire.beginTransmission()

Esta función comienza una transferencia de datos hacia el esclavo con la dirección indicada.

Donde:

Wire.write()

Esta función se puede utilizar para enviar datos desde un esclavo a un maestro ante una solicitud de datos, o para enviar datos a un esclavo desde un maestro después de ejecutar la función Wire.beginTransmission(). Este método cuenta con tres sobrecargas

La primera sobrecarga de la función admite un parámetro.

Donde:

Esta función envía un byte utilizando el bus I2C.

La segunda sobrecarga de la función admite un parámetro.  

Donde:

Esta función envía una cadena de caracteres como una secuencia de bytes.

La tercera y última sobrecarga de la función admite dos parámetros.

Donde:

También es posible utilizar Wire.print() y Wire.println() para enviar datos, tal y como se realiza al utilizar la clase Serial.

Wire.endTransmission()

Termina una transmisión iniciada con Wire.beginTransmission() a un esclavo y transmite los bytes que fueron almacenados en la cola utilizando la función Wire.write().

Cuando se utiliza, cualquiera de las sobrecargas de la función Wire.write() desde un maestro, los datos no son enviados directamente. En su lugar los encola para enviarlos cuando se ejecute la función Wire.endTransmission().

Wire.available()

Esta función retorna el número de bytes disponibles para leer utilizando la función Wire.read(). Esta función debe ser ejecutada en un maestro después de una llamada a la función Wire.requestFrom(), o en un esclavo dentro de la función de recepción.

Wire.read()

Esta función retorna un byte leído. En un maestro esta función debe ser ejecutada después de  una  llamada a Wire.requestFrom(), en un esclavo debe ser ejecutada dentro de la función de recepción.

Wire.setClock()

Esta función permite especificar la velocidad de comunicación para el puerto I2C

Donde:

Hay procesadores que soportan valores mayores, pero si planeas obtener un código 100% portable te recomiendo que utilices estos valores.

Wire.onReceive()

Este método permite registrar una función para que se ejecute cuando un esclavo recibe una transmisión desde un maestro.

Donde:

Wire.onRequest()

Este método permite registrar una función para que se ejecute en un esclavo cuando un maestro le solicite datos a este.

Donde:

Comunicación entre dos placas Arduino con I2C

Ya sabes como conectar varias placas o módulos para que se comuniquen utilizando I2C y también conoces la librería que permite implementar la comunicación. Ahora te muestro un ejemplo que te permitirá aplicar todos esos conocimientos. 

En el siguiente proyecto se muestra como establecer una comunicación bidireccional entre dos placas Arduino, donde una cumple el rol de maestro y la otra de esclavo.

Para este proyecto necesitarás:

* Puedes utilizar cualquier otra placa Arduino de 5 voltios, yo he utilizado estas porque eran las que tenía a mano.

** Puedes utilizar cualquier resistencia con un valor entre 2 kΩ y 5 kΩ.

El esquema del circuito a montar se muestra en la siguiente imagen.

Si realizas el montaje utilizando una placa de prototipos deberías obtener un resultado similar al circuito de la siguiente imagen.

El Arduino MEGA 2560 cumplirá la función de maestro y le solicitará al Arduino Nano (esclavo) el tiempo en milisegundos desde que fue alimentado. En otras palabras, el esclavo le debe responder con el valor retornado por la función millis().

Código del Arduino maestro o controlador 

El código del maestro comienza incluyendo la librería Wire.

Luego viene la función setup(). Aquí se utiliza la función Wire.begin() sin argumentos para inicializar la librería Wire y configurar el Arduino como un dispositivo I2C maestro. También se configura el puerto serial para enviar los datos recibidos desde el esclavo al monitor serie.

En la función loop() lo primero que se realiza es enviar el carácter ‘S’ al esclavo 23. Esto le indica al esclavo que debe preparar los datos, ya que próximamente se le solicitarán.

Luego se utiliza la función Wire.requestFrom() para solicitar un byte del mismo esclavo. El byte es obtenido utilizando la función Wire.read()

El byte leído indica la cantidad de bytes que el esclavo enviará en la próxima transferencia.  

Ahora le realiza una solicitud al esclavo con la cantidad de bytes de la información que este posee.

Los bytes son obtenidos utilizando la función Wire.read() y enviados al monitor serie para comprobar que la comunicación funciona correctamente. Finalmente se ejecuta un retardo de 500 milisegundos antes de repetir el ciclo.

Código del Arduino esclavo o periférico

Al igual que en el código del maestro, el sketch del esclavo comienza incluyendo la librería Wire para el manejo de la interfaz I2C.

Seguidamente se han declarado dos variables globales:

En la función setup() se inicializa la librería Wire. Para esto, en primer lugar, se ejecuta la función Wire.begin() utilizando el número 23 como parámetro. Eso implica que la placa Arduino se unirá al bus I2C como un esclavo con la dirección 23.

Además, se utiliza la función Wire.onRequest() para que la función eventoSolicitud() se ejecute cuando un dispositivo maestro solicite datos utilizando la función Wire.requestFrom(). También se utiliza la función Wire.onReceive() para que la función eventoRecepcion() se ejecute cuando un maestro envíe datos al dispositivo.

En la función loop() no se realiza ningún procesamiento, simplemente se realiza un retardo de 100 milisegundos.

La función eventoRecepcion() es ejecutada cuando un maestro envía datos al esclavo. En este caso, en la función se comprueba si el byte recibido es un carácter ‘S’. En caso afirmativo significa que hay que enviarle al maestro el resultado retornado por la función millis().

Cuando se recibe una ‘S’ a la variable S se le asigna el valor de true y el valor retornado por millis() es almacenado como un String en la variable message.

La función eventoSolicitud() se ejecuta siempre que un maestro intente obtener datos desde este esclavo utilizando la función Wire.requestFrom().

La función comprueba que la variable S esté asignada con el valor true. En caso positivo S es puesta a false y solamente se envía la cantidad de bytes almacenados en la variable message. En caso negativo se envían todos los caracteres almacenados en message

Una vez programes las placas solo te queda conectar el dispositivo maestro al ordenador utilizando el cable USB y abrir el monitor serie. Deberías obtener una salida como la que se muestra en la siguiente figura.

A continuación te dejo los códigos completos para cada dispositivo. Siéntete libre de modificarlos, pero no olvides que la dirección utilizada para el esclavo debe coincidir en los dos sketchs.

Código maestro completo:

Código esclavo completo

Principales problemas de la comunicación en I2C

Para terminar, te dejo algunos de los problemas más frecuentes que puedes encontrar al implementar una comunicación I2C con Arduino.

Ausencia de resistencias pull-up

La principal fuente de problemas en la comunicación I2C con Arduino es la ausencia de las resistencias pull-up. Esto se debe a que algunos módulos o placas no cuentan con estas resistencias. Por lo tanto, lo más recomendable es incluir ambas resistencias de forma externa, tal y como se muestra en el ejemplo anterior.

Cables de conexión largos

Otro factor que puede provocar errores en la comunicación es el uso de cables extremadamente largos para I2C. Esto se debe a que es un protocolo pensado para conectar circuitos integrados relativamente cercanos (distancias en el orden de los centímetros).

Uso de dispositivos de 5 voltios con otros de 3.3 voltios

Cuando utilizas I2C, todos los dispositivos deben trabajar al mismo voltaje, es decir, que en un mismo bus no pueden existir dispositivos que trabajen a 3.3 voltios con otros que operen a 5 voltios. Por poner un ejemplo, no puedes comunicar directamente un NodeMCU y un Arduino UNO utilizando I2C.

Esto se debe a que el nivel lógico alto (HIGH) en ambos es diferente. Por lo tanto, si se utiliza 5 voltios para las resistencias pull-up el ESP8266 resultaría dañado. Si por el contrario se utilizara 3.3 voltios para las resistencias, los dispositivos que operen a 5 voltios podrían no identificar correctamente un estado alto.

Conclusión sobre la comunicación por I2C

Hasta aquí el tutorial sobre la comunicación I2C con Arduino. ya puedes crear y gestionar proyectos con el protocolo.

Veamos lo aprendido hasta el momento.

Se empieza por definir su funcionamiento básico.

Luego, se ven las ventajas frente a protocolos como Serial o SPI.

También tienes una explicación sobre el protocolo I2C a nivel de bits definiendo:

Luego desglosamos las funciones de la librería Wire para I2C. Sin duda la forma mas sencilla de utilizar el protocolo.

Pasando al plano real, se practica la comunicación I2C conectando dos placas Arduino entre si.

Antes de concluir, dare las ultimas aclaraciones.

Recuerda que no solo puedes encontrar este protocolo como «I2C». Hay lugares que lo simbolizan como «II2» (en inglés Inter-Integrated Circuit) o también «TWI» (en ingles Two Wired Interface).

Por otro lado, si eres de los usuarios que buscan hardware plug and play en este caso para el protocolo i2c puedes probar la conexion qwiic. Se trata de conectores JST de 4 pines que no requieren soldadura y están polarizados para no confundir SDA y SCL. Con el puedes controlar fácilmente multitud de sensores.

Hablando de complementos para el protocolo, puede que necesites al multiplexor TCA9548A que propone una solución cuando te encuentras con sensores con una dirección I2C fija. Se trata de una forma de obtener hasta 8 dispositivos I2C de la misma dirección conectados a un microcontrolador.

Si te gustó el contenido, puedes agradecer dejando tu comentario.

Gracias a Shutterstock por la cesión de las imágenes.

Salir de la versión móvil