I2C y memoria externa

Conocemos como I2C o I2C (Inter-Integrated Circuit) a una interfaz que permite la comunicación en serie entre varios circuitos. Basta con dos hilos (además del de referencia) para poder conectar más de 100 componentes. Uno de los hilos sirve para los datos (SDA), mientras que el otro lleva una señal de sincronización (SCL). En su forma original, en I2C hay un solo circuito que coordina todas las comunicaciones, el maestro; los demás componentes (esclavos) toman o entregan la información en o desde SDA cuando el maestro lo indica.

En esta entrada sólo te presento un ejemplo en el que incorporo memoria externa a un dsPIC30F4013. Si requieres más información de los fundamentos del I2C, puedes consultar las secciones 6.2 y 6.2.2 de Microcontroladores PIC16, fundamentos y aplicaciones.

Para escribir la entrada utilicé los documentos siguientes:

Además del dsPIC30F4013 y la memoria 24FC1025, necesitarás un módulo convertidor USB-USART y el programa Gtk Term.

Yo ocupé el módulo que está etiquetado con el número 4 en la entrada: Comunicación RS232 a través del USB. En esa misma entrada hay indicaciones que te servirán para instalar y habilitar Gtk Term.

En la figura 1 está el diagrama de pruebas y el código para el dsPIC es MemoriaI2c.c.

Diagrama del circuito de pruebas.
Figura 1. Diagrama del circuito de pruebas. Observa que tomaremos la energía del módulo USB, es decir, las etiquetas +5V sólo sirven para indicar conexión.

Mediante GTk Term enviaremos ordenes desde la PC al dsPIC, y recibiremos resultados desde el dsPIC en la PC. A su vez, el dsPIC controlará la memoria EEPROM 24FC1025. Deberás configurar el puerto correcto en Gtk Term y activar el manejo automático del retorno de carro y el salto de línea, figura 2.

Captura de pantalla al configurar GtkTerm.
Figura 2. Captura de pantalla al configurar GtkTerm. Además de configurar el puerto seleccionando el módulo USB, asegúrate de marcar la opción CR LF auto en el menú Configuration.

En la siguiente tabla te muestro las letras que usaremos como comando y la tarea que realizarán.

Tabla 1. Caracteres o comandos a enviar desde Gtk Term al dsPIC
Tecla Función Tipo devuelto
b Lee 128 bytes a partir de una dirección que se indicó previamente ASCII
d Lee el contenido de una dirección Hexa
e Escribe 128 datos en la memoria e
l Lee el contenido de la dirección actual e incrementa dicha dirección Hexa
A-Z
espacio
Escribe el carácter en la memoria A-Z
espacio
0-9 Establece dirección = número*128 Hexa

La función principal (main) es:

void main(void)
{
 unsigned char c;
 unsigned short int dir=0;
 
 // Prepara el puerto serie para trabajar a 9600 bps a partir de el oscilador
 // interno de 7.47 MHz, y usar las terminales alternas (15-U1ATX y 16-U1ARX).
 U1MODEbits.ALTIO=1;
 U1BRG=11;
 U1MODEbits.UARTEN=1;
 U1STAbits.UTXEN=1;
 // Enciende el módulo I2C 
 // y lo configura para trabajar a aproximadamente 100 kHz.
 I2CCONbits.I2CEN=1;
 I2CBRG=16;
 // Repite indefinidamente.
 while(1)
 {
  // Espera comandos desde la PC y despacha a la función correspondiente.
  while(IFS0bits.U1RXIF!=1);
  IFS0bits.U1RXIF=0;
  c=U1RXREG;
  switch(c)
  {
   // Lee como códigos ASCII 128 datos a partir de una dirección.
   case 'b': lee_bloque(dir); break;
   // Lee como hexadecimal el contenido de una dirección.
   case 'd': envia_pc(lee_dir(dir)); break;
   // Llena con constantes 128 localidades a partir de una dirección.
   case 'e': escribe_bloque(dir); break;
   // Lee como hexadecimal el contenido de la dirección actual 
   // e incrementa dicha dirección.
   case 'l': envia_pc(lee()); break;
   // Establece como dirección número recibido * 128.
   case '0':
   case '1':
   case '2':
   case '3':
   case '4':
   case '5':
   case '6':
   case '7':
   case '8':
   case '9': dir=128*(c-'0'); envia_pc(dir); break; 
   // Si se recibe una letra mayúscula un carácter de espacio,
   // lo almacena en la dirección actual.
   default: 
    if ((c>='A' && c<='Z') || c==' ') { escribe(dir,c); dir++; } break;
  }
 }
}

