I2C看起来是最简单的协议——两根线、几个状态,谁都能写个状态机跑起来。但能跑起来和能稳定跑之间,隔着一道天堑。
在FPGA里实现I2C,很多坑不是协议本身的问题,而是FPGA和I2C协议的“性格冲突”——I2C是异步的、需要等待的、有仲裁的,而FPGA是同步的、时序驱动的、没有天然超时机制的。
这篇文章列出了4个最常见的坑,每个坑都有错误做法和正确做法的对比,帮你在上板前就避开。
坑一:状态机没有超时保护——从机不响应,整个系统挂死
问题现象
I2C模块在正常情况下工作完美,但某天从设备意外断电或地址配错,ACK永远不来。状态机一直停在等待ACK的状态,整个FPGA系统的后续逻辑全部卡死。
仿真里测不到这个问题,因为仿真里从设备永远会响应。只有上板后,当你遇到真实的通信失败,才会发现这个致命缺陷。
根本原因
I2C状态机是基于“事件驱动”的——发一个字节→等ACK→ACK来了→继续。但如果ACK永远不会来,状态机就没有“出口”,永远卡在等待状态。FPGA不像单片机有看门狗,一个死锁的状态机会拖死整个系统。
错误做法
verilog
// 典型错误:只靠ACK信号驱动状态转移
always @(posedge clk or negedge rst_n) begin
if (!rst_n) state <= IDLE;
else case (state)
SEND_ADDR: if (ack_received) state <= SEND_DATA;
// 没有else分支!如果ack永远不来,永远卡在这里
endcase
end
正确做法
每个等待ACK的状态都要有超时计数器,超时后强制回到IDLE并上报错误。
localparam TIMEOUT_CNT = 500_000; // 10ms @ 50MHz
reg [19:0] timeout_cnt;
reg error_flag; // 错误标志位定义
always @(posedge clk or negedge rst_n) begin
if (!rst_n) state <= IDLE;
else case (state)
SEND_ADDR: begin
timeout_cnt <= timeout_cnt + 1;
if (ack_received)
state <= SEND_DATA;
else if (timeout_cnt >= TIMEOUT_CNT) begin
state <= IDLE; // 超时保护
error_flag <= 1;
end
end
// 其他等待状态同理:超时则强制回到IDLE
endcase
end
关键点:
-
超时时间设置在5~10ms范围(I2C标准模式一个字节传输约100µs,10ms足够覆盖100个字节)
-
超时后必须上报错误,而不是静默恢复——否则你永远不知道通信失败过
-
每个状态切换都要清零计数器
坑二:7位地址和8位地址搞混——地址左移还是不左移?
问题现象
用I2C IP核配置从设备地址,明明地址是对的(datasheet写着0x50),但ACK就是不回来。换了好几个传感器都一样,寄存器读写全部失败。
根本原因
这是I2C最经典的混乱点——从设备地址的编码方式。
| Datasheet写法 | 实际含义 | 需要左移吗? |
|---|---|---|
| 0x50 | 已经是7位地址 | 需要左移1位 |
| 0xA0 | 已经是8位地址(含R/W位) | 不需要 |
| A0 A1 A2 | 3位可编程地址 | 需要拼接后左移1位 |
I2C协议传输时,地址字节是 [A6 A5 A4 A3 A2 A1 A0 R/W],共8位。其中前7位是设备地址,最低位是读写方向。
错误做法
verilog
// 错误:直接把datasheet的7位地址当8位用
wire [7:0] tx_data = (state == SEND_ADDR) ? 8'h50 : data_reg;
// 0x50是7位地址,直接发给总线,从设备看到的实际地址是0x28(右移了1位)
正确做法
verilog
// 正确:7位地址左移1位,最低位填R/W
wire [7:0] addr_byte = {slave_addr[6:0], 1'b0}; // 写操作
wire [7:0] addr_read = {slave_addr[6:0], 1'b1}; // 读操作
always @(*) begin
tx_data = (state == SEND_ADDR) ?
(rw_mode == WRITE ? addr_byte : addr_read) : data_reg;
end
自检方法:
-
先确认datasheet上的地址是7位还是8位——看描述里有没有"7-bit address"或写的是A0~A6
-
如果datasheet给了两个值(如0x50和0xA0),0xA0是8位格式,直接用;0x50是7位格式,左移1位
-
用示波器抓一下SCL/SDA波形,数一下地址字节里到底发了什么
坑三:上拉电阻选错——信号上升沿太慢,数据出错
问题现象
I2C通信时好时坏。低速(100kHz)正常,一换到快速模式(400kHz)就开始出错。示波器上看,SCL和SDA的上升沿非常缓慢,像个斜坡而不是方波。
根本原因
I2C的SCL和SDA是开漏输出 + 外部上拉结构。信号的低电平是器件主动拉低,高电平靠上拉电阻充电恢复。
上升时间由 RC时间常数 决定:
-
R = 上拉电阻值
-
C = 总线电容(线长 + 器件寄生电容)
I2C标准规定的最大上升时间:
-
标准模式(100kHz):1000ns
-
快速模式(400kHz):300ns
-
快速模式Plus(1MHz):120ns
如果上拉电阻太大或总线电容太大,上升时间超标,在SCL上升沿还没到高电平时,接收端就采样了——采到的可能是0而不是1。
错误做法
verilog
// 错误:直接用FPGA引脚驱动SDA/SCL,不用开漏
assign SDA = (sda_out_en) ? sda_data : 1'bz;
// 但如果引脚配置成推挽输出,两个主设备同时驱动会烧管脚
若硬件搭配10kΩ上拉电阻运行400kHz模式,信号上升时间会严重超标。
正确做法
硬件层面:
根据总线电容选择上拉电阻:
| 总线电容 | 推荐上拉电阻 | 适用模式 |
|---|---|---|
| < 200pF | 4.7kΩ | 标准模式 + 快速模式 |
| 200~400pF | 2.2kΩ | 快速模式 |
| 400pF~ | 1kΩ 或降低速率 | 需要评估 |
FPGA端开漏配置:
verilog
// SDA/SCL必须是开漏输出(三态)
assign SDA = (sda_oe) ? 1'b0 : 1'bz;
assign SCL = (scl_oe) ? 1'b0 : 1'bz;
关键点:
-
FPGA引脚必须配置为开漏(Open Drain),不能配成推挽
-
示波器看波形是第一验证手段——如果上升时间>300ns(快速模式),先换上拉电阻
💡 提示:如果示波器看到上升沿明显呈RC充电曲线,且上升时间超标,优先减小上拉电阻。
坑四:时钟拉伸(Clock Stretching)没处理——和某些从设备完全不兼容
问题现象
FPGA写的I2C主机模块在大部分从设备上工作正常,但换到某个EEPROM或传感器后,读操作总是丢数据,读回来的数据每个字节都错位。写操作反而正常。
根本原因
I2C协议有一个经常被忽略的特性:时钟拉伸(Clock Stretching)。从设备有权在传输过程中拉低SCL,告诉主机“我还没准备好,等一下”。
这在以下场景特别常见:
-
EEPROM的页写入后需要内部编程时间(几ms)
-
某些传感器的ADC转换需要时间
-
多字节数据传输时从设备处理速度跟不上
大部分FPGA工程师写I2C主机时,SCL是固定频率产生的,根本不管从设备有没有拉低SCL——直接在下一个时钟周期就继续驱动了。
错误做法
verilog
// 错误:SCL固定周期翻转,不管从设备状态
always @(posedge clk) begin
if (scl_cnt == SCL_HALF)
scl_out <= ~scl_out;
end
// 当从设备拉低SCL请求等待时,主设备无视,导致读数据错位
正确做法
SCL释放为高阻后,必须实时采样总线SCL电平;若从设备主动拉低SCL进行时钟拉伸,主机需暂停时序、持续等待。
verilog
localparam SCL_WAIT_TIMEOUT = 1000; // 自定义等待超时阈值
// SCL状态机(只展示核心等待逻辑)
always @(posedge clk) begin
case (scl_state)
SCL_LOW: begin
// 拉低SCL半周期,然后释放
if (scl_cnt == SCL_HALF) scl_state <= SCL_HIGH_WAIT;
end
SCL_HIGH_WAIT: begin
// 释放SCL(scl_oe=0),等待总线SCL真正被拉高
if (scl_cnt >= 2 && SCL == 1'b1)
scl_state <= SCL_HIGH;
else if (scl_cnt >= SCL_WAIT_TIMEOUT) begin // 防止死锁
scl_state <= SCL_LOW;
error_flag <= 1;
end
end
SCL_HIGH: begin
// 保持高电平半周期,然后回到低电平
if (scl_cnt == SCL_HALF) scl_state <= SCL_LOW;
end
endcase
end
关键点:
-
SCL释放为高阻后,检测实际引脚电平(
SCL == 1'b1),确认从设备也释放了SCL -
为防止短时毛刺,连续采样2~3个周期确认高电平再进入
SCL_HIGH状态 -
仍然需要超时保护——防止从设备故障导致SCL永远拉低
-
时钟拉伸检测要在SCL的每个高电平之前都执行,不能只在特定状态检查(因为从设备随时可能拉低SCL)
调试自检清单
| 检查项 | 通过条件 | 你的项目 |
|---|---|---|
| 状态机有超时保护 | 每个等待状态都有计数器+超时退出 | ☐ |
| 地址编码正确 | 7位左移1位,8位直接用 | ☐ |
| 上拉电阻合适 | 4.7kΩ(标准/快速),≤300ns上升时间 | ☐ |
| SCL支持时钟拉伸 | 释放SCL后等待从设备确认 | ☐ |
| 引脚配置开漏 | 不是推挽输出,SDA/SCL为inout | ☐ |
| 示波器验证波形 | SCL/SDA边沿干净,无毛刺 | ☐ |
FAQ
Q:为什么不直接用Xilinx/Altera的I2C IP核?
A:IP核当然能用,但IP核的配置参数多,出问题不好排查。理解了底层的坑,用IP核时能更快定位问题。而且很多项目要求自定义I2C逻辑(比如多主仲裁、特殊时序),这时候必须自己写。
Q:时钟拉伸在实际项目里常见吗?
A:比你想象的多。大部分EEPROM都支持,很多传感器也支持。你之前不遇到只是因为恰好用的设备不需要拉伸,换个设备可能就踩坑了。
Q:上拉电阻用1kΩ会不会功耗太高?
A:VCC=3.3V时,1kΩ上拉电阻的静态电流约3.3mA,一个I2C总线两根线最多6.6mA。功耗不高,但在低功耗设计中需要考虑。
Q:总线长度超过1米怎么办?
A:考虑换成I2C缓冲器(如PCA9600)或降低速率到标准模式。超过1米的I2C总线,信号完整性和上升时间都会出问题。
Q:FPGA作为I2C主机,能否同时支持100kHz和400kHz?
A:可以,通过配置分频计数器实现可变速率的时钟生成,在状态机中根据速率选择不同的分频参数。
⚠️ 注意事项
-
超时保护不是可选项,是必选项。没有超时保护的I2C状态机等于埋了一颗定时炸弹。
-
先确认地址格式再写代码。7位和8位地址搞混是最浪费时间的低级错误,但每个人都会犯至少一次。
-
示波器是最好的调试工具。I2C的很多问题在逻辑分析仪里看不出来(因为电平不标准),必须用示波器看模拟波形。
-
时钟拉伸的处理要在每个SCL高电平之前都检查,不能只检查某几个特定的状态——因为你不知道从设备什么时候会拉低SCL。
关注我,这个系列,接口协议避坑专题。FPGA定制开发、项目调试、IP定制,开发服务可私。

657

被折叠的 条评论
为什么被折叠?



