VictorJAM.zapto.org
Arduino  I2C 

2021-04-24

El bus i2c

I2C (Inter-integrated circuit) es un bus de comunicaciones serie desarrollado por Philips en los años 80. Su objetivo era poder comunicar varios dispositivos entre ellos minimizando el número de cables.

El bus se hizo muy popular con lo que otros fabricantes copiaron el modelo, aunque lo llamaron de otra forma por motivos de licencia. Por ejemplo, Atmel, llamó al bus TWI (Two-WIre), aunque es totalmente compatible con I2C.

Interfaz física

El bus I2C es un bus de comunicaciones serie síncrono, por lo tanto para realizar la comunicación es necesario además de una línea de datos, una línea de reloj. A la línea de datos se le llama SDA (Serial Data Line) y a la línea de reloj se le llama SCL (Serial Clock Line).

Las velocidades estándar de reloj son 100KHz en el modo normal y 400KHz en el modo rápido. Recientes versiones del estándar incluyen velocidades del 1MHz, 3.4MHz y 5MHz, pero aún es difícil encontrar dispositivos que trabajen a esas velocidades

El bus tiene una tipología maestro-esclavo, donde un maestro es el encargado de generar la señal de reloj y establecer la comunicación con los esclavos. Para ello cada esclavo tiene una dirección de 7 bits (10 bits en el modo extendido) con lo que el total de dispositivos en el bus será de 127.

I2C es un bus multi-maestro, es decir, en el mismo circuito puede haber uno o más maestros.

En realidad debido a las características eléctricas del bus, no se puede exceder de la capacitancia de 400pF, lo cual limita la longitud a unos pocos centímetros dentro de una placa de circuitos.

Ambas líneas (SDA y SCL) son bidireccionales y de colector abierto lo que hace necesario el uso de resistencias PULL-UP.

La lógica de control permite leer el estado del bus mediante un búfer que no interfiere en la lectura de la línea. Por otro lado hay conectado al bus el drenador de un transistor mosfet. La puesta de dicho transistor es controlada también por la lógica de control.

Cuando la puerta del transistor no tiene tensión, en la línea habrá una tensión impuesta por la resistencia PULL-UP que será la tensión de VCC. Si en este momento leemos a través del búfer obtendremos un valor HIGH.

El bus estará en estado flotante o libre para uso, cuando la línea tenga un valor HIGH.

Si por el contrario la lógica de control activa el transistor esté cierra el circuito llevándolo a GND. Si en ese momento leemos la línea a través del búfer tendremos un valor LOW.

Una de las ventajas de este tipo de control es que no puede haber cortocircuito. Si hubieran varios maestros que quisieran transmitir un dato, llevarían a LOW la línea del bus, pero no pasaría nada.

Funcionamiento general del I2C.

Hay dos operaciones típicas que se van a realizar sobre el bus: enviar datos a un esclavo o recibir datos de un esclavo.

El procedimiento general que un maestro usa para comunicarse con un esclavo es el siguiente:

  1. El maestro quiere mandar datos al esclavo.
    • El maestro envía una condición de inicio o START y la dirección del esclavo.
    • El maestro manda uno o varios datos al esclavo.
    • El maestro termina la transferencia con una condición de parada o STOP
  2. El maestro quiere recibir datos del esclavo.
    • El maestro envía un START y la dirección del esclavo.
    • El maestro envía el registro requerido para leer del esclavo.
    • El maestro recibe datos del esclavo.
    • El maestro termina la transferencia con una condición de STOP.

Veremos estos procedimientos con mas detenimiento, pero antes hemos de saber que todos los dispositivos esclavos que nos conectemos generalmente funcionaran a base de registros.

Un esclavo tendrá un juego de registros o posiciones de memoria que son los que podremos leer o escribir. Ese juego de registros será el mapa de memoria del esclavo I2C. Por ejemplo el reloj DS3231 tiene un mapa de memoria en cuya posición 0 tendremos los segundos, en la 1 los minutos y en la 2 las horas; y desde la posición 14 la configuración del reloj.

El control de los dispositivos I2C se reduce, por lo tanto, a leer y escribir registros.

Condición de START y STOP

Para iniciar la comunicación con el esclavo el maestro genera una condición de START en el bus. Para ello, mientras la línea SCL esta en HIGH provoca un flanco de bajada en la línea SDA.

Para terminar la comunicación con el esclavo el maestro genera una condición de STOP en el bus. Para ello, mientras la línea SCL esta en HIGH provoca un falco de subida en la línea SDA.

Una condición START repetida, es igual a nivel físico, pero se diferencia en que se produce después de otro START sin haberse producido el STOP, con lo que el bus no está libre. En útil en peticiones de datos.

Envió de datos.

Cada bit de datos es enviado en cada ciclo de reloj o pulso de la señal SCL. En la línea se transmitirán hasta 8 bits (un byte) que podrá ser una dirección, registro o dato.

El primer bit transmitido es el más significativo y el último el menos significativo. El bit debe estar estable en la señal SDA mientras dure la parte alta del reloj, ya que si no en caso contrario se interpretará como una condición de START o STOP.

