El I2C es un bus serie para la transferencia de datos. Se usa para comunicar dos partes de un circuito, como podría ser un controlador con otros circuitos periféricos. Utiliza sólo tres cables, uno por donde el maestro envía la señal de reloj (SCL), otro bidireccional, por donde se transfieren los datos (SDA) y por último una tierra común.
Para que la comunicación funcione tiene que haber un protocolo, ya que no se puede emitir por el mismo cable a la vez. Una vez establecida la comunicación, mientras un dispositivo está enviando información, el otro ha de estar escuchando.
Concepto teórico
El I2C funciona como un circuito de colector abierto, esto significa que el chip puede poner su salida a baja o dejar la línea abierta (alta impedancia), es decir, puede poner la línea a tierra, pero no puede ponerla por sí mismo a alta. Para que las líneas de SCL y SDA puedan tener un “1 lógico” será necesario una fuente externa y poner resistencias de “pull-up”. En caso de trabajar con más de un dispositivo, sólo hará falta poner una resistencia para cada línea (SCL y SDA), no por cada dispositivo.
El esquema se puede ver en la imagen superior. El transistor Mosfet funciona como un interruptor. Cuando está abierto, deja pasar Vin, poniendo en el cable esa tensión, y cuando está cerrado, conduce la línea a tierra, lo que se traduce en una tensión de 0V. De esta forma es como se generan los “1” y “0” en este bus.
El estándar I2C permite tener múltiples dispositivos conectados al mismo bus, pero para que se entiendan es necesario un protocolo de comunicación. Todos los dispositivos estarán escuchando hasta que detecten un cambio particular en las líneas que indique el comienzo de transferencia. A continuación, el máster enviará una dirección para referirse a uno de los dispositivos y comenzar la comunicación con él. Hay casos en los que un esclavo también puede iniciar una comunicación con otro esclavo.
Protocolo
El protocolo permite dos operaciones distintas, la escritura y la lectura, empezaré con el primero de ellos.
Cuando el sistema está inactivo, las dos líneas están en alta impedancia, por lo que al tener conectadas las resistencias “pull-up”, las dos líneas están en “alta”. Para indicar el comienzo de una operación (condición de inicio), la línea de datos tiene que ponerse a ‘0’ antes que la del reloj. El reloj empezará a funcionar marcando el ritmo de la transmisión. Para terminar la transmisión (condición de parada), hay que poner el canal SCL a ‘1’ antes que el SDA. Seguirán ambos a ‘1’ hasta que se vuelva a hacer la condición de inicio.
En el protocolo hay que tener en cuenta que cada 8 bits hay un ACK. Lo primero que se hace es enviar la dirección del dispositivo (7bits) seguido de un bit de lectura/escritura. El ‘0’ indica que se desea escribir y el ‘1’ que se desea leer. El esclavo responderá con un ACK, que el maestro leerá y lo tendrá en consideración. El ACK consiste en poner la línea a ‘0’ si todo va bien, y a ‘1’ en caso contrario
Para la escritura, después del ACK, se envía la dirección del registro donde se desea escribir (1Byte), y se espera de nuevo al ACK por parte del esclavo. Por último, se envía el dato que se desea escribir (1Byte), se espera al ACK y se termina con la condición de parada, que consiste en SCL en alta antes que SDA. En caso de que se quiera escribir en dos o más registros consecutivos, se enviaría el siguiente dato después del ACK y se terminaría con la condición de parada.
El caso de la lectura es un poco diferente, pero sigue siendo sencillo. Se empieza la trama como si fuera una lectura, con su condición de inicio, la dirección del dispositivo seguido del bit de escritura y después del ACK se envía la dirección del registro que se desea leer. Se espera al ACK. Aquí viene lo diferente. Después del ACK, se vuelve a repetir la condición de inicio, y se envía de nuevo la dirección del dispositivo seguido del bit de lectura, el esclavo escribirá el bit de ACK y el maestro se quedará escuchando. El esclavo está enviando el contenido del registro que se le ha solicitado con anterioridad, el maestro debe de leerlo y almacenarlo, ya que le están respondiendo. Si se desea seguir leyendo registros consecutivos se le envía al esclavo un ACK a ‘0’, en caso de querer finalizar la comunicación se le manda un NACK y se efectúa la condición de parada.
Implementación en hardware y montaje
Materiales:
- Software:
- Quartus II Lite 18.0
- ModelSim
- Digiview 9 (Software del analizador lógico)
- Hardware
- Terasic DE-1 SoC (Placa de desarrollo)
- NXP OM13488 (Expansor de GPIO)
- Cables y dos resistencias de 10kΩ
- Otros recursos: Polímetro, osciloscopio y analizador lógico.
Una vez estudiado el funcionamiento del protocolo lo he implementado en VHDL. Hasta la fecha había usado el bus I2C con Arduino y PIC24, pero había usado librerías predefinidas para comunicarme con los dispositivos. En este caso he tenido que bajar de nivel y escribir en hardware todo el protocolo de comunicación, teniendo en cuenta cómo generar las señales, la importancia de la sincronía, el envío y recepción de datos y la interconexión de los circuitos.
Para probar el protocolo sobre un hardware real he usado una placa de pruebas que funciona como expansor de GPIO. He tenido que estudiarla, ver sus circuitos internos, mover algunos jumpers para configurarla… Tenía bastante más trabajo que el conectarla y esperar a que funcionara. Las pruebas que he realizado han sido tanto con un osciloscopio como con un analizador lógico, para ver toda la trama.
Este ha sido el montaje:
He utilizado una placa de desarrollo Terasic DE-1 con una FPGA Cyclone V de Altera/Intel. Del puerto de GPIO de la placa he sacado una tensión de 3.3V para alimentar el periférico y he configurado otros dos puertos digitales para las señales de SCL y SDA. Todo está ordenado por colores, de forma que la alimentación son los cables rojo y negro, el color verde es para la línea SCL y el amarillo para el SDA. El valor de las resistencias de pull-up es de 10kΩ.
He utilizado una placa de prototipado para poner las resistencias «pull-up» en las líneas de reloj y datos. Aunque una ristra de cables vaya al lado izquierdo de la placa, se puede acceder también por el lado derecho. He comprobado con un polímetro que internamente tienen la misma ruta, es decir, he mirado la continuidad.
En la imagen superior se observa cómo he conectado el osciloscopio para ver la señal que estoy generando por la salida del octavo pin. Lo he configurado como salida y estoy escribiendo alternativamente ‘0’ y ‘1’.
Tras hacer las pruebas, los datos obtenidos son satisfactorios. Puedo sacar una señal de GPIO a través del periférico y manipularla. Si conecto un generador de funciones y lo configuro como entrada, puedo leer esa línea del periférico a través de la FPGA. Y si escribo en los registros de la tarjeta expansora, también puedo leerlos en la FPGA y comprobar que la escritura se ha hecho correctamente. Estas son las capturas que he obtenido (ambas son iguales, pero una está ampliada):
En esta primera captura accedo al dispositivo 72h, esta es su dirección. Entro a escribir en el registro 03h, que es el de configuración. Determina que pines son de entrada y cuales de salida. Pongo los cuatro primeros bits a ‘0’ (lectura) y los cuatro últimos a ‘1’, escritura 0Fh.
La segunda escritura la hago en el registro 01h, que es el de datos de escritura. Los pines que he puesto como entrada da igual que tengan un ‘0’ o un ‘1’, se van a tratar igual, pero en los de escritura, si pongo un ‘1’ saldrá voltaje de 3.3V y si pongo un ‘0’ no habra tensión. Alterando el valor de este registro puedo generar una señal GPIO.
La última parte de la trama se corresponde a una lectura. Leo uno de los registros internos de la memoria, concretamente el 01h. Veo que me devuelve el dato con el valor que le he asignado. Se termina con el envío de un NACK y la condición de parada.
En los siguientes enlaces se puede encontrar el código generado en VHDL y el repositorio de adquisición de datos.
Repositorio de adquisición de datos
Protocolo I2C en VHDL para FPGA