STM32F0通过SMBus读取BQ40Z50电量计数据实现笔记
2026/9/7 7:10:32 网站建设 项目流程

简介:面向嵌入式开发者和电池管理方向的学习者,这份资源基于STM32F030单片机,通过软件模拟SMBus总线协议,与TI公司的BQ40Z50电池管理芯片正常通信,给出了读取电压、电流、温度、荷电状态SOC等电池状态量的完整实现。压缩包共141个文件,大小2.3MB,以C源码和头文件为主,涵盖标准外设库、I2C转SMBus底层驱动、BQ40Z50寄存器读写示例,并附带Keil工程配置、hex/axf固件以及map、lst编译输出;目录按Core、HardWare、Fwlib、User模块划分,便于从底层驱动到应用层逐项对照学习。已有3071人学习下载。读者可借此掌握单片机GPIO和I2C接口配置、SMBus时序控制、电量计命令与寄存器映射,理解从编译、链接到下载调试的完整流程;对嵌入式驱动开发、电池管理芯片应用和低速总线协议设计均有实际参考价值。 做电池类项目,绕不开电量计。BQ40Z50这颗芯片在备电、电动工具、便携设备里几乎是默认选择,TI内部做了阻抗追踪算法,能直接给出SOC、电压、电流、温度这些关键数据,省去自己在MCU里做查表和积分。我最近把一套BQ40Z50通讯正常的旧工程整体整理到了STM32F0上,重新梳理了SMBus通讯底层代码,这里把完整实现过程和踩过的坑记下来。

这个项目不涉及BQ40Z50的化学配置和电量计参数标定,重点聚焦在STM32F0怎么通过SMBus把电量计里的数据稳定读回来。适合三类人看:刚开始做BMS却不知道怎么打通通讯的,手上只有低成本的M0核想跑SMBus的,以及已经能读到数据但经常被时序、PEC、地址问题折腾的。如果你只是想知道怎么把BQ40Z50里的电压温度读出来用,这份笔记也够用。

说实话,STM32F0这颗芯片的硬件I2C外设在F0系列上并不算好用,很多人调了半天都是卡在状态机上。我后来直接改成软件模拟I2C,反而一次跑通。下方内容是实测后整理的笔记,代码可以直接拿来改。

1. BQ40Z50通讯方案选型:为什么用SMBus模拟I2C

1.1 BQ40Z50到底解决什么问题

BQ40Z50是TI推出的多节电池电量计,支持1-4节串联,内部除了ADC采集电压电流以外,还有一套完整的阻抗追踪知识库。MCU通过SMBus总线读它内部寄存器,就能拿到当前电芯电压、平均电流、剩余容量和温度等状态。SMBus协议本身是基于I2C的,地址、起始位、停止位、ACK机制几乎一致,只是更规范,比如定义了命令字、块读、PEC校验等。

在备电或者锂电池工具项目里,用BQ40Z50比自己在MCU里做库仑计要可靠得多。你的程序只负责“问”,电量计负责“算”,分工很清晰。通讯接口上,BQ40Z50暴露出来的是两根线SMBC和SMBD,一个时钟一个数据,再配合电源和地,总共四根线就能把电池状态全部拿走。

需要注意一点,BQ40Z50不是上电就能用的,它需要先配置电芯化学成分ID、串联节数、容量等参数。我这个工程里通讯正常,是因为这部分已经在量产状态里配置好了。如果你拿到的板子还是空片,先别急着测通讯,读出来的数据很可能全是0或者异常值。这时候排除问题要回到data flash配置,而不是怀疑I2C代码。

1.2 为什么选STM32F0和软件模拟I2C

STM32F0是ST在入门Cortex-M0核上出货量很大的一颗芯片,价格便宜、主频48MHz、内置完整的外设,大部分场景用标准外设库开发效率也不错。我手头这个项目选它,主要为了替代老平台把成本压下来,同时保留足够的串口、GPIO和定时器资源给上位机协议用。

