嵌入式基础 -- I²C 信号与位层规则
I²C 信号与位层规则
- 两根线:
SCL
(时钟)与SDA
(数据),开漏/开集输出,必须上拉电阻(2.2k–10k 典型,取决于总线电容与速率)。FPGA 只能“拉低或高阻”,绝不主动拉高。 - 空闲:
SCL=1
、SDA=1
。 - START:
SCL=1
时,SDA
从 1→0。 - STOP:
SCL=1
时,SDA
从 0→1。 - 位有效窗口:数据在
SCL=1
期间有效、SCL=0
期间允许变化(除 START/STOP)。 - 第 9 个时钟为 ACK 位:每发完 8 bit 字节后,接收方在第 9 个 SCL 周期拉低 SDA 作为 ACK;不拉低即 NACK。
- 时序等级(常用):Standard(100 kHz)、Fast(400 kHz)、Fast+(1 MHz)、High-Speed(3.4 MHz)。速率越高,需要更小的上拉与更紧的布线。
- 时钟拉伸(Clock Stretching):从设备可在位间隙拉低 SCL 拖延时间,主机必须等待 SCL 真的被释放为高再继续。
- 多主仲裁:
SCL=1
期间,主机边发边读回 SDA;若自己想发“1”(释放)却读到“0”,判定丢失仲裁并立刻退让。
常见事务流程(7 位地址为例)
1) 主机写从机寄存器(典型“寄存器指针+写数据”)
START
- 发送
ADDR[6:0] + W(0)
→ 从机 ACK - 发送
REG_ADDR
(子地址/寄存器指针)→ 从机 ACK - 发送
DATA0
→ ACK;可继续DATA1/2/...
(每字节均有 ACK) STOP
2) 主机读取从机寄存器(“写指针 + 重启 + 读数据”)
-
START
-
ADDR + W
→ ACK -
发送
REG_ADDR
→ ACK -
Repeated START(不放
STOP
) -
ADDR + R(1)
→ ACK -
主机在每个数据字节后:
- 若还想继续读 → 回 ACK
- 读最后一字节 → 回 NACK
-
STOP
3) 直接顺序读取
START
→ADDR + R
→ 字节流(主机对前面字节回 ACK,最后回 NACK)→STOP
10 位地址、General Call(0x00)、PEC(SMBus) 都是扩展场景,FPGA 侧按需支持。
在 FPGA 里怎么实现(主机/从机通用要点)
IO 级:开漏输出
// 以通用三态举例:当 oe=1 时拉低,oe=0 时高阻(交给上拉)
assign sda = (sda_oe) ? 1'b0 : 1'bz;
wire sda_i = sda;assign scl = (scl_oe) ? 1'b0 : 1'bz; // 主机实现时钟用,或从机用于拉伸
wire scl_i = scl;
Lattice/Intel/Xilinx 可用 IOBUF/OBUFT 原语;不要驱动“1”。开发板“内置上拉(PULLMODE=UP)”强度很弱,只适合极低速+实验,实机务必用外部上拉。
位采样/去抖与同步
- 把
SCL/SDA
当异步输入,各加两级同步器与毛刺过滤(如 3~5 采样取多数票)。 - 接收侧在
SCL
上升沿锁存SDA
;发送侧在SCL
低电平期间改变SDA
。
主机 FSM 关键状态
IDLE → START → ADDR → ACK_A → (REG/WRITE/REPEAT/READ) … → STOP
- 波特率发生器:
soc_clk
分频产生SCL
低/高时间;发每一位时遵守“先改 SDA,再拉高 SCL 让对端采样”。
从机 FSM 关键状态
- 监测 START/STOP;在
SCL
高期间取样SDA
识别位流。 - 地址匹配后按读写分流;写路经收子地址更新“寄存器指针”,读路经从该指针出数并自增。
- buffer 不就绪时可拉伸 SCL(把
scl_oe=1
拉低)争取时间。
CDC/约束
SCL/SDA
→soc_clk
的路径为异步,用同步器并设定 false path或set_clock_groups -asynchronous
。- 若主机用
soc_clk
分频出SCL
,对SCL
建generated clock;不要门控时钟,用时钟使能。
常见边界/异常
- NACK:地址不匹配、寄存器不可用、数据区空等。
- 总线挂死(SDA 被某器件一直拉低):主机可输出9 个 SCL 脉冲尝试释放,再发
STOP
。 - 上拉过大:上升沿太慢,高速不达标;过小:电流大、功耗高、器件下拉能力吃紧。按总线电容计算 RC 常数选值。
- 仲裁丢失:必须立刻退总线,等待空闲再发起。