前阵子帮人返工一块小板子,主控是 STM32,总线上挂了颗电池管理芯片和一个温度传感器。代码是从另一个项目直接拷过来的标准 I2C 驱动,写寄存器一切正常,读数据却时不时返回 0xFFFF,跑上十几分钟还会整个总线卡死、BUSY 位置起来再也不掉。换芯片、换板子、改地址、换上拉,折腾了整整两天,最后拿逻辑分析仪一测才发现两件事:一是模块自带的 10 kΩ 上拉把上升沿拖到了 900 ns 左右,二是从机在被拉低超过 25 ms 之后自己把内部状态机复位了——这两条都是SMBus的硬性规定,而标准 I2C 代码里根本没有对应的处理。
SMBus 这个东西,本质上是 I2C 的一个"加了规矩的子集":电气参数被钉死、时序窗口被限制、还强制要求 CRC 校验和超时复位。它出现的场景很集中——笔记本电池、服务器电源管理、温度/电压监控、内存条上的 SPD、各种板级管理芯片。这些场合的共同点是"必须能自愈",一条挂在机箱里的总线,不能因为某一个器件抽风就把整个系统拖死,所以 SMBus 才把超时和 PEC 写进了规范。适合读这篇的人大概分三类:手上正好有一颗 SMBus 器件要接 STM32 的;I2C 代码能跑但偶发出错想搞清楚原因的;以及想弄明白 STM32 的 I2C 外设里那几个 SMBUS、SMBTYPE、ENPEC、ENARP 位到底是干什么的。
下面我按"差异分析—寄存器推导—协议层实现—排错—实测验收"的顺序走一遍,代码以 STM32F1 的寄存器操作为主线,其他系列(F0/F3/F4/L4/G0 等)思路完全一致,只是超时寄存器和部分位名有差别,我会在对应位置标出来。
1. SMBus 与 I2C 的四处硬差异:不搞清这些配置必然翻车
很多人以为 SMBus 就是"I2C 加个名字",把 I2C 跑通就相当于支持 SMBus 了。实际调试下来,出问题的几乎全部集中在下面四组差异上,而且这四组差异是互相耦合的:电气参数不对会拖慢边沿,边沿慢了从机就更容易被超时复位,超时复位之后地址不应答,你就开始怀疑地址配错了。
1.1 一张表看清哪些参数被"钉死"了
先把结论摆出来,下面这张表是我自己调试时贴在工位上的那版,只保留了对写代码和画板子有影响的行:
| 项目 | I2C 标准模式 | SMBus 2.0 | 实际影响 |
|---|---|---|---|
| 时钟频率范围 | 无下限,可直流停住 | 10 kHz ~ 100 kHz | 不能为了省事把 SCL 调到 1 kHz 去"看清楚波形" |
| 逻辑门限 | 0.3·VDD / 0.7·VDD(比例式) | VIL ≤ 0.8 V,VIH ≥ 2.1 V(固定) | 1.8 V 器件直接接 3.3 V SMBus 有风险 |
| 上升/下降沿 | ≤ 1000 ns | ≤ 300 ns | 上拉电阻必须重算,10 kΩ 通常不合格 |
| 总线超时 | 无规定 | SCL 低/高电平超过 25~35 ms 视为异常 | STM32 要开超时检测,软件要有复位链路 |
| 数据校验 | 无标准算法 | PEC,CRC-8,多项式 0x07 | 硬件 ENPEC 或软件算都行,但必须一致 |
| 地址应答 | 从机可不应答 | 除"忙"以外必须应答 | 收到 AF 基本可以判定是地址/电气问题 |
| 中断线 | 无 | SMBALERT# + ARA(0x0C) | 可选,但接上之后能省掉大量轮询 |
表格里最容易被忽略的是"固定门限"和"300 ns 上升沿"这两条,因为它们不在代码里,在硬件上。我见过太多次把 1.8 V 的传感器直接挂到 3.3 V 总线上,代码层面完全看不出问题,示波器上看波形也挺漂亮,但就是偶发不应答——原因就是 1.8 V 器件的高电平输出达不到 SMBus 要求的 2.1 V VIH。这种情况必须加电平转换,不要指望"上拉拽上去",SMBus 是开漏总线,高电平完全由上拉电阻提供,器件自己不驱动高电平。
1.2 10 kHz 下限:为什么不能用低速"慢慢读"
SMBus 规定 SCL 最低 10 kHz,这个规定乍看莫名其妙,其实和超时机制是配套的:从机要靠"SCL 长时间不动"来判断主机是不是挂了,如果允许主机用 1 kHz 慢慢磨,那 25 ms 的超时窗口就完全失去意义了。所以 10 kHz 下限的真正含义是——总线上任何一个字节都必须在有限时间内传完。
实测中这条限制有两种踩法。第一种是为了兼容长线缆,把 CCR 调大让 SCL 降到 5 kHz,结果某些严格的电池管理芯片直接不应答;第二种是在软件里手动翻转 GPIO 模拟时序(bit-banging),翻转间隔随手写了 200 µs,等效频率 2.5 kHz,同样不合规。真要做软件模拟,也得保证半周期不超过 50 µs,也就是整体不低于 10 kHz。
1.3 25 ms 超时:STM32 各系列的实现差别很大
超时是 SMBus 和 I2C 差别里最"隐蔽"的一条。它的表现不是通信报错,而是从机悄悄把你的连接断掉——主机还在等 ACK,从机已经复位回空闲状态,于是你收到一个 NACK 或者干脆读到全 1。
STM32 各系列在这块的硬件支持并不一样:
- F1 系列:I2C 外设没有可编程的超时寄存器,SMBus 模式下由硬件做固定窗口的超时检测,超时后在 SR1 里置 TIMEOUT 标志(配合 ITERREN 可以进中断)。也就是说你只能"知道"超时了,不能改窗口大小。
- F0/F3/F4/L4/G0 等:有独立的
I2C_TIMEOUTR寄存器,可以分别使能"总线空闲超时"和"SCL 低电平超时",并自行设定计数值。
TIMEOUTR的计数单位是 2048 个外设时钟周期,公式是:
t_timeout = (TIMEOUT + 1) × 2048 × T_pclk以 F4 的 I2C1 为例,APB1 = 42 MHz,T_pclk = 23.8 ns,那么 2048 × 23.8 ns ≈ 48.8 µs,想得到 25 ms 的超时窗口:
TIMEOUT = 25 ms / 48.8 µs - 1 ≈ 511填 0x1FF 就差不多了。填完之后记得把TIMEOUTEN(总线超时使能)置 1,需要检测 SCL 被拉死的情况还要打开TEXTEN(扩展超时使能)。
注意:超时检测只能"报告",不能"修复"。真正的自愈要靠软件——检测到 TIMEOUT 之后,必须走一次总线恢复流程(本文第 3.4 节),否则下一次通信还是照样失败。
1.4 PEC:不是可选装饰,是协议的一部分
PEC(Packet Error Code)就是一个 CRC-8,x^8 + x^2 + x + 1,即多项式 0x07,初值 0x00,MSB 优先,不做反转也不做最终异或。关键在于计算范围:从起始条件之后的第一个地址字节开始,一直到最后一个数据字节,全部参与计算,包括地址字节本身,也包括读操作里那个"重复起始 + 地址+R"。
这一点是新手最容易算错的地方。举个具体例子,读一个字节(Read Byte)时,参与 PEC 计算的字节序列是:
[addr<<1 | 0] → [命令字] → [addr<<1 | 1] → [数据字节]而不是只有数据字节。我第一次实现的时候只把数据丢进 CRC,对了几十次都对不上,后来把地址字节补进去立刻就对了。
2. 从时钟树到寄存器:STM32 侧配置的完整推导
搞清楚差异之后,配置就变成了一道算术题。这一节把 CCR、TRISE、CR1 里那几个位逐个拆开,给出可以直接抄的数字,并且说明为什么这么算。
2.1 CCR 与 TRISE 的算法和一组现成数字
STM32 标准模式下DUTY位为 0 时,高电平和低电平时间是相等的,都等于CCR × T_pclk,所以:
CCR = f_pclk / (2 × f_SCL)TRISE寄存器填的是"允许的最大上升沿时间",单位是外设时钟周期:
TRISE = t_r_max / T_pclk + 1I2C 标准模式允许 1000 ns 的上升沿,所以 ST 的 HAL 库统一按FREQ + 1来填;但 SMBus 要求 300 ns,必须自己覆盖掉。常用的几组值我列在下面,可以直接查:
| PCLK1 | 目标 SCL | CCR | TRISE(SMBus,300 ns) |
|---|---|---|---|
| 8 MHz | 100 kHz | 40 | 3 |
| 36 MHz | 100 kHz | 180 | 11 |
| 42 MHz | 100 kHz | 210 | 13 |
| 48 MHz | 100 kHz | 240 | 15 |
| 36 MHz | 50 kHz | 360 | 11 |
顺带说一句上拉电阻怎么算,因为这事和TRISE是一体的。SMBus 的 300 ns 上升沿上限,配合总线电容Cb,可以反推出电阻上限:
Rp ≤ t_r / (0.8473 × Cb)假设一条短板上总线电容 100 pF:
Rp ≤ 300 ns / (0.8473 × 100 pF) ≈ 3.5 kΩ下限则由 VOL 决定,3.3 V 供电、VOL 最大 0.4 V、灌电流 3 mA:
Rp ≥ (3.3 - 0.4) / 3 mA ≈ 970 Ω所以 3.3 V 系统、100 pF 负载下,1.8 kΩ ~ 2.2 kΩ是比较舒服的取值。而市面上大量传感器模块自带的 10 kΩ 上拉,按上面的公式算下来上升沿约 850 ns,已经超出 SMBus 上限近三倍——这就是我开头那块板子出问题的直接原因。把模块上的上拉拆掉或者并联一个 2.2 kΩ,故障率立刻降下来。
2.2 CR1 里那几个位到底该摆成什么状态
以 F1 的I2C_CR1为例,和 SMBus 相关的有五位,它们的含义和推荐值如下:
| 位 | 名称 | 作用 | 主机读从机时的推荐值 |
|---|---|---|---|
| bit1 | SMBUS | 切换到 SMBus 模式,启用电平门限与超时检测 | 1 |
| bit3 | SMBTYPE | 0 = Device,1 = Host | 0 |
| bit4 | ENARP | 使能 SMBALERT 引脚与 ARP | 需要中断线时置 1 |
| bit5 | ENPEC | 使能硬件 PEC 计算 | 用软件 PEC 时置 0 |
| bit7 | NOSTRETCH | 从机模式禁止时钟拉伸 | 主机模式无影响,保持 0 |
SMBTYPE这一位特别容易搞反。很多人看到"主机"两个字就想置 1,其实它控制的是作为从机被寻址时的地址匹配行为——置 1 之后,接口会对 SMBus Host 地址(0x08)作出应答,配合 SR2 里的 SMBHOST 标志,用于 Host Notify 协议。当你只是"用 STM32 去读写一颗电池芯片"时,要的是 Device 模式,这一位保持 0。
ENPEC我也建议先关掉,用软件算。原因不是硬件算得不对,而是硬件 PEC 的计算范围取决于接口实际发出和收到的每一个字节,一旦中间插入 DMA、或者传输被中断打断重启,PECR里的值就对不上了,排查起来比软件算法麻烦得多。软件 PEC 每字节一次 CRC 更新,100 kHz 下最多几十微