Lo que hace es:

  • Configurar el puerto serie, líneas 36 a 39.
  • Configurar el módulo I2C, líneas 42 y 43.
  • Repetir indefinidamente un ciclo que: 1) Recibe instrucciones desde el puerto serie. 2) Llama a las funciones que ejecutan las ordenes recibidas.

El código de la función lee_bloque es:

void lee_bloque(unsigned short int dir)
{
 unsigned short int n;
 char dato;
 
 I2CCONbits.SEN=1;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 I2CTRN=MEM0E;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 I2CTRN=dir>>8;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 I2CTRN=dir&0xFF;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 I2CCONbits.RSEN=1;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 I2CTRN=MEM0L;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 for (n=0; n<127; n++)
 {
  I2CCONbits.RCEN=1;
  while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
  dato=I2CRCV;
  I2CCONbits.ACKDT=0; I2CCONbits.ACKEN=1;
  while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
  while(IFS0bits.U1TXIF!=1);
  IFS0bits.U1TXIF=0;
  if (dato>=32 && dato<=126) U1TXREG=dato;
  else U1TXREG=' ';
 }
 I2CCONbits.RCEN=1;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 dato=I2CRCV;
 I2CCONbits.ACKDT=1; I2CCONbits.ACKEN=1;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 I2CCONbits.PEN=1;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 while(IFS0bits.U1TXIF!=1); IFS0bits.U1TXIF=0;
 U1TXREG=dato;
 while(IFS0bits.U1TXIF!=1); IFS0bits.U1TXIF=0; U1TXREG=10;
}

Cuando la función principal recibe una b, llama a lee_bloque usando como argumento la dirección a partir de la cual esta función debe leer 128 localidades de la memoria. Lo que sucede es:

  • Se ordena al módulo I2C que genere una condición de inicio, línea 88.
  • Se espera a que ocurra eléctricamente la condición de inicio, línea 89. De hecho, el contenido de esta línea se repite muchas veces en el código. En general la instrucción verifica que ha concluido alguna operación I2C: inicio, reinicio, paro, envío, recepción, etcétera.
  • Envía la dirección que identifica a la EEPROM (línea 90), NO importa si la memoria es lo único conectado a las líneas SDA y SCL, o si hay muchos dispositivos conectados al bus. Como parte de la dirección el maestro le avisa al esclavo que efectuara una operación de escritura; en este caso particular, también se le avisa cuál bloque de la memoria usaremos (observa que hay 4 direcciones declaradas en las líneas 17 a 20 del código, más detalles en la hoja de datos de la 24FC1025).
  • Se espera a que el dsPIC haya entregado al bus la dirección, línea 91.
  • Se envía la parte alta de la dirección a la que se quiere acceder en la EEPROM, líneas 92 y 93.
  • Se envía la parte baja de la dirección a la que se quiere acceder en la EEPROM, líneas 94 y 95.
  • Se emite una orden de reinicio, líneas 96 y 97.
  • Se reenvía la dirección que identifica a la EEPROM, esta vez indicando operaciones de lectura, líneas 98 y 99.
  • Se da la orden de recibir un dato, y éste se obtiene del registro I2CRCV, líneas 102 a 104.
  • Se envía un bit de reconocimiento (ACK), que le indica a la memoria que se recibió el dato y que se le pedirán más, líneas 105 y 106.
  • Se transmite el dato como carácter ASCII a la PC, líneas 107 a 110.
  • Se repiten 127 veces los últimos 3 pasos.
  • Se solicita el último dato a la memoria. Para indicar que se trata del último dato, en lugar de enviar un ACK se envía un NACK. Líneas 112 a 116.
  • Se envía una condición de paro, líneas 117 y 118.
  • Finalmente se envía el último dato y un salto de línea a la PC, líneas 119 a 121.

