IMPLEMENTACIÓN DE RUTINAS I2C PARA MICROCONTOLADORES 16F8X
Autor: The-Next
Bueno, este trabajito lo hice en mi primer contacto con los microcontroladores. El objetivo era añadirle a un microbot que usaba el 16F84 unos cuantos dispositivos que funcionaban a través del bus I2C. Evidentemente este micro, a diferencia de otros mas avanzados no dispone de funciones especiales para la comunicación serie así que se tuvo que diseñar una.
Antes de ponernos al meollo hay que concretar que el algoritmo que escribí no contempla todas las funcionalidades de dicho bus. Existen dos aspectos de la comunicación I2C que no se han implementado ya que no han hecho falta. La primera es el tema del Arbitraje. El 16F84 iba a ser la única unidad principal por lo que no hace falta ningún tipo de arbitraje. El segundo aspecto no implementado son los estados de espera. Todos los dispositivos I2C con los que trabajamos eran lo suficientemente rápidos por lo que tampoco ha habido necesidad de que el 16F84 se preocupara de ello. De todas formas, si son necesarios alguno de los dos supongo que no será muy difícil modificar el algoritmo que voy a enseñar.
Los dispositivos a los que accedíamos con este bus eran una memoria EEPROM, una brújula digital y un sensor de ultrasonidos.
Primeramente, una de las características que tiene el bus I2C es que las líneas son en colector abierto, es decir, el nivel alto realmente es alta impedancia, por lo que se necesitan 2 resistencias conectadas a VCC para que pongan estas líneas a nivel bajo Esto tiene la ventaja, de que si un elemento pone nivel alto y otro deja nivel bajo, en la línea se lee nivel bajo, pudiéndose detectar situaciones que veremos más adelante.
Así que en vez de conectar el bus directamente a las patillas del PIC tuvimos que pasarlas antes por un inversor en colector abierto y poner dos resistencias de pull-up:
El 16F84 estaba conectado al “picbus”, del cual las lineas correspondientes a las patillas RB1 y RB4, que eran utilizadas para la señal de reloj y de datos se pasaban por el inversor en colector abierto. La patilla RB5 se utilizaba para leer del bus los datos.
El esquema de lectura de un registro de un dispositivo atendía al siguiente esquema (ejemplo de la lectura de la orientación de un compás digital):
Esta es la secuencia de procedimientos para la lectura:
– El 16F84 Genera la secuencia de Start. A continuación Manda la dirección de la brújula (7 bits) y finalmente un 0 para indicar que desea escribir.
– El 16F84 Deja SDA en nivel alto y entonces el compás digital realiza el ACK (poniendo “0” en la línea).
– A continuación el microcontrolador manda el registro de la brújula al que quiere acceder y el compás le responde con un ACK.
– El 16F84 genera una secuencia de Restart, manda la dirección de la brújula ( 7 bits) y un 1 indicando que desea leer.
– El 16F84 Deja SDA en nivel alto y entonces el compás digital realiza el ACK.
– A partir de este momento El PIC mantiene SDA a nivel alto y comienza a leer los datos. Cuando haya leído los bytes deseados generará la secuencia de Stop y la comunicación habrá finalizado.
A continuación vamos a mostrar y explicar el código ensamblador que realiza la lectura de un registro de un dispositivo I2C.
Antes de empezar sería recomendable tener en cuenta dos cosas.
1º La placa implementada para conectarse con los dispositivos I2C lleva un inversor por lo que los impulsos generados por el PIC serán justo lo contrario que los de la gráfica.
Lo que decidimos en un primer lugar es crear una serie de subrutinas para cada acción, secuencia de inicio, enviar byte, ect… Las rutinas son las siguientes (configuración necesaria del PIC incluida):
INCLUDE "Macros.inc" ;Inclusi¢n de macros. list p=16f84 org 0 ;----------------------------------- I2C--------------------------------------------------------- ;----------------------------------- I2C--------------------------------------------------------- ;vamos a utilizar rb4 como se¤al de reloj , rb1 como puerto de escritura de datos , rb5 como lectura de DATOS ;RB1 Escritura SDA ;RB2 Lectura de nada ;RB4 Escritura SCL ;RB5 Lectura SDA ; ATENCION LOS COMENTARIOS ESTAN AL REVES YA QUE SE PASA POR UN INVERSOR EN CONECTOR ABIERTO, POR LO QUE LAS SALIDAS ; QUE PONE EL PIC LUEGO SE INVIERTEN!!!! ; RB4 ---> Inversor ---> SCL ; RB1 ---> Inversor ---> SDA | ; RB5 <-------------- / ;----------------------------------- I2C--------------------------------------------------------- ;----------------------------------- I2C--------------------------------------------------------- bsf 0x03,5 ;pasamos al banco de memoria 1 bsf 0x0b,7 movlw b'11101001' ; configuramos en el registo option las resistencias pull-up inactivas , Tmr0 aunmenta con el t0ck1 movwf 0x01 movlw 0x00 movwf 0x06 ; configuramos RB como salidas bsf 0x06,5 ; RB5 como entrada bsf 0x06,2 ; RB2 como entrada bsf 0x06,3 ; RB3 como entrada bsf 0x06,0 ; RB0 como entrada bsf 0x06,6 ; RB6 como entrada bsf 0x06,7 ; RB7 como entrada movlw 0x00 movwf 0x05 ;configuramos RA como salidas bsf 0x05,4 ;RA 4 entrada bcf 0x03,5 ;pasamos al banco de memoria 0 movlw 0x00 movwf 0x06 ;ponemos a nivel bajo todos los RB ( RB1 y RB4 a "1") ;---------------------------------------------------------------------------------------- ;---------------------------------------------------------------------------------------- ;------------------------------- MANEJO DE I2C --------------------------------- ;---------------------------------------------------------------------------------------- ;---------------------------------------------------------------------------------------- ;--------------------------------------------------------------------- ;------------------------------ MEOLLO DEL ASUNTO!!! ----------------- ;--------------- se parte con las lineas SDA y SCL a 0 logico--------- ;--------------------------------------------------------------------- i2csec; Manda un byte almacenado en el registro 0x10 por el bus i2c ; Usamos el registro 0x0f para guardar la respuesta del ack bcf 0x0f,0; BSF 0X06,1 ;Pone a 0 SDA BTFSC 0X10,7 ; Salta si el bit X de 0x10 esta a cero BCF 0X06,1; ;pone a 1 SDA call retardo bcf 0x06,4 ; ponemos a 1 SCL call retardo bsf 0x06,4 ; ponemos a 0 SCL BSF 0X06,1 ;Pone a 0 SDA BTFSC 0X10,6 ; Salta si el bit X de 0x10 esta a cero BCF 0X06,1; ;pone a 1 SDA call retardo bcf 0x06,4 ; ponemos a 1 SCL call retardo bsf 0x06,4 ; ponemos a 0 SCL BSF 0X06,1 ;Pone a 0 SDA BTFSC 0X10,5 ; Salta si el bit X de 0x10 esta a cero BCF 0X06,1; ;pone a 1 SDA call retardo bcf 0x06,4 ; ponemos a 1 SCL call retardo bsf 0x06,4 ; ponemos a 0 SCL BSF 0X06,1 ;Pone a 0 SDA BTFSC 0X10,4 ; Salta si el bit X de 0x10 esta a cero BCF 0X06,1; ;pone a 1 SDA call retardo bcf 0x06,4 ; ponemos a 1 SCL call retardo bsf 0x06,4 ; ponemos a 0 SCL BSF 0X06,1 ;Pone a 0 SDA BTFSC 0X10,3 ; Salta si el bit X de 0x10 esta a cero BCF 0X06,1; ;pone a 1 SDA call retardo bcf 0x06,4 ; ponemos a 1 SCL call retardo bsf 0x06,4 ; ponemos a 0 SCL BSF 0X06,1 ;Pone a 0 SDA BTFSC 0X10,2 ; Salta si el bit X de 0x10 esta a cero BCF 0X06,1; ;pone a 1 SDA call retardo bcf 0x06,4 ; ponemos a 1 SCL call retardo bsf 0x06,4 ; ponemos a 0 SCL BSF 0X06,1 ;Pone a 0 SDA BTFSC 0X10,1 ; Salta si el bit X de 0x10 esta a cero BCF 0X06,1; ;pone a 1 SDA call retardo bcf 0x06,4 ; ponemos a 1 SCL call retardo bsf 0x06,4 ; ponemos a 0 SCL BSF 0X06,1 ;Pone a 0 SDA BTFSC 0X10,0 ; Salta si el bit X de 0x10 esta a cero BCF 0X06,1; ;pone a 1 SDA call retardo bcf 0x06,4 ; ponemos a 1 SCL call retardo bsf 0x06,4 ; ponemos a 0 SCL bcf 0x06,1 ;ponemos a 1 SDA call retardo bcf 0x06,4 ; ponemos a 1 SCL btfsc 0x06,5 ;salta si se ha recibido el ack bsf 0x0f,0; ;COMPROBAMOS EL ACK call retardo bsf 0x06,4 ; ponemos a 0 SCL call retardo ; La rutina finaliza con SCL a 1 y SDA a 1 return ;---------------------------------------------------------------- ; La siguiente rutina obtiene 2 bytes leidos del bus i2c, en caso de la brujula solo te toma en cuenta el primer byte ; Los bytes usados seran 0x0C y 0x0D --- BYTE DE MAYOR PESO 0X0D ;---------------------------------------------------------------- i2crec clrf 0x0c; clrf 0x0d; call retardo ; Leemos los datos y los almacenamos en 0x0d bcf 0x06,4 ; ponemos a 1 SCL btfsc 0x06,5 bsf 0x0d,7 call retardo bsf 0x06,4 ; ponemos a 0 SCL call retardo bcf 0x06,4 ; ponemos a 1 SCL btfsc 0x06,5 bsf 0x0d,6 call retardo bsf 0x06,4 ; ponemos a 0 SCL call retardo bcf 0x06,4 ; ponemos a 1 SCL btfsc 0x06,5 bsf 0x0d,5 call retardo bsf 0x06,4 ; ponemos a 0 SCL call retardo bcf 0x06,4 ; ponemos a 1 SCL btfsc 0x06,5 bsf 0x0d,4 call retardo bsf 0x06,4 ; ponemos a 0 SCL call retardo bcf 0x06,4 ; ponemos a 1 SCL btfsc 0x06,5 bsf 0x0d,3 call retardo bsf 0x06,4 ; ponemos a 0 SCL call retardo bcf 0x06,4 ; ponemos a 1 SCL btfsc 0x06,5 bsf 0x0d,2 call retardo bsf 0x06,4 ; ponemos a 0 SCL call retardo bcf 0x06,4 ; ponemos a 1 SCL btfsc 0x06,5 bsf 0x0d,1 call retardo bsf 0x06,4 ; ponemos a 0 SCL call retardo bcf 0x06,4 ; ponemos a 1 SCL btfsc 0x06,5 bsf 0x0d,0 call retardo bsf 0x06,4 ; ponemos a 0 SCL bsf 0x06,1 ;ponemos a 0 SDA, ACK call retardo bcf 0x06,4 ; ponemos a 1 SCL call retardo bsf 0x06,4 ; ponemos a 0 SCL bcf 0x06,1 ;ponemos a 1 SDA call retardo ; Leemos los datos y los almacenamos en 0x0c bcf 0x06,4 ; ponemos a 1 SCL btfsc 0x06,5 bsf 0x0c,7 call retardo bsf 0x06,4 ; ponemos a 0 SCL call retardo bcf 0x06,4 ; ponemos a 1 SCL btfsc 0x06,5 bsf 0x0c,6 call retardo bsf 0x06,4 ; ponemos a 0 SCL call retardo bcf 0x06,4 ; ponemos a 1 SCL btfsc 0x06,5 bsf 0x0c,5 call retardo bsf 0x06,4 ; ponemos a 0 SCL call retardo bcf 0x06,4 ; ponemos a 1 SCL btfsc 0x06,5 bsf 0x0c,4 call retardo bsf 0x06,4 ; ponemos a 0 SCL call retardo bcf 0x06,4 ; ponemos a 1 SCL btfsc 0x06,5 bsf 0x0c,3 call retardo bsf 0x06,4 ; ponemos a 0 SCL call retardo bcf 0x06,4 ; ponemos a 1 SCL btfsc 0x06,5 bsf 0x0c,2 call retardo bsf 0x06,4 ; ponemos a 0 SCL call retardo bcf 0x06,4 ; ponemos a 1 SCL btfsc 0x06,5 bsf 0x0c,1 call retardo bsf 0x06,4 ; ponemos a 0 SCL call retardo bcf 0x06,4 ; ponemos a 1 SCL btfsc 0x06,5 bsf 0x0c,0 call retardo bsf 0x06,4 ; ponemos a 0 SCL bsf 0x06,1 ;ponemos a 0 SDA, ACK call retardo bcf 0x06,4 ; ponemos a 1 SCL call retardo bcf 0x06,1 ;ponemos a 1 SDA Bit de stop call retardo ; La rutina finaliza con SCL y SDA a 1 return ;---------------------------------------------------------------- ;---------------------------------------------------------------- ;--------Subrutinas peque¤as a las que se llaman desde arriba---- ;---------------------------------------------------------------- ;---------------------------------------------------------------- paro ; genera la secuencia de stop call retardo bsf 0x06,4 ; ponemos a 0 SCL bsf 0x06,1 ;ponemos a 0 SDA call retardo bcf 0x06,4 ; ponemos a 1 SCL call retardo bcf 0x06,1 ;ponemos a 1 SDA return inicio ; Genera una secuencia de inicio bsf 0x06,1 ;ponemos a 0 SDA call retardo bsf 0x06,4 ; ponemos a 0 SCL ; Sda y scl quedan a 0 return reinicio ; Genera un bit de reinicio, scl debe estar a cero bcf 0x06,1 ;ponemos a 1 SDA bcf 0x06,4 ; ponemos a 1 SCL RE-INICIO call retardo bsf 0x06,1 ;ponemos a 0 SDA call retardo bsf 0x06,4 ; ponemos a 0 SCL call retardo ; Scl y sca quedan a cero return retardo ;espera 10 microsegundos ( 5 us del cambio de contexto y otros 5 de las nops) nop nop nop nop nop return
Con estas subrutinas implementadas, el comunicarse con un dispositivo es relativamente fácil. Lo que hice ahora fue hacer otras subrutinas que trabajaban sobre las que he implementado de forma que al final de todo solo tuviera que llamar a 1 subrutina para realizar toda la secuencia:
Secuencia de lectura del registro 01 de la brújula:
; ---------- ( BRUJULA) ----------- brujula iniciob call retardo call inicio movlw 0xc0; movwf 0x10; PONEMOS c0 en 0x10 call i2csec; btfsc 0x0f,0; goto errorb call retardo movlw 0x01; movwf 0x10; PONEMOS 01 en 0x10 call i2csec; btfsc 0x0f,0; goto errorb call retardo call reinicio movlw 0xc1; movwf 0x10; PONEMOS c1 en 0x10 call i2csec; btfsc 0x0f,0; goto errorb call retardo call i2crec; return errorb call paro goto iniciob
Secuencia de lectura y secuencia de escritura para leer de una memoria EEPROM, en nuestro caso era una tarjeta chip.
chipescritura call inicio movlw 0xa0 movwf 0x10; PONEMOS a0 en 0x10 (a0 es la dirección de la memoria EEPROM) call i2csec; btfsc 0x0f,0; goto stop3 call retardo movfw 0x0d movwf 0x10; PONEMOS lo que hay en 0x0d en 0x10 ( la dirección del registro estará almacenada en 0x0d) call i2csec; btfsc 0x0f,0; goto stop3 call retardo movfw 0x0c movwf 0x10; PONEMOS lo que hay en 0x0c en 0x10 ( el dato que queremos guardar estará en 0x0c ) call i2csec; btfsc 0x0f,0; goto stop3 call retardo call paro return stop3 call paro goto chipescritura chiplectura call inicio movlw 0xa0 movwf 0x10; PONEMOS a0 en 0x10 call i2csec; btfsc 0x0f,0; goto rein call retardo movfw 0x0d movwf 0x10; PONEMOS lo que hay en 0x0d en 0x10 (direccion del registro que queremos leer) call i2csec; btfsc 0x0f,0; goto rein call retardo call reinicio movlw 0xa1 movwf 0x10; PONEMOS a1 en 0x10 call i2csec; btfsc 0x0f,0; goto rein call retardo call i2crec return rein call paro goto chiplectura
Finalmente, en nuestro programa principal simplemente tenemos que llamar a estas rutinas y tendremos los valores deseados de los dispositivos en los registros indicados. Para terminar pongo una pequeña lista de los registros que utilicé:
0X10 → Se almacena el byte a enviar por el bus I2C
0X0F → Se utiliza para recibir los ACK de los dispositivos
0X0C → Se almacena el 2º byte recibido por el bus I2C
→ Se almacena el dato que se quiere escribir en la tarjeta chip
0X0D → Se almacena el 1º byte recibido por el bus I2C
→ Se almacena la dirección del registro de la tarjeta chip a la que se quiere acceder
Bueno, eso es todo, creo que ésta guía puede ser útil para utilizarla en pequeños microcontroladores que no tengan implementadas por hardware las funcionalidades del bus I2C. Espero que os sirva.
No he explicado tal vez mucho el código, si tenéis alguna duda podéis consultarme, si me acuerdo xd que hace año y medio que escribí el código este.