Recuerdo la primera vez que vi estas siglas en la hoja de datos, si no mal recuerdo se trataba de un sensor de distancia por ultrasonido, por lo tanto si quería trabajar con el sensor de distancia tenia que saber de que se trataba el I2C.
En este tutorial vamos a ver paso a paso como se puede implementar este protocolo de comunicación de forma fácil y rápida, algo así como:
Implementando I2C sin morir en el intento y sin protoboards lanzadas por la ventana.
No es que el protocolo en sí sea complicado, de echo es uno de los mas fáciles de entender, pero como siempre, tiene su truquillo para hacerlo funcionar correctamente.
Por cierto, antes de seguir adelante, este tutorial va a utilizar un microcontrolador de microchip, el código esta escrito en ensamblador, y es necesario contar con un microcontrolador que tenga al menos un modulo Master Synchronous Serial Port (MSSP), yo voy a utilizar el PIC16F1827, pero repito, cualquier microcontrolador con un modulo de comunicación sirve.
IMPORTANTE:
Este proyecto utiliza un microcontrolador de microchip, por lo que es necesario tener un programador de picmicros o cualquier microcontrolador con modulo MSSP.
Teoría:
¿Que es I2C?
I cuadrada C o I2C viene de las siglas Inter Integrated Circuit, es un protocolo de comunicación síncrono y bidireccional que permite el intercambio de información de manera rápida y fiable entre uno o más dispositivos.
¿Protocolo?
Un protocolo de comunicación no es otra cosa más que una serie de pasos (secuencias) que se deben seguir para poderse comunicar dos o más dispositivos.
Aunque ambos dispositivos hablen el mismo idioma (binario), no se podrían comunicar debido a diferentes factores, por ejemplo: ambos hablarían al mismo tiempo, o bien uno habla y el otro no le responde, etc; Por lo tanto es necesario tener un protocolo que les permita comunicarse de forma ordenada, de tal manera que puedan intercambiar información.
Existen diferentes protocolos de comunicación, tanto síncoronos como asíncronos por ejemplo:
SPI, RS232, One wire, 4-20mA, RS485, TCP, AS-interface, etc ....
Todo ellos tienen secuencias especificas, o sea cada uno tienen un protocolo especifico para poderse comunicar con ellos.
El modelo I2C se basa en maestro - esclavo, esto quiere decir que el maestro manda el pulso de inicio y fin de la comunicación, mientras que el esclavo su única función es enviar o recibir información, una analogía directa sería el modelo que utiliza Internet, el cual esta basado en cliente - servidor, el servidor es el que manda la estructura y secuencias de la pagina web, mientras que el cliente recibe estos datos y los muestra en pantalla.
El protocolo I2C utiliza dos lineas de comunicación:
- SDA: Linea de datos.- envía datos desde o para el maestro.
- SCL: Linea de reloj.- controla cuando los datos son enviados y cuando han sido leídos.
Desmenuzando el protocolo I2C
NOTA: El microcontrolador se encarga de todo esto, por lo tanto no es necesario leerlo a menos que quieras entender mejor como funciona el protocolo.
Al principio mencione que I2C es un protocolo síncrono, esto quiere decir que antes de comenzar el intercambio de información, el primer paso es informar al dispositivo esclavo que queremos establecer un intercambio de datos.
Para lograr esto el dispositivo maestro inicializa el bus de la siguiente manera:
Antes:
•SDA = 1
•SCL = 1
Después:
•SDA = 0
•SCL = 1
El pin SDA pasa de uno a cero, esta es la secuencia de inicio Start (en algunas hojas de datos la identifican con la letra S).
Una vez inicializada la comunicación, se envía un bloque que tiene una longitud de 1 byte (8 bits), la linea SDA controla el valor del bit, mientras que SCL debe estar en alto para que el dato sea valido.
•SDA = Controla el valor del bit, por lo tanto, si SDA = 1 entonces el bit tendrá un valor de uno, y viceversa.
•SCL = Los datos solo serán validos en el flanco de subida.
En esta imagen podemos ver lo que llevamos hasta ahora, podemos ver la secuencia de inicio, y el intercambio de datos, observa como los datos son validos solamente cuando SCL esta cambiando de estado de cero a uno (flanco de subida), en la practica esta primer secuencia corresponde al Control byte.
Cuando terminamos de mandar los datos del Control Byte, necesitamos que el esclavo nos indique que recibió los datos y por lo tanto, ya esta listo para recibir nuevos datos, a esto se le conoce como Acknowledges o simplemente ACK para lograr tal cometido el esclavo pone en bajo la linea SDA en el noveno pulso de SCL.
•SDA = 0
•SCL = Noveno pulso de reloj.
Alguno dispositivos necesitan que mantengamos la linea de datos en alto para detener la transmisión, esto se conoce como Negatively acknowledges o simplemente NACK, el principio de funcionamiento es el mismo que ACK, lo único que cambia es que en lugar de poner a cero SDA se mantiene en uno.
•SDA = 1
•SCL = Noveno pulso de reloj.
Una vez que ya hemos transmitido todos los datos es necesario indicarle al dispositivo esclavo que ya puede dejar la linea, ya sea para que el maestro se dedique a otra tarea o bien para comunicarse con otro dispositivo, para lograr esto, el maestro manda la secuencia de Stop o simplemente P.
•SDA = 1
•SCL = 1
Una vez que terminamos de mandar el bloque de datos mandamos la secuencia NACK seguida de una secuencia STOP, para liberar las lineas.
NOTA:
En la practica existe una secuencia particular que se debe utilizar si por ejemplo queremos escribir y luego leer los datos, o bien escribir en forma secuencial, esto es muy común en las memorias EEPROM, y algunos sensores, esta secuencia se conoce como Restart o simplemente R.
Se compone de dos secuencias la primera seria P seguida de S.
•SDA = 1
•SCL = 1
•SDA = 0
•SCL = 1
Esto seria todo en cuanto al protocolo I2C, por ultimo ten en cuenta que este protocolo se puede emular mediante software, por lo tanto es un protocolo que te puede servir si por algún motivo te quedas solo con dos pines libres y necesitas mandar datos.
Hardware:
El diagrama de conexión está creado en diptrace, presiona sobre la imagen para descargarlo.
Lista de materiales:
• Microcontrolador (PIC16F1827)
• Memoria EEPROM (24LC512)
• Regulador de voltaje (LM7805)
• 2 resistencias de 10k Ω @ 1/4 watt
• Capacitor electrolítico de 1µF @ 16 Volts
• Pila o batería de 9 Volts
Hardware para comprobar la lectura aleatoria:
• Microcontrolador (PIC16F1827)
• Memoria EEPROM (24LC512)
• Regulador de voltaje (LM7805)
• 2 resistencias de 10k Ω @ 1/4 watt
• 3 resistencias de 330 Ω @ 1/4 watt
• 3 diodo led de cualquier color
• Capacitor electrolítico de 1µF @ 16 Volts
• Pila o batería de 9 Volts
Por parte del hardware lo importante es lo siguiente:
• Los pines de la memoria EEPROM A0,A1,A2 y WP se conectan a tierra.
• Las resistencia pull-up del bus dependen de dos factores la capacitancia de la placa y la velocidad de transmisión, para saber que valor utilizar revisa las hojas de datos del dispositivo y del microcontrolador, por lo general estos valores van desde 1kΩ hasta 10kΩ, para este tutorial voy a utilizar un valor de resistencia de 10kΩ.
Código ASM:
Una cosa que he notado con los PIC16F1827 a diferencia de su antecesor PIC16F886, es que el reloj interno esta descalibrado de fabrica, no sé si son solo mis microcontroladores pero he programado en dos chips diferentes y es la misma cosa pero bueno, no importa, por fortuna podemos calibrar el reloj interno, esto lo menciono porque en los archivos asm que suelo dejar para que comprueben su código van a notar un segmento que no tiene nada que ver con la configuración del modulo MSSP así que de una vez lo pongo para que no se espanten.
NOTA: El código no utiliza las definiciones del archivo inc.
Esto es por dos razones, la primera es porque me gusta explorar el microcontrolador, ir a su banco y echarle ojo a los registros, y la segunda es porque le agrega un grado de ofuscación al código, así si por algún motivo alguien quiere usar tu código, de mínimo le va a costar trabajo interpretarlo, pero bueno ya queda al gusto de cada quien, de todas maneras a la derecha dejo su registro correspondiente para que lo cambien en caso de usar el inc.
;Configurando el reloj a 8MHZ
bsf 0x08,0 ;Banco 1
movlw 0x72
movwf 0x99 ;OSCCON
btfss 0x9A,4;OSCSTAT
goto $-1
btfss 0x9A,3
goto $-1
btfss 0x9A,0
goto $-1
Empezamos con la escritura-lectura de la memoria EEPROM, en este caso voy a utilizar la 24LC512.
Si leyeron la teoría, la primer secuencia después de iniciar la comunicación corresponde al Byte de control, pues bien, aquí esta el porque se le llama así, se debe a que este byte contiene el nibble de control < 4bits>, dirección del dispositivo < 3 bits> y el modo de operación (escritura/lectura) < 1 bit>.
El nibble de control, no es otra cosa si no un identificador, por ejemplo, supongamos que tenemos una memoria 24LC512 y un RTCC sobre el mismo bus, si quisiéramos acceder a la memoria, obviamente necesitaríamos un identificador, caso contrario, estaríamos accediendo al RTCC o viceversa, es por eso que cada dispositivo cuenta con un identificador.
Los siguientes 3 bits corresponden a la dirección, la memoria tiene 3 pines para indicar la dirección, los pines que lo controlan son los siguientes:
- A0
- A1
- A2
El valor va a depender de la conexión de los pines, por ejemplo, si ponen A0,A1 y A2 a gnd (Vss), este tendría un valor de 000, caso contrario, si A0 =+5V (Vcc), A1 y A2 los ponemos a tierra (Vss), el valor seria 001, y así sucesivamente.
Por lo tanto, esto quiere decir que sobre el mismo bus podemos acceder hasta 8 memorias diferentes, haciendo cuentas, esta memoria es de 8x64K = 512Kbytes, ahora 8x512k = 4096kbytes = 4 Mbytes de memoria.
El último bit, establece el modo de escritura/lectura, para la memoria 24LC512 el byte de control para:
•Escritura: <nibble>< 3 bits de dirección><0>
•Lectura: <nibble>< 3 bits de dirección><1>
Ejemplo practico, memoria 24LC512:
El nibble (id) para este dispositivo es 1010 (0xA), los siguientes 3 bits corresponden a la dirección del dispositivo (depende de la conexión de los pines A0,A1 y A2), entonces:
Para escritura el Byte de control sería: 0xA0 ↦ [id = 1010] [dirección = 000] [modo = 0] (escritura)
Para lectura el Byte de control sería: 0xA1 ↦ [id = 1010] [dirección = 000] [modo = 1] (lectura)
Escritura
Podemos escribir los datos de dos formas diferentes, ya sea de manera secuencial (page write), donde solo es necesario indicar la posición de inicio y la memoria se encarga automáticamente de guardar los datos en la posición siguiente, o de forma manual (byte write), donde tu le indicas en que posición quieres guardar el dato, para datos pequeños, algo asi como 4 registros o menos supongo que la manera manual esta bien, pero ya para datos muy grandes estar asignando las direcciones no se me hace muy eficiente, pero bueno, tu lo puedes hacer como más se adapte a tus necesidades.
Como soy muy buena persona lo vamos a hacer de las dos maneras, solo no olvides presionar el botón de donaciones.
Ya sea que utilices byte o page write en ambos casos necesitamos una subrutina para saber si el modulo esta listo para enviar y recibir datos al dispositivo esclavo.
Subrutina de espera:
i2c_espera_escritura clrf 0x08 ;Banco 0
btfss 0x11,3 ;SSP1IF = 1?
goto $-1 ;El modulo I2C todavia no esta listo
bcf 0x11,3 ;SSP1IF =0 (El modulo ya esta listo)
bsf 0x08,2 ;Banco 4 (Los registros del modulo estan en el banco 4)
return ;Necesita explicación? (Salimos de la subrutina)
Byte Write
bsf 0x216,0 ;SSP1CON2 (Secuencia S)
call i2c_espera_escritura ;Esta listo el modulo?
movlw 0xA0 ;Byte de control para escritura.
movwf 0x211 ;SSP1BUF (Enviamos el dato por la linea)
call i2c_espera_escritura ;Esta listo el modulo?
btfsc 0x216,6 ;ACKSTAT = 0? (ACK)
goto $-1 ;Esperamos el ACK
movlw 0x00 ;Adress High
movwf 0x211 ;SSP1BUF
call i2c_espera_escritura
btfsc 0x216,6 ;ACKSTAT = 0?
goto $-1
movlw 0x00 ;Adress Low
movwf 0x211 ;SSP1BUF
call i2c_espera_escritura
btfsc 0x216,6 ;ACKSTAT = 0?
goto $-1
movlw 0x0E ;Valor del dato
movwf 0x211 ;SSP1BUF
call i2c_espera_escritura
btfsc 0x216,6 ;ACKSTAT = 0?
goto $-1
bsf 0x216,1 ;R (Recuerdas la teoria?)
call i2c_espera_escritura ;Esta listo el modulo?
movlw 0xA0 ;Byte de control para escritura.
movwf 0x211 ;SSP1BUF (Enviamos el dato por la linea)
call i2c_espera_escritura ;Esta listo el modulo?
btfsc 0x216,6 ;ACKSTAT = 0? (ACK)
goto $-1 ;Esperamos el ACK
movlw 0x00 ;Adress High
movwf 0x211 ;SSP1BUF
call i2c_espera_escritura
btfsc 0x216,6 ;ACKSTAT = 0?
goto $-1
movlw 0x01 ;Adress Low
movwf 0x211 ;SSP1BUF
call i2c_espera_escritura
btfsc 0x216,6 ;ACKSTAT = 0?
goto $-1
movlw 0x0D ;Valor del dato
movwf 0x211 ;SSP1BUF
call i2c_espera_escritura
btfsc 0x216,6 ;ACKSTAT = 0?
goto $-1
bsf 0x216,2 ;STOP = 1
call i2c_espera_escritura
goto $+0 ;Fin
Eso sería todo en cuanto al método por byte write, como te puedes dar cuenta no es muy eficiente porque tienes que volver a mandar el byte de control cada vez que quieres añadir un nuevo dato, el más afectado sería la memoria programa del microcontrolador, por lo tanto no lo recomiendo, pero como siempre, todo depende de ti y tu aplicación.
Page Write
Para utilizar la escritura secuencial, es necesario definir dos variables, que nos van a ayudar a controlar la escritura.
clrf 0x220 ;Index del Program Counter.
movlw 0x08 ;Numero total de registros.
movwf 0x221
bsf 0x216,0 ;SPP1CON2 (Secuencia S)
call i2c_espera_escritura ;Esta listo el modulo?
movlw 0xA0 ;Byte de control para escritura
movwf 0x211 ;SSP1BUF (Lo enviamos por la linea)
call i2c_espera_escritura
btfsc 0x216,6 ;ACKSTAT = 0?
goto $-1
movlw 0x00 ;Hight adress byte
movwf 0x211 ;SSP1BUF
call i2c_espera_escritura
btfsc 0x216,6 ;ACKSTAT = 0?
goto $-1
movlw 0x00 ;Low adress byte
movwf 0x211 ;SSP1BUF
call i2c_espera_escritura
btfsc 0x216,6 ;ACKSTAT = 0?
goto $-1
n_datos movf 0x220,0
call datos
movwf 0x211 ;SSP1BUF
call i2c_espera_escritura
btfsc 0x216,6 ;ACKSTAT = 0?
goto $-1
incf 0x220,1
decfsz 0x221,1
goto n_dato
bsf 0x216,2 ;STOP = 1
call i2c_espera_escritura
goto $+0 ;Fin
datos addwf 0x02,1
dt 0x00,0x01,0x02,0x3,0x4,0x5,0x06,0x07 ;Datos a guardar en la EEPROM
Esto seria todo en cuanto a la programación por pagina, con solo este pequeño código podemos almacenar la cantidad de información que queramos, desde 4 bytes hasta 4Mb, para esta ejemplo solo guardamos ocho datos (0x00-0x07).
¿Te atreves a intentarlo con byte write?.
Lectura
Al igual que la escritura, la lectura se puede hacer dos maneras distintas, la primera es secuencial, en este modo necesitamos indicarle desde que posición de memoria (registro) queremos empezar y la memoria se encarga de ir automáticamente a la siguiente, y el segundo método es de manera aleatoria, en este modo se puede acceder a cualquier registro de la memoria de manera aleatoria, ambos modos utilizan la misma secuencia de inicialización, la única diferencia es que en modo secuencial después de que se transmite el primer byte de datos, el maestro le envia un ACK en lugar de un STOP como sucede en el modo aleatorio.
Hasta el día de hoy solo he utilizado el modo secuencial un par de ocaciones y eso para tareas muy especificas, en la practica he utilizado más el modo aleatorio, este modo es especialmente útil cuando por ejemplo, quieres mostrar un formato de fecha especifico basado en el DS1307, o quieres leer los registros de manera decremental, cosa que no se podría hacer en modo secuencial debido a que es n+1 posiciones de memoria.
NOTA: Con lo anterior no quiero decir que no es posible lograrlo de manera secuencial, por ejemplo se podría hacer la lectura de todos los registros de memoria, almacenarlos en algún registro ram local y después darle el formato, la única desventaja que le veo es que tienes que utilizar RAM y pudiera ser que mas ciclos de reloj de los necesarios.
Vamos a utilizar de nuevo la subrutina de i2c_espera_escritura, de echo vamos a definirla con otro nombre para que no haya confusiones, pero ten en cuenta que son exactamente lo mismo.
i2c_espera clrf 0x08
btfss 0x11,3
goto $-1
bcf 0x11,3
bsf 0x08,2
return
Lectura Aleatoria
clrf 0x220 ;Indice Program Counter
movlw 0x09
movwf 0x221 ;Numero total de registros+1
inicio decfsz 0x221,1
goto $+2
goto$+0
repetir bsf 0x216,1
call i2c_espera
movlw 0xA0 ;Control Byte (Escritura)
movwf 0x211 ;SSP1BUF
call i2c_espera
btfsc 0x216,6 ;ACKSTAT=0?
goto repetir ;Algunas veces es necesario volver a enviarle toda la secuencia
movlw 0x00 ;Hight address byte
movwf 0x211 ;SSP1BUF
call i2c_espera
btfsc 0x216,6 ;ACKSTAT=0?
goto $-1
movf 0x220,0
call registros ;Orden de las localidades de memoria a leer
movwf 0x211 ;SPP1BUF
call i2c_espera
btfsc 0x216,6 ;ACKSTAT=0?
goto $-1
bsf 0x216,1 ;RSEN
call i2c_espera
movlw 0xA1 ;Control Byte (Lectura)
movwf 0x211 ;SSP1BUF
call i2c_espera
btfsc 0x216,6 ;ACKSTAT=0?
goto $-1
bsf 0x216,3 ;RCEN=1 ;Obtenermos el dato
call i2c_espera
bsf 0x216,5 ;ACKDT=1
bsf 0x216,4 ;ACKEN=1
call i2c_espera
bsf 0x216,2 ;PEN=1
call i2c_espera
movf 0x211,0 ;Guardando lo recibido
call transmite ;Enviando el dato
incf 0x220,1
goto inicio
registros addwf 0x02,1
dt 0x07,0x06,0x05,0x04,0x03,0x02,0x01,0x00 ;Registros con la secuencia de lecura
Con este código podemos leer de forma secuencial incremental, secuencial decremental o aleatoria la memoria EEPROM, por cierto la llamada a la subrutina transmite depende de su aplicación pueden por ejemplo sacar los datos por el puerto, o guardarlos en la EEPROM del microcontrolador, tal vez hacer operaciones, etc, la puse para que sepan en que parte ya pueden utilizar los datos.
Si tienen alguna duda de como usarlo, en la sección de Resultados pueden encontrar el código para leer la memoria EEPROM y mandar la salida por el puerto A del microcontrolador, espero que con esto les quede más claro como se utiliza.
ADVERTENCIA 1: Recuerden que los registros que controlan el MSSP para este microcontrolador están en el banco #4, por lo tanto su subrutina a la salida debe de poner de nuevo el registro BSR en 4.
ADVERTENCIA 2: Este código es validó únicamente para comunicarnos con la memoria 24LC512 o cualquier otro dispositivo que tenga las mismas secuencias, esto es debido a que utiliza dos bytes para establecer la dirección del registro, esto no quiere decir que no se pueda usar con otro dispositivo, solo es necesario revisar la secuencia y quitar ya sea el Hight o Low Address Byte.
Resultado y palabras finales:
Aquí pongo las salidas de los diferentes metodos que utilizamos.
Borrando la memoria
Voy a borrar la memoria para que no queden datos anteriores, se puede sobrescribir los datos, pero mejor empezamos con una memoria completamente limpia.
Byte Write
Como puedes observar en las posiciones 0x00 y 0x01 de la memoria EEPROM se encuentran los datos 0x0E 0x0D, por lo tanto el código funciona.
Si por algún motivo no obtuvieron el mismo resultado les dejo los archivos para que lo prueben en su hardware.
Archivo con código asm: i2c_byte.asm
Archivo Hex: i2c_byte.hex
Page Write
Se puede ver la secuencia que le enviamos a la memoria 0x01,0x02,0x03,0x04,0x05,0x06, y 0x07.
Si por algún motivo no obtuvieron el mismo resultado les dejo los archivos para que lo prueben en su hardware.
Archivo con código asm: i2c_page.asm
Archivo Hex: i2c_page.hex
Lectura aleatoria
Dicen que una foto dice mas que mil palabras, entonces supongo que un vídeo son como un millón, aquí se comprueba que el código de lectura funciona, si lo quieres comprobar por ti mismo el diagrama de conexión se encuentra en la sección hardware de este tutorial.
Si por algún motivo no obtuvieron el mismo resultado les dejo los archivos para que lo prueban en su hardware.
Archivo con código asm: i2c_random.asm
Archivo Hex: i2c_random.hex
Esto sería todo en cuanto a este tutorial del protocolo I2C, ahora ya puedes acceder al amplio abanico de dispositivos, sensores y demás circuitos que utilizan este protocolo, ¡vamos!, que esperas, ve a comprar unos cuantos.
No se olviden de recomendar esta pagina en facebook, twitter, google plus o cualquier otra red social, foro, pagina web, o medio de comunicación, también pueden seguirme en twitter @edx_twin3 donde pongo las ultimas noticias sobre la página.
Como siempre cualquier duda, sugerencia, comentario, etc ...., la pueden enviar ya sea mediante la pagina de contacto, mediante correo electrónico edx@edx-twin3.org o utilizando el blog.
Por último pero no menos importante cualquier donativo es de gran ayuda para seguir haciendo tutoriales como este, gracias :1.