La función lee_dir es:

unsigned short int lee_dir(unsigned short int dir)
{
 unsigned short int leido;
 
 I2CCONbits.SEN=1;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 I2CTRN=MEM0E;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 I2CTRN=dir>>8;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 I2CTRN=dir&0xFF;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 I2CCONbits.RSEN=1;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 I2CTRN=MEM0L;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 I2CCONbits.RCEN=1;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 leido=I2CRCV;
 I2CCONbits.ACKDT=1; I2CCONbits.ACKEN=1;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 I2CCONbits.PEN=1;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 return leido;
}

Cuando la función principal recibe una d, llama a lee_dir usando como argumento la dirección a la que se debe acceder en la memoria para leer su contenido:

  • Se envía una condición de inicio, líneas 129 y 130.
  • Se envía la dirección de la EEPROM en el bus indicando escritura, 131 y 132.
  • Se envía la dirección a acceder dentro de la EEPROM, 133 a 136.
  • Se envía una condición de reinicio y se cambia a modo lectura, 137 a 140.
  • Se solicita el dato de la EEPROM, 141 a 143.
  • Se envía un NACK, 144 y 145.
  • Se envía condición de paro, 146 y 147.
  • lee_dir devuelve el valor que obtuvo de la EEPROM.

El código de escribe_bloque es:

void escribe_bloque(unsigned short int dir)
{
 short int n;
 char cadena[33]="http://148.204.64.217/     ASCII:";
 
 I2CCONbits.SEN=1;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 I2CTRN=MEM0E;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 I2CTRN=dir>>8;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 I2CTRN=dir&0xFF;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 for (n=0; n<33; n++)
 {
  I2CTRN=cadena[n];
  while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 }
 for (n=32; n<127; n++)
 {
  I2CTRN=n;
  while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 }
 I2CCONbits.PEN=1;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 while(IFS0bits.U1TXIF!=1);
 IFS0bits.U1TXIF=0;
 U1TXREG='e';
 while(IFS0bits.U1TXIF!=1);
 IFS0bits.U1TXIF=0;
 U1TXREG=10;
}

escribe_bloque graba 128 datos en la EEPROM y lo hace a partir de la dirección que recibe como argumento. Obtiene la información que almacenará de la variable cadena y de una secuencia numérica:

  • La declaración y llenado de cadena está en la línea 157.
  • Condición de inicio, líneas 159 y 160.
  • Dirección de la EEPROM en el bus modo escritura, 161 y 162.
  • Dirección interna de la EEPROM para iniciar a escribir, 163 a 166.
  • Escribe los 33 elementos de la cadena. La dirección interna de la EEPROM se actualiza con cada escritura. Líneas 167 a 171.
  • Escribe o envía a la EEPROM para grabar los códigos ASCII visibles (del 32 al 127). Líneas 172 a 176.
  • Condición de paro, 177 y 178.
  • Envía una letra e y un salto de línea a la PC para que el operador de Gtk Term sepa que concluyó la escritura del bloque. Líneas 179 a 184.

Función lee:

unsigned short int lee(void)
{
 unsigned short int leido;
 
 I2CCONbits.SEN=1;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 I2CTRN=MEM0L;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 I2CCONbits.RCEN=1;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 leido=I2CRCV;
 I2CCONbits.ACKDT=1; I2CCONbits.ACKEN=1;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 I2CCONbits.PEN=1;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 return leido;
}

Como su nombre lo indica la función lee sirve para leer el contenido de la EEPROM en la dirección actual, cada lectura implica un incremento automático de dirección:

  • Condición de inicio, líneas 193 y 194.
  • Dirección de la EEPROM en el bus modo lectura, 195 y 196.
  • Lectura de un dato, 197 a 199.
  • Envío de un NACK para decirle a la EEPROM que se ha recibido el dato y que NO se solicitarán más, 200 y 201.
  • Condición de paro, 202 y 203.
  • lee devuelve el valor que obtuvo de la EEPROM.