Cuando el byte enviado es una dirección hemos de recordar que está solo ocupa 7 bits, por lo tanto nos sobra un bit de datos. Dicho bit se usa para indicar si vamos a realizar una operación de lectura o lectura. El bit se denomina R/W, cuando valga 1 se realizara la lectura y cuando valga 0 se realizada una escritura de datos.

Bit de reconocimiento ACK y NACK

Después de cada byte enviado el maestro espera una bit de reconocimiento procedente desde el esclavo, para indicar el estado de la operación.

Para ello, el maestro durante el bit 9 de la trama libera el bus, lo cual permite al esclavo modificar la línea SDA de datos.

Si el envió del maestro llega correctamente, el esclavo pondrá un bit ACK llevando la línea SDA a LOW. Si algo ha fallado, la línea SDA permanecerá HIGH indicando un estado de NACK (Not Acknowledge - No Reconocimiento).

La condición de NACK pueden ocurrir por:

  • El esclavo no es capaz de recibir ni de transmitir por que está ocupado realizando alguna tarea y no responde.
  • Durante una transferencia el esclavo recibe un dato o comando que no entiende.
  • Durante una transferencia el esclavo no puede recibir mas datos.
  • Cuando el maestro esta listo para leer datos y se lo indica al esclavo con un NACK.

Escribiendo en un esclavo

Para escribir en el bus, el maestro envía la condición START, después envía la dirección del esclavo seguido del bit de R/W puesto a 0 (indicando una escritura). El esclavo enviará el bit ACK. Entonces el maestro enviará el dirección del registro que se quiere escribir y leerá el ACK por parte del esclavo. Luego el maestro empezará enviar datos todos, leyendo cada 8 bits el bit de ACK. Cuando haya terminado de mandar datos, el maestro generará la señal de STOP.

Leyendo datos de un esclavo

Leer datos de un esclavo es muy similar a una escritura pero con algún paso extra. Para leer datos del esclavo el maestro primero le tiene que indicar el registro desde el cual quiere leer. Para hacer esto empieza la transmisión enviando la dirección con el registro R/W a 0 indicando una escritura, y enviando posteriormente la dirección del registro que queremos leer. Cuando el esclavo ya conoce el registro, el maestro vuelve a enviar una condición de START (condición de START repetida) y envía la dirección del esclavo con bit R/W a 1 (indicando una lectura).

A partir de aquí, el maestro se vuelve receptor, liberando la línea SDA pero continua dando la señal de reloj. El esclavo entonces será capaz de enviar datos en cada ciclo de reloj al maestro.

Por cada byte que envía el esclavo, el maestro generará el bit de ACK para indicarle que lo ha recibido. Una vez que el maestro ha recibido el número de datos requeridos este genera un NACK para indicarle que ya no quiere más y genera la señal de STOP.

Pasemos ahora a ver como utilizar Arduino con la librería Wire.

Referencia de la librería Wire de Arduino.

begin() - inicia la librería Wire. Solo admite un parámetro que es la dirección de esclavo que tomará. Si no le pasamos ningún parámetro será el maestro del bus.

requestFrom(a,q,s) - se utiliza para pedir datos al esclavo. 'a' es la dirección del esclavo, 'q' la cantidad de bytes que queremos y 's' es un booleano que es opcional. Si 's' es true, mandará un mensaje de parada (stop) después de la petición, liberando el bus. Si 's' es false, enviará un reinicio después de la petición manteniendo la conexión al bus. Generalmente 's' no se utiliza. La función devolverá el número de bytes enviados por el esclavo.

beginTransmission(a) - inicia la transmisión con el esclavo cuya dirección es 'a'. En realidad, no hace nada, solo prepara el buffer de envió rellenando solo con la dirección. Después de iniciar la transmisión usaremos la función write para enviar datos.

endTransmission() - Finaliza la transmisión. Ahora es cuando se envía el buffer con los bytes que habíamos escrito usando write(). Acepta un parámetro opcional que es del tipo booleano. Si es true, manda un mensaje de parada liberando el bus; si es false, manda un mensaje de reinicio y no libera el bus. Esta función retorna un byte cuyo valor puede ser:

0Todo correcto.
1Demasiados datos en el buffer.
2Recibió un NACK en la transmisión de la dirección.
3Recibió un NACK en la transmisión de datos.
4Otro error.

write() - Se comporta de dos maneras diferentes según el rol que tenga el dispositivo. Si es un esclavo, se utiliza para enviar datos cuando el maestro le ha pedido datos mediante una petición (requestFrom). Si es un maestro, encolará los bytes para que se enviarán, estará entre las funciones beginTransmision/endTransmision.

available() - Devuelve el número de bytes disponibles en el buffer de recepción. Generalmente la usaremos en el maestro para comprobar que hay datos después de realizar un requestFrom. En el esclavo se usará dentro del manejador onReceive().

read() - Lee el búfer de recepción, que previamente habremos comprobado con available que hay datos. En el caso del maestro después de realizar una petición con requestFrom.

onReceive(q) - Registra una función que será llamada cuando el esclavo reciba una transmisión del maestro. El parámetro 'q' indica el número de bytes recibidos del maestro.

