12、I2C总线通信:I2C协议时序、起始/停止条件、应答机制、EEPROM读写控制器设计
各位同学,今天我们来聊聊I2C总线。这个协议在嵌入式系统里太常见了,几乎每个FPGA工程师都会碰到。我个人习惯把I2C比作一个“多设备对话系统”——一根时钟线,一根数据线,就能挂一堆设备,省引脚又灵活。
但别小看它。我见过不少新手在I2C上栽跟头,尤其是时序细节没把握好,导致读写EEPROM时数据错乱。嗯,今天我们就把它彻底讲透。
12.1 I2C协议基础
I2C总线只有两根线:SCL(时钟线)和SDA(数据线)。所有设备都挂在这两根线上,通过地址来区分谁跟谁通信。
说白了,这就是个主从结构。主设备(比如我们的FPGA)负责产生时钟,发起通信。从设备(比如EEPROM)响应主设备的指令。你想想看,如果多个设备同时抢着说话,那不乱套了?所以协议里规定了严格的时序规则。
关键点: I2C是开漏输出,需要外部上拉电阻。这个细节很多人忽略,结果通信不稳定。我曾经遇到过一块板子,I2C死活调不通,最后发现是上拉电阻焊错了阻值。
12.2 起始条件与停止条件
I2C通信的开始和结束,都有特定的信号标志。
- 起始条件(START): SCL为高电平时,SDA从高电平跳变到低电平。
- 停止条件(STOP): SCL为高电平时,SDA从低电平跳变到高电平。
这两个条件由主设备产生。注意,起始和停止条件都是SCL在高电平时采样SDA的变化。正常数据传输时,SDA只能在SCL低电平时变化。这是协议的核心规则,千万别搞反了。
我的经验: 写FPGA代码时,我习惯把起始和停止条件做成独立的模块。这样复用起来方便,调试时也容易定位问题。
12.3 数据传输与应答机制
数据传输时,每个字节是8位,高位在前。每发送完一个字节,接收方需要回复一个应答位(ACK)。
- 应答(ACK): 第9个时钟周期,SDA被拉低,表示“收到了,继续”。
- 非应答(NACK): 第9个时钟周期,SDA保持高电平,表示“没收到”或“不想收了”。
为什么要有应答?说白了就是确认机制。主设备发送数据后,从设备必须告诉主设备“我收到了”。如果没收到应答,主设备就知道出问题了,可以重发或者报错。
我记得有一次调试EEPROM,读出来的数据全是0xFF。查了半天,发现是从设备根本没应答——原来是地址写错了。嗯,这种低级错误,犯过一次就记住了。
12.4 EEPROM读写控制器设计
好了,理论讲完,我们来实战。以AT24C02为例,设计一个EEPROM读写控制器。
AT24C02的容量是2Kbit(256字节),I2C地址是0xA0(写)和0xA1(读)。写操作需要先发送设备地址,再发送存储地址,最后发送数据。读操作则要先发送设备地址和存储地址,然后重新发送起始条件,再发送读命令。
12.4.1 写操作时序
START + 设备地址(写) + ACK + 存储地址 + ACK + 数据 + ACK + STOP
这里要注意,EEPROM写操作后需要一段时间(约5ms)来完成内部编程。这段时间内,EEPROM不会响应任何I2C命令。所以,连续写多个字节时,必须等待这个时间。
避坑指南: 我曾经在项目中连续写EEPROM,没加延时,结果数据全丢了。后来查手册才发现,每个写操作后必须等待内部编程完成。可以用查询应答位的方式来判断——EEPROM在编程期间会返回NACK。
12.4.2 读操作时序
START + 设备地址(写) + ACK + 存储地址 + ACK +
START + 设备地址(读) + ACK + 数据 + NACK + STOP
读操作比写操作多了一个“重新起始条件”。为什么?因为要先告诉EEPROM要读哪个地址,然后再切换成读模式。这个“重新起始条件”不能直接用STOP+START代替,否则会破坏总线状态。
12.4.3 FPGA控制器设计要点
用FPGA实现I2C控制器,核心是一个状态机。我个人习惯把状态机分成几个阶段:
- IDLE: 空闲状态,等待启动信号。
- START: 产生起始条件。
- ADDR: 发送设备地址,等待应答。
- WORD_ADDR: 发送存储地址,等待应答。
- DATA: 发送或接收数据,等待应答。
- STOP: 产生停止条件。
每个状态都要处理SCL和SDA的时序。SCL的频率决定了I2C的速率,标准模式是100kHz,快速模式是400kHz。FPGA里用计数器分频产生SCL时钟。
小技巧: 设计状态机时,我习惯把SCL的上升沿和下降沿作为状态跳转的触发条件。这样时序更清晰,不容易出错。
12.5 代码示例:I2C主控状态机
下面是一个简化的I2C写操作状态机代码(Verilog风格):
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
scl <= 1'b1;
sda <= 1'b1;
end else begin
case (state)
IDLE: begin
if (start_en) state <= START;
end
START: begin
// SCL高电平时,SDA拉低
scl <= 1'b1;
sda <= 1'b0;
state <= ADDR;
end
ADDR: begin
// 发送8位地址 + 1位应答
// 每个bit在SCL低电平时改变,高电平时采样
...
end
// 其他状态类似
endcase
end
end
注意,这只是框架代码。实际项目中,还需要处理时钟分频、数据移位、应答检测等细节。我建议你先用仿真验证时序,再上板调试。
12.6 常见问题与调试技巧
最后,分享几个我在项目中遇到的I2C问题:
- 总线挂死: SDA一直被拉低。通常是某个设备没有释放总线。可以尝试复位从设备,或者用示波器看波形。
- 应答丢失: 主设备收不到ACK。检查设备地址是否正确,上拉电阻是否合适。
- 数据错位: 读出来的数据顺序不对。检查MSB/LSB顺序,以及数据移位逻辑。
总结一下: I2C协议不复杂,但细节决定成败。起始/停止条件、应答机制、时序约束,每一个点都要吃透。写FPGA代码时,多用状态机,多仿真,多抓波形。等你调通第一个EEPROM读写,后面就顺了。
好了,这一章就到这里。下一章我们讲SPI总线,又是一个经典协议。到时候见。