前言
EEPROM有多种型号,操作方式略有不同。
从图中可以看到24C04占用了A0寻址地址作为数据寻址地址,24C08和24C16也占用了器件地址作为数据寻址地址,而24C32、24C64、24C128、24C256则使用2个字节为数据寻址地址。
一、EEPROM_TypeDef
抽象出EERPOM的结构体,包括IIC接口、页大小、和启动传输的方式等。
typedef enum
{
AT24C02 = 0,
AT24C04,
AT24C08,
AT24C16,
AT24C32 = 4,
AT24C64,
AT24C128,
AT24C256 = 7,
} EEPROM_Type;
typedef struct
{
IIC_TypeDef IIC;//IIC接口
EEPROM_Type Type;//EEPROM类型
uint16_t PageSize;//页大小
uint16_t PageCount;//页数
uint16_t TotalCapacity;//总容量
//因为EEPROM器件型号不同启动传输的操作也有所不同,所以使用的启动方式也有所不同,如AT24C04在启动一次传输时需要占用一个bit的器件地址做寻址地址
uint8_t (*EEPROM_StartTransmission)(void* self, uint16_t address);
} EEPROM_TypeDef;
一、应答查询
很多EEPROM的驱动程序都是写入之后添加一个10ms的延时等待在下一次查询,但EEPROM的数据手册上都是推荐使用应答查询的机制,在使用过程中也发现写入的字节少时等待的时间也会减少,使用延时的方法会造成一定的性能损耗。
/**
* @brief 判断EEPROM是否在忙状态,包含一个开始和发送设备地址的过程
* @param EEPROM_TypeDef *pEEPROM
* @param uint8_t devAdd 设备地址
* @retval 0:不忙可以继续操作,1:忙禁止操作
*/
static uint8_t EEPROM_IsBusy(EEPROM_TypeDef *const pEEPROM, uint8_t devAdd)
{
for (uint8_t i = 0; i < EEPROM_BUSY_CNT; i++)
{
IIC_Start(&pEEPROM->IIC);
if(IIC_SendAndSack(&pEEPROM->IIC, devAdd) == 0)
{
return 0;
}
if(i < EEPROM_BUSY_CNT - 1)
{
IIC_Stop(&pEEPROM->IIC);
Delay_ms(1);
}
}
return 1;
}
二、初始化
在初始化时设置根据器件型号设置一些参数,页大小,寻址操作函数和总容量。
/**
* @brief 寻址处理函数,支持AT24C02,AT24C04,AT24C08,AT24C16
* @param void* self
* @param uint16_t address
* @retval:
*/
uint8_t AT24C0X_StartTransmission(void* self, uint16_t address)
{
EEPROM_TypeDef* pEEPROM = self;
uint8_t add = pEEPROM->IIC.ADD | ((address>>8) & ~(0xff << (uint8_t)pEEPROM->Type));
if(EEPROM_IsBusy(pEEPROM, add))
return 1;
if (IIC_SendAndSack(&pEEPROM->IIC, address & 0xFF))
return 2;
return 0;
}
/**
* @brief 寻址处理函数,支持AT24C32,AT24C64,AT24C128,AT24C256
* @param void* self
* @param uint16_t address
* @retval:
*/
uint8_t AT24CXX_StartTransmission(void* self, uint16_t address)
{
IIC_TypeDef* IIC = self;
if (EEPROM_IsBusy(self, IIC->ADD))
return 1;
if (IIC_SendAndSack(IIC, address >> 8))
return 2;
if (IIC_SendAndSack(IIC, address & 0xFF))
return 3;
return 0;
}
/**
* @brief 初始化EEPROM
* @param EEPROM_TypeDef *pEEPROM
* @param EEPROM_Type type EEPROM类型
* @retval 0:成功,其他失败
*/
uint8_t EEPROM_Init(EEPROM_TypeDef *const pEEPROM, EEPROM_Type type)
{
IIC_Init(&pEEPROM->IIC);
switch(type)
{
case AT24C02:
case AT24C04:
case AT24C08:
case AT24C16:
pEEPROM->Type = type;
if (type == AT24C02) pEEPROM->PageSize = 8;
else pEEPROM->PageSize = 16;
pEEPROM->EEPROM_StartTransmission = &AT24C0X_StartTransmission;
break;
case AT24C32:
case AT24C64:
case AT24C128:
case AT24C256:
pEEPROM->Type = type;
if (type == AT24C256) pEEPROM->PageSize = 64;
else pEEPROM->PageSize = 32;
pEEPROM->EEPROM_StartTransmission = &AT24CXX_StartTransmission;
break;
default:
return 2;
}
pEEPROM->TotalCapacity = 256;
for (uint8_t i = 0; i < (uint8_t)type; i++)
{
pEEPROM->TotalCapacity *= 2;
}
pEEPROM->PageCount = pEEPROM->TotalCapacity / pEEPROM->PageSize;
return EEPROM_Check(pEEPROM);
}
三、写数据
1.写一个字节
/**
* @brief 写入一个字节
* @param EEPROM_TypeDef *pEEPROM
* @param uint16_t address 写入地址
* @param uint8_t dat 数据
* @retval 0:成功,其他失败
*/
uint8_t EEPROM_WriteByte(EEPROM_TypeDef *const pEEPROM, uint16_t address, uint8_t dat)
{
uint8_t result = 0;
if(pEEPROM->EEPROM_StartTransmission(pEEPROM, address))
{
result = 1;
goto err;
}
// 发送数据
if (IIC_SendAndSack(&pEEPROM->IIC, dat))
{
result = 2;
}
err:
IIC_Stop(&pEEPROM->IIC);
return result;
}
2.页写
从图中可以看到页写为在写入一个字节后继续发送数据,并且需要进行页对齐,这里是使用递归来实现的。
/**
* @brief:
* @param EEPROM_TypeDef *pEEPROM
* @param uint16_t address
* @param uint8_t *memory
* @param uint16_t size
* @retval:
*/
uint8_t EEPROM_Write(EEPROM_TypeDef *const pEEPROM, uint16_t address, uint8_t *memory, uint16_t size)
{
uint8_t result = 0;
//开始一次IIC传输
if(pEEPROM->EEPROM_StartTransmission(pEEPROM, address))
{
result = 1;
goto err;
}
// 写入数据
for (uint16_t i = 0; i < size; i++)
{
if (IIC_SendAndSack(&pEEPROM->IIC, memory[i]))
{
result = 2 + i;
break;
}
// 结束
if (i + 1 == size)
break;
//如果下一个地址是新页的开头结束本次传输
if ((address + i + 1) % pEEPROM->PageSize == 0)
{
IIC_Stop(&pEEPROM->IIC);//结束本页传输
//递归操作剩下的数据
return EEPROM_Write(pEEPROM, address + i + 1, memory + i + 1, size - i - 1);
}
}
err:
IIC_Stop(&pEEPROM->IIC);
return result;
}
四、读数据
1.读一个字节
从图中可以看出在发送完操作地址之后会再次发送一个开始时序和一个器件地址然后才开始读取数据。
/**
* @brief 读取一个字节
* @param EEPROM_TypeDef *pEEPROM
* @param uint16_t address
* @param uint8_t* dat
* @retval 0:成功,其他失败
*/
uint8_t EEPROM_ReadByte(EEPROM_TypeDef *const pEEPROM, uint16_t address, uint8_t* dat)
{
uint8_t result = 0;
// 发送写设备地址
if(pEEPROM->EEPROM_StartTransmission(pEEPROM, address))
{
result = 1;
goto err;
}
// 重启IIC
IIC_Start(&pEEPROM->IIC);
// 发送读设备地址
if (IIC_SendReadAddress(&pEEPROM->IIC))
{
result = 2;
goto err;
}
*dat = IIC_ReadAndAck(&pEEPROM->IIC, 0);
err:
IIC_Stop(&pEEPROM->IIC);
return result;
}
2.读多个数据
读多个数据较为简单,不需要像写数据一样进行页对齐。
/**
* @brief 读取数据
* @param EEPROM_TypeDef *pEEPROM
* @param uint16_t address
* @param uint8_t *memory
* @param uint16_t size
* @retval 0:成功,其他失败
*/
uint8_t EEPROM_Read(EEPROM_TypeDef *const pEEPROM, uint16_t address, uint8_t *memory, uint16_t size)
{
uint8_t result = 0;
// 发送写设备地址
if(pEEPROM->EEPROM_StartTransmission(pEEPROM, address))
{
result = 1;
goto err;
}
// 重启IIC
IIC_Start(&pEEPROM->IIC);
// 发送读设备地址
if (IIC_SendReadAddress(&pEEPROM->IIC))
{
result = 2;
goto err;
}
IIC_Read(&pEEPROM->IIC, memory, size )
err:
IIC_Stop(&pEEPROM->IIC);
return result;
}