onRequest() - Registra una función que será llamada cuando el esclavo reciba una petición de datos del maestro.

Ejemplos para leer un dispositivo I2C

Veamos un ejemplo de como usar la librería Wire. Para ello conectaremos reloj en tiempo real DS3232 al Arduino y subiremos el siguiente programa (en los comentarios están las explicaciones):

/* 
 *  Ejemplo de lectura de la hora de un RTC DS3232
 */
 
#include <Wire.h>

#define I2C_SLAVE 0x68

byte hora;
byte minuto;
byte segundo;

void setup() {
  // Iniciamos el Arduino como maestro del bus I2C.
  Wire.begin();  
  // Iniciamos el puerto seria para debug.
  Serial.begin(9600);
}

void loop() {
  // Hemos de indicarle al esclavo desde que registro queremos leer
  // los datos. En el DS3232 tenemos los siguientes registros:
  // 0x00 - Segundos.
  // 0x01 - Minutos.
  // 0x02 - Horas.
  Wire.beginTransmission(I2C_SLAVE);
  Wire.write(0x00);
  Wire.endTransmission();

  // Ahora realizamos la petición de datos, solo queremos 3 
  // registros:
  Wire.requestFrom(I2C_SLAVE, 3);

  // Comprobamos que hemos recibido datos y los mostramos.
  if ( Wire.available() ) {
    segundo = Wire.read();
    minuto  = Wire.read();
    hora    = Wire.read();
    // La hora, minuto y segundo esta codificado en BCD (binario codificado
    // decimal), lo que significa que dentro del byte se ha codificado las
    // unidades y decenas en binario, por eso hay que transformar para que
    // se muestre correctamente. Recomiendo ver la hoja de datos del chip
    // para ver como funciona.
    Serial.print(hora>>4 & 0x01);
    Serial.print(hora&0xf);
    Serial.print(F(":"));
    Serial.print(minuto>>4&0x0f);
    Serial.print(minuto&0x0f);
    Serial.print(F(":"));
    Serial.print(segundo>>4 & 0x0f);
    Serial.println(segundo&0xf);
  }
  delay (1000);
}

En el siguiente ejemplo veremos como un Arduino Uno que hará de esclavo lee un encoder y le pasa el valor a otro Arduino (maestro).

He aquí el código del MAESTRO:

#include <Wire.h>

#define i2c_slave  0x20

long dato;
long dato2;

void setup() {
  Wire.begin();
  Serial.begin(9600);
  envio();
  delay(1000);
}


void loop() {
  peticion();
  Serial.println(dato2);
}

void envio() {
  Wire.beginTransmission(i2c_slave);
  Wire.write((byte*)&dato, sizeof(dato));
  Wire.endTransmission();
}

void peticion() {
  Wire.requestFrom(i2c_slave, 4);
  uint8_t i=0;
  byte *p = (byte*)&dato2;
  while ( Wire.available() )
  {
    *(p+i)=Wire.read();
    i++;
  }
}

En el esclavo usaremos la librería PinChangeInterrupt para leer el encoder. Cuando el maestro envía datos se establece la posición del encoder (útil para poder ponerlo a cero) y cuando solicita datos el esclavo le indica la posición del encoder.

#include <Wire.h>
#include <PinChangeInterrupt.h>
#include <PinChangeInterruptBoards.h>
#include <PinChangeInterruptPins.h>
#include <PinChangeInterruptSettings.h>

#define i2c_slave 0x20

#define FASEA 3
#define FASEB 4

long    posicion;
uint8_t anterior;
uint8_t actual;

void isr() {
  actual = digitalRead(FASEA)<<1 | digitalRead(FASEB);
  switch ( actual ) {
    case 0:
      if ( anterior==2 ) posicion++;
      if ( anterior==1 ) posicion--;
      break;
    case 1:
      if ( anterior==0 ) posicion++;
      if ( anterior==3 ) posicion--;
      break;
    case 2:
      if ( anterior==3 ) posicion++;
      if ( anterior==0 ) posicion--;
      break;
    case 3:
      if ( anterior==1 ) posicion++;
      if ( anterior==2 ) posicion--;
      break;
    default:
      break;
  }
  anterior = actual;
}


void eventoRecepcion(int bytes) {
  byte  *p = (byte*)&posicion;
  uint8_t i=0;
  while ( Wire.available() ) 
  {
    *(p+i) = Wire.read();
    i++;
  }
  
}

void eventoPeticion() {
  Wire.write((byte*)&posicion, sizeof(posicion));
}

void setup() {
  Serial.begin(9600);
  Wire.begin(i2c_slave);
  Wire.onReceive(eventoRecepcion);  
  Wire.onRequest(eventoPeticion); 
  attachPinChangeInterrupt(digitalPinToPinChangeInterrupt(FASEA), isr, CHANGE);
  attachPinChangeInterrupt(digitalPinToPinChangeInterrupt(FASEB), isr, CHANGE); 
}

void loop() {
} 
En el artículo ENCODERS está la información necesaria para saber como funciona un encoder y como leerlo con Arduino.

Referencias

Application Report SLVA740: Undertating I2C Bus