尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
STM32 SMBus从零配置:时序、PEC校验与总线恢复实战
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.7VDDSMBus 用的是固定阈值VIL 最大 0.8VVIH 最小 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主机/从机、ENPECPEC 使能、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 不太一样。总线电容推荐上拉上升时间估算备注 100pF4.7kΩ~400ns短距离板内连接100~200pF2.2kΩ~4.7kΩ400~800ns最常见的情况200~300pF1.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/PB7SMBA 在 PB5。这三个脚都必须开漏其中 SCL 和 SDA 配复用开漏SMBA 配通用开漏因为它不属于 I2C 外设的复用功能只是借用同一个引脚。static void SMBus_GpioInit(void) { GPIO_InitTypeDef gpio; /* I2C1 挂在 APB1GPIOB 挂在 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 就是 36MHzFREQ 36。这一步填错后面所有时序全歪而且现象很迷惑——通信能通但波形频率跟你要的对不上。第二步算 CCR。标准模式≤100kHz下高低电平时间相等Tpclk1 1 / 36MHz ≈ 27.78ns目标 100kHz周期 10usThigh Tlow 5usCCR Thigh / Tpclk1 5000ns / 27.78ns ≈ 180快速模式DUTY 2即低:高 2:1下周期 3 × Thigh400kHz 对应 Thigh ≈ 833nsCCR 833 / 27.78 ≈ 30。第三步算 TRISE。这个值限制的是 SCL 上升沿的最大允许时间公式是 TRISE (最大上升时间 / Tpclk1) 1。标准模式最大上升时间 1000nsTRISE 1000/27.78 1 ≈ 37快速模式 300nsTRISE 300/27.78 1 ≈ 11。把不同主频下的结果整理成表方便直接对照APB1 频率目标速率模式FREQCCRTRISE36MHz100kHz标准361803736MHz400kHz快速 DUTY236301172MHz100kHz标准723607372MHz400kHz快速 DUTY27260228MHz100kHz标准8409再强调一次SMBus 只能用 100kHz 那一行。上表中 400kHz 的行留着是因为同一条总线上如果有纯 I2C 器件、而且你确认没有 SMBus 器件才用得上。注意CCR 的值不能小于 4手册有明确下限也不能为 0。低速场景比如 10kHz36MHz 下 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 送出去。如果设成 CurrentPEC 会在当前字节之前发这个在特殊的块传输场景才用得上常规读写别乱改。还有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 再读 SR2STM32 的硬件要求这个顺序虽然标准库的GetFlagStatus已经读了 SR1但后面还必须补一句读 SR2否则 ADDR 清不掉总线会卡在地址阶段。第二发送最后一个字节之后要等 BTF 而不是 TXETXE 只是说数据寄存器空了不代表数据已经从移位寄存器发出去了。第三出错路径里一定要发 STOP否则总线一直被占着下一次通信直接失败。提示I2C_Send7bitAddress的第二个参数是已经左移过一位的地址很多人忘了移位结果地址变成两倍怎么都收不到 ACK。4.2 PEC 校验硬件算一遍软件再验一遍硬件 PEC 打开之后发送时最后一个字节之后会自动补上 PEC接收时也会把收到的 PEC 跟算出来的对比。但调试阶段我强烈建议自己再实现一份软件 CRC一是用来验证硬件值对不对二是在某些不支持硬件 PEC 的芯片上也能跑。/* SMBus PECCRC-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 代替 STOP5.2 几个真踩过的坑第一个坑把 SMBus 模式当成了“打开就能用”的开关。实际上置位 SMBUS 位只是让外设进入 SMBus 的状态机PEC、超时、通用呼叫这些还是得单独配。我一开始以为置了 SMBUS 位硬件就自动处理超时了结果一颗从机掉电之后整个系统挂了半小时。第二个坑DMA 和 PEC 一起用的时候顺序乱了。用 DMA 发数据最后 PEC 那一个字节有时候发不出去因为 DMA 传输完成的时刻和 PEC 插入的时刻没对齐。解决办法是在 DMA 传输完成中断里手动把 PEC 值塞进 DR 再发一次或者干脆不用 DMASMBus 速率低一个字节一个字节发完全够用。第三个坑块读的时候字节计数没处理。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 硬件算软件核。这套组合不花哨但能扛住大部分现场干扰和器件异常。至于那些花在调试上的时间多半不是花在“配置没写对”而是花在“不知道哪里没写对”——所以把每一步的预期结果想清楚比多写几行代码值钱得多。
RELATED

相关推荐

Superpowers 技能框架:让 Claude Code 与 Codex CLI 更靠谱地写代码

Superpowers 技能框架:让 Claude Code 与 Codex CLI 更靠谱地写代码

1. 从“superpowers”说起:这套技能框架到底在解决什么问题第一次看到“superpowers”这个词,很多人会以为是某个超级英雄题材的游戏或者插件。但如果你最近在折腾 Claude Code、Codex CLI 这类命令行 AI 编程工具,大概率已经在各种社区里刷到…

📅 2026/11/4 15:16:53
县域中心药房:数字化药品供应链管理实践

县域中心药房:数字化药品供应链管理实践

1. 中心(云)药房的概念与背景中心(云)药房,全称"县域医疗卫生共同体中心药房(云药房)",是近年来医药卫生体制改革中涌现出的创新模式。简单来说,它就像是一个县…

📅 2026/11/4 15:16:53
MySQL查询生命周期与性能优化全解析

MySQL查询生命周期与性能优化全解析

1. 一条SQL查询的生命周期探秘当你在MySQL客户端敲下SELECT * FROM users WHERE id1;并按下回车时,这条看似简单的查询语句实际上经历了一场精心编排的数据库交响乐。作为从业十余年的数据库工程师,今天我想带大家深入MySQL内核,看看这条查询…

📅 2026/11/4 15:11:52
MORE NEWS

更多资讯

📰

Kimi-K3 上手指南:从零到首次推理的极简路径

Kimi-K3 上手指南:从零到首次推理的极简路径 【免费下载链接】Kimi-K3 项目地址: https://ai.gitcode.com/MoonshotAI/Kimi-K3 Kimi-K3 是 MoonshotAI 开源权重的原生多模态 Agentic 大模型,2.8T 参数、百万级上下文、MXFP4 原生量化。本文带你完…

📰

如何快速在本地跑通 GitHub Actions?actrun 新手完整教程

【免费下载链接】actrun GitHub Actions compatible local runner 项目地址: https://gitcode.com/gh_mirrors/ac/actrun 点击查看 免费下载 还在为 GitHub Actions 的排队和慢启动烦恼吗?本文带你用 actrun(一个用 MoonBit 编写的 GitHub A…

📰

Windows 冗余驱动怎么找、怎么删:Driver Store Explorer 新手实操

Windows 冗余驱动怎么找、怎么删:Driver Store Explorer 新手实操 【免费下载链接】DriverStoreExplorer Driver Store Explorer 项目地址: https://gitcode.com/gh_mirrors/dr/DriverStoreExplorer Windows 用了好几年,驱动仓库悄悄吃掉好几个 G…

📰

猫抓 Cat-Catch 使用指南:3 步嗅探并下载网页里的任何媒体

猫抓 Cat-Catch 使用指南:3 步嗅探并下载网页里的任何媒体 【免费下载链接】cat-catch 猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension 项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch 视频页面明明能播放&#xf…

📰

Hermes Agent 量子机器学习实战:3 条命令装好 PennyLane 与 Qiskit 技能包

Hermes Agent 量子机器学习实战:3 条命令装好 PennyLane 与 Qiskit 技能包 想在 Hermes Agent 里做量子机器学习,又不想先啃完量子力学?最短的路:克隆仓库,装两个技能包,把 PennyLane 或 Qiskit 的电路交给…

📰

带宽被谁吃光?终端用 bandwhich 快速定位指南

带宽被谁吃光?终端用 bandwhich 快速定位指南 【免费下载链接】bandwhich Terminal bandwidth utilization tool 项目地址: https://gitcode.com/gh_mirrors/ba/bandwhich 半夜,服务器出口带宽被顶满,你翻接口统计却查不到是谁干的。打…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