14. 模拟I2C读取EEPROM
一、AT24C02简介
AT24C02 是一个 2K bit 的串行 EEPROM 存储器,内部含有 256 个字节。在 24C02 里面还有一个 8 字节的页写缓冲器。该设备的通信方式 I2C,通过其 SCL 和 SDA 与其他设备通信,芯片的引脚图如下图所示。
上图中有一个 WP,这个是写保护引脚,接高电平只读,接地允许读和写。每一个设备都有自己的设备地址,AT24C02 也不例外,但是 AT24C02 的设备地址是包括不可编程部分和可编程部分,可编程部分是根据上图的硬件引脚 A0、A1 和 A2 所决定。设备地址最后一位用于设置数据的传输方向,即读操作/写操作,0 是写操作,1 是读操作,具体格式如下图 所示:
二、AT24C02写时序
AT24C02 支持 字节写模式 和 页写模式。字节写模式 就是一个地址一个数据写入。页写模式 就是连续写入数据。只需要写一个地址,连续写入数据时地址会自增,但存在页的限制,超过一页时,超出数据覆盖原先写入数据。
2.1、字节写模式
主机在 I2C 总线发送第 1 个字节的数据为 AT24C02的设备地址 0xA0,用于寻找总线上找到 AT24C02,在获得 AT24C02 的应答信号之后,继续发送第 2 个字节数据,该字节数据是 AT24C02 的内存地址,再等到 AT24C02 的应答信号,主机继续发送第 3 字节数据,这里的数据即是写入在第 2 字节内存地址的数据。主机完成写操作后,可以发出停止信号,终止数据传输。
#define EEPROM_ADDRESS 0x50
/**
* @brief EEPROM写一个字节函数
*
* @param address 待写入数据的内存地址
* @param data 待写入的数据
*/
void EEPROM_WriteOneByte(uint16_t address, uint8_t data)
{
I2C_Simulate_Start(); // 1、产生起始信号
I2C_Simulate_SendOneByte(EEPROM_ADDRESS << 1); // 2、发送写操作地址
I2C_Simulate_WaitAck(); // 3、等待应答信号
I2C_Simulate_SendOneByte(address); // 4、发送内存地址
I2C_Simulate_WaitAck(); // 5、等待应答信号
I2C_Simulate_SendOneByte(data); // 6、发送数据
I2C_Simulate_WaitAck(); // 7、等待应答信号
I2C_Simulate_Stop(); // 8、产生停止信号
Delay_ms(10); // 9、等待EEPROM写入完成
}
2.2、页写模式
在单字节写时序时,每次写入数据时都需要先写入设备的内存地址才能实现,在页写时序中,只需要告诉 AT24C02 第一个内存地址 1,后面数据会按照顺序写入到内存地址 2,内存地址 3等,大大节省了通信时间,提高了时效性。因为 AT24C02 每次只能 8bit 数据,所以它的页大小也就是 1 字节。页写时序的操作方式跟上面的单字节写时序差不多。
/**
* @brief EEPROM在一页中写入多个字节
*
* @param address 待写入数据的内存地址
* @param pBuffer 待写入的数据
* @param length 待写入数据的个数
*
* @note EEPROM的页写模式存在页的限制,超过一页(8字节)时,超出数据覆盖原先写入数据
*/
static void EEPROM_WriteOnePage(uint16_t address, uint8_t *pBuffer, uint8_t length)
{
I2C_Simulate_Start(); // 1、产生起始信号
I2C_Simulate_SendOneByte(EEPROM_ADDRESS << 1); // 2、发送写操作地址
I2C_Simulate_WaitAck(); // 3、等待应答信号
I2C_Simulate_SendOneByte(address); // 4、发送内存地址
I2C_Simulate_WaitAck(); // 5、等待应答信号
for (uint8_t i = 0; i < length; i++)
{
I2C_Simulate_SendOneByte(pBuffer[i]); // 6、发送数据
I2C_Simulate_WaitAck(); // 7、等待应答信号
}
I2C_Simulate_Stop(); // 8、产生停止信号
Delay_ms(10); // 9、等待AT24C02写入完成
}
三、AT24C02读时序
AT24C02 支持 当前地址读模式,随机地址读模式 和 顺序读模式。AT24C02 的读操作会自动翻页。当前地址读模式 是基于上一次读/写操作的最后位置继续读出数据。随机地址读模式 是指定地址读出数据。顺序读模式 是连续读出数据。
3.1、当前地址读模式
内部地址计数器保存着上次访问时最后一个地址加 1 的值。只要芯片有电,该地址就一直保存。当读到最后页的最后字节,地址会回转到 0;当写到某页尾的最后一个字节,地址会回转到该页的首字节。接收器件地址(读/写选择位为 "1")、EEPROM 应答 ACK 后,当前地址的数据就随时钟送出。主器件无需应答 "0",但需发送停止条件。
3.2、随机地址读模式
AT24C02 读取数据的过程是一个复合的时序,其中包含写时序和读时序。先看第一个通信过程,这里是写时序,起始信号产生后,主机发送 AT24C02 设备地址 0xA0,获取从机应答信号后,接着发送需要读取的内存地址;在读时序中,起始信号产生后,主机发送 AT24C02 设备地址 0xA1,获取从机应答信号后,接着从机返回刚刚在写时序中内存地址的数据,以字节为单位传输在总线上,假如主机获取数据后返回的是应答信号,那么从机会一直传输数据,当主机发出的是非应答信号并以停止信号发出为结束,从机就会结束传输。
/**
* @brief EEPROM读一个字节函数
*
* @param address 待读取数据的内存地址
* @return uint8_t 读取的数据
*/
uint8_t EEPROM_ReadOneByte(uint16_t address)
{
uint8_t receive = 0;
I2C_Simulate_Start(); // 1、产生起始信号
I2C_Simulate_SendOneByte(EEPROM_ADDRESS << 1); // 2、发送写操作地址
I2C_Simulate_WaitAck(); // 3、等待应答信号
I2C_Simulate_SendOneByte(address); // 4、发送内存地址
I2C_Simulate_WaitAck(); // 5、等待应答信号
I2C_Simulate_Start(); // 6、再次产生起始信号
I2C_Simulate_SendOneByte((EEPROM_ADDRESS << 1) | 1); // 7、发送读操作地址
I2C_Simulate_WaitAck(); // 8、等待应答信号
receive = I2C_Simulate_ReadOneByte(1); // 9、读取数据,并发送非应答信号
I2C_Simulate_Stop(); // 10、产生停止信号
return receive; // 11、返回读取的数据
}
3.3、顺序读模式
顺序读可以通过 “当前地址读” 或 “随机读” 启动。主器件接收到一个数据后,应答 ACK。只要 EEPROM 接收到 ACK,将自动增加字地址并继续随时钟发送后面的数据。若达到存储器地址末尾,地址自动回转到 0,仍可继续顺序读取数据。主器件发送非应答信号,即可结束顺序读操。
/**
* @brief EEPROM顺序读取多个字节数据函数
*
* @param address 起始地址
* @param pBuffer 保存读取的数据的缓冲区
* @param length 读取字节的个数
*/
void EEPROM_ReadBytes(uint16_t address, uint8_t *pBuffer, uint16_t length)
{
I2C_Simulate_Start(); // 1、产生起始信号
I2C_Simulate_SendOneByte(EEPROM_ADDRESS << 1); // 2、发送写操作地址
I2C_Simulate_WaitAck(); // 3、等待应答信号
I2C_Simulate_SendOneByte(address); // 4、发送内存地址
I2C_Simulate_WaitAck(); // 5、等待应答信号
I2C_Simulate_Start(); // 6、再次产生起始信号
I2C_Simulate_SendOneByte((EEPROM_ADDRESS << 1) | 1); // 7、发送读操作地址
I2C_Simulate_WaitAck(); // 8、等待应答信号
for (uint8_t i = 0; i < length; i++)
{
if (i < length - 1)
{
pBuffer[i] = I2C_Simulate_ReadOneByte(0); // 9、读取数据,并发送应答信号
}
else
{
pBuffer[i] = I2C_Simulate_ReadOneByte(1); // 10、读取最后一个数据,并发送非应答信号
}
}
I2C_Simulate_Stop(); // 11、产生停止信号
}
四、原理图
从原理图上,我们可以看出 AT24C02 的 A0、A1 和 A2 引脚都接地,因此 AT24C02 的设备地址为 0x50
。
五、程序源码
/** 根据不同的 24CXX 型号, 发送高位地址
* 1、24C16以上的型号,分 2 个字节发送地址
* 2、24C16及以下的型号,,分 1 个低字节地址 + 占用器件地址的 bit1~bit3 位用于表示高位地址,最多 11 位地址
* 对于 24C01/02,其器件地址格式(8bit)为: 1 0 1 0 A2 A1 A0 R/W
* 对于 24C04, 其器件地址格式(8bit)为: 1 0 1 0 A2 A1 a8 R/W
* 对于 24C08, 其器件地址格式(8bit)为: 1 0 1 0 A2 a9 a8 R/W
* 对于 24C16, 其器件地址格式(8bit)为: 1 0 1 0 a10 a9 a8 R/W
* R/W : 读/写控制位 0: 表示写; 1: 表示读;
* A0/A1/A2 : 对应器件的 1、2、3引脚(只有 24C01/02/04/8 有这些脚)
* a8/a9/a10: 对应存储整列的高位地址,11bit 地址最多可以表示 2048 个位置, 可以寻址 24C16 及以内的型号
*/
AT24Cxx | 容量(bit) | 页数 | 页内字节数 | 数据地址(占用bit数) | 设备地址 | 寄存器高地址 | 寄存器低地址 |
---|---|---|---|---|---|---|---|
AT24C01 | 1 K bit(128 B) | 16 | 8 Byte | 7 bit | 1 0 1 0 A2 A1 A0 R/W |
a6 a5 a4 a3 a2 a1 a0 |
|
AT24C02 | 2 K bit(256 B) | 32 | 8 Byte | 8 bit | 1 0 1 0 A2 A1 A0 R/W |
a7 a6 a5 a4 a3 a2 a1 a0 |
|
AT24C04 | 4 K bit(512 B) | 32 | 16 Byte | 9 bit | 1 0 1 0 A2 A1 a8 R/W |
a7 a6 a5 a4 a3 a2 a1 a0 |
|
AT24C08 | 8 K bit(1024B) | 64 | 16 Byte | 10 bit | 1 0 1 0 A2 a9 a8 R/W |
a7 a6 a5 a4 a3 a2 a1 a0 |
|
AT24C16 | 16 K bit(2048 B) | 128 | 16 Byte | 11 bit | 1 0 1 0 a10 a9 a8 R/W |
a7 a6 a5 a4 a3 a2 a1 a0 |
|
AT24C32 | 32 K bit(4096 B) | 128 | 32 Byte | 12 bit | 1 0 1 0 A2 A1 A0 R/W |
a11 a10 a9 a8 |
a7 a6 a5 a4 a3 a2 a1 a0 |
AT24C64 | 64 K bit(8192 B) | 256 | 32 Byte | 13 bit | 1 0 1 0 A2 A1 A0 R/W |
a12 a11 a10 a9 a8 |
a7 a6 a5 a4 a3 a2 a1 a0 |
AT24C128 | 128 K bit(16384 B) | 256 | 64 Byte | 14 bit | 1 0 1 0 A2 A1 A0 R/W |
a13 a12 a11 a10 a9 a8 |
a7 a6 a5 a4 a3 a2 a1 a0 |
AT24C256 | 256 K bit(32768 B) | 512 | 64 Byte | 15 bit | 1 0 1 0 A2 A1 A0 R/W |
a14 a13 a12 a11 a10 a9 a8 |
a7 a6 a5 a4 a3 a2 a1 a0 |
AT24C512 | 512 K bit(65535 B) | 512 | 128 Byte | 16 bit | 1 0 1 0 A2 A1 A0 R/W |
a15 a14 a13 a12 a11 a10 a9 a8 |
a7 a6 a5 a4 a3 a2 a1 a0 |
EEPROM 写数据函数:
/**
* @brief EEPROM写数据函数
*
* @param address 待写入数据的内存地址
* @param pBuffer 待写入的数据
* @param length 待写入数据的个数
*/
void EEPROM_WriteBytes(uint16_t address, uint8_t *pBuffer, uint16_t length)
{
uint16_t offset = address % 8; // 在第一页中的偏移地址
uint16_t remain = 8 - offset; // 一页剩余空间大小
uint16_t pageNum = (length - remain) / 8; // 去除第一页后还需要写入的整页数
uint8_t lastPageByteNum = (length - remain) % 8; // 最后一页需要写入的字节数
// 当写入的数据小一页剩余空间大小,将写入的字节数赋值给页中剩余空间大小
remain = (length <= remain ? length : remain);
EEPROM_WriteOnePage(address, pBuffer, remain); // 写入第一页数据
address += remain;
pBuffer += remain;
while (pageNum--)
{
EEPROM_WriteOnePage(address, pBuffer, 8); // 写入整页数据
address += 8;
pBuffer += 8;
}
if (lastPageByteNum)
{
EEPROM_WriteOnePage(address, pBuffer, lastPageByteNum); // 写入最后一页数据
}
}
main() 函数内容如下:
int main(void)
{
uint8_t data[] = "hello world!";
uint8_t length = sizeof(data) / sizeof(data[0]);
uint8_t address = 5;
HAL_Init();
System_Clock_Init(8, 336, 2, 7);
Delay_Init(168);
UART_Init(&g_usart1_handle, USART1, 115200);
I2C_Simulate_Init();
EEPROM_WriteBytes(address, data, length);
EEPROM_ReadBytes(address, data, length);
printf("%s\r\n", data);
while (1)
{
}
return 0;
}