关于通讯方式,有人可能会问:F0明明有硬件I2C外设,为什么还要用软件模拟I2C?答案很简单:在BQ40Z50这颗芯片的通讯场景里,软模拟反而比硬件I2C好控制。F0的硬件I2C遇到NACK、总线繁忙、时钟延展等情况时,状态机处理起来比较绕,出问题时你很难判断是哪一步卡住。而软件模拟I2C每一拍都在代码里,拿逻辑分析仪一看就能对应上时序,排查起来非常直接。

代价也有,就是比较占CPU。但SMBus这种100kHz低速总线对我来说根本不算负担,一次寄存器读取大概几十微秒,放在100ms的轮询周期里完全不影响其他任务。况且F0本来就便宜,多出的这点CPU占用换来的是一套一眼能看懂的通讯流程,后期维护的人也不用去翻F0参考手册里的状态机图。对BQ40Z50的通讯场景来说,稳定性和可排查性远比省那点CPU更重要。

2. 硬件接线与工程环境准备

2.1 关键引脚与接线要求

BQ40Z50对外通讯相关的引脚主要有SMBC、SMBD、PRES、电源和地。SMBC和SMBD本质上就是标准I2C的SCL和SDA,接线时必须要加上拉电阻。一般选4.7kΩ,总线长度在30cm以内够用,如果环境干扰大一些,可以降到2.2kΩ,但不能太小,否则SMBus低电平可能拉不干净。我用的是4.7kΩ贴片电阻,实际测试很长一段时间没有出现因上拉导致的通讯异常。

STM32F0的GPIO有开漏输出模式,这对模拟I2C非常关键。开漏模式下GPIO输出1时引脚是高阻,配合外部上拉电阻才能正确抬起电平;输出0时引脚拉低。这样同一根线既能输出又能接收,不需要反复切换输入输出方向。只要BQ40Z50和STM32F0的供电电压都在3.3V左右,SMBC/SMBD可以直接连,不需要额外电平转换。

PRES引脚一般接到普通GPIO上做电池在位检测,代码里读电平就能知道电池是否插着。要注意PRES的默认逻辑和data flash配置有关,不同配置可能反过来,我一般在硬件上做上拉或下拉,然后软件里通过打印确认当前插拔状态再决定怎么判。电源方面,BQ40Z50和STM32F0共地是必须的,否则通讯会发生很奇怪的偶发失败。

2.2 固件库准备与工程要点

STM32F0的工程我保留了标准外设库方式,ST官方固件库包叫STSW-STM32048,网站上搜一下“STM32F0标准外设库”都能找到。如果你的项目是基于STM32CubeMX生成的HAL,也没关系,本文代码只用到了GPIO和延时,移植到HAL工程只是改一下宏定义的事情。

工程里最重要的一个配置是GPIO模式。模拟SMBus的两个引脚要设置为开漏输出,不是推挽。如果配置成推挽,SDA读取时会读到寄存器输出值而不是外部电平,通讯会直接失败。另一个核心是延时函数,它决定总线频率。我用的F0默认时钟HSI 8MHz,延时循环40次大约产生5微秒,总线频率约100kHz,刚好满足SMBus规范。如果主频改了,循环次数必须重新标定。

调试初期建议把延时调大一点,也就是把总线频率降到50kHz甚至更低,先确认通讯能通起来,再把频率慢慢提上去。我习惯用一段可调的分频数组来观察波形,找到最低可用的循环次数后,再把固定值写死。很多“时通时不通”的现象,本质上就是上升沿不够快或者时序裕量不足,而不是代码逻辑错了,把频率降低一级往往就消失了。

3. SMBus通讯代码实现与验证

3.1 模拟I2C底层时序

先看SMBus最底层的基础时序。起始条件是时钟线SCL为高时,数据线SDA拉低;停止条件反过来,SCL为高时,SDA拉高。写数据时在SCL低电平期间改变SDA,读数据时在SCL高电平期间采样。这些和标准I2C一致,代码可以直接写成下面这样。

