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 字节写入

字节写入是最基本的操作。主机发送设备地址、字地址,然后发送一个字节的数据。流程如下:

  1. 主机发送起始条件
  2. 主机发送设备地址(7位地址 + 写位0)
  3. 从机应答(ACK)
  4. 主机发送字地址(高字节)
  5. 从机应答
  6. 主机发送字地址(低字节)
  7. 从机应答
  8. 主机发送数据字节
  9. 从机应答
  10. 主机发送停止条件

你想想看,每次只写一个字节,如果数据量大的话,效率很低。所以就有了页写入。

3.2.2 页写入

页写入允许一次写入多个字节,最多可以写一页(通常是8、16、32或64字节,具体看芯片型号)。流程和字节写入类似,只是在发送完第一个数据字节后,继续发送后续字节,直到停止条件。

避坑指南:我曾经在页写入时,没注意页边界。结果数据跨页了,后面的数据全写到了错误的地址。记住:页写入不能跨页!如果地址到达页边界,会自动回卷到页首。所以你要自己控制写入长度。

页写入的时序图如下:

SCL SDA S 设备地址 字地址 数据1 数据N P

3.3 随机读取与顺序读取

读取操作也有两种:随机读取和顺序读取。随机读取是先写地址再读数据,顺序读取是连续读多个字节。

3.3.1 随机读取

随机读取需要先执行一个“伪写入”操作来设置地址,然后重新发送起始条件和设备地址(读位1),再读取数据。流程如下:

  1. 主机发送起始条件
  2. 主机发送设备地址(写位0)
  3. 从机应答
  4. 主机发送字地址(高字节)
  5. 从机应答
  6. 主机发送字地址(低字节)
  7. 从机应答
  8. 主机发送重复起始条件
  9. 主机发送设备地址(读位1)
  10. 从机应答
  11. 从机发送数据字节
  12. 主机发送非应答(NACK)
  13. 主机发送停止条件

注意:随机读取时,主机在读取最后一个字节后必须发送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这块基本就没问题了。

专注资料整理