基于SH367309与STM32的BMS设计:从选型到代码实现
简介面向嵌入式与BMS开发者的基于STM32SH367309的BMS参考代码包用于实现单节锂电池电压、电流、温度采集及充放电控制、均衡保护等核心逻辑适合需要快速上手SH367309的工程师与学生参考。压缩包共289个文件涵盖C源码、H头文件、启动汇编、Keil工程配置uvprojx/uvoptx以及编译产物axf、hex、map、lst等总大小约8.54MB工程结构完整可直接用Keil打开查阅或二次编译验证。已有798人学习下载。代码内提供I2C_DEMO与BMS_DEMO两个示例工程清晰展示SH367309的I2C通信、寄存器配置、电池数据解析、故障诊断与均衡策略同时包含调试备份与工程记录文件便于对照实际项目梳理BMS软件框架是学习和评估该芯片方案的有效参考资料。1. 项目概述与适用范围做BMS的老哥们应该都有这种感觉市面上的方案要么是TI的BQ系列随便一加就是好几块美金的成本要么是国产芯片但资料残缺不全遇到问题只能对着数据手册硬啃。我这次用的SH367309是上海南芯的方案定位是3到16串锂电池组的模拟前端集成度很高片内自带均衡MOS驱动、ADC采样、多种保护逻辑搭配一颗STM32做策略层整体BOM成本压得很低非常适合做电动工具、两轮车、便携储能这类对成本敏感的产线。这套代码我从硬件方案定稿到产线测试跟了一整个周期前前后后踩了不少坑但最终跑通了量产版本。如果你正准备做BMS项目或者已经在用SH367309但还没完全吃透这套代码的思路这篇博文应该能帮你省下不少折腾时间。我会把项目整体规划、代码结构、核心功能实现、调试过程中遇到的典型问题和解决方案都掰开揉碎讲一遍代码风格尽量保持简洁清晰方便你直接移植到自己的工程里。和网络流传的一些“示例代码”不同这套代码不是拿来做演示的玩具而是能真正跑在产线测试老化流程里边的。整个软件架构主要分三层底层驱动负责和SH367309通过SPI通信中间层做数据解析、故障判断和控制状态机上层协议负责和上位机或者整车控制器交互。我会从底层往上讲这样哪怕你手头没有现成的硬件也能对这套系统的运行逻辑有个整体把握。2. 硬件选型与整体方案设计2.1 为什么选SH367309而不是BQ76940在项目立项时我其实对比过TI的BQ76940和SH367309这两颗料。BQ76940在老工程师圈子里口碑很好资料全、生态成熟但价格确实不友好而且交期经常出问题。SH367309的好处是它支持3到16串的宽范围配置硬件上只需要改分压电阻就能适配不同串数的电池组这一颗料就能覆盖公司多个产品线备料压力小了很多。另外SH367309内部集成的保护逻辑非常丰富包括过压、欠压、过流、短路、过温保护而且每个保护阈值都支持寄存器配置不用像传统方案那样强行改硬件电阻参数。更关键的是它自带均衡MOS驱动外部只需要加均衡电阻和MOS管就能实现被动均衡功能。对于小批量多品种的生产模式来说这种灵活性非常实用。2.2 整体架构STM32做大脑SH367309做感觉神经这套系统的分工很明确。SH367309负责所有模拟量的采集和初级判断它内部有一个16位的ADC可以轮流采样每一串电芯的电压、芯片温度以及外部NTC温度。同时它的保护比较器是硬件级的检测到过压、过流这类紧急故障时能在几百微秒内直接拉低放电MOS的驱动信号这个过程完全不需要软件干预避免因MCU卡死导致保护失效。STM32负责的工作是更高层面的开机自检后配置SH367309的寄存器、定时读取所有采样数据、运行SOC估算算法、控制均衡动作、管理充放电状态机同时把电池状态通过CAN或者485总线上报给上位机。两边的分工相当于一个是硬件保护墙一个是软件决策中心各司其职系统可靠性才能上去。2.3 通信接口设计SPI链路SH367309支持SPI和I2C两种通信模式。我最终选择了SPI主要原因有两条一是SPI通信速率高读取14串电压加若干温度数据只需几毫秒方便上层做实时性要求高的算法二是SPI的抗干扰能力比I2C好对BMS这种靠近功率线的场景来说非常关键。I2C在长走线阻抗匹配不好的情况下容易出现总线锁死排查起来非常痛苦。硬件上我用的是硬件SPI1PA5、PA6、PA7分别接SCLK、MISO、MOSI片选用普通的GPIO控制。如果要复用这个代码到其他系列芯片SPI引脚映射改一下就行初始化逻辑基本不用动。SPI工作模式必须要匹配SH367309的时序设置为模式0CPOL0CPHA0如果时序不匹配会让采样数据经常跳变。3. 代码架构与核心功能模块拆解3.1 整体目录结构与模块划分代码写到最后整个工程比较庞杂所以从一开始我就按功能做了目录划分。顶层是main.c负责初始化和循环调度bms_config.h放所有配置参数比如串数、保护阈值、均衡开启压差等。硬件驱动层拆成sh367309.c和sh367309.h所有和芯片直接相关的操作都封装在这个文件里。策略层有soc.c、balance.c、protect.c分别对应SOC计算、均衡逻辑、故障状态管理。应用层有comm.c负责CAN/485数据上报。User/ ├── main.c ├── bms_config.h ├── bms_comm.c ├── balance.c ├── protect.c ├── sh367309.c ├── sh367309.h ├── soc.c └── stm32f1xx_it.c命名方式比较简单直白这样做的好处是这个工程从我一个人开发变成团队协作时大家找文件、改参数都特别方便。建议各位在做BMS这种逻辑复杂、状态繁多的项目时一定要重视模块划分不然光Protect状态机就够你头疼的。3.2 底层驱动SPI通信和寄存器读写SH367309的通信数据帧结构比较规整每个数据包由一个命令字节加两个数据字节组成。写寄存器时发送命令字节加16位数据读寄存器时发送命令字节后再额外发一个0x00填充字节从机就会把寄存器内容放在MISO线上返回。uint16_t SH36_ReadReg(uint8_t reg) { uint8_t buf[3]; uint16_t val; buf[0] (reg 1) | 0x01; // 读命令最高位固定为读标志 buf[1] 0x00; buf[2] 0x00; CS_LOW(); HAL_SPI_TransmitReceive(hspi1, buf, buf, 3, 100); CS_HIGH(); val (buf[1] 8) | buf[2]; return val; } uint8_t SH36_WriteReg(uint8_t reg, uint16_t data) { uint8_t buf[3]; uint16_t val; buf[0] reg 1; // 写命令 buf[1] (data 8) 0xFF; buf[2] data 0xFF; CS_LOW(); HAL_SPI_Transmit(hspi1, buf, 3, 100); CS_HIGH(); val SH36_ReadReg(reg); if (val ! data) return 1; // 回读校验 return 0; }每次写完寄存器后我都强制做一次回读校验。这颗芯片的寄存器写入偶尔会因时序竞争失败如果不校验你以为是开启过压保护了实际上芯片可能还是默认配置这在产线上会导致不可预估的后果。回读校验虽然多花几毫秒但对系统安全性来说完全值得。3.3 核心数据解析电压、电流、温度SH367309的ADC是16位的测量精度能做到1mV以内。每一串电芯电压的原始值都存储在对应的寄存器对里高低字节拼接后换算成电压值。这里有个细节电压寄存器的LSB是1.25mV也就是说原始值乘以1.25再除以1000才是电压值单位V。如果直接按1mV的刻度去换算误差会越来越大。float SH36_GetCellVoltage(uint8_t cell_index) { uint16_t raw; float voltage; raw SH36_ReadReg(SH36_REG_CELL1_VOLT cell_index * 2); voltage raw * 0.00125f; return voltage; }温度采样用的是外部NTC电阻分压SH367309提供一个恒流源输出接上NTC后分压点电压会随温度变化。代码里做了电压到温度的一维查表加线性插值温度范围覆盖-20度到80度精度做到正负1度以内。查表法比用公式计算更稳定NTC的B值公式在高低温端误差会变大表格数据是从实际标定得到的更贴合这颗料的特性。电流采样我是用外部分流器配合运放放大后送入SH367309的电流检测引脚。寄存器返回的是带符号的16位值符号位为正表示充电为负表示放电。换算系数取决于分流器阻值和运放增益我这边用的是1mR分流器加50倍增益所以LSB对应的电流是0.25A左右。拿到这个值之后SOC算法才能进行库仑积分。3.4 SOC估算开路电压法与安时积分融合SOC是BMS的核心参数算法烂的话客户用着用着电量从30%直接掉到5%谁用谁骂娘。我这边采用的是开路电压法加安时积分法的融合算法简单来说就是系统上电静置时用OCV查表得到初始SOC运行过程中用电流对时间积分来更新SOC同时根据电压修正SOC漂移。OCV曲线数据我一开始用的是电芯厂商给的典型值但后来发现不同批次的电芯曲线差异还是比较明显的。最终方案是在产线老化测试时增加了一个自学习校准流程通过上位机记录实际充放电数据反向修正OCV表格的值。这套自学习逻辑只跑在产线模式里量产模式下只读表格避免算法过于复杂影响实时性。安时积分的核心代码如下注意每隔一段时间要修正一下积分漂移不然电流采样误差一直累加SOC会越来越离谱。void SOC_Update(float current, float voltage, uint32_t dt_ms) { static float integrated_capacity 0.0f; float delta_capacity; delta_capacity current * ((float)dt_ms / 3600000.0f); integrated_capacity delta_capacity; soc_now soc_ocv (integrated_capacity / battery_capacity) * 100.0f; soc_now CLAMP(soc_now, 0.0f, 100.0f); if (voltage 3.0f soc_now 10.0f) soc_now 10.0f; }3.5 均衡控制逻辑均衡逻辑是所有BMS软件里最容易被忽略但线下问题最多的模块。SH367309支持被动均衡思路就是检测到哪一串电芯电压偏高就通过内部MOS把这一串通过功率电阻放电让电压降下去等待其他串慢慢追上。均衡策略我采用的是压差触发式当所有电芯的电压差超过30mV时开启均衡功能每次只均衡电压最高的一串等它和最低串压差小于20mV后再切到下一串。这个策略相比一次均衡多串的方式发热更分散避免PCB局部过热也减少了均衡MOS的寿命损耗。void Balance_Task(void) { float vmax, vmin; uint8_t idx_max; Get_CellVoltages(cell_voltages); idx_max Get_MaxVoltageIndex(cell_voltages, vmax); vmin Get_MinVoltage(cell_voltages); if ((vmax - vmin) BALANCE_TRIGGER_MV) { Set_BalanceChannel(idx_max, ENABLE); } else { Set_BalanceChannel(0xFF, DISABLE); } }在实际调试中发现陶瓷电容的漏电流会对均衡判断产生影响。具体表现是电压已经均衡得很好了但均衡MOS还在一直打开导致某些电芯被过放。这个问题在代码层面加上滞回区间就能解决同时PCB设计时也要注意均衡电阻底下的铜箔散热区域避免焊盘附近温度过高。4. 保护逻辑与状态机设计4.1 分级保护机制硬件直连加软件联动SH367309的硬件保护是独立的它监控每串电芯电压和总回路电流一旦超出设定的阈值立即通过硬件逻辑关闭充放电MOS。这种保护不依赖MCU即使STM32死机了电池组的过充、过放、大电流短路保护依然有效这对安全性要求较高的储能应用来说是必须的。软件保护是在硬件保护基础上加一层“软保护”主要针对一些不需要立刻断开MOS的异常工况。比如单串电芯欠压但还没到硬件保护阈值时软件可以先上报一次告警消息如果持续一段时间后电压继续下探再强制关闭放电MOS。这么做可以减少硬件保护的触发频率避免因瞬时干扰误触发保护导致系统反复重启。4.2 保护状态机的四个核心状态整个保护逻辑我设计成状态机来实现状态转移关系非常清晰。四个状态分别对应正常、告警、保护、恢复。系统一般停留在正常状态当检测到任何一项目异常但未达到硬件保护阈值时进入告警状态同时开启一个延时计时器。如果异常持续时间超过500ms就进入保护状态并关闭MOS。恢复状态比较特殊它是在保护发生后经过一段时间延时和条件检查确认故障解除后自动回到正常状态。typedef enum { BMS_STATE_NORMAL, BMS_STATE_WARNING, BMS_STATE_PROTECT, BMS_STATE_RECOVERY } BMS_State_t;保护状态的设计有一个工程经验不要从保护状态直接回到正常状态一定要通过恢复状态过渡并且恢复状态下要持续检测故障是否真实消除。我刚开始做的时候没加这个过渡导致的问题是这样的电池组在放电过程中触发了过流保护软件立刻关断MOS负载切掉后电流瞬间没了软件以为故障恢复就直接打开了MOS但实际上此时负载并未移除MOS一打开又立刻触发过流保护形成了反复重启的振荡。4.3 保护参数配置从理论值到实际值SH367309的保护阈值和延时时间都是通过寄存器配置的。这里有个容易踩坑的地方过压保护阈值和恢复阈值是分两个寄存器设置的而且最小步进是50mV你要是想设个精确到个位数的阈值寄存器可能没那么细。所以设计初期就要和电芯厂商确认好电芯允许的最大充电电压留足安全余量再来倒推寄存器配置。以我这边14串三元锂电池组为例充电截止电压是4.2V硬件过压保护阈值设置4.25V恢复阈值4.15V过压保护延时2秒目的是避开充电末端电压波动导致的误动作。欠压保护阈值按电芯数据手册设置2.8V恢复阈值3.0V。放电过流保护阈值则按电芯最大持续放电倍率再加1.5倍余量来设定。所有参数我都放在bms_config.h里单独管理方便产线不同规格的产品直接改配置。5. 调试过程与常见问题排查5.1 通信数据异常读取电压全是0xFF这是最典型的问题。拿到新的PCB板子程序烧进去后读取所有电压寄存器返回的全是0xFF。当时排查第一步是用示波器抓了SPI的CLK、MOSI、MISO信号发现MISO线一直是高电平说明从机没有把数据放到总线上。继续查发现SH367309的复位引脚被拉低了芯片一直处于复位状态根本不会响应SPI请求。查原理图复位电路的下拉电阻焊接的是0欧姆这不是坑人吗这个问题的教训是新的样板回来后首先用万用表检查芯片每个电源引脚和复位引脚的电压不要着急上电跑程序。芯片供电、复位、时钟都确认正常之后再来查通信时序。SPI读不到数据时90%的问题出在芯片根本没工作而不在通信代码本身。5.2 电压采样跳变刷新率过高惹的祸另一类常见问题是电压采样值跳变厉害特别是开启均衡的时候跳变幅度能达到几十毫伏。排查下来有几个原因。一个是采样周期太短SH367309内部ADC完成一次全通道扫描是需要时间的如果你读寄存器读得太快可能读到ADC正在更新过程中间的中间值数据自然不稳定。我在代码里做了处理两次采集之间至少间隔50ms电压数据稳定很多。另一个原因是均衡放电时均衡电流会在采样点产生压降。SH367309内部实际上有滤波机制但需要配置滤波寄存器才能开启。我在初始化时开启了内部的数字滤波功能软件层再做一次滑动平均滤波经过这两层处理之后电压数据的波动被控制在正负2mV以内。5.3 SOC跳变充电末端电压修正过于激进SOC跳变的问题往往出现在充电末端。本来已经充到80%突然跳到95%客户看到肯定心里发毛。原因出在电压修正算法上OCV法在充电末端电压曲线陡峭电压稍微波动一点点换算出来的SOC变化就会非常大。解决方案是限制电压修正的步长每次修正SOC的幅度不超过1%同时只有当电压处于静置状态时才启用电压修正。充电过程中电流不为零此时电芯的端电压包含极化电压分量用这个电压查OCV表本身就是错的。加了这两个条件后SOC跳变的问题就基本消失了。这个过程也印证了一个道理BMS软件里很多问题并不是算法本身不先进而是使用条件没约束好。5.4 均衡功能失效MOS驱动电压不足均衡功能调试时发现MOS打不开测量均衡MOS的栅极电压只有1.2V。SH367309内部均衡MOS驱动是开漏输出外部要加上拉电阻到芯片的均衡电源引脚如果没有这个上拉电阻驱动电压根本建立不起来。原理图和PCB评审的时候没发现这个问题调试时全靠示波器一步步追才找到原因。所以画板之前一定要仔细阅读芯片手册里关于均衡驱动电路的外围元器件要求这部分信息往往藏在寄存器描述或者应用说明里不细看很容易漏掉。6. 扩展方向与后续优化建议目前这套代码在量产项目上已经稳定跑了几个月老化测试数据、产线直通率都在可接受范围内。但BMS这个领域永远有优化空间。如果后续产品对SOC精度要求更高可以考虑加入卡尔曼滤波或者扩展卡尔曼滤波算法。现在很多开源方案已经有相对成熟的实现但移植到嵌入式MCU上时要注意计算资源消耗实测下来如果跑在STM32F103上单次EKF更新耗时大约在3到5毫秒对200毫秒的SOC更新周期来说完全够用。另外如果想要支持更大规模的储能系统可以考虑再加一颗MCU做通信管理主机跑核心算法从机专门负责协议转换和远程监控软件架构上预留好接口就行。从无线方向考虑如果能加入BLE或者WiFi通信模块就能把BMS数据实时传到云端实现远程监控、故障预警、历史数据分析这些增值功能。目前很多便携储能产品都在往这个方向走硬件上预留一路UART通信接口就能对接代码上只需要在现有协议栈里增加心跳包、报警主动上报这些逻辑工作量不大但用户感知的差异会很明显。最后再说一点个人体会BMS软件调试本质上是个逻辑加时序的活很多问题不是单个代码块有问题而是模块和模块之间的配合出了问题。建议在开发初期就建立起一套完整的测试用例和自动化验证流程哪怕是一个简单的脚本定期跑一遍寄存器读写、均衡开关、故障注入测试都能帮你提前发现大量隐藏问题。我这边现在每次改完代码都会在专门搭的HIL测试平台上跑一遍回归测试再谈现场调试的事。这套流程看起来前期投入大但长期看节省的时间和返工成本完全值回票价。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →