目录

一、概述

二、芯片介绍

1、引脚说明

 2、规格参数

 3、器件寻址

三 、读写时序

1、字节写

2、页写

3、当前地址读

4、随机读

 5、顺序读

 四、读写驱动主要代码

1、宏定义

2、写单个字节 

3、读单个数据

 4、写多个数据

5、读多个数据

6、EEPROM读写操作主要程序

五、注意事项


一、概述

        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均正常。

参考以下代码
IIC_EEPROM · RONG/STM32_CODE - 码云 - 开源中国

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