SMBus 这个总线,很多做嵌入式的朋友都是“先听说、后踩坑”。它长得跟 I2C 几乎一模一样,两根线一挂,时序图看上去也没什么差别,于是不少人第一次遇到 SMBus 设备的时候,直接拿普通 I2C 的初始化代码怼上去,结果要么读回来的数据全是 0xFF,要么设备干脆不响应,要么跑几个小时之后总线锁死。STM32 的 I2C 外设确实原生支持 SMBus 模式,但“支持”和“配好”之间隔着若干个寄存器位和一堆时序细节。这篇内容就是把这层隔阂拆掉:从协议层面的关键差异讲起,到 GPIO 复用、CCR/TRISE 参数怎么算、SMBus 专属寄存器位怎么置、PEC 校验怎么实现、35ms 超时怎么兜底、总线锁死怎么救回来,全程按“从零实现”的路径走一遍。适合刚接触片上 I2C/SMBus 外设的初学者,也适合手上有电池管理、电源监控、传感器阵列这类需要 SMBus 通信的项目、但被时序和 PEC 卡住的中级开发者。
1. 先把 SMBus 这层窗户纸捅破:它和 I2C 到底差在哪
1.1 三个硬指标:速率上限、超时、电平
很多人以为 SMBus 就是 I2C 的一个“马甲”,其实它是在 I2C 电气规范之上加了一整套约束的子系统级协议。最容易被忽略的是三个点,而这三个点几乎决定了你的配置能不能一次跑通。
第一是速率。SMBus 规范的时钟范围是10kHz 到 100kHz,上限就是 100kHz,没有快速模式这一说。而 I2C 允许 400kHz 甚至 1MHz。我见过最典型的事故是:前一版代码为了迁就一个 I2C 的 OLED 屏,把 I2C1 配成 400kHz,后来同一路总线上挂了一颗 SMBus 的电源监控芯片,直接不响应。原因很简单,那颗芯片内部的状态机就是按 100kHz 上限设计的,400kHz 下它的采样窗口根本对不上。所以只要你的总线上有任何一个 SMBus 器件,整条总线就得退回 100kHz。
第二是超时。SMBus 规定从机在检测到 SCL 持续为低超过一个时间窗口(规范给的范围大致在 25ms 到 35ms 之间)时,必须复位自己的总线状态机,释放 SDA。这条规则的出发点是防止某个从机被复位或者掉电导致总线被永久拉死,主机也跟着一起死。I2C 没有这条硬性要求,所以你可以看到很多 I2C 从机在时钟停住之后就一直卡着。
第三是电平。I2C 的输入阈值是按 VDD 比例算的,一般是 0.3VDD 和 0.7VDD;SMBus 用的是固定阈值,VIL 最大 0.8V,VIH 最小 2.1V。这意味着在 3.3V 系统里,你的上拉必须把高电平拉到 2.1V 以上,同时低电平要压到 0.8V 以下。用 5V 器件和 3.3V 器件混挂的时候,这一点尤其要注意,因为很多老式 5V 器件的 VOL 在较大灌电流下会超过 0.8V。
注意:如果你的板子上有 SMBus 器件和普通 I2C 器件混挂,速率按最低的那个来,电平按最严的那个来,别想着“折中一下”。
1.2 PEC 校验:SMBus 真正的护城河
如果说速率和超时只是“习惯差异”,那 PEC 就是 SMBus 的核心竞争力了。PEC 全称 Packet Error Checking,本质是一个 CRC-8,多项式是 x⁸ + x² + x + 1,对应十六进制的 0x07,初值为 0。它覆盖的范围是从起始条件之后的第一个地址字节开始,一直到最后一个数据字节,也就是说地址字节也要参与计算,这一点极其容易搞错。
为什么需要 PEC?因为 I2C 本身只有 ACK,没有数据完整性校验。一个字节在传输过程中被干扰翻转了一位,从机的 ACK 照样是 ACK,主机完全不知道数据已经错了。SMBus 面向的是电池管理、电源状态、温度读数这类“错了会出事”的场景,所以引入了逐字节的 CRC。
STM32 的 I2C 外设有硬件 PEC 计算单元,置位 ENPEC 之后,收发过程中的每个字节都会自动送进 CRC 移位寄存器,算完的结果放在 SR2 寄存器的 PEC[7:0] 位里。这一点很省事,但你得知道它算得对不对——我后面会给一份软件 CRC 实现,专门用来跟硬件结果做交叉验证,这在调试阶段非常有用。
还有一个细节:PEC 是可选的,不是所有 SMBus 器件都强制要求。有的器件通过配置位打开,有的器件永远不带 PEC。所以你的代码里最好把“带 PEC”和“不带 PEC”做成两条路径,而不是写死。
1.3 STM32 各系列对 SMBus 的支持边界
这一点必须提前说清楚,因为它决定了你的配置代码能写到什么程度。STM32 的 I2C 外设在 CR1 里有一组 SMBus 相关的位:SMBUS(模式选择)、SMBTYPE(主机/从机)、ENPEC(PEC 使能)、ENGC(通用呼叫使能)。这些位在老系列到新系列上基本都有,但超时检测这一块的差异很大。
| 系列 | SMBus 模式位 | 硬件 PEC | 硬件超时检测 | 备注 |
|---|---|---|---|---|
| F1 系列 | 有(SMBUS + SMBTYPE) | 有(ENPEC) | 仅有 TIMEOUT 标志 | 门限不可配,靠软件兜底更稳 |
| F4 系列 | 有 | 有 | 有 TIMEOUTR 寄存器 | 可配超时门限,主机从机都能用 |
| L4 系列 | 有 | 有 | 有 TIMEOUTR | 低功耗场景下注意时钟门控 |
| G0/G4 系列 | 有 | 有 | 有 TIMEOUTR | 新外设,寄存器布局有变化 |
我在项目里最常用的做法是:不管芯片有没有硬件超时,软件层都自己实现一套毫秒级超时。原因很实际——硬件超时能保护总线,但保护不了你的上层业务逻辑;如果某个 SMBus 器件因为供电异常一直不回数据,硬件超时会把总线释放掉,但你的 while 循环如果写的是while(!I2C_GetFlagStatus(...)),程序照样卡死在那里。所以硬件超时是“第二道防线”,软件超时才是“第一道”。
提示:动手之前先翻一遍你手上那颗芯片参考手册里 I2C 章节的 SMBus 小节,把 CR1、CR2、SR1、SR2 四个寄存器里跟 SMBus 有关的位标出来。不同系列的位定义有细微差别,照抄别人的代码是最容易翻车的地方。
2. 硬件设计先过关:上拉、电平与走线
2.1 上拉电阻到底取多大
软件配得再漂亮,上拉不对一样白搭。SMBus 的上升时间要求是标准模式下不超过 1000ns,这个值是从 VIL 的 0.8V 上升到 VIH 的 2.1V 所需的时间。上升时间由总线电容和上拉电阻共同决定,粗略估算可以用 t = 0.847 × R × C 这个一阶 RC 公式(0.847 对应 0.8V 到 2.1V 那段的充电比例)。
举个实际的例子:一块小板子上挂了 4 颗 SMBus 器件,走线不长,总线电容实测在 120pF 左右。套公式,1000ns = 0.847 × R × 120pF,解出来 R ≈ 9.8kΩ。这是上限,也就是说上拉不能大于这么大,否则边沿太缓。再看下限:低电平时从机要能把 SDA 拉到 0.4V 以下,一般 SMBus 器件的灌电流能力在 3mA 到 6mA 之间,按 3.3V 和 3mA 算,R 不能小于 3.3/0.003 = 1.1kΩ。所以合理的窗口是1.1kΩ 到 9.8kΩ,工程上一般取 4.7kΩ 或者 2.2kΩ。
总线电容大的时候要格外小心。我遇到过一块板子,SMBus 走线绕了大半个板子,还并了个测试点排针,电容飙到 300pF 以上,4.7kΩ 上拉算下来上升时间接近 1.2us,超了。最后把上拉改成 2.2kΩ 才压到 600ns 左右。改完之后静态功耗上去了,但通信稳定了——这种取舍在 SMBus 上很常见,因为它的速率上限低,功耗和可靠性之间的平衡点跟普通 I2C 不太一样。
| 总线电容 | 推荐上拉 | 上升时间估算 | 备注 |
|---|---|---|---|
| < 100pF | 4.7kΩ | ~400ns | 短距离板内连接 |
| 100~200pF | 2.2kΩ~4.7kΩ | 400~800ns | 最常见的情况 |
| 200~300pF | 1.5kΩ~2.2kΩ | 500~1000ns | 需要实测确认 |
| > 300pF | 建议加缓冲器 | - | 单靠减小上拉功耗代价太大 |
2.2 影响超时判定的硬件因素
SMBus 的超时是“SCL 保持低电平”累计计时。这意味着如果你的时钟线上有额外的电容或者串了电阻,导致低电平被拉长,就可能触发从机的超时复位。有些人在 SCL 上串一个 100Ω 的电阻做限流保护,看着不起眼,但配合几百 pF 的电容,低电平边沿会变缓,从机采样时容易把一次正常传输误判成时钟卡死。
另一个坑是时钟拉伸。SMBus 允许从机拉低 SCL 来延长时间,但持续时间同样受超时约束。如果你的从机是个慢速的电池管理芯片,它在处理命令的时候可能会拉低 SCL 几毫秒,这个没问题;但如果它拉低超过 25ms,主机这边就该判超时并且复位总线了。所以主机侧的超时窗口要和从机的时钟拉伸上限对齐,一般取 30ms 左右比较稳妥。
2.3 SMBA 引脚不是摆设
SMBus 有一个 I2C 没有的信号:SMBALERT,也就是报警线。从机有事情要通知主机(比如电池温度越限、电源故障)的时候,会把这条线拉低,并且用自己的设备地址响应主机的“报警响应地址”(ARA,通常是 0x0C 附近,具体看规范)。主机检测到 SMBA 变低之后,发一个接收字节命令到 ARA,就能拿到报警设备的地址。
STM32 的 I2C 外设上,SMBA 是复用在某个 GPIO 上的(F1 系列通常是 PB5 对应 I2C1,具体看数据手册的复用表)。这个引脚在硬件上是开漏双向的:从机拉低,主机也要能拉低(主机在扫描 ARA 的时候会主动拉低一段)。所以配置的时候不能简单配成浮空输入,而应该配成开漏输出,同时外加上拉。
实际使用中还有个细节:SMBA 的中断不走 I2C 外设中断。SR1 里虽然有 SMBALERT 标志位,但它不是标准库I2C_ITConfig能直接使能的那种中断源,多数情况下需要把 SMBA 引脚额外接到一个 EXTI 线上去。这个设计在当年让我折腾了半天,因为我一直以为打开 I2C 的错误中断就能收到报警。
3. 软件配置全流程:从时钟到寄存器
3.1 引脚与时钟:先把地基打好
配置顺序上,我习惯先把 GPIO 和时钟搞定,再动外设。原因很实在:I2C 外设一旦使能,如果引脚还没配好,总线上的电平是不确定的,某些从机会把这个状态当成一次起始条件,导致状态机错乱。
以常见的 F1 系列为例,I2C1 的 SCL/SDA 在 PB6/PB7,SMBA 在 PB5。这三个脚都必须开漏,其中 SCL 和 SDA 配复用开漏,SMBA 配通用开漏(因为它不属于 I2C 外设的复用功能,只是借用同一个引脚)。
static void SMBus_GpioInit(void) { GPIO_InitTypeDef gpio; /* I2C1 挂在 APB1,GPIOB 挂在 APB2,两路时钟都要开 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); /* SCL = PB6, SDA = PB7,复用开漏 */ gpio.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; gpio.GPIO_Speed = GPIO_Speed_50MHz; gpio.GPIO_Mode = GPIO_Mode_AF_OD; GPIO_Init(GPIOB, &gpio); /* SMBA = PB5,普通开漏,作为报警线的双向接口 */ gpio.GPIO_Pin = GPIO_Pin_5; gpio.GPIO_Speed = GPIO_Speed_50MHz; gpio.GPIO_Mode = GPIO_Mode_Out_OD; GPIO_Init(GPIOB, &gpio); GPIO_SetBits(GPIOB, GPIO_Pin_5); /* 默认释放为高 */ }这里有个经验:AFIO 时钟一定要开。虽然 I2C1 用的 PB6/PB7 默认就是复用功能,理论上不需要重映射,但很多人在GPIO_PinRemapConfig上吃过亏,为了保险,AFIO 时钟开着不占什么成本。
3.2 时序参数怎么手算:CCR 与 TRISE
这是整篇内容最核心的一块,也是最多人含糊带过的一块。STM32 的 I2C 速率由三个寄存器位域决定:CR2 的 FREQ[5:0]、CCR 的 CCR[11:0] 和 DUTY、以及 TRISE 的 TRISE[5:0]。标准库会帮你算,但算出来的值对不对,你得能自己验证。
第一步确定 FREQ。FREQ 填的是 I2C 外设所在 APB 总线的时钟频率,单位 MHz。注意不是系统主频。F1 系列 I2C1 挂在 APB1 上,如果系统跑 72MHz、APB1 二分频,那 APB1 就是 36MHz,FREQ = 36。这一步填错,后面所有时序全歪,而且现象很迷惑——通信能通,但波形频率跟你要的对不上。
第二步算 CCR。标准模式(≤100kHz)下,高低电平时间相等:
- Tpclk1 = 1 / 36MHz ≈ 27.78ns
- 目标 100kHz,周期 10us,Thigh = Tlow = 5us
- CCR = Thigh / Tpclk1 = 5000ns / 27.78ns ≈ 180
快速模式(DUTY = 2,即低:高 = 2:1)下,周期 = 3 × Thigh,400kHz 对应 Thigh ≈ 833ns,CCR = 833 / 27.78 ≈ 30。
第三步算 TRISE。这个值限制的是 SCL 上升沿的最大允许时间,公式是 TRISE = (最大上升时间 / Tpclk1) + 1。标准模式最大上升时间 1000ns,TRISE = 1000/27.78 + 1 ≈ 37;快速模式 300ns,TRISE = 300/27.78 + 1 ≈ 11。
把不同主频下的结果整理成表,方便直接对照:
| APB1 频率 | 目标速率 | 模式 | FREQ | CCR | TRISE |
|---|---|---|---|---|---|
| 36MHz | 100kHz | 标准 | 36 | 180 | 37 |
| 36MHz | 400kHz | 快速 DUTY=2 | 36 | 30 | 11 |
| 72MHz | 100kHz | 标准 | 72 | 360 | 73 |
| 72MHz | 400kHz | 快速 DUTY=2 | 72 | 60 | 22 |
| 8MHz | 100kHz | 标准 | 8 | 40 | 9 |
再强调一次:SMBus 只能用 100kHz 那一行。上表中 400kHz 的行留着,是因为同一条总线上如果有纯 I2C 器件、而且你确认没有 SMBus 器件,才用得上。
注意:CCR 的值不能小于 4(手册有明确下限),也不能为 0。低速场景比如 10kHz,36MHz 下 CCR = 5000/27.78 ≈ 1800,超出 12 位范围了吗?没有,4095 以内都合法,但实际很少这么用。
3.3 SMBus 专属寄存器位怎么置
标准库的I2C_InitTypeDef结构体里有个I2C_Mode字段,填I2C_Mode_SMBusHost或者I2C_Mode_SMBusDevice,库函数内部就会去写 CR1 的 SMBUS 位和 SMBTYPE 位。这一步很多人做了,但后面几个位漏了,导致 PEC 不工作。
static void SMBus_HostInit(void) { I2C_InitTypeDef i2c; SMBus_GpioInit(); I2C_DeInit(I2C1); i2c.I2C_Mode = I2C_Mode_SMBusHost; /* 关键:主机模式 */ i2c.I2C_DutyCycle = I2C_DutyCycle_2; i2c.I2C_OwnAddress1 = 0x00; /* 主机不用本地址 */ i2c.I2C_Ack = I2C_Ack_Enable; i2c.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; i2c.I2C_ClockSpeed = 100000; /* SMBus 上限 */ I2C_Init(I2C1, &i2c); /* 打开硬件 PEC 计算,PEC 位置设为"下一个字节" */ I2C_TransmitPEC(I2C1, ENABLE); I2C_PECPositionConfig(I2C1, I2C_PECPosition_Next); /* 允许通用呼叫(部分器件靠这个做地址分配) */ I2C_GeneralCallCmd(I2C1, ENABLE); I2C_ITConfig(I2C1, I2C_IT_ERR | I2C_IT_EVT | I2C_IT_BUF, ENABLE); I2C_Cmd(I2C1, ENABLE); }几个位的作用得说清楚。I2C_TransmitPEC(I2C1, ENABLE)对应 CR2 的 ENPEC 位,置位后硬件才开始算 CRC。I2C_PECPositionConfig对应 CR1 的 ENPEC 位置控制,一般用 Next,意思是在当前字节传输完成之后把 PEC 送出去。如果设成 Current,PEC 会在当前字节之前发,这个在特殊的块传输场景才用得上,常规读写别乱改。
还有I2C_GeneralCallCmd,对应 CR1 的 ENGC。SMBus 里通用呼叫地址 0x00 有时候用来做设备发现和地址重新分配,如果你的从机不吃这套,开着也无妨;但如果总线上有别的器件对 0x00 敏感,最好关掉。
3.4 HAL 库的对照写法
现在新项目大多用 HAL 了,配置思路一样,只是 API 变了。HAL 里没有直接叫Mode_SMBusHost的宏,得手动改寄存器或者用HAL_I2C_Init之后补一句:
hi2c1.Instance = I2C1; hi2c1.Init.ClockSpeed = 100000; hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 = 0x00; hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_ENABLE; hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; HAL_I2C_Init(&hi2c1); /* HAL 没有直接暴露 SMBus 模式,直接写 CR1 更省事 */ I2C1->CR1 |= I2C_CR1_SMBUS | I2C_CR1_SMBTYPE; /* Host 模式 */ I2C1->CR2 |= I2C_CR2_PECEN; /* 打开 PEC */HAL 的NoStretchMode要设成 DISABLE,也就是允许从机时钟拉伸。SMBus 是允许拉伸的,如果你的从机是个慢速器件,禁止拉伸会直接导致 NACK。
4. 代码实现:主机读写、PEC 与超时
4.1 从字节写开始:把事件序列走一遍
SMBus 的传输类型比 I2C 多不少:Quick Command、Send Byte、Receive Byte、Write Byte、Read Byte、Write Word、Read Word、Block Write、Block Read、Process Call。实际项目里用得最多的是 Write Byte、Read Byte、Write Word 和 Block Read。先把最基础的“写一个字节到指定寄存器”走通,其余的都是在这个骨架上加减。
STM32 的 I2C 是事件驱动的,每个步骤都要等对应的标志位。写字节的完整序列是:START → 等 SB → 发地址+W → 等 ADDR 并清除 → 发命令字节 → 等 TxE → 发数据字节 → 等 TxE → 等 BTF → STOP。
#define SMBUS_TIMEOUT_MS 30 typedef enum { SMBUS_OK = 0, SMBUS_ERR_TIMEOUT, SMBUS_ERR_NACK, SMBUS_ERR_PEC } SMBus_Status; static SMBus_Status SMBus_WaitFlag(uint32_t flag, uint32_t timeout_ms) { uint32_t start = SMBus_GetTick(); while (I2C_GetFlagStatus(I2C1, flag) == RESET) { if ((SMBus_GetTick() - start) > timeout_ms) { return SMBUS_ERR_TIMEOUT; } } return SMBUS_OK; } SMBus_Status SMBus_WriteByte(uint8_t dev_addr, uint8_t cmd, uint8_t data) { /* 1. 总线必须空闲 */ if (SMBus_WaitFlag(I2C_FLAG_BUSY, SMBUS_TIMEOUT_MS) == SMBUS_OK) { /* BUSY 位为 1 说明总线被占用,等一下都不空闲就报错 */ } while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)) { /* 等待释放 */ } /* 2. 起始条件 */ I2C_GenerateSTART(I2C1, ENABLE); if (SMBus_WaitFlag(I2C_FLAG_SB, SMBUS_TIMEOUT_MS) != SMBUS_OK) goto fail; /* 3. 地址 + 写方向 */ I2C_Send7bitAddress(I2C1, dev_addr << 1, I2C_Direction_Transmitter); if (SMBus_WaitFlag(I2C_FLAG_ADDR, SMBUS_TIMEOUT_MS) != SMBUS_OK) goto fail; (void)I2C1->SR2; /* 读 SR2 清除 ADDR,顺序不能反 */ /* 4. 命令字 */ if (SMBus_WaitFlag(I2C_FLAG_TXE, SMBUS_TIMEOUT_MS) != SMBUS_OK) goto fail; I2C_SendData(I2C1, cmd); /* 5. 数据字节 */ if (SMBus_WaitFlag(I2C_FLAG_TXE, SMBUS_TIMEOUT_MS) != SMBUS_OK) goto fail; I2C_SendData(I2C1, data); /* 6. 等最后一个字节彻底发完,再 STOP */ if (SMBus_WaitFlag(I2C_FLAG_BTF, SMBUS_TIMEOUT_MS) != SMBUS_OK) goto fail; I2C_GenerateSTOP(I2C1, ENABLE); return SMBUS_OK; fail: I2C_GenerateSTOP(I2C1, ENABLE); return SMBUS_ERR_TIMEOUT; }这段代码看起来平平无奇,但有三个地方是新手最容易翻车的。第一,清除 ADDR 标志必须读 SR1 再读 SR2,STM32 的硬件要求这个顺序,虽然标准库的GetFlagStatus已经读了 SR1,但后面还必须补一句读 SR2,否则 ADDR 清不掉,总线会卡在地址阶段。第二,发送最后一个字节之后要等 BTF 而不是 TXE,TXE 只是说数据寄存器空了,不代表数据已经从移位寄存器发出去了。第三,出错路径里一定要发 STOP,否则总线一直被占着,下一次通信直接失败。
提示:
I2C_Send7bitAddress的第二个参数是已经左移过一位的地址,很多人忘了移位,结果地址变成两倍,怎么都收不到 ACK。
4.2 PEC 校验:硬件算一遍,软件再验一遍
硬件 PEC 打开之后,发送时最后一个字节之后会自动补上 PEC,接收时也会把收到的 PEC 跟算出来的对比。但调试阶段我强烈建议自己再实现一份软件 CRC,一是用来验证硬件值对不对,二是在某些不支持硬件 PEC 的芯片上也能跑。
/* SMBus PEC:CRC-8,多项式 0x07,初值 0 */ static uint8_t SMBus_PecUpdate(uint8_t pec, uint8_t byte) { uint8_t i; pec ^= byte; for (i = 0; i < 8; i++) { if (pec & 0x80) pec = (uint8_t)((pec << 1) ^ 0x07); else pec = (uint8_t)(pec << 1); } return pec; } /* 计算一次写事务的预期 PEC:地址字节 + 命令字 + 数据 */ uint8_t SMBus_CalcPecForWrite(uint8_t dev_addr, uint8_t cmd, uint8_t data) { uint8_t pec = 0; pec = SMBus_PecUpdate(pec, (uint8_t)(dev_addr << 1)); /* 注意是写方向 */ pec = SMBus_PecUpdate(pec, cmd); pec = SMBus_PecUpdate(pec, data); return pec; }调试的时候用逻辑分析仪抓波形,把最后一个字节读出来跟软件算的对比。我踩过的一个坑是:把地址字节算成了 7 位地址本身,没加方向位。SMBus 的 PEC 是要覆盖完整的 8 位地址字节(含 R/W 位)的,所以地址左移之后还要或上方向位。这个错误非常隐蔽,因为软件算出来的 CRC 总是对不上硬件,而且每次错的值都不一样,看着像随机干扰。
读取 PEC 的硬件值,用I2C_GetPEC(I2C1)直接读 SR2 的低 8 位即可。读取事务的 PEC 校验逻辑是:收完数据之后,最后一个字节就是 PEC,把前面积累的软件 CRC 跟它比较。
4.3 35ms 超时与总线锁死恢复
前面说过,STM32 有些系列没有可配的硬件超时。所以我在所有 SMBus 代码里都包了一层软件超时,核心就是这个SMBus_WaitFlag。超时时间取 30ms 而不是 35ms,是因为要给总线上其他器件的超时判断留点余量,主机先超时、先释放总线,总比所有器件一起卡着好。
真正麻烦的是总线锁死。现象是:I2C_FLAG_BUSY一直是 1,怎么都不释放,重新初始化 I2C 外设也没用。原因是某个从机在传输中途掉电或者复位,把 SDA 一直拉低不放。这时候 I2C 外设自己救不了自己,必须做“总线清除”:把 GPIO 切成普通开漏输出,手动打 9 个时钟脉冲,把从机的移位寄存器走完,然后手动产生一个 STOP 条件。
void SMBus_BusRecover(void) { GPIO_InitTypeDef gpio; uint8_t i; /* 1. 关掉 I2C 外设,避免它干扰引脚 */ I2C_Cmd(I2C1, DISABLE); I2C_DeInit(I2C1); /* 2. 引脚切成普通开漏输出 */ gpio.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; gpio.GPIO_Speed = GPIO_Speed_50MHz; gpio.GPIO_Mode = GPIO_Mode_Out_OD; GPIO_Init(GPIOB, &gpio); GPIO_SetBits(GPIOB, GPIO_Pin_6 | GPIO_Pin_7); SMBus_DelayUs(10); /* 3. 打 9 个时钟,每个脉冲 5us 低 + 5us 高,约 100kHz */ for (i = 0; i < 9; i++) { GPIO_ResetBits(GPIOB, GPIO_Pin_6); SMBus_DelayUs(5); GPIO_SetBits(GPIOB, GPIO_Pin_6); SMBus_DelayUs(5); } /* 4. 在 SCL 为高时让 SDA 由低变高,产生 STOP */ GPIO_ResetBits(GPIOB, GPIO_Pin_6); SMBus_DelayUs(5); GPIO_ResetBits(GPIOB, GPIO_Pin_7); SMBus_DelayUs(5); GPIO_SetBits(GPIOB, GPIO_Pin_6); SMBus_DelayUs(5); GPIO_SetBits(GPIOB, GPIO_Pin_7); SMBus_DelayUs(10); /* 5. 重新初始化外设 */ SMBus_HostInit(); }为什么是 9 个时钟?因为从机最多可能在传输第 8 个数据位时被中断,加上前面的 ACK 位,最多 9 个脉冲就能让它把当前字节走完并释放 SDA。打完脉冲之后必须补一个 STOP,否则从机还停在“传输中”状态,下一次 START 它认不出来。
这个恢复函数我一般会在两种情况下调用:初始化时检测 BUSY 位为高,以及单次传输超时之后。调用位置很重要,放在中断里执行不合适,因为里面有一堆微秒级延时会阻塞整个系统。我通常把它放到主循环的错误处理分支里。
4.4 从机模式与 SMBALERT 响应
如果 STM32 本身要作为 SMBus 从机(比如做一个电池模拟器,或者一个被监测的板卡),配置上只需要把I2C_Mode改成I2C_Mode_SMBusDevice,并且把I2C_OwnAddress1填成你自己的 7 位地址。之后的收发走中断或者 DMA,跟普通 I2C 从机没本质区别,唯一要额外处理的是 PEC 和地址匹配。
SMBALERT 的响应流程值得单独说一下。报警线拉低之后,主机需要向 ARA(报警响应地址)发一个接收字节命令,此时所有拉低了 SMBA 的从机都会响应,但只有一个能成功发送自己的地址,其余的在仲裁中失败。主机收到这个地址之后,就知道是谁报警了,然后再针对这个设备去做具体的读取。
void SMBus_HandleAlert(void) { uint8_t alert_addr = 0; /* 向 ARA 地址读一个字节,返回的就是报警设备的地址 */ if (SMBus_ReadByte(SMBUS_ARA_ADDR, &alert_addr) == SMBUS_OK) { /* 按设备地址分发处理 */ if (alert_addr == 0x16) { SMBus_ReadWord(0x16, 0x09); /* 读温度告警寄存器 */ } } }需要注意,从机在响应 ARA 之前必须先把自己的 SMBA 引脚释放掉,否则总线上的电平和仲裁会出问题。这个时序在规范里有详细描述,实现的时候最好照着规范的状态图走,别自己发挥。
5. 调试实录:常见问题与排查套路
5.1 症状、原因、对策速查
下面这张表是我这些年攒下来的,基本上能覆盖 SMBus 调试里 80% 的现场问题。遇到问题先按症状查,别急着改代码。
| 现象 | 最可能的原因 | 排查手段 | 对策 |
|---|---|---|---|
| 地址发出去没 ACK | 上下拉不对、地址填错、速率超限 | 逻辑分析仪看波形电平与频率 | 确认上拉与 100kHz 上限 |
| 数据全 0xFF | 总线被拉高、从机没上电、地址错误 | 万用表量 SDA/SCL 静态电平 | 检查供电与地址跳线 |
| 偶尔丢字节 | 上升沿太缓、总线电容过大 | 测上升时间是否 > 1us | 减小上拉电阻 |
| PEC 校验总是失败 | 计算范围错、地址没带方向位 | 软件 CRC 与硬件 SR2 对比 | 确认覆盖到地址字节 |
| 跑一段时间后 BUSY 卡死 | 从机掉电、超时未处理 | 读 SR1 的 TIMEOUT / BERR | 加总线恢复流程 |
| 读操作拿到上次的数据 | 没等 BTF 就发 STOP | 抓波形看 STOP 位置 | 最后一个字节等 BTF |
| 从机不响应重复起始 | 把 RESTART 写成了 STOP | 看 SR1 的 SB 标志是否再次置位 | 用 GenerateSTART 代替 STOP |
5.2 几个真踩过的坑
第一个坑:把 SMBus 模式当成了“打开就能用”的开关。实际上置位 SMBUS 位只是让外设进入 SMBus 的状态机,PEC、超时、通用呼叫这些还是得单独配。我一开始以为置了 SMBUS 位硬件就自动处理超时了,结果一颗从机掉电之后整个系统挂了半小时。
第二个坑:DMA 和 PEC 一起用的时候顺序乱了。用 DMA 发数据,最后 PEC 那一个字节有时候发不出去,因为 DMA 传输完成的时刻和 PEC 插入的时刻没对齐。解决办法是在 DMA 传输完成中断里手动把 PEC 值塞进 DR 再发一次,或者干脆不用 DMA,SMBus 速率低,一个字节一个字节发完全够用。
第三个坑:块读的时候字节计数没处理。SMBus 的块传输第一个字节是长度,有的器件会把它也算进 PEC,有的不算。这个必须查具体器件的手册,不能想当然。我遇到过两颗同型号不同批次的芯片,一个把长度字节计入 PEC,一个不计入,最后靠软件配置项区分。
第四个坑:中断优先级冲突。SMBus 的错误中断如果和别的高优先级中断冲突,可能导致错误标志被覆盖,I2C_GetLastEvent拿到的是个莫名其妙的值。把 I2C 中断优先级调高一点,或者在高优先级中断里别做长时间操作,能规避大部分这种问题。
5.3 用逻辑分析仪的正确姿势
调试 SMBus 没有逻辑分析仪基本是盲人摸象。我的习惯是:采样率至少 10MS/s,触发条件设在 SDA 下降沿(也就是起始条件),解码协议选 I2C,然后手动把地址、命令、数据、PEC 四段标注出来。很多分析仪的 I2C 解码器不认识 PEC,会把它当普通数据字节显示,这时候你要自己数:如果一次写事务应该有 4 个字节(地址 + 命令 + 数据 + PEC),解码出来只有 3 个,那就是 PEC 没发出去。
还有一个技巧:把 SCK 和 SDA 都抓进来之后,量一下从 VIL 到 VIH 的上升时间。很多“通信不稳定”的问题根源就在这儿,量一下比猜一百次都管用。
我个人在几个电池管理和电源监控项目里反复验证下来,最省心的配置组合是:100kHz 标准模式、CCR 按 APB1 频率手算而不是全靠库、2.2kΩ 上拉、软件 30ms 超时加总线恢复函数、PEC 硬件算软件核。这套组合不花哨,但能扛住大部分现场干扰和器件异常。至于那些花在调试上的时间,多半不是花在“配置没写对”,而是花在“不知道哪里没写对”——所以把每一步的预期结果想清楚,比多写几行代码值钱得多。