Cuando queremos transmitir información de un circuito electrónico (o chip )a otro se nos presentan muchas opciones. Una de ellas es por medio de comunicación serie, y un bus específicamente diseñado para ello es el Bus I2C.
El protocolo de circuito inter-integrado (I2C) es un protocolo destinado a permitir que varios circuitos integrados digitales «esclavos» («chips») se comuniquen con uno o más chips «maestros».
El bus I2C requiere únicamente dos cables para su funcionamiento, uno para la señal de reloj (CLK) y otro para el envío de datos (SDA).
En el bus cada dispositivo dispone de una dirección, que se emplea para acceder a los dispositivos de forma individual. Esta dirección puede ser fijada por hardware (en cuyo caso, frecuentemente, se pueden modificar los últimos 3 bits mediante jumpers o interruptores) o totalmente por software.
En general, cada dispositivo conectado al bus debe tener una dirección única. Si tenemos varios dispositivos similares tendremos que cambiar la dirección de alguno de ellos.
El bus I2C tiene una arquitectura de tipo maestro-esclavo. El dispositivo maestro inicia la comunicación con los esclavos, y puede mandar o recibir datos de los esclavos. Los esclavos no pueden iniciar la comunicación (el maestro tiene que preguntarles), ni hablar entre si directamente.
Librería Wire
Esta librería permite la comunicación mediante el protocolo I2C (también llamado TWI) de la placa Arduino con dispositivos externos, u otras placas. La comunicación se establece a través de los pines SDA (línea de datos, ubicada en pin analógico 4 en la placa Arduino UNO) y SCL (línea de reloj, ubicada en el pin analógico 5). Estos pines también estan ubicados al lado del pin AREF.
Las funciones de esta librería son las siguientes:
- begin()
- requestFrom()
- beginTransmission()
- endTransmission()
- write()
- available()
- read()
- SetClock()
- onReceive()
- onRequest()
Veamos ahora las más importantes.
Wire.begin(direccion)
Inicializa la librería Wire y configura el bus I2C como maestro o esclavo. Como parámetro tiene la direccion de 7 bits de esclavo (opcional); si no se específica, se configura como maestro.
Wire.beginTransmission(direccion)
Inicia una transmisión al dispositivo esclavo I2C con la dirección dada. Posteriormente, se colocan los bytes para la transmisión con la función write () y luego se transmiten llamando endTransmission (). Como parámetro tiene la dirección de 7 bits del esclavo.
Wire.write()
Escribe los dato desde un dispositivo esclavo en respuesta a una petición de un maestro, o pone bytes en cola para la transmisión desde un maestro a un esclavo (en el medio de llamadas beginTransmission() y endTransmission()).
Sintaxis
Wire.write(value)
Wire.write(string)
Wire.write(data, length)
Parametros
value: un valor a enviar como un byte
string: una cadena para enviar (una serie de bytes)
data: una matriz de datos (a enviar como bytes)
length: el numero de bytes a transmitir
Wire.endTransmission()
Finaliza una transmisión a un esclavo que fue empezada por beginTransmission() y transmite los bytes que fueron preparados por write().
Ejemplo 1 – Dispositivo Maestro que escribe (funciona con el ejemplo 2)
#include <Wire.h> byte x = 0; void setup() { Wire.begin(); // Se inicializa en el bus I2C como maestro } void loop() { Wire.beginTransmission(8); // inicia una transmicion al dispositivo #8 Wire.write("x is "); // envia 5 bytes Wire.write(x); // envia 1 byte Wire.endTransmission(); // transmite los bytes x++; // incrementa la variable x delay(500); }
Wire.onReceive(handler)
Registra una función a ser llamada cuando un dispositivo esclavo recibe una transmisión de un maestro. Esta función recibe como parámetro un entero (el número de bytes recibidos desde un maestro) y no devuelve nada, por ejemplo : void recibirDatos (int numeroBytes)
Ejemplo 2 – Dispositivo Esclavo que lee (funciona con el ejemplo 1)
#include <Wire.h> void setup() { Wire.begin(8); // se inicializa en el bus I2C como esclavo con el #8 Wire.onReceive(recepcionEvento); // registra la función de recepción Serial.begin(9600); // inicializa el puerto serial } void loop() { delay(100); } void recepcionEvento(int cuantos) { //define la función de recepción while (1 < Wire.available()) { // recibe todos los bytes menos 1 char c = Wire.read(); // recibe los bytes como caracteres Serial.print(c); // imprime los caracteres } int x = Wire.read(); // recibe un byte como un entero Serial.println(x); // imprime el entero }
Wire.requestFrom(direccion, cantidad)
Es usado por el maestro para solicitar bytes de un dispositivo esclavo. Los bytes pueden ser recibidos con las funciones available() y read(). Requiere como parámetros la dirección del dispositivo y la cantidad de bytes solicitados.
Wire.available()
Devuelve el numero de bytes disponibles para recuperar con read(). Debería ser llamada por un maestro después de llamar a requestFrom() o por un esclavo dentro de la función a ejecutar por onReceive(). Devuelve el numero de bytes disponibles para lectura.
Wire.read()
Lee un byte que fue transmitido por un dispositivo esclavo a un maestro después de un llamado con requestFrom() o fue transmitido por un maestro a un esclavo.
Ejemplo 3 – Dispositivo Maestro que lee (funciona con el ejemplo 4)
#include <Wire.h> void setup() { Wire.begin(); // se inicializa en el bus I2C como maestro Serial.begin(9600); // inicializa el puerto serial } void loop() { Wire.requestFrom(8, 6); // solicita 6 bytes al dispositivo esclavo #8 while (Wire.available()) { // mientras hay datos lee los datos recibidos char c = Wire.read(); // recibe bytes como caracteres Serial.print(c); // imprime el carácter } delay(500); }
Wire.onRequest(handler)
Registra una función que será llamada por el dispositivo esclavo cuando un maestro solicite datos. Como parámetro (handler) se debe indicar la función que será llamada cuando el maestro solicite datos
Ejemplo 4 – Dispositivo esclavo que escribe (funciona con el ejemplo 3)
#include <Wire.h> void setup() { Wire.begin(8); // se inicializa en el bus I2C como esclavo con el #8 Wire.onRequest(escrituraEvento); // registra la función de atención del evento de escritura } void loop() { delay(100); } void escrituraEvento() { //define la función de envio Wire.write("Hola"); //responde con un mensaje de 5 bytes }
Ejercicios
Ejercicio 1: Conectar dos arduinos con I2C – Maestro lee
Conectar dos placas Arduino con la configuración indicada, esta sería la usada típicamente para leer sensores. Mantener la placa que lee conectada al ordenador para verificar la información recibida usando el monitor serial.
Ejercicio 2: Conectar dos arduinos con I2C – Maestro escribe
Conectar dos placas Arduino con la configuración indicada, esta sería la usada típicamente para programar sensores. Mantener la placa que lee conectada al ordenador para verificar la información recibida usando el monitor serial.
I2C Scan
Este programa busca en todas las direcciones I2C disponibles en una red I2C y nos indica si hay algún dispositivo conectado y que dirección tiene. Muy útil cuando no sabemos qué dirección tiene asignada nuestro dispositivo.
#include "Wire.h" extern "C" { #include "utility/twi.h" } void scanI2CBus(byte from_addr, byte to_addr, void(*callback)(byte address, byte result) ) { byte rc; byte data = 0; for( byte addr = from_addr; addr <= to_addr; addr++ ) { rc = twi_writeTo(addr, &data, 0, 1, 0); callback( addr, rc ); } } void scanFunc( byte addr, byte result ) { Serial.print("addr: "); Serial.print(addr,DEC); Serial.print( (result==0) ? " Encontrado!":" "); Serial.print( (addr%4) ? "\t":"\n"); } const byte start_address = 8; const byte end_address = 119; void setup() { Wire.begin(); Serial.begin(9600); Serial.print("Escaneando bus I2C..."); scanI2CBus( start_address, end_address, scanFunc ); Serial.println("\nTerminado"); } void loop() { delay(1000); }