#define SMBC_PORT GPIOA #define SMBC_PIN GPIO_Pin_6 #define SMBD_PORT GPIOA #define SMBD_PIN GPIO_Pin_7 #define SCL_1() GPIO_SetBits(SMBC_PORT, SMBC_PIN) #define SCL_0() GPIO_ResetBits(SMBC_PORT, SMBC_PIN) #define SDA_1() GPIO_SetBits(SMBD_PORT, SMBD_PIN) #define SDA_0() GPIO_ResetBits(SMBD_PORT, SMBD_PIN) #define SDA_READ() GPIO_ReadInputDataBit(SMBD_PORT, SMBD_PIN) static void smbus_delay(void) { volatile int i; for (i = 0; i < 40; i++); } void smbus_start(void) { SDA_1(); SCL_1(); smbus_delay(); SDA_0(); smbus_delay(); SCL_0(); smbus_delay(); } void smbus_stop(void) { SDA_0(); SCL_0(); smbus_delay(); SCL_1(); smbus_delay(); SDA_1(); smbus_delay(); }

起始和停止之间,每一帧都以SCL低电平开始,这也是总线释放的基础。真正容易出错的是读字节函数里的ACK处理:主机读完第8个bit后,第9个时钟要把SDA拉高释放,然后在SCL高电平期间读从机返回的ACK。写字节则相反,第8个bit结束后释放SDA,然后读从机是否拉低ACK。如果从机忙或者地址不对,它会保持高电平,这时候返回1表示NACK。

读取字节的时候还要区分是发送ACK还是NACK。在SMBus读操作中,除了最后一个字节返回NACK通知从机“不用再发了”,其余字节都要回ACK。这个逻辑写错,最典型的现象就是读到的数据错位或者通讯后面直接挂死。

3.2 BQ40Z50寄存器读写与块读

有了底层时序,读BQ40Z50的寄存器就是拼一个完整的总线事务。以读取16位数据为例,SMBus的Word Read流程是:起始→写入设备地址+写位→写入寄存器命令字→重复起始→写入设备地址+读位→读取低字节并回ACK→读取高字节并回NACK→停止。BQ40Z50默认地址常见是0xAA(写)和0xAB(读),实际值以数据Flash中的DEVICE_ADDRESS配置为准,所以调不通时先确认地址。

unsigned short smbus_read_word(unsigned char reg) { unsigned char lo, hi; smbus_start(); smbus_write_byte(0xAA); smbus_write_byte(reg); smbus_start(); /* repeated start */ smbus_write_byte(0xAB); lo = smbus_read_byte(1); /* ACK */ hi = smbus_read_byte(0); /* NACK */ smbus_stop(); return (unsigned short)((hi << 8) | lo); }

直接拿这段代码去读温度寄存器0x06、电芯电压寄存器0x08/0x09/0x0A都行,单位分别是0.1K和mV,SOC和电流也有对应的标准寄存器可以读。实际项目里我不建议一个寄存器一个寄存器去读,因为每次完整事务都需要起始和停止,读多了效率会下降。更好的做法是把多个寄存器打包,用块读一次拉回来,既减少了总线占用,也避免了频繁切换重复起始带来的时序抖动。BQ40Z50支持Block Read,从机在响应时先返回一个字节长度,后面按顺序吐出寄存器数据,主机的代码逻辑只多了一个长度字段的处理。

这里提醒一句,BQ40Z50如果配置了PEC(包错误校验),SMBus时序里会在数据末尾多一个PEC字节,这个字节由地址、命令和数据计算出来,多项式是0x07,初值是0。调试阶段建议先在数据Flash里把PEC选项关掉,等基本读写稳定后再打开。开了PEC之后,主机的读取流程要重新计算一遍CRC并进行比对,不匹配的数据要丢弃。

unsigned char smbus_pec_calc(unsigned char *data, unsigned char len) { unsigned char crc = 0; unsigned char 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; }

3.3 实测读取温度与电压的示例

把上述函数组合起来,主循环里就能周期刷新电池数据。我测试时大概这样的:每200ms读一次温度、两节电芯电压和BatteryStatus,如果读取失败,计数加一,连续失败就点亮故障灯。

unsigned short temp_raw, volt1, volt2, status; temp_raw = smbus_read_word(0x06); volt1 = smbus_read_word(0x08); volt2 = smbus_read_word(0x09); status = smbus_read_word(0x16); /* 温度:0.1K单位,减去2731得到0.1摄氏度 */ int temp_c = (int)temp_raw - 2731; /* 电压:mV单位,直接显示 */

实测下来,F0每次读一组数据大约耗时几百微秒,200ms轮询完全不会影响系统负担。要注意温度寄存器是16位有符号数,如果读到类似0xFFFF这种值,直接转成int再计算,别用unsigned short硬算,否则负数会变成很大的正数,显示出来就是几千度。从逻辑分析仪波形看,整个事务的起始、重复起始、地址、命令、数据和停止位非常清晰,这也验证了模拟I2C在这个场景下的可靠性。

4. 通讯异常排查方法与经验整理

4.1 现象速查表

我在调试过程中整理了一个速查表,遇到读数据不对的时候直接对着排查,比重新翻协议图解方便得多。

现象大概率原因排查方向
读回全0xFF从机没响应或SDA被外部上拉检查地址、供电、时序
读回全0x00SDA被拉死或GPIO配置成推挽示波器看SDA是否恒低
起始后无ACK地址错误或从机在忙确认DEVICE_ADDRESS,降低频率
首字节正常后续超时PEC配置开启或重复起始异常先关闭PEC再测
电压电流全为0BQ40Z50未配置化学ID或电芯未插入检查PRES和数据Flash配置

这个表在项目移植的时候也可以直接当checklist用,每一行对应一类问题,省得反复盲试。我实际用下来,出现频率最高的是前两类,也就是地址和GPIO配置问题。曾有一块板子用了新供应商提供的改版样品后读回全0xFF,查了半天才发现新样品的SMBD焊盘虚焊,表里列的因素越早排除,越能节省后面的调代码时间。

4.2 排除步骤和几个容易踩的坑

如果通讯完全不通,我习惯按这个顺序排查:先看硬件,用万用表量SMBC和SMBD对地电阻,确认上拉存在且阻值合理;再用示波器或逻辑分析仪看启动瞬间线上有没有波形;接着用树莓派或者USB转I2C工具做主站直接读一次BQ40Z50,这样能快速判断是BQ40Z50本身没工作还是STM32F0这侧的问题。

软件方面最容易踩的坑有三个。第一个是GPIO没有配成开漏,推挽输出模式下SDA被输出寄存器强制控制,读输入引脚电平根本没意义。第二个是重复起始没写对,也就是读操作前先发了一个停止再发起始,这在SMBus里虽然允许但很多从机不喜欢,最好严格按照“先start→写完命令→再start→读数据”的顺序来。第三个是地址位宽问题,写地址和读地址差了最低位,很多人换算错了,结果从机永远不响应。

还有一个小坑在延时上。模拟I2C在内部主频48MHz和8MHz下,同样一次延时循环的时长相差6倍。如果你把网上找到的工程直接拿到48MHz系统时钟下跑,总线频率可能会到600kHz,远超BQ40Z50的100kHz上限,自然通不了。所以不管移植谁的代码,第一时间把延时函数标定好。

4.3 把数据交给上位机:集成FreeModbus的思路

很多项目里STM32F0不光是要读BQ40Z50,还要把电池状态上报给上位机或者HMI。最省事的做法是在同一个工程里集成FreeModbus从站,走Modbus RTU协议,串口输出。这样上位机只要通过一组保持寄存器就能读到温度、电压、电流、SOC,完全不用接触SMBus底层的细节。

我实现的时候把BQ40Z50的读取放在主循环里,每200ms刷新一次,更新到一个全局结构体;Modbus保持寄存器数组直接映射到结构体对应的地址。FreeModbus的eMBPoll放在定时器中断调用,上位机随时读取,两个模块互不干扰。这个扩展在资源紧张的MCU上也能跑起来,F0的RAM和Flash虽然不大,但跑一个FreeModbus从站加几组保持寄存器完全没问题,总共增加的代码量很小,却能大幅降低上位机开发人员的工作量。

最后分享一个我在这类项目里反复用到的技巧:不管上层协议用什么,底层那个读取BQ40Z50的函数里尽量返回成功或失败标志,而不是直接舍弃错误数据。Modbus侧如果发现连续几次SMBus读取失败,就把寄存器里的值标记成无效,上位机看到失效值就知道该告警,而不是显示一个假的电压。这个设计在可靠性和用户体验上都很关键。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询