
简介适用于 STM32 嵌入式开发者的 SMBUS 通信调试资料包重点解决使用普通 GPIO 口模拟 SMBUS 时序、与 TI bq 系列电池管理芯片通信的问题面向 STM32 单片机开发、嵌入式软硬件调试以及电池管理相关项目工程人员。内容来自真实项目调试过程配套详细调试文档和完整工程代码完整记录了 IO 模拟 SMBUS 时序的搭建、寄存器读写操作、通信异常排查等关键环节既适合初次接触 SMBUS 协议的嵌入式初学者理解底层时序设计也能为单片机开发工程师提供成熟排错思路和参考实现。资源包为 RAR 压缩格式共 319 个文件大小约 7.35MB主体包含 .h/.c 源码文件、.md 调试笔记、.ioc 引脚配置、.ld 链接脚本以及编译构建相关的 .mk/.xml 工程文件同时还有较多 .o/.d 等编译中间文件便于观察工程整体构建结构目录结构完整可导入 STM32CubeIDE 查看学习。已有 1494 人学习使用。借助其中的调试文档与工程代码可以快速掌握 IO 模拟 SMBUS 的时序要点熟悉 TI bq 芯片的寄存器读写流程和实际调试方法遇到通信异常时也能更高效定位问题。1. 先把 SMBus 与 bq 芯片的关系说清楚SMBus 在很多工程师手里就是“带一组更高要求时序的 I2C”但在 TI bq 系列电量计芯片这里它不仅是物理层更是完整的命令协议。bq40z50、bq27546、bq34z100 这些芯片的寄存器读写、数据 Flash 操作、安全命令校验全都要走 SMBus 的命令帧。你要读一个剩余电量至少需要“起始 → 写地址 → 写命令码 → 重复起始 → 读地址 → 读低字节 → 读高字节”七步少一步或者 ACK 相位不对读回来的就是废数据。标题里的“IO 模拟时序”不是伪需求。STM32 的硬件 I2C 外设在 SMBus 场景下并不省心——Block Read 的 NACK 位置、PEC 计算、从机时钟延展等待硬件外设要么不支持要么行为不直观。SMBus 本身只跑 100kHzGPIO 翻转速度绰绰有余于是“用普通 GPIO 把时序拨出来”反而成为调试 bq 芯片最常见、最可控的方案。本文就是从协议、时序、工程代码到调试排错的完整记录。2. STM32 与 bq 芯片建立 SMBus 通信的前提2.1 bq 芯片的 SMBus 地址与寄存器模型多数 TI bq 电量计在 7 位地址空间里占用 0x0B左移一位后写地址是 0x16读地址是 0x17。如果你在测试时读回的全是 0xFF先别急着怀疑时序用万用表确认 SMBus 引脚电平、确认芯片供电再拿逻辑分析仪看地址字节是不是 0x16/0x17。地址认错是入门阶段最高频的失误。寄存器模型分为两类。一类是标准寄存器比如 Voltage0x09、Current0x0A、RemainingCapacity0x0F、StateOfCharge0x0D、FullChargeCapacity0x10这些是 Word 读取一次命令码加一次重复起始再读两个字节。另一类是 ManufacturerAccess0x00和 ManufacturerData0x23前者需要先写一个命令字再读取结果后者走 Block Read第一个返回字节是数据长度。调试时建议从标准寄存器开始通信通了再碰 Block Read。2.2 为什么选择 IO 模拟而不是 STM32 硬件 I2CSTM32 的硬件 I2C 外设在标准模式下可以配置到 100kHz理论上满足 SMBus 低速段的要求。但实际调试 bq 芯片时硬件外设至少有三个别扭点。第一Block Read 的最后一个字节需要主机回 NACK硬件 I2C 的 NACK 配置在 HAL 库里要精确卡在读第 N 个字节之前很容易错位。第二SMBus 从机尤其 bq769x0 这类在 Flash 擦写时有内部操作会在准备数据时拉低 SCL 做时钟延展硬件 I2C 对长延展的处理依赖超时寄存器。第三PEC 字节需要在数据帧末尾追加硬件外设不支持自动追加要么手动多发一个字节要么用普通发送模式补。IO 模拟则完全不同。SCL 和 SDA 的每个上升沿、下降沿都在你自己的延时函数控制下从机拉低 SCL 就死等读到 NACK 就 Stop整个时序完全透明。SMBus 标准频率上限 100kHz一个位周期 10µs72MHz 主频下 GPIO 翻转一次是几十纳秒级别软件开销可以忽略。所以做 bq 芯片适配时IO 模拟不是“没有硬件 I2C 的退路”而是调试效率更高的主动选择。2.3 硬件连接与上拉电阻的选型SMBus 和 I2C 一样是开漏结构SCL 与 SDA 必须外部上拉。bq 芯片数据手册对上升时间有要求上拉电阻取 2.2kΩ 到 4.7kΩ 是常见的做法。总线上挂的设备越少、线缆越短上拉可以越大如果线长超过 20cm建议降到 2.2kΩ 甚至 1kΩ。电平方面注意匹配bq 芯片如果是 3.3V VCC可以直接和 STM32 的 3.3V GPIO 互连如果是 1.8V 逻辑的电量计则需要电平转换否则长期运行会损伤芯片。提示开漏配置时 STM32 的 GPIO 要设置为GPIO_MODE_OUTPUT_OD同时把输出速度调到GPIO_SPEED_FREQ_HIGH。推送输出在拉低阶段没问题但释放总线时无法让 SDA 回到高电平这是新手常犯的错误。3. STM32 IO 模拟 SMBus 时序的核心实现3.1 最小工程结构工程代码的组织直接决定调试效率。我一般会把 SMBus 部分拆成三层smbus_phy.c负责 GPIO 底层的起始、停止、收发字节smbus_proto.c负责 Read Word、Write Word、Block Read 这些命令帧bq_app.c面向业务比如读电压、读电量、读厂商信息。拆开之后调 PHY 层不会动了协议调协议层不会影响业务逻辑。// smbus_phy.h #define SMBUS_SCL_PORT GPIOB #define SMBUS_SCL_PIN GPIO_PIN_6 #define SMBUS_SDA_PORT GPIOB #define SMBUS_SDA_PIN GPIO_PIN_7 #define SMBUS_SDA_READ() HAL_GPIO_ReadPin(SMBUS_SDA_PORT, SMBUS_SDA_PIN) #define SMBUS_SCL_HIGH() HAL_GPIO_WritePin(SMBUS_SCL_PORT, SMBUS_SCL_PIN, GPIO_PIN_SET) #define SMBUS_SCL_LOW() HAL_GPIO_WritePin(SMBUS_SCL_PORT, SMBUS_SCL_PIN, GPIO_PIN_RESET) #define SMBUS_SDA_HIGH() HAL_GPIO_WritePin(SMBUS_SDA_PORT, SMBUS_SDA_PIN, GPIO_PIN_SET) #define SMBUS_SDA_LOW() HAL_GPIO_WritePin(SMBUS_SDA_PORT, SMBUS_SDA_PIN, GPIO_PIN_RESET)注意SMBUS_SDA_READ()需要在读取前把 SDA 对应的 GPIO 切到输入模式读取完成后再切回开漏输出。这一层虽然零碎但把模式切换集中封装后上层代码看起来会干净很多。3.2 起始、停止与字节收发void SMBus_Delay_US(uint32_t us) { // 使用 DWT 或 SysTick 实现微秒延时 // 避免 HAL_Delay 的毫秒粒度影响时序 }起始条件定义为 SCL 高电平时 SDA 产生下降沿停止条件为 SCL 高电平时 SDA 产生上升沿。SMBus 规范要求起始和停止建立时间不小于 4µs所以延时给 5µs 最稳妥。void SMBus_Start(void) { SMBus_SDA_HIGH(); SMBus_SCL_HIGH(); SMBus_Delay_US(5); SMBus_SDA_LOW(); // SDA 拉低产生起始条件 SMBus_Delay_US(5); SMBus_SCL_LOW(); // 拉低 SCL准备发送数据 } void SMBus_Stop(void) { SMBus_SDA_LOW(); SMBus_SCL_HIGH(); SMBus_Delay_US(5); SMBus_SDA_HIGH(); // SDA 拉高产生停止条件 SMBus_Delay_US(5); }字节发送函数里最关键的细节是 SDA 只能在 SCL 低电平时变化。先放数据再拉高 SCL等从机拉低延展再拉低 SCL 准备下一位。uint8_t SMBus_SendByte(uint8_t data) { uint8_t i, ack; for (i 0; i 8; i) { if (data 0x80) SMBus_SDA_HIGH(); else SMBus_SDA_LOW(); data 1; SMBus_Delay_US(2); SMBus_SCL_HIGH(); // 等待从机释放 SCL处理时钟延展 uint32_t timeout 10000; while (SMBus_SCL_READ() 0 timeout--) {} SMBus_Delay_US(2); SMBus_SCL_LOW(); } // 释放 SDA准备接收 ACK SMBus_SDA_INPUT(); SMBus_Delay_US(2); SMBus_SCL_HIGH(); timeout 10000; while (SMBus_SCL_READ() 0 timeout--) {} ack SMBus_SDA_READ(); // 0 表示 ACK1 表示 NACK SMBus_Delay_US(2); SMBus_SCL_LOW(); SMBus_SDA_OUTPUT(); return ack; // 返回 0 表示从机应答 }这段代码里有两处等待 SCL 释放的逻辑并非冗余。SMBus 从机在做 EEPROM 写入或内部校准计算时会把 SCL 拉低以暂停通信主机必须等到 SCL 恢复高电平才能继续。如果从机持续拉低超过 25msSMBus 规范要求主机复位总线即发送停止条件并等待 35ms 以上再重新初始化。字节接收函数和发送对称差别在于第 9 个时钟由主机决定回 ACK 还是 NACK。读最后一个字节时回 NACK 告诉从机“别再发了”。uint8_t SMBus_RecvByte(uint8_t ack) { uint8_t i, data 0; SMBus_SDA_INPUT(); for (i 0; i 8; i) { SMBus_SCL_HIGH(); uint32_t timeout 10000; while (SMBus_SCL_READ() 0 timeout--) {} data 1; if (SMBus_SDA_READ()) data | 0x01; SMBus_Delay_US(2); SMBus_SCL_LOW(); SMBus_Delay_US(2); } SMBus_SDA_OUTPUT(); if (ack) SMBus_SDA_HIGH(); // NACK else SMBus_SDA_LOW(); // ACK SMBus_Delay_US(2); SMBus_SCL_HIGH(); timeout 10000; while (SMBus_SCL_READ() 0 timeout--) {} SMBus_Delay_US(2); SMBus_SCL_LOW(); return data; }3.3 读 Word 与 Block Read 命令帧SMBus 读 Word 的帧结构是 bq 芯片最常用的操作。以读电池电压0x09为例先发起始写从机地址 0x16发命令码 0x09然后发重复起始写读地址 0x17读低字节回 ACK读高字节回 NACK最后停止。uint8_t SMBus_ReadWord(uint8_t reg, uint16_t *value) { uint8_t lo, hi; SMBus_Start(); if (SMBus_SendByte(0x16)) // 写地址 0x0B1 { SMBus_Stop(); return 1; } if (SMBus_SendByte(reg)) { SMBus_Stop(); return 1; } SMBus_Start(); // 重复起始不是停止后再起始 SMBus_SendByte(0x17); // 读地址 lo SMBus_RecvByte(0); // 低字节回 ACK hi SMBus_RecvByte(1); // 高字节回 NACK SMBus_Stop(); *value ((uint16_t)hi 8) | lo; return 0; }重复起始的“重复”体现在 SDA 在 SCL 高电平时拉低但没有先前的停止条件。如果在这里错误地先 Stop 再 Start部分 bq 芯片会认为事务被中断直接丢弃这次请求。这是实现时最不容易察觉的时序陷阱。Block Read 适用于 ManufacturerData0x23这类寄存器。命令码发送后在重复起始的读事务中第一个返回字节是数据长度之后才是真正的数据。注意长度字节本身也要参与 ACK 控制。uint8_t SMBus_BlockRead(uint8_t reg, uint8_t *buf, uint8_t maxlen) { uint8_t len, i; SMBus_Start(); if (SMBus_SendByte(0x16)) { SMBus_Stop(); return 0; } if (SMBus_SendByte(reg)) { SMBus_Stop(); return 0; } SMBus_Start(); SMBus_SendByte(0x17); len SMBus_RecvByte(0); if (len maxlen) len maxlen; for (i 0; i len; i) { if (i len - 1) buf[i] SMBus_RecvByte(1); // 最后一个数据字节 NACK else buf[i] SMBus_RecvByte(0); // 中间字节 ACK } SMBus_Stop(); return len; }Block Read 的 NACK 位置和读 Word 不同读 Word 是两个字节低字节 ACK、高字节 NACKBlock Read 是长度字节 ACK数据中间字节全 ACK只有最后一个数据字节 NACK。把这个关系写进注释里后续维护代码的人会感谢你。4. 调试 bq 芯片时的关键参数与排错记录4.1 延时参数与 SCL 频率的实际关系IO 模拟的延时参数决定了 SCL 频率而 SCL 频率又直接关系到 SMBus 规范约束。SMBus 低速模式允许 10kHz100kHz快速模式允许 100kHz400kHzbq 电量计通常只支持到 100kHz。下面是实测的延时与频率对照主频 72MHzDWT 延时SCL 低电平延时SCL 高电平延时实测 SCL 频率适用场景5µs5µs100kHzSMBus 标准推荐10µs10µs50kHz长线缆抗干扰2µs2µs250kHz仅 I2C 设备不用于 bq如果信号线上有干扰或者线缆较长把延时从 5µs 往上调比加滤波电容更直接。SMBus 的频率没有下限跑慢一点只是效率低不会造成通信失败。反过来如果只调代码不调硬件发现频率已经超过 120kHz 还时不时出错就把高低电平延时各加 1µs重新测。4.2 从机无响应与 ACK 异常现象是SMBus_SendByte(0x16)返回 1也就是收到了 NACK。排查顺序应该是先确认 0x16 这个地址是在 bq 芯片的数据手册里查到的而不是照抄网上代码再确认从机供电bq 芯片的 SMBus 引脚如果没有内部上拉外部上拉缺失会导致 SDA 无法拉高最后用万用表量 SDA 在通信时是否真的有翻转。如果是逻辑分析仪显示地址是 0x16但 SDA 波形幅度只有 1V那上拉电阻太小或总线电容过大换 2.2kΩ 再试。ACK 拿到了但读回的数据不对则优先检查重复起始是否真的“重复”。示波器上看波形如果两个 Start 之间有一个 Stop那就是实现了 I2C 的“Stop-Start”而不是 SMBus 要求的“Repeated Start”。bq 芯片对这种区别很敏感命令码已经接收但被 Stop 打断后部分寄存器会直接回到 Idle 状态。# 逻辑分析仪解码 SMBus 时的预期输出以 lsusb 类工具为例 # 0x16 W ACK 0x09 ACK 0x17 R ACK 0x0A ACK 0x10 NACK STOP # 解析结果: Voltage 0x100A 4106 mV需根据芯片量程换算4.3 时钟延展导致的卡死bq 芯片在 Flash 写入或内部计算时会拉低 SCL。如果代码里没有等待 SCL 释放的逻辑主机会在第 9 个时钟后继续拉高 SCL此时从机还在拉低总线冲突通信卡死。前文SMBus_SendByte里那个while (SMBus_SCL_READ() 0)就是为了处理这个场景。注意时钟延展的等待必须有超时。SMBus 规范规定从机拉低 SCL 超过 25ms 视为总线故障主机应发送停止条件并等待至少 35ms。代码里写死一个 10000 次循环的超时虽然粗略但在调试阶段足够暴露问题。产品化时建议用 SysTick 换算绝对时间。uint32_t timeout 25000; // 25ms 1MHz 循环计数 while (SMBus_SCL_READ() 0 timeout--) { SMBus_Delay_US(1); // 每次延 1µs } if (timeout 0) { SMBus_Stop(); HAL_Delay(35); // 总线复位时间 return 1; }顺带说明一点SCL 被拉低时 SDA 的状态不要乱动。有些 IO 模拟代码在等待时钟释放时会把 SDA 拉高这在 SMBus 上属于非法行为可能让从机误判为停止条件。正确的做法是保持 SDA 当前状态不变只等待 SCL 恢复。4.4 PEC 校验开启后的帧结构变化bq40z50 这类芯片可以通过数据 Flash 配置使能 PEC 校验。一旦从机侧打开 PEC主机就必须在每次读写的最后一个字节后追加一个 PEC 字节。PEC 是 CRC-8 校验多项式为x^8 x^2 x 1即 0x07初始值为 0x00覆盖从地址到最后一个数据字节不包含停止条件不包含 ACK 位。计算 PEC 的一个简单实现如下uint8_t SMBus_CalculatePEC(uint8_t *data, uint8_t len) { uint8_t crc 0x00; uint8_t i, j; for (i 0; i len; i) { crc ^ data[i]; for (j 0; j 8; j) { if (crc 0x80) crc (crc 1) ^ 0x07; else crc 1; } } return crc; }PEC 计算器的作用范围必须准确写事务是“地址 命令码 数据”读事务是“地址 命令码 读地址 数据”。如果读事务漏算了读地址PEC 永远对不上。启用 PEC 之后所有 Read Word 的返回值都要先做 PEC 校验再使用否则数据不可信。4.5 SMBus 超时与总线锁死恢复总线锁死的一个典型画面是SCL 为高SDA 为低且长时间不变。这通常是主机在通信过程中异常复位从机还在等待剩余位导致的。恢复办法是先发 9 个以上的 SCL 时钟脉冲让从机跳出当前字节再发停止条件。代码实现如下void SMBus_Recover(void) { uint8_t i; SMBus_SCL_HIGH(); SMBus_SDA_HIGH(); for (i 0; i 9; i) { SMBus_SCL_LOW(); SMBus_Delay_US(5); SMBus_SCL_HIGH(); SMBus_Delay_US(5); } SMBus_Stop(); }这个恢复过程对 I2C 总线也是通用做法放在 PHY 层后遇到卡死可以先调用再重新初始化。注意恢复函数本身也受总线状态影响如果 SCL 被从机拉死脉冲根本发不出去那就只能断电重启从机了。5. 快速验证 SMBus 通信是否正确的三个方法拿到一段 IO 模拟 SMBus 代码后不要直接去做 bq 芯片的复杂功能先做三项验证每一项都能单独确认一部分正确性。第一个方法是读 ManufacturerAccess 的 DeviceType 寄存器0x00命令字 0x0001。对 bq40z50 系列这个命令返回芯片的化学 ID 和器件 ID对 bq27546 则是另一组值。具体数值以数据手册为准但重点是这个寄存器只读不回写也不依赖数据 Flash 配置是验证“重复起始 读 Word 地址正确”三件事的最短路径。uint8_t buf[2] {0x01, 0x00}; // LSB 在前0x0001 为 DeviceType 命令 uint16_t device_id; // 先写命令字到 ManufacturerAccess SMBus_WriteWord(0x00, 0x0001); // 再读回结果 SMBus_ReadWord(0x00, device_id); // bq40z50 系列应返回 0x0440 附近的数值具体以手册为准第二个方法是读温度寄存器0x08并做单位换算。bq 系列的温度单位通常是 0.1K返回 0x0B3A即 2874代表 287.4K减去 273.1 得到 14.3°C。如果读数随加热/降温变化说明读 Word 路径彻底打通且数据在时间上是连续的。如果读到的温度恒定不变检查一下是不是读错了寄存器地址或者芯片处于 Shutdown 模式。第三个方法是 Block Read 的往返校验。先通过 ManufacturerAccess 写入一块 32 字节的临时数据再通过 ManufacturerData0x23读回来逐字节比对。这个验证的目的不仅是确认 Block Read 的长度字节和 NACK 位置正确更是验证芯片的数据 Flash 读写通路没有损坏。注意不要使用芯片保留的 Flash 块选择器件手册中标注为“可写”的厂商信息块。uint8_t test_data[32]; uint8_t readback[32]; uint8_t rlen; for (int i 0; i 32; i) test_data[i] (uint8_t)i; // 写入测试数据此处函数参照 Write Word 协议扩展 SMBus_WriteManufacturerData(test_data, 32); // 读回 rlen SMBus_BlockRead(0x23, readback, 32); if (rlen 32 memcmp(test_data, readback, 32) 0) { // Block Read 通路的长度、ACK、NACK、数据顺序全部正确 }最后一个技巧放在工程代码层面在 PHY 层加一个调试计数器记录起始次数、停止次数、ACK 失败次数和超时次数。调试时每隔 1 秒打印一次这组数值。正常工作的系统ACK 失败和超时必须为零。如果超时计数在稳定增长说明时钟延展等待逻辑还有漏洞如果 ACK 失败偶尔出现优先检查电源纹波和上拉电阻而不是继续改代码。这个计数器成本极低但能从根本上改变“凭感觉调时序”的调试方式。本文还有配套的精品资源点击获取