La función escribe recibe un dato desde la PC, lo almacena en la dirección de la EEPROM que le indica su argumento, y devuelve el mismo carácter recibido a la PC para que el usuario de Gtk Term sepa que el proceso de escritura concluyó. El código es:

void escribe(unsigned short int dir, char dato)
{
 I2CCONbits.SEN=1;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 I2CTRN=MEM0E;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 I2CTRN=dir>>8;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 I2CTRN=dir&0xFF;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 I2CTRN=dato;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 I2CCONbits.PEN=1;
 while(IFS0bits.MI2CIF!=1); IFS0bits.MI2CIF=0;
 while(IFS0bits.U1TXIF!=1);
 IFS0bits.U1TXIF=0;
 U1TXREG=dato;
}

La última función que constituye MemoriaI2C.c es envia_pc. Su tarea es convertir los valores de datos y direcciones a números hexadecimales codificados en ASCII, es decir, cuatro caracteres que representen el valor hexadecimal y siempre puedan ser visualizados en Gtk Term (los valores entre 0 y 31 NO tienen una representación visible, valores mayores a 127 implicarían el uso del ASCII extendido). envia_pc también agrega un salto de línea, y su código es:

void envia_pc(unsigned short int c)
{
 unsigned short int digito;
 short int n;
 
 digito=0xF000;
 for (n=3; n>=0; n--) 
 {
  while(IFS0bits.U1TXIF!=1);
  IFS0bits.U1TXIF=0;
  switch((c&digito)>>(4*n))
  {
   case  0: U1TXREG='0'; break;
   case  1: U1TXREG='1'; break;
   case  2: U1TXREG='2'; break;
   case  3: U1TXREG='3'; break;
   case  4: U1TXREG='4'; break;
   case  5: U1TXREG='5'; break;
   case  6: U1TXREG='6'; break;
   case  7: U1TXREG='7'; break;
   case  8: U1TXREG='8'; break;
   case  9: U1TXREG='9'; break;
   case 10: U1TXREG='A'; break;
   case 11: U1TXREG='B'; break;
   case 12: U1TXREG='C'; break;
   case 13: U1TXREG='D'; break;
   case 14: U1TXREG='E'; break;
   case 15: U1TXREG='F'; break;
   default: U1TXREG='e'; break;
  }
  digito>>=4;
 }
 while(IFS0bits.U1TXIF!=1);
 IFS0bits.U1TXIF=0;
 U1TXREG=10;
}

Finalmente, la figura 3 muestra una captura de pantalla durante una prueba en la que:

  • Abrí y configuré Gtk Term: puerto ttyUsb0, 9600 bps, manejo automático del retorno de carro y el salto de línea (CR LF auto).
  • Ordené establecer la dirección 0. Oprimí la tecla 0.
  • Ordené escribir un bloque, tecla e.
  • Ordené establecer la dirección 0x0080. Oprimí la tecla 1 (1*128=128=0x80).
  • Ordené escribir un bloque, tecla e.
  • Regresé a la dirección 0, tecla 0.
  • Leí un bloque, letra b.
  • Cambié a la dirección 0x400, tecla 8 (8*128=1152=0x400).
  • Grabé en la EEPROM la cadena “DSPIC GRABANDO MEMORIA EEPROM” al teclear en mayúsculas el texto de la cadena.
  • Regresé a la dirección 0x400.
  • Me equivoqué al intentar ordenar desplegar el contenido de un bloque, tenía puestas las mayúsculas.
  • Regresé a la dirección 0x400.
  • Desplegué un bloque.
  • Regresé a la dirección 0x400.
  • Escribí una D.
  • Regresé a la dirección 0x400.
  • Desplegué un bloque.
  • Ordené al programa poner a 0 la variable que controla la dirección, tecla 0.
  • Asigne la dirección en la EEPROM y leí su contenido oprimiendo d.
  • Obtuve el contenido de las siguientes direcciones, oprimiendo 4 veces la letra l.
Captura de pantalla que muestra a Gtk Term durante la prueba de MemoriaI2C.c.
Figura 3. Captura de pantalla que muestra a Gtk Term durante la prueba de MemoriaI2C.c.

¡Hasta la próxima!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *