简介:基于STM32与SH367309的BMS参考工程代码,面向嵌入式开发者与新能源BMS入门学习者,用于理解单节锂电池电压、电流、温度实时监控以及充放电管理的基本实现。资源共289个文件,压缩包约8.54MB,包含C/H源码、STM32工程文件(uvprojx/uvoptx)、编译生成的axf/hex可执行文件、map映射文件及I2C相关示例,源码结构与工程配置较为完整,适合对照学习芯片驱动、I2C通信和电池状态采集流程。已有798人学习下载。参考代码覆盖SH367309的配置与数据读取、故障诊断和均衡管理思路,同时给出ST官方工程框架下的编译产物与链接脚本,可帮助读者快速定位主控与监测芯片之间的协作方式,节省环境搭建和底层调试时间,作为实际项目开发或课程设计的参考资料具有较高性价比。
1. 项目概述
做这几年嵌入式,电池管理这块始终绕不过去。手头这个项目用了STM32搭配SH367309的方案,整体跑下来还算稳定,就把这套BMS代码拿出来聊聊。SH367309是上海南芯半导体推出的一颗多节电池保护IC,支持3到16串锂电池组的采集与保护,天然就是为储能和电动车场景准备的。STM32作为主控负责通信、状态计算、均衡和异常处理,SH367309负责前端采集电压和温度。
这套方案能做的事情很明确:实时采集每一节电芯的电压和温度、计算充放电电流和剩余电量、做电池组的均衡控制、在出现过压、欠压、过流、短路、过温这些危险情况时及时切断MOS并上报状态。不管你是做电动工具、两轮车、便携储能还是小型UPS,这套代码的思路和结构都有很强的参考价值。
适合谁来看?第一类是已经在做BMS但想换方案或者优化现有架构的工程师,第二类是刚接手电池管理项目、需要快速搭建原型的嵌入式开发者,第三类是想弄明白BMS内部到底怎么工作的学生或爱好者。这篇文章我会按照硬件设计、软件架构、核心代码逻辑、实测数据和坑点排查这个顺序来写,尽量把细节讲透。
2. 系统总体架构与方案选型
2.1 为什么选SH367309而不是bq76940
很多朋友一上来就会问,TI的bq76940不香吗?这里要说明一下,不是bq76940不好,而是要看你到底要什么。SH367309在功能上跟bq76940确实属于同类产品,但有几个差异点在实际项目中影响很大。
首先是成本。SH367309的物料成本比bq76940低一个档次,在批量出货的产品中,这个差距会直接反映到毛利上。其次是寄存器操作方式,SH367309的操作要更直接一些,内部寄存器定义清晰,没有TI那么多历史包袱,初期上手会轻松不少。
还有一个关键的差异——SH367309内置了比较完整的保护功能。过压、欠压、过流、短路、过温这些保护阈值都可以独立配置,而且保护动作可以直接由硬件完成,即使MCU死机了,硬件保护链路依然在运行。这是我在选型时最看重的一点,BMS这种安全攸关的系统,硬件保护和软件保护就应该分开走。
2.2 系统硬件架构
STM32和SH367309之间用I2C通信,SH367309作为从机,STM32作为主机。为什么不用SPI?因为SH367309只提供I2C接口,这是芯片固定的,你只能顺着它来。不过I2C在BMS场景下其实也够用,电压采集周期本身不需要太高的频率。
整体信号链路是这样的:电芯组正负极通过采样线连接到SH367309的VC引脚,SH367309内部完成电压采集、ADC转换、滤波和阈值比较,然后通过I2C把数据交给STM32。STM32拿到原始数据后做软件滤波、SOC估算、均衡控制和异常处理。
放电回路里还有一个关键器件——分流器。电流采样我用的是一颗毫欧级的分流电阻,而不是霍尔传感器。为什么?分流器精度高、成本低、温漂小,对于储能和两轮车这种直流场景足够用了。霍尔传感器虽然隔离性好,但精度和温漂都不如分流器,而且贵。
MOS方面,充放电共用一组背靠背的N-MOS管。为什么用背靠背?因为BMS需要在充电过压、放电欠压时都能切断回路,背靠背MOS可以在两个方向都实现关断。而且N-MOS导通电阻小,发热低,在BMS这种需要长时间大电流通过的场景里是标准做法。
2.3 为什么需要MCU和前端IC共存的架构
如果你是第一次接触BMS,可能会觉得既然SH367309自己就能采集电压、判断保护,那还要STM32干什么?直接让SH367309控制MOS不就行了?
这个问题问得很关键。SH367309确实可以独立工作,但只能做最简单的保护,它做不了SOC估算、做不了均衡策略调整、做不了与上位机的远程通信、也做不了历史记录存储。更重要的是,它没办法根据不同的应用场景动态调整保护阈值。
打个比方,SH367309像一个只会执行固定规则的保安,而STM32是保安队长——保安发现异常会立刻拉响警报,但到底怎么处理、要不要放宽限制、需不需要记录这次异常,这些判断还是要队长来做。
所以这套架构的分工是:SH367309负责毫秒级甚至微秒级的硬件保护,STM32负责策略层面的软件管理和通信交互。两者互为备份,SH367309在MCU失效时依然能保护电池组不被损坏,这是整套设计的安全底线。
3. 核心功能模块与代码设计
3.1 电压采集与滤波
SH367309会把所有电芯的电压数据放在内部的ADC寄存器里,STM32通过I2C读取。这里要特别提醒一个细节——读取到的原始值不是最终的电压值,需要根据芯片的参考电压和ADC位数做换算。
我在项目中用的换算公式是:
cell_voltage = raw_adc_value * 0.000305这个0.000305是哪里来的?SH367309内部ADC为16位,参考电压大概是20V(具体值以芯片手册为准),所以每个LSB对应的电压就是20 / 65535 ≈ 0.000305V。这个换算系数在代码里应该是宏定义,不要散落在各处写魔法数字。
首次读取的电压值不建议直接使用,因为上电瞬间采样线可能还没稳定,而且ADC本身会有随机噪声。我做了个一阶低通滤波,代码实现很简单:
float filtered_v = 0.0f; #define ALPHA 0.15f float voltage_filter(float new_sample) { filtered_v = ALPHA * new_sample + (1.0f - ALPHA) * filtered_v; return filtered_v; }ALPHA取0.15意味着滤波响应比较慢,对瞬时噪声抑制效果好,但会造成一定延迟。如果你需要更快的响应速度,可以适当调大ALPHA。实测下来0.15在这个项目里比较合适,既能过滤掉采样线上的共模干扰,又不会让电压更新显得太迟钝。
3.2 电流采样与库仑计SOC估算
电流采样用的是分流器方案,采样电阻阻值为1毫欧。通过精密运放将分流器两端压差放大后送到STM32的ADC引脚。为什么用运放而不是直接接入ADC?因为1毫欧电阻在10A电流下只有10mV压差,这个信号太微弱,不放大根本没法精确采集。
SOC估算我用了库仑计法加开路电压校准的融合策略。库仑计法原理很简单:把电流对时间积分,累加得到充入或放出的电量。
// 每100ms调用一次 void soc_update(float current_a) { static float accumulated_capacity_mAh = 0.0f; accumulated_capacity_mAh += current_a * (100.0f / 3600000.0f) * 1000.0f; current_soc_percent = (total_capacity_mAh - accumulated_capacity_mAh) / total_capacity_mAh * 100.0f; }这里100ms是积分周期,换算成小时就是0.0000278小时,再乘以当前电流就得到了这段时间充入或放出的毫安时。纯库仑计法有个致命弱点——时间久了误差会累积。所以我在电池组静置超过30分钟且没有充放电时,会读取开路电压来修正SOC,把电池的OCV-SOC曲线做成查找表,用线性插值去匹配当前SOC值。
3.3 均衡控制策略
电池组里每一节电芯的容量和内阻不可能完全一致,长期使用后电压差会越来越大,差的电芯会拖垮整个电池组。均衡就是这个问题的解决方案。
我采用的是被动均衡方案,就是通过电阻把电压高的电芯多余的电量放掉。为什么不用主动均衡?被动均衡电路简单、可靠性高、成本低,对于16串以下的电池组完全够用。主动均衡效率高,但电路复杂、成本高,更适合电动大巴这种大型电池系统。
判断均衡的启动条件也很关键。我设置的是:当某一节电芯电压高于平均电压30mV时,开启对这一节的均衡,每节电芯单独控制均衡开关。均衡电流我限制在100mA左右,这样既能有效拉低高压电芯,又不会因为电流太大导致电芯发热。
有个实际经验想分享:均衡不要一直开着,不然高压电芯被放电、低压电芯没变化,很容易出现过度均衡。我在代码里加了个均衡超时保护,单次均衡最长时间限制为2小时,时间到了自动暂停,等电压重新稳定后再决定是否继续均衡。
3.4 保护逻辑实现
保护逻辑分成两层:硬件保护和软件保护。硬件保护由SH367309独立完成,包括过压、欠压、过流、短路、过温。这些阈值在初始化时写入SH367309的对应寄存器,之后即使STM32完全不干预,芯片也能独立完成保护动作。
软件保护主要是做二次确认和状态恢复。有时候瞬时的大电流冲击会导致电压短暂跌到欠压阈值以下,如果硬件立即锁死MOS,用户体验就很差。所以我的软件保护策略是:如果硬件保护触发了,先记录事件,然后延迟5秒尝试恢复,如果故障已经消失就自动恢复,如果故障还在就继续保持保护状态。
typedef enum { BMS_STATE_NORMAL, BMS_STATE_OVP, BMS_STATE_UVP, BMS_STATE_OCP, BMS_STATE_OTP, BMS_STATE_SHORT_CIRCUIT } bms_state_t; void bms_state_machine_run(void) { switch (current_state) { case BMS_STATE_NORMAL: break; case BMS_STATE_OVP: if (max_cell_voltage < OVP_RELEASE_THRESHOLD) { current_state = BMS_STATE_NORMAL; enable_charge_mos(); } break; default: break; } }在这里要特别说明一点:不同的保护状态对MOS的控制要求是不一样的。过压只需要断开充电MOS,放电MOS仍然保持导通;欠压则相反,只需要断开放电MOS,充电MOS保持导通。这个细节很容易做错,如果错误地把两个MOS都断开,会导致电池无法充电也无法放电,这是直接让用户返修的低级问题。
4. 软件流程与通信实现
4.1 主循环任务调度
BMS软件不适合跑裸机大循环里堆if-else,也不建议直接上RTOS(除非你需要复杂的任务调度)。我的做法是写了一个轻量级的定时任务调度器,利用STM32的SysTick产生1ms时基,各个任务按不同的周期执行。
void main_loop(void) { if (timer_1ms) { timer_1ms = 0; key_scan(); } if (timer_10ms) { timer_10ms = 0; led_display_update(); } if (timer_100ms) { timer_100ms = 0; bms_data_update(); // 电压电流温度采集和处理 } if (timer_1s) { timer_1s = 0; soc_update(get_current()); protection_check(); } }这样设计的好处是:每个任务都有独立的执行周期,长时间运行的任务不会卡死紧急任务。比如按键检测每个1ms就扫一次,响应速度很快,而SOC更新不需要那么频繁,1秒一次就够了。如果一个任务偶尔需要紧急执行,可以在中断里置标志位,主循环下一次循环就立刻判断处理,响应延迟不超过1ms。
4.2 I2C通信细节与坑点
I2C通信是这套系统最容易出问题的地方。SH367309的I2C从机地址是固定的,具体地址要根据芯片手册确认。这里有个坑——I2C通信时必须在读取数据前加上寄存器地址的写入,否则芯片不知道你想读哪个寄存器。
uint8_t i2c_read_reg(uint8_t dev_addr, uint8_t reg_addr) { uint8_t data = 0; i2c_start(); i2c_send_byte(dev_addr << 1 | 0x00); // 写模式,发送寄存器地址 i2c_send_byte(reg_addr); i2c_start(); // 重复起始信号 i2c_send_byte(dev_addr << 1 | 0x01); // 读模式 data = i2c_recv_byte(); i2c_stop(); return data; }I2C总线一定要接上拉电阻,阻值选4.7kΩ比较合适。如果上拉电阻太大,总线的上升沿会变缓,在快速模式下容易出现通信错误;太小了又会导致功耗偏高,影响低功耗设计。另外SH367309的I2C是纯软件模拟还是硬件I2C,代码里要考虑清除总线异常状态,我习惯在读操作前先检查总线上是否有设备应答,没有应答就立刻重新初始化总线,避免卡死在某个等待状态。
4.3 上位机通信协议设计
为了方便调试,我在STM32的串口上加了一个简单的通信协议,上位机通过串口可以实时读取所有电芯电压、电流、温度、SOC、保护状态这些参数。协议设计得很简单:
帧头(0xA5) + 长度(1字节) + 命令字(1字节) + 数据(N字节) + CRC16校验(2字节)为什么加CRC16?BMS的工作环境通常电机、电源转换电路这些干扰源比较多,串口通信过程中很容易出现单个字节的错误,如果没有校验,错误数据会导致上位机显示错误的电压电流值,误导调试判断。CRC16能检出绝大多数错误,而且软件实现也不复杂,算是一个性价比很高的保护。
5. 实测数据与性能表现
5.1 电压采集精度验证
在实验室环境下用6位半万用表做了精度比对,实测数据如下:
| 电芯序号 | 万用表读数(V) | BMS读数(V) | 误差(mV) |
|---|---|---|---|
| 1 | 3.6521 | 3.651 | 1.1 |
| 2 | 3.6713 | 3.672 | 0.7 |
| 3 | 3.6845 | 3.683 | 1.5 |
| 4 | 3.6598 | 3.660 | 0.2 |
| 5 | 3.6602 | 3.661 | 0.8 |
| 6 | 3.6544 | 3.655 | 0.6 |
最大误差1.5mV左右,这个精度对于BMS应用来说完全在可接受范围内。要知道,一般电芯电压的精度要求是±10mV以内,这个结果有充足余量。
5.2 保护动作响应时间
我用电子负载和信号发生器做了跳变测试。把电子负载从1A瞬间跳到50A,通过示波器抓取MOS控制和电压跌落波形。硬件短路保护的响应时间大概在200微秒到500微秒之间,这个很快,主要靠SH367309硬件比较器触发。软件过流保护要经过ADC采样和软件判断,耗时大约在20毫秒到100毫秒之间。这里想提醒一下——如果电流快速冲到100A以上,不靠硬件保护,光靠软件是反应不过来的,所以硬件保护的阈值一定要设置保守一些,给软件留出冗余时间。
5.3 均衡效果
我拿了一组压差较大的电芯做测试,最大压差89mV,开启均衡后大约3小时压差降到了18mV以内,效果很明显。电池在充电停止后电压会有一个回弹过程,这会影响均衡效果判断。我在做均衡测试时参考的是静置电压,而不是充电电压,这样数据才可靠。
6. 常见问题与排查技巧
6.1 电压采样偏差过大
如果发现BMS读到的某节电芯电压跟万用表实测差距很大,先不要怀疑芯片坏了。百分之八九十的情况是采样线接触不良。SH367309的VC引脚采样线是直接接到电芯连接片上的,如果连接片氧化或者螺丝没有拧紧,接触电阻就会变大,导致电压采样偏低。
排查方法很简单:用万用表直接量芯片引脚上的电压,再对比BMS读到的数据。如果芯片引脚上的电压跟万用表一致,说明是采样线到电芯之间的连接问题;如果芯片引脚上的电压就已经不对了,那可能是芯片本身的ADC出问题了。
6.2 I2C通信偶发失败
这个问题我调试了很久才发现原因——SH367309的供电电压纹波太大。当MOS开关进行切换的时候,瞬间电流突变会导致电源电压跌落几十毫伏,I2C在这个瞬间通信就会出错。
解决办法有两个方向:一是给SH367309的供电脚加一个10uF的陶瓷电容,把高频纹波滤掉;二是在软件里做I2C重试机制,如果连续3次读取失败就重新初始化I2C并复位总线。两个办法加在一起之后,从原来一天数次通信错误降到了几乎为零。
6.3 开机后MOS无法导通
这个症状通常出现在刚焊接完的新板子上。排查步骤建议按顺序来:先用万用表确认电池组总电压是否正常,再确认SH367309的供电是否正常,然后用示波器检查MCU的I2C是否有波形,最后查看芯片的保护寄存器——大概率是有某个保护标志位被触发了。
我遇到过一种情况是焊接时烙铁静电击穿了MCU的某个GPIO,导致控制MOS的引脚一直是高电平不变化。这个很难查,最后是替换MCU才定位到问题。所以焊接这类板子时,防静电措施一定要做好,尤其是冬天干燥环境下,静电问题会更突出。
6.4 休眠电流过大
BMS低功耗在这个项目里也是重点。整套系统在不工作时要降到极低的静态电流,否则电池放几天就空了。我把STM32进入STOP模式,SH367309保持正常供电但停止频繁上报数据。
实测整机休眠电流在60uA左右,对于一般储能应用来说可以接受。如果想要更低的功耗,可以给SH367309加一个独立的负载开关,在休眠时完全断电,这样能把电流降到10uA以下,但代价是休眠唤醒后会有一段重新初始化的时间。
7. 项目总结与后续扩展方向
这套基于STM32和SH367309的BMS方案经过大量测试和实际运行,在稳定性、精度和成本之间取得了较好的平衡。整个开发过程中最大的感受就是——BMS的核心不是功能实现,而是健壮性设计。你不仅要考虑正常工作的逻辑,更要考虑各种异常情况下的安全兜底。
如果后续有朋友想做扩展,我建议从这几个方向入手:第一,加CAN接口,在电动车上与整车控制器通信;第二,加蓝牙模块,配合手机App做远程监控和参数配置;第三,把SOC估算算法换成卡尔曼滤波或者深度学习模型,精度会进一步提升。最后分享一个小技巧:在代码里加一个版本号宏定义,每次修改烧录后通过串口打印出来。这个习惯会让你在调试多个固件版本时少走很多弯路,别问我怎么知道的。
本文还有配套的精品资源,点击获取