Circuito y Driver CCS C para 4 Memorias EEPROM I2C 1024 Kbits x 4 : 8 = 512 Kbytes con 4 chips 24AA1025 de Microchip Descripción del Proyecto: La finalidad de este proyecto es la de dotar de una cantidad suficiente de memoria no volátil a nuestra
RRBOARD2. De los varios tipos de memorias no volátiles que podríamos utilizar vamos en este caso a usar una Array de EEPROMS serie externas, todas ellas accesibles mediante un único bus I2C.
La idea central es la de desarrollar una Driver en CCS C mediante el que podamos acceder a la totalidad de la memoria disponible "en línea". Desde el primer byte del primer chip hasta el último byte del último chip como si fuese un chip único que englobase la suma de todas las posiciones de memoria direccionables de forma continua.
Para este proyecto en concreto vamos a utilizar la EEPROM externa
24AA1025, desarrollada por la gente de Microchip y que han tenido la gentileza de enviarme tres unidades para realizar estos experimentos.
Por limitaciones de Hardware inherentes al diseño de las 24AA1025 solo podemos "apilar" un máximo de cuatro unidades de este tipo, por lo que nuestro proyecto va a ceñirse a esta limitación y vamos a diseñar todo en función de este número de elementos.
Así la conclusión del proyecto está en leer y/o escribir un byte, mediante las dos funciones correspondientes, en las posiciones de memoria
desde 0x00000 hasta 0x7FFFF, ambas inclusive. Un total de
0x80000 posiciones diferentes, o lo que es lo mismo 524.288 posiciones direccionables directamente, lo que significan
524.288 / 1024 = 512 Kbytes.
Implementación y consideraciones Hardware: Como hemos mas arriba vamos a utilizar para nuestro pequeño proyecto cuatro chips del modelo
24AA1025. Cada uno de ellos dispone de un total de
1024 Kbits lo que significan 1024/8 =
128 Kbytes de memoria, distribuidos en
dos bancos de 64 Kbytes cada uno de ellos.
Estos chips son accesibles mediante un bus
I2C, dos líneas son necesarias para ello:
SDA y
SCL con sus correspondientes resistencias Pull-Up a Vcc, que corresponden a los Pines 5 y 6 del chip. Las resistencias Pull-Ups que vamos a utilizar son de 10 KOhmios cada una de ellas.
Para
direccionar cada uno de los cuatro chips debemos usar las distintas configuraciones hardware que podemos implementar usando los terminales
A0 y
A1 de cada uno de los chips, Pines 1 y 2 respectivamente, nótese que
A2, correspondiente al Pin 3 del chip, no es posible usarlo para este fin ya que ha de ser siempre conectado a Vcc. Estas direcciones hardware posibles combinando A0 y A1 conectándolos a VCC (1) y GND (0) según cada caso son:
00, 01, 10 y 11 y de ahí la limitación a cuatro chips de que hablábamos antes.
Podríamos usar el terminal
WP, Pin 7 del Chip, para proteger contra escritura nuestra memoria EEPROM ya que éste deshabilita la posibilidad de modificar el contenido de la EEPROM si es conectado a Vcc, pero en este caso no vamos a hacerlo y vamos a conectarlo permanentemente a GND para habilitar siempre la posibilidad de modificación de su contenido.
Los Pines 4 y 8 del chip corresponden con la alimentación del mismo y han de ir conectados correspondientemente a
GND y VCC.
Las distintas funciones de cada uno de los pines puede verse en la siguiente tabla extraída del Datasheet del 24AA1025:
La distribución física de los pines en el Chip es la siguiente:
Esquema: Y este es el esquema definitivo que vamos a construir, en el que podemos destacar algunos detalles:
1º.- La comunicación con la RRBOARD2 la realizamos mediante nuestro viejo amigo el conector
CON-ML10 para cable plano de 10 hilos (alimentación y un puerto completo de 8 bits)
2º.-
JP1-PB y
JP2-PC permiten seleccionar la conexión de los
SDA y
SCL del I2C a los pines 0..1 ó 3..4 del puerto al que estén conectados (recordad de la RRBOARD2 está diseñada para las familias 18F4550 y 16F877 y el I2C lo implementa la primera en los pines RB0 y RB1 y la segunda en los RC3 y RC4)
3º.- Dependiendo de dónde conectemos nuestro circuito en la RRBOARD2 podemos necesitar o no las resistencias
Pull-Up imprescindibles para el bus I2C. Si lo conectamos al PORTB tenemos disponibles la internas del PIC, en cualquier otro caso podemos hacer uso del jumper
JP3-PU para conectar dichas resistencias Pull-Up a VCC.
4º.- Por oscuras razones de diseño que ni yo mismo soy capaz de explicarme del todo, a pesar de haber realizado personalmente el mismo, he optado por asignar las
direcciones Hardware de cada uno de los chips en orden inverso al natural: Al chip número uno la dirección 11, al dos la 10, al tres la 01 y al cuatro la 00. La verdad es que es irrelevante a la hora de utilizarlos ya que muy fácilmente podremos cambiar la configuración del driver para usar cualquier combinación de direcciones que deseemos utilizar. (Este extremo se verá en detalle en la sección dedicada al driver)
Circuito impreso: El esquema anterior debidamente ruteado a un tamaño de QUARTER_EUROBOARD genera el siguiente PCB:
Recursos: Datasheet del chip
24AA1025 (PDF 357 Kb)
Driver y Software: Créditos: Todo lo aquí desarrollado se basa en dos trabajos que no he realizado yo mismo:
El programa de ejemplo
EX_EXTEE.C de la gente de
CCS C y
El
Driver Microchip 24AA512 512k I2C EEPROM de
UFAnders Pero dejémonos de mas dilaciones innecesarias y entremos en faena con nuestro Driver I2C para memorias EEPROM de gran tamaño:
Según la tabla anterior en que se nos muestra el método I2C de asignar una dirección de memoria a las 24AA1025 para leer o escribir sobre ella, y siguiendo el modo y manera en que CCS C hace uso del bus I2C debemos implementar dos simples funciones que
esquemáticamente van a ser como sigue:
1ª.-
Escribir un byte (data) en una dirección de memoria (memAddress):
Es este caso inicializamos el bus I2C con start(), enviamos con write() el Control Byte, que nosotros llamamos baseAddress, y a continuación los dos bytes de dirección de memoria a escribir, memAddress, empezando con el más significativo y después del menos significativo, seguido del byte, data, que deseamos guardar.
void writeByte24AA1025(int32 memAddress, int8 data){
i2c_start();
i2c_write(baseAddress);
i2c_write(memAddress>>8);
i2c_write(memAddress);
i2c_write(data);
i2c_stop();
}
2ª.-
Leer un byte de una dirección de memoria (memAddress):
Para leer en una posición de memoria debemos realizar el mismo posicionamiento en una dirección de memoria tal como hicimos en la función de escritura seguido de una nueva función start() y una función write() con el bit Read del Control Byte activado. A continuación realizamos un read() del bus I2C y recibiremos el contenido de dicha dirección de memoria.
int8 readByte24AA1025(int32 memAddress){
int8 returnByte=0;
i2c_start();
i2c_write(baseAddress);
i2c_write(memAddress>>8);
i2c_write(memAddress);
i2c_start();
i2c_write(baseAddress|1);
returnByte = i2c_read(0);
i2c_stop();
return returnByte;
}
Todo esto está muy bien en principio pero hay una serie de importantes detalles que no hemos tomado en cuenta aún y que son imprescindibles si deseamos llevar a buen puerto nuestro proyecto:
En primer lugar está el asunto que cómo configurar apropiadamente el
Control Byte. Es un único Byte y en él debemos incluir:
El
Control Code. Que siempre son los mismo Bits, 7..4, y que para estos chips es 1010.
El
Block Select Bit. Que indica en que Banco de los dos de 65535 bytes que tiene cada chip es al que nos estamos refiriendo.
El
Chip Select Bits. Que es la dirección por Hardware que le asignamos a cada uno de los cuatro chips con los que estamos trabajando.
El
Read/Write bit. Donde indicamos si estamos leyendo o escribiendo una posición de memoria.
En segundo lugar debemos hacer una conversión de la dirección de memoria donde queremos escribir desde la
absoluta que enviamos a nuestras funciones, entre 0x00000 y 0x7FFFF, a la
relativa de Chip/Banco/0x0000 a 0x1FFFF que es la máxima dirección accesible dentro de un Banco dentro de un Chip en concreto.
O sea que en función de la dirección absoluta que enviemos a nuestras funciones debemos componer el Control Byte seleccionando el Chip al que nos referimos, el Banco dentro de ese chip y la dirección concreta por la que estamos preguntando.
Partimos de una Control Byte base de:
int8 const baseAddress24AA1025 = 0b10100000; Y le configuramos los distintos bits. Esto lo solventamos con las siguientes funciones auxiliares:
int8 memAbsoluteAddress2deviceOrder(int32 memAddress); Que nos devuelve 1, 2, 3 ó 4 dependiendo de si memAddress es mayor 0x00000 y menor que 0x1FFFF, ó es mayor que 0x20000 y menor que 0x3FFFF ... etc. etc.
int8 memAbsoluteAddress2deviceAddress(int32 memAddress); Que nos devuelve la dirección Hardware del Chip 1, 2, 3 ó 4 al que corresponde la dirección absoluta memAddress. Esta dirección hardware es la parte Chip Select Bits del Control Byte.
Estas deciveAddress las extraemos en función del anterior deviceOrder haciendo uso de la siguiente tabla de asignación de direcciones Hardware:
// device hardware address
int8 const deviceAddress1 = 0b00000011; // #3
int8 const deviceAddress2 = 0b00000010; // #2
int8 const deviceAddress3 = 0b00000001; // #1
int8 const deviceAddress4 = 0b00000000; // #0
int32 memAbsoluteAddress2menRelativeAddress(int8 deviceOrder,int32 memAddress); Que nos va a ajustar memAddress a la dirección relativa dentro de cada uno de los Chip, haciendo que cada una de ellas comienze realmente en 0x00000.
int1 menRelative2BankSelect(int32 memRelativeAddress); Y ésta que por último nos selecciona el Banco, alto o bajo, de cada uno de los Chips. Corresponde al Block Select Bit del Control Byte.
Con todo esto podemos ya completar nuestras funciones principales, añadiéndoles las cabeceras correspondientes para ajustar todos y cada uno de los parámetros:
int8 baseAddress;
int8 deviceAddress;
int8 deviceOrder=0;
int32 memRelativeAddress;
int8 bank=0;
deviceAddress = memAbsoluteAddress2deviceAddress(memAddress);
deviceOrder = memAbsoluteAddress2deviceOrder(memAddress);
memRelativeAddress = memAbsoluteAddress2menRelativeAddress(deviceOrder, memAddress);
bank = menRelative2BankSelect(memRelativeAddress);
baseAddress = baseAddress24AA1025 + (bank<<3) + (deviceAddress<<1);
El driver definitivo queda así:
///////////////////////////////////////////////////////////////////////////////
//
// 4 x EEPROM 24AA1025 Driver with absolute memory addressing
//
///////////////////////////////////////////////////////////////////////////////
#define EEDEBUG
// device hardware address
int8 const deviceAddress1 = 0b00000011; // #3
int8 const deviceAddress2 = 0b00000010; // #2
int8 const deviceAddress3 = 0b00000001; // #1
int8 const deviceAddress4 = 0b00000000; // #0
// baseAddress = 8 bits which mean ...
//
// Fixed (4 bits) -> 1010
// EE internal Block (1 bit) -> 0 for 0000/FFFF, 1 for 10000/1FFFF
// Dev Hard Address (2 bits) -> 00 or 01 or 10 or 11
// R/W (1 bit) -> 0 -> Write / 1 -> Read
int8 const baseAddress24AA1025 = 0b10100000;
// Aux Functions //////////////////////////////////////////////////////////////
int1 menRelative2BankSelect(int32 memRelativeAddress){
int1 returnBit=0;
if(memRelativeAddress>(int32) 0xffff) returnBit = 1;
return returnBit;
}
int32 memAbsoluteAddress2menRelativeAddress(int8 deviceOrder, int32 memAddress){
int32 returnInt32 = 0;
switch(deviceOrder){
case 1: returnInt32 = memAddress;
break;
case 2: returnInt32 = memAddress - 0x20000;
break;
case 3: returnInt32 = memAddress - 0x40000;
break;
case 4: returnInt32 = memAddress - 0x60000;
break;
}
return returnInt32;
}
int8 memAbsoluteAddress2deviceOrder(int32 memAddress){
int8 returnByte=0;
if((memAddress>0x05ffff)&&(memAddress<0x080000)) returnByte=4;
if((memAddress>0x03ffff)&&(memAddress<0x060000)) returnByte=3;
if((memAddress>0x01ffff)&&(memAddress<0x040000)) returnByte=2;
if( (memAddress<0x020000)) returnByte=1;
return returnByte;
}
int8 memAbsoluteAddress2deviceAddress(int32 memAddress){
int8 deviceOrder=0, returnByte=0;
deviceOrder = memAbsoluteAddress2deviceOrder(memAddress);
switch(deviceOrder){
case 0: returnByte = 0xff;
break;
case 1: returnByte = deviceAddress1;
break;
case 2: returnByte = deviceAddress2;
break;
case 3: returnByte = deviceAddress3;
break;
case 4: returnByte = deviceAddress4;
break;
}
return returnByte;
}
// Main Functions /////////////////////////////////////////////////////////////
void writeByte24AA1025(int32 memAddress, int8 data){
int8 baseAddress;
int8 deviceAddress;
int8 deviceOrder=0;
int32 memRelativeAddress;
int8 bank=0;
deviceAddress = memAbsoluteAddress2deviceAddress(memAddress);
deviceOrder = memAbsoluteAddress2deviceOrder(memAddress);
memRelativeAddress = memAbsoluteAddress2menRelativeAddress(deviceOrder, memAddress);
bank = menRelative2BankSelect(memRelativeAddress);
baseAddress = baseAddress24AA1025 + (bank<<3) + (deviceAddress<<1);
#ifdef EEDEBUG
printf("\r\n\nAbsolute Address: %LX Relative Address: %Lx Device: %u Bank: %u\r\n",memAddress
,memRelativeAddress
,deviceOrder
,bank
); #endif
i2c_start();
i2c_write(baseAddress);
i2c_write(memAddress>>8);
i2c_write(memAddress);
i2c_write(data);
i2c_stop();
delay_ms(5);
}
int8 readByte24AA1025(int32 memAddress){
int8 returnByte=0;
int8 baseAddress;
int8 deviceAddress;
int8 deviceOrder=0;
int32 memRelativeAddress;
int8 bank=0;
deviceAddress = memAbsoluteAddress2deviceAddress(memAddress);
deviceOrder = memAbsoluteAddress2deviceOrder(memAddress);
memRelativeAddress = memAbsoluteAddress2menRelativeAddress(deviceOrder, memAddress);
bank = menRelative2BankSelect(memRelativeAddress);
baseAddress = baseAddress24AA1025 + (bank<<3) + (deviceAddress<<1);
#ifdef EEDEBUG
printf("\r\n\nAbsolute Address: %LX Relative Address: %Lx Device: %u Bank: %u\r\n",memAddress
,memRelativeAddress
,deviceOrder
,bank
); #endif
i2c_start();
i2c_write(baseAddress);
i2c_write(memRelativeAddress>>8);
i2c_write(memRelativeAddress);
i2c_start();
i2c_write(baseAddress|1);
returnByte = i2c_read(0);
i2c_stop();
return returnByte;
}
///////////////////////////////////////////////////////////////////////////////
Como os dije mas arriba el siguiente programa es una adaptación del programa de ejemplo
EX_EXTEE.C de la gente de CCS C que podéis encontrar en el directorio examples de la instalación de dicho compilador.
///////////////////////////////////////////////////////////////////////////////
//
// EEPROM_24AA1025x1.C
//
// by RedPic from http://picmania.garcia-cuervo.net
//
// October 2006
//
///////////////////////////////////////////////////////////////////////////////
#include <18F4550.h>
#fuses HS,NOWDT,NOPROTECT,NOLVP
#use delay(clock=20000000)
#use rs232(baud=9600, xmit=PIN_C6, rcv=PIN_C7)
#use i2c(master, sda=PIN_B0, scl=PIN_B1)
#include <input.c>
#include <24AA1025x4.c>
const char Version[]="1.0.F\0";
///////////////////////////////////////////////////////////////////////////////
//
// Main
//
///////////////////////////////////////////////////////////////////////////////
void main() {
BYTE value, cmd;
int32 address;
int8 device;
delay_ms(300);
printf("[RRBOARD2] EEPROM Total Commander %s\r\n",version
); printf("based on 24AA1025 Microchip Hardware\r\n"); printf("\xa9 10.2006 by RedPic\r\n\n");
do {
do {
printf("\r\nDo you Read or Write? : "); } while ( (cmd!='R') && (cmd!='W') );
printf("\n\rGive me EEPROM internal absolute Address (24 bits) in hex : ");
address = gethex();
address = (address<<8)+gethex();
address = (address<<8)+gethex();
device = memAbsoluteAddress2deviceAddress(address);
if(cmd=='R')
printf("\r\nReturn Value (8 bits) in hex is : %X\r\n",readByte24AA1025
(address
));
if(cmd=='W') {
printf("\r\nEnter new value (8 bits) in hex : "); value = gethex();
writeByte24AA1025(address, value );
}
} while (TRUE);
}
Funcionando: Para realizar las primeras pruebas he realizado físicamente el siguiente esquema y PCB que implementa uno de estos chips 24AA1025:
Ea, ahí queda eso. Mañana más.
Nota: El driver en CCS C definitivo está a partir de este
post (Enero 2008)