3、EEPROM读写时序:I2C起始/停止条件、字节写入与页写入、随机读取与顺序读取、时序参数详解(tWR、tWC等)
各位工程师朋友,大家好。今天我们聊聊EEPROM的读写时序。说实话,这章内容看起来有点枯燥,但它是你调通I2C通信的基石。我见过太多项目,代码逻辑看起来没问题,但就是读写不对,最后查出来是时序踩坑了。
咱们先理清一个概念:EEPROM是通过I2C总线操作的。I2C协议本身有一套严格的时序要求,而EEPROM芯片在此基础上又加了自己的规矩。说白了,你得先懂I2C的“交通规则”,再懂EEPROM的“停车规则”。
3.1 I2C起始与停止条件
I2C总线上,每次通信都以一个起始条件开始,以一个停止条件结束。这两个信号由主机(通常是MCU)产生。
- 起始条件(S):SCL为高电平时,SDA从高电平切换到低电平。
- 停止条件(P):SCL为高电平时,SDA从低电平切换到高电平。
嗯,这里要注意:起始和停止条件都是SCL高电平时发生的。数据位的传输则是在SCL低电平时改变SDA,SCL高电平时采样SDA。千万别搞混了。
我个人习惯:在写驱动时,把起始和停止条件封装成两个独立的函数。这样代码清晰,调试时也容易定位问题。
我曾经在一个项目中,因为起始条件没拉够时间,导致从机一直没响应。后来用示波器一看,SDA下降沿还没稳定,SCL就拉高了。嗯,这就是典型的时序违规。
3.2 字节写入与页写入
EEPROM的写入操作有两种:字节写入和页写入。它们的区别在于一次能写多少数据。
3.2.1 字节写入
字节写入是最基本的操作。主机发送设备地址、字地址,然后发送一个字节的数据。流程如下:
- 主机发送起始条件
- 主机发送设备地址(7位地址 + 写位0)
- 从机应答(ACK)
- 主机发送字地址(高字节)
- 从机应答
- 主机发送字地址(低字节)
- 从机应答
- 主机发送数据字节
- 从机应答
- 主机发送停止条件
你想想看,每次只写一个字节,如果数据量大的话,效率很低。所以就有了页写入。
3.2.2 页写入
页写入允许一次写入多个字节,最多可以写一页(通常是8、16、32或64字节,具体看芯片型号)。流程和字节写入类似,只是在发送完第一个数据字节后,继续发送后续字节,直到停止条件。
避坑指南:我曾经在页写入时,没注意页边界。结果数据跨页了,后面的数据全写到了错误的地址。记住:页写入不能跨页!如果地址到达页边界,会自动回卷到页首。所以你要自己控制写入长度。
页写入的时序图如下:
3.3 随机读取与顺序读取
读取操作也有两种:随机读取和顺序读取。随机读取是先写地址再读数据,顺序读取是连续读多个字节。
3.3.1 随机读取
随机读取需要先执行一个“伪写入”操作来设置地址,然后重新发送起始条件和设备地址(读位1),再读取数据。流程如下:
- 主机发送起始条件
- 主机发送设备地址(写位0)
- 从机应答
- 主机发送字地址(高字节)
- 从机应答
- 主机发送字地址(低字节)
- 从机应答
- 主机发送重复起始条件
- 主机发送设备地址(读位1)
- 从机应答
- 从机发送数据字节
- 主机发送非应答(NACK)
- 主机发送停止条件
注意:随机读取时,主机在读取最后一个字节后必须发送NACK,否则从机会继续发送数据。我见过有人忘了发NACK,结果读出来的数据全是乱的。
3.3.2 顺序读取
顺序读取和随机读取类似,但主机在收到第一个数据字节后,发送ACK而不是NACK,然后继续读取下一个字节。直到主机想停止时,才发送NACK和停止条件。
顺序读取的代码示例:
// 顺序读取多个字节
uint8_t eeprom_sequential_read(uint16_t addr, uint8_t *buf, uint16_t len) {
// 伪写入:设置地址
i2c_start();
i2c_write(DEVICE_ADDR | 0); // 写位
i2c_wait_ack();
i2c_write(addr >> 8); // 地址高字节
i2c_wait_ack();
i2c_write(addr & 0xFF); // 地址低字节
i2c_wait_ack();
// 重复起始,切换为读模式
i2c_start();
i2c_write(DEVICE_ADDR | 1); // 读位
i2c_wait_ack();
// 读取数据
for (uint16_t i = 0; i < len; i++) {
buf[i] = i2c_read();
if (i == len - 1) {
i2c_send_nack(); // 最后一个字节发NACK
} else {
i2c_send_ack(); // 其他字节发ACK
}
}
i2c_stop();
return 0;
}
3.4 时序参数详解:tWR、tWC等
EEPROM的时序参数是很多工程师容易忽略的地方。说白了,这些参数决定了你的MCU和EEPROM之间能不能正常通信。
| 参数 | 说明 | 典型值 | 注意事项 |
|---|---|---|---|
| tWR | 写入周期时间(Write Cycle Time) | 5ms(典型) | 从停止条件到内部编程完成的时间 |
| tWC | 写入周期时间(Write Cycle Time,同tWR) | 5ms(典型) | 有些厂商用tWC,有些用tWR,一回事 |
| tHD:STA | 起始条件保持时间 | 4μs(100kHz) | SCL高电平后,SDA保持低电平的时间 |
| tSU:STO | 停止条件建立时间 | 4μs(100kHz) | SCL高电平前,SDA需要提前拉高的时间 |
| tBUF | 总线空闲时间 | 4.7μs(100kHz) | 停止条件和下一个起始条件之间的最小间隔 |
重点:tWR(或tWC)是EEPROM内部编程的时间。在写入操作结束后,EEPROM需要一段时间来完成内部编程。在这段时间内,EEPROM不会响应任何I2C命令。所以,你必须在每次写入后等待tWR时间,才能进行下一次操作。
我建议你在驱动中实现一个“等待写入完成”的函数。可以用轮询ACK的方式,也可以用延时。我个人习惯用轮询ACK,因为这样更高效。具体做法是:在写入后,发送设备地址,如果收到ACK,说明内部编程完成;如果收到NACK,说明还在编程中,继续等待。
避坑指南:我曾经在一个项目中,用了延时5ms来等待tWR。结果因为晶振误差和温度变化,实际延时不够,导致数据写错。后来改用轮询ACK,问题就解决了。所以,能用轮询就别用延时。
轮询ACK的代码示例:
// 等待EEPROM写入完成
void eeprom_wait_write_complete(void) {
while (1) {
i2c_start();
if (i2c_write(DEVICE_ADDR | 0) == 0) { // 发送设备地址,检查ACK
i2c_stop();
break; // 收到ACK,写入完成
}
// 收到NACK,继续等待
i2c_stop();
delay_ms(1); // 稍微延时一下,避免频繁查询
}
}
好了,关于EEPROM的读写时序,我们就聊到这里。记住:起始/停止条件是基础,字节/页写入是操作,随机/顺序读取是技巧,时序参数是保障。把这几点吃透了,EEPROM这块基本就没问题了。