EEPROM读写驱动程序
EEPROM作为嵌入式常用的芯片,驱动较为简单,采用IIC接口,但是仍然需要注意几个点,以AT系列为例,AT24C04~AT24C1024需要注意它们的页选择位等,本文会说明多个芯片的区别,并集成在同一个驱动中。对于AT24Cxx系列的芯片,A0、A1、A2作为地址输入引脚,最多可级联八个该器件(地址0xA0、0xA2、0xA4、0xA6、0xA8、0xAA、0xAC、0xAE#define EE
目录
一、概述
EEPROM作为嵌入式常用的芯片,驱动较为简单,采用IIC接口,但是仍然需要注意几个点,以AT系列为例,AT24C04~AT24C1024需要注意它们的页选择位等,本文会说明多个芯片的区别,并集成在同一个驱动中。
二、芯片介绍

1、引脚说明

对于AT24Cxx系列的芯片,A0、A1、A2作为地址输入引脚,最多可级联八个该器件(地址0xA0、0xA2、0xA4、0xA6、0xA8、0xAA、0xAC、0xAE)。
2、规格参数
| 型号 | 容量bit | 容量byte | 页数 | 字节/页 | 器件寻址 | 可寻址器件数 | 位数/字节数 | 备注 |
| AT24C01 | 1k | 128 | 16 | 8 | A2A1A0 | 8 | 7/1 | 1个字节 |
| AT24C02 | 2k | 256 | 32 | 8 | A2A1A0 | 8 | 8/1 | 1个字节 |
| AT24C04 | 4k | 512 | 32 | 16 | A2A1 | 4 | 9/1 | 1个字节+P0位 |
| AT24C08 | 8k | 1024 | 64 | 16 | A2 | 2 | 10/1 | 1个字节+P0、P1位 |
| AT24C16 | 16k | 2048 | 128 | 16 | - | 1 | 11/1 | 1个字节+P0、P1、P2位 |
| AT24C32 | 32k | 4k | 128 | 32 | A2A1A0 | 8 | 12/2 | 2个字节 |
| AT24C64 | 64k | 8k | 256 | 32 | A2A1A0 | 8 | 13/2 | 2个字节 |
| AT24C128 | 128k | 16k | 256 | 64 | A1A0 | 4 | 14/2 | 2个字节 |
| AT24C256 | 256k | 32k | 512 | 64 | A1A0 | 4 | 15/2 | 2个字节 |
| AT24C512 | 512k | 64k | 512 | 128 | A2A1A0 | 8 | 16/2 | 2个字节 |
| AT24C1024 | 1M | 128k | 512 | 256 | A2A1 | 4 | 17/2 | 2个字节+P0位 |
简要:
1、AT24C02内有32页,每页8个字节,总共256个字节,需要8bit寻址。
2、AT24C04内有32页,每页16个字节,总共512个字节,需要8bit+P0位寻址。
3、AT24C32内有32页,每页8个字节,总共256个字节,需要16bit寻址。
以此类推...
3、器件寻址
器件地址信息由"1"、"0"序列组成,前4位如图中所示,对于所有串行EEPROM都是一样的。
对于24C01/02/32/64,随后3位A2,A1和A0为器件地址位,必须与硬件输入引脚保持一致。
对于24C04,随后2位A2和A1为器件地址位,另1位为页地址位。A2和A1必须与硬件输入引脚保持一致,而A0是空脚。
对于24C08,随后1位A2为器件地址位,另2位为页地址位。A2必须与硬件输入引脚保持一致,而A1和A0是空脚。
对于24C16,无器件地址位,3位都为页地址位,而A2,A1和A0是空脚。器件地址信息的LSB为读/写操作选择位,高为读操作,低为写操作。若比较器件地址一致,EEPROM将输出应答“0”,如果不一致,则返回到待机状态。

其它容量可以对照上面的规格参数。
三 、读写时序
AT24C01/02寄存器地址只有一个字节,没有页选择位;AT24C04~AT24C16也是寄存器有一个字节,但是有页选择位;AT24C32~AT24C512寄存器有两个地址,没有页选择位(不放图)。
1、字节写
写操作要求在接收器件地址和ACK应答后,接收8位的字地址(24C32以上是16位地址)。接收到这个地址后EEPROM应答"0",然后是一个8位数据。在接收8位数据后,EEPROM应答"0",接着必须由主器件发送停止条件来终止写序列。
此时EEPROM进入内部写周期twr(twr,即写入时的延时,一般为10ms),数据写入非易失性存储器中,在此期间所有输入都无效。直到写周期完成,EEPROM才会有应答。

上图是24C01/24C02的写单个字节时序图

上图是以24C04单字节写为例,对照上面的规格参数可以分析得到24C08/24C16等时序,都需要添加页选择位。

上图是以24C1024单字节写为例,对照上面的规格参数可以分析得到24C32/24C64等时序,都需要添加页选择位,且寄存器地址需要写16个字节。
2、页写
24C02器件按8字节/页执行页写,24C04/08/16器件按16字节/页执行页写,24C32/64器件按32字节/页执行页写。
页写初始化与字节写相同,只是主器件不会在第一个数据后发送停止条件,而是在EEPROM收到每个数据后都应答"0”。最后仍需由主器件发送停止条件,终止写序列。
接收到每个数据后,字地址的低3位(24C02)或4位(24C04/08/16)或5位(24C32/64)内部自动加1,高位地址位不变,维持在当前页内。当内部产生的字地址达到该页边界地址时,随后的数据将写入该页的页首。如果超过8个(24C02)或16个(24C04/08/16)或32个(24C32/64)数据传送给了EEPROM,字地址将回转到该页的首字节,先前的字节将会被覆盖。

上图是24C01/24C02多字节页写时序图

上图是以24C04页写为例,器件地址只有两位,另一位为页选择位,对照上面的规格参数可以分析得到24C08/24C16等时序,都需要添加页选择位。
3、当前地址读
内部地址计数器保存着上次访问时最后一个地址加1的值。只要芯片有电,该地址就一直保存。当读到最后页的最后字节,地址会回转到0;当写到某页尾的最后一个字节,地址会回转到该页的首字节。
接收器件地址(读/写选择位为"1")、EEPROM应答ACK后,当前地址的数据就随时钟送出。
主器件无需应答"0",但需发送停止条件。

4、随机读
随机读需要先写一个目标字地址,来载入数据字节的地址(就是读指定单个地址的数据)。EEPROM接收到器件地址和数据地址且应答后,单片机必须产生新的起始信号,发送器件地址,读写选择位置1,开始“读当前地址”操作。EEPROM应答器件地址,并输出数据字。主器件无需应答"0",但需发送停止条件。

上图是24C02/24C02的读任意地址时序。

上图是24C04/24C08/24C16的读任意地址时序,第一次发送的器件地址包含页选择位P0-P2。
5、顺序读
顺序读,既可以由“读当前地址”开始,也可以由“任意读”开始(由“读当前地址”开始,则读取的地址是地址计数器里的地址+1,由“任意读”开始,则需要先发送一个地址)。
单片机收到一个数据,回复一个应答信号,只要EEPROM收到应答,就继续自动递增数据的地址,并输出数据。当到达存储地址的极限时,也会发生“翻转”现象,继续“顺序读”。顺序读直到单片机不应答而是产生停止信号才停止。

上图是24C01/24C02的读顺序地址时序。

上图是24C04/24C08/24C16的读顺序地址时序。
四、读写驱动主要代码
以下代码只给出一部分,并且读写单个字节驱动只适用24C01/02,读写多个数据需要配合EEPROM驱动。需注意24C32以上容量的EEPROM需要多写一次寄存器地址,因为他们的寄存器地址占两个字节。
1、宏定义
#define EE_02_DEV_ADDR 0xA0
#define EE_04_DEV_ADDR 0xA0
#define EE_32_DEV_ADDR 0xA0
#define EE_1M_DEV_ADDR 0xA0
#define EE_01_PAGE_NUM 16 //页数
#define EE_01_PAGE_SIZE 8 //页面大小(字节)
#define EE_01_SIZE (EE_01_PAGE_NUM * EE_01_PAGE_SIZE) //总容量(字节)
#define ADDR_BYTE_NUM 1 //地址字节个数
#define EE_02_PAGE_NUM 32 //页数
#define EE_02_PAGE_SIZE 8 //页面大小(字节)
#define EE_02_SIZE (EE_02_PAGE_NUM * EE_02_PAGE_SIZE) //总容量(字节)
#define ADDR_BYTE_NUM 1 //地址字节个数
#define EE_04_PAGE_NUM 32 //页数
#define EE_04_PAGE_SIZE 16 //页面大小(字节)
#define EE_04_SIZE (EE_04_PAGE_NUM * EE_04_PAGE_SIZE) //总容量(字节)
#define EE_08_PAGE_NUM 64 //页数
#define EE_08_PAGE_SIZE 16 //页面大小(字节)
#define EE_08_SIZE (EE_08_PAGE_NUM * EE_08_PAGE_SIZE) //总容量(字节)
#define EE_16_PAGE_NUM 128 //页数
#define EE_16_PAGE_SIZE 16 //页面大小(字节)
#define EE_16_SIZE (EE_16_PAGE_NUM * EE_16_PAGE_SIZE) //总容量(字节)
#define EE_32_PAGE_NUM 128 //页数
#define EE_32_PAGE_SIZE 32 //页面大小(字节)
#define EE_32_SIZE (EE_32_PAGE_NUM * EE_32_PAGE_SIZE) //总容量(字节)
#define EE_64_PAGE_NUM 256 //页数
#define EE_64_PAGE_SIZE 32 //页面大小(字节)
#define EE_64_SIZE (EE_64_PAGE_NUM * EE_64_PAGE_SIZE) //总容量(字节)
#define EE_128_PAGE_NUM 256 //页数
#define EE_128_PAGE_SIZE 64 //页面大小(字节)
#define EE_128_SIZE (EE_128_PAGE_NUM * EE_128_PAGE_SIZE)//总容量(字节)
#define EE_256_PAGE_NUM 512 //页数
#define EE_256_PAGE_SIZE 64 //页面大小(字节)
#define EE_256_SIZE (EE_256_PAGE_NUM * EE_256_PAGE_SIZE)//总容量(字节)
#define EE_512_PAGE_NUM 512 //页数
#define EE_512_PAGE_SIZE 128 //页面大小(字节)
#define EE_512_SIZE (EE_512_PAGE_NUM * EE_512_PAGE_SIZE)//总容量(字节)
#define EE_1M_PAGE_NUM 512 //页数
#define EE_1M_PAGE_SIZE 256 //页面大小(字节)
#define EE_1M_SIZE (EE_1M_PAGE_NUM * EE_1M_PAGE_SIZE) //总容量(字节)
2、写单个字节
/**
* @brief 写单个数据
* @param dev:iic设备结构体
* @param reg_addr:寄存器地址
* @param pdata:发送的数据
* @retval 0:写入失败,1:写入成功
*/
uint8_t i2c_write_byte(i2c_dev_t *dev, uint16_t reg_addr, uint8_t pdata)
{
I2C_Start(dev->bus); //dev->bus:IIC的哪一条线
I2C_SendByte(dev->addr, dev->bus); //dev->addr:IIC的地址
if(!I2C_Ackword_E) //应不应答
return 0;
if(dev->bus != IIC_SW_1) //不等于24C01-16时需要添加这行,24C32-512不需要
{
if(dev->attr & IDA_EEPROM)
{
I2C_SendByte(reg_addr >> 8, dev->bus); //这里需要注意高低位
if(!I2C_Ackword_E)
return 0;
}
}
I2C_SendByte(reg_addr, dev->bus); //寄存器地址
if(!I2C_Ackword_E)
return 0;
I2C_SendByte(pdata, dev->bus);
if(!I2C_Ackword_E)
return 0;
I2C_Stop(dev->bus);
if(dev->attr & IDA_EEPROM) //EEPROM
Delay_ms(10); //typical write cycle time of EEPROM twr延迟,对eeprom写完一页之后需要延时
return 1;
}
3、读单个数据
/**
* @brief 读单个数据
* @param dev:iic设备结构体
* @param reg_addr:寄存器地址
* @retval 读取到的值/0
*/
uint8_t i2c_read_byte(i2c_dev_t *dev, uint16_t reg_addr)
{
uint8_t pdata;
I2C_Start(dev->bus);
I2C_SendByte(dev->addr, dev->bus);
if(!I2C_Ackword_E)
return 0;
if(dev->bus != IIC_SW_1) //不等于24C01-16时需要添加这行,24C32-512不需要
{
if(dev->attr & IDA_EEPROM)
{
I2C_SendByte(reg_addr >> 8, dev->bus);
if(!I2C_Ackword_E)
return 0;
}
}
I2C_SendByte(reg_addr, dev->bus);
if(!I2C_Ackword_E)
return 0;
I2C_Start(dev->bus);
I2C_SendByte(dev->addr + 1, dev->bus); //read
if(!I2C_Ackword_E)
return 0;
pdata = I2C_ReceiveByte(dev->bus);
I2C_Ackn(0, dev->bus); //no ack
I2C_Stop(dev->bus);
return pdata;
}
4、写多个数据
/**
* @brief 写多个数据
* @param dev:iic设备结构体
* @param reg_addr:寄存器地址
* @param pbuf:发送的数据首地址
* @param size:发送的数据首地址
* @retval 0:写入失败,1:写入成功
*/
uint8_t i2c_write_more_bytes(i2c_dev_t *dev, uint16_t reg_addr, uint8_t *pbuf, uint32_t size)
{
uint16_t i;
if((dev == NULL) || (size == 0) || (pbuf == NULL))
return 0;
I2C_Start(dev->bus); //dev->bus:IIC的哪一条线
I2C_SendByte(dev->addr, dev->bus); //dev->addr:IIC的地址
if(!I2C_Ackword_E) //应不应答
{
printf("write dev addr erro\r\n");
return 0;
}
// if(dev->bus != IIC_SW_1) //不等于24C01-16时需要添加这行,24C32-512不需要
{
if(dev->attr & IDA_EEPROM)
{
I2C_SendByte(reg_addr >> 8, dev->bus);
if(!I2C_Ackword_E)
return 0;
}
}
I2C_SendByte(reg_addr, dev->bus); //寄存器地址
if(!I2C_Ackword_E)
return 0;
for(i = 0; i < size; i++) {
I2C_SendByte(pbuf[i], dev->bus);
if(!I2C_Ackword_E)
return 0;
}
I2C_Stop(dev->bus);
if(dev->attr & IDA_EEPROM) //EEPROM
Delay_ms(10); //typical write cycle time of EEPROM twr延迟,对eeprom写完一页之后需要延时
return 1;
}
5、读多个数据
/**
* @brief 读多个数据
* @param dev:iic设备结构体
* @param reg_addr:寄存器地址
* @param pbuf:发送的数据首地址
* @param size:发送的数据首地址
* @retval 0:读取失败,1:读取成功
*/
uint8_t i2c_read_more_bytes(i2c_dev_t *dev, uint16_t reg_addr, uint8_t *pbuf, uint32_t size)
{
uint16_t i;
if((dev == NULL) || (size == 0) || (pbuf == NULL))
return 0;
I2C_Start(dev->bus);
I2C_SendByte(dev->addr, dev->bus);
if(!I2C_Ackword_E)
{
printf("dev->addr is no ack\r\n");
return 0;
}
// if(dev->bus != IIC_SW_1) //不等于24C01-16时需要添加这行,24C32-512不需要
{
if(dev->attr & IDA_EEPROM)
{
I2C_SendByte(reg_addr >> 8, dev->bus);
if(!I2C_Ackword_E)
return 0;
}
}
I2C_SendByte(reg_addr, dev->bus);
if(!I2C_Ackword_E)
{
printf("dev->reg_addr is no ack\r\n");
return 0;
}
I2C_Start(dev->bus);
I2C_SendByte(dev->addr + 1, dev->bus); //read
if(!I2C_Ackword_E)
{
printf("dev->addr is no ack\r\n");
return 0;
}
for(i = 0; i < (size-1); i++) {
*pbuf = I2C_ReceiveByte(dev->bus);
I2C_Ackn(1, dev->bus); //ack
pbuf++;
}
*pbuf = I2C_ReceiveByte(dev->bus);
I2C_Ackn(0, dev->bus); //no ack
I2C_Stop(dev->bus);
return 1;
}
6、EEPROM读写操作主要程序
uint32_t x24Cxx_write_bytes(EEPROM_TYPE eeType, uint32_t regAddr, uint8_t *pWrData, uint32_t nBytes)
{
i2c_dev_t dev = {IIC_EE, 0,IDA_EEPROM};
uint16_t firstBytes, lastBytes, pageSize, pageCount, i, reg_addr;
uint32_t pageAddr, totalSize, pageAddrMask;
if(eeType == EE_24C02)
{
totalSize = EE_02_SIZE; //总容量
pageSize = EE_02_PAGE_SIZE; //页面大小
}else if(eeType == EE_24C04) {
totalSize = EE_04_SIZE;
pageSize = EE_04_PAGE_SIZE;
}else if(eeType == EE_24C32) {
totalSize = EE_32_SIZE;
pageSize = EE_32_PAGE_SIZE;
}else if(eeType == EE_24CM01) {
totalSize = EE_1M_SIZE;
pageSize = EE_1M_PAGE_SIZE;
}else
return 0;
if((pWrData == NULL) || (nBytes == 0)) //如果数据为空或者长度为0就返回0
return 0;
if((regAddr + nBytes) > totalSize) //寄存器地址+长度大于总容量返回0
return 0;
pageAddrMask = totalSize - pageSize; //总容量-页面大小=页地址掩码
pageAddr = regAddr & pageAddrMask; //用于确定起始页地址
firstBytes = pageAddr + pageSize - regAddr; //计算第一个页内要写入的字节数firstBytes
if(nBytes > firstBytes) { //数据长度>
pageCount = (nBytes - firstBytes) / pageSize; //计算页数
lastBytes = (nBytes - firstBytes) % pageSize; //计算不足页数的最后一页的数据长度
} else {
firstBytes = nBytes; //不足一页,按照原有长度计算
pageCount = 0; //页数为0
lastBytes = 0;
}
if(eeType == EE_24C02){
dev.bus = IIC_SW_1;
dev.addr = EE_02_DEV_ADDR;
reg_addr = regAddr;
}else if(eeType == EE_24C04){
dev.bus = IIC_SW_1;
dev.addr = EE_04_DEV_ADDR|(((uint8_t)((pageAddr >> 8) & 0x07)) << 1);//器件寻址+页选择位;
reg_addr = regAddr; //寄存器地址从regAddr起始地址开始
}else if(eeType == EE_24C32){
dev.addr = EE_32_DEV_ADDR;
reg_addr = regAddr;
}else if(eeType == EE_24CM01){
dev.addr = EE_1M_DEV_ADDR|(uint8_t)((regAddr&(totalSize>>1))>>15);
reg_addr = regAddr&0xFFFF;
}
i2c_write_more_bytes(&dev,reg_addr, pWrData, firstBytes); //首先写入第一页的数据
pWrData += firstBytes; //数据包起始地址+第一页的全部数据的数量=还没写入数据包的首地址
for(i = 1; i <= pageCount; i++) { //因为第一页已经写入了,所以从第二页开始
pageAddr += pageSize; //页地址+页大小=现在地址
if(eeType == EE_24C02) {
dev.bus = IIC_SW_1;
dev.addr = EE_02_DEV_ADDR;
reg_addr = pageAddr;
}else if(eeType == EE_24C04) {
dev.bus = IIC_SW_1;
dev.addr = EE_04_DEV_ADDR|(((uint8_t)((pageAddr >> 8) & 0x07)) << 1);//器件寻址+页选择位;
reg_addr = pageAddr;
} else if(eeType == EE_24C32) {
dev.addr = EE_32_DEV_ADDR;
reg_addr = pageAddr;
} else if(eeType == EE_24CM01) {
dev.addr = EE_1M_DEV_ADDR|(uint8_t)((regAddr&(totalSize>>1))>>15);
reg_addr = pageAddr&0xFFFF;
}
i2c_write_more_bytes(&dev,reg_addr, pWrData, pageSize); //写入每页的长度数据
pWrData += pageSize; //数据包地址偏移页的长度
}
if(lastBytes > 0) { //剩余不足一页的长度
pageAddr += pageSize;
if(eeType == EE_24C02) {
dev.bus = IIC_SW_1;
dev.addr = EE_02_DEV_ADDR;
reg_addr = pageAddr;
} else if(eeType == EE_24C04) {
dev.bus = IIC_SW_1;
dev.addr = EE_04_DEV_ADDR|(((uint8_t)((pageAddr >> 8) & 0x07)) << 1);//器件寻址+页选择位;
reg_addr = pageAddr;
}else if(eeType == EE_24C32) {
dev.addr = EE_32_DEV_ADDR;
reg_addr = pageAddr;
} else if(eeType == EE_24CM01) {
dev.addr = EE_1M_DEV_ADDR|(uint8_t)((regAddr&(totalSize>>1))>>15);
reg_addr = pageAddr&0xFFFF;
}
i2c_write_more_bytes(&dev,reg_addr, pWrData, lastBytes);
}
return nBytes;
}
uint8_t x24Cxx_read_bytes(EEPROM_TYPE eeType, uint32_t regAddr, uint8_t *pRdData, uint32_t nBytes)
{
i2c_dev_t dev = {IIC_EE, 0, IDA_EEPROM};
uint16_t reg_addr;
uint32_t totalSize;
if(eeType == EE_24C02) {
dev.bus = IIC_SW_1;
dev.addr = EE_02_DEV_ADDR;
reg_addr = regAddr;
totalSize = EE_02_SIZE;
}else if(eeType == EE_24C04) {
dev.bus = IIC_SW_1;
dev.addr = EE_04_DEV_ADDR|(((uint8_t)((regAddr >> 8) & 0x07)) << 1);//器件寻址+页选择位
reg_addr = regAddr;
totalSize = EE_04_SIZE;
}else if(eeType == EE_24C32) {
dev.addr = EE_32_DEV_ADDR;
reg_addr = regAddr;
totalSize = EE_32_SIZE;
}else if(eeType == EE_24CM01) {
dev.addr = EE_1M_DEV_ADDR|(uint8_t)((regAddr&(EE_1M_SIZE>>1))>>15);
reg_addr = regAddr&0xFFFF;
totalSize = EE_1M_SIZE;
}else
return 0;
if((NULL == pRdData) || (0 == nBytes))
return 0;
if((regAddr + nBytes) > totalSize) //寄存器地址+长度大于总容量
nBytes = totalSize - regAddr;
return i2c_read_more_bytes(&dev,reg_addr, pRdData, nBytes);
}
计算偏移量时注意不要用HEX计算,尽量用计算器的十进制计算。
五、注意事项
1、本章工程代码之所以写得那么冗余,是想后面用到的时候主要去删除或者修改就行,不用添加大量的新代码。
2、在写底层驱动的时候建议把全部库函数都换成底层寄存器操作,这样可能会增加程序的效率。
3、调用写入程序(无论是单字节写入还是多字节写),需要延时10ms(即twr,具体延时多少需要对照手册)后再对器件进行操作。4、测试时发现FM24C1024A这款芯片只能读写到0x1FDFF,还有最后的512字节读写不上,目前测试的AT24C02、AT24C04、AT24C32均正常。
更多推荐



所有评论(0)