简介面向中颖SH367309锂电池保护板开发者的源码解析资料适合从事嵌入式BMS、电池管理及量产固件开发的软件人员参考。资源共包含3个文件以HTML说明页为主体辅以inscode工程入口和gitignore配置压缩包仅7KB便于快速浏览代码结构与设计要点。解析采用自底向上的方式从汇编启动、CMSIS核心文件到ADC、Flash模拟EEPROM、外设中断、键盘、LCD等驱动并覆盖SMBus从机、电量计量与保护状态机清晰呈现一套体积小于64kB、适合掩膜量产的寄存器版HAL工程。可靠性方面重点说明了双备份Flash、CRC16、掉电续写、硬件看门狗等机制也指出ADC改用DMA双缓冲、SMBus增加PEC校验等可优化方向有助于二次开发和移植。目前已有94人学习浏览适合需要深入理解该芯片软件框架及可靠性设计的开发人员。 做嵌入式这几年电池保护板相关的项目我接触了不少但真正让我觉得值得写一篇源码解析的还是这个基于中颖SH367309的项目。这颗芯片在中低端锂电池保护方案里出镜率很高尤其在电动工具、吸尘器、储能电源这些多串锂电池场景下属于典型的“功能集成度高、外围电路简单”的保护IC。拿到这套项目代码的时候我第一反应是——保护逻辑这种东西代码量通常不大但真正把每一行和芯片内部机制对应起来才是解析的重点。这篇就来从头到尾拆一遍这套源码把核心模块、关键参数、调试心得一次说清楚。1. 项目背景与代码整体设计1.1 SH367309是什么源码解决什么问题SH367309是中颖推出的一颗多串锂电池保护IC支持3到16串电池组内部集成了高精度ADC、比较器、充电泵驱动以及均衡控制逻辑。它和传统分立方案最大的区别在于保护阈值、延时时间、均衡策略这些参数都可以通过I2C接口配置而不是靠外部电阻硬调。这就意味着整套保护逻辑的“大脑”其实在MCU侧——芯片负责采集电压和电流MCU负责读取数据、判断状态、下发控制指令。拿到这套源码时我第一反应是——保护逻辑这种东西产品形态再变内核不外乎“采样—判断—动作”三个环节。但真正去读代码才发现坑全藏在细节里寄存器配置顺序、ADC数据的滤波方式、保护触发后的状态机迁移甚至I2C读时序的稳定性都会决定一块保护板是“正常工作”还是“偶尔抽风”。所以源码解析不是把代码贴出来念一遍而是要把每段逻辑和芯片手册里的寄存器、时序图一一对应起来。这套代码在项目中担任的角色是完成SH367309的初始化配置、实时读取电池组电压和温度、执行过充/过放/过流/短路保护判断、控制均衡策略并通过指示灯或通信接口上报状态。适合做电池保护板开发、BMS底层软件以及想搞懂“保护ICMCU如何配合”的嵌入式工程师参考。1.2 代码目录结构与模块划分项目代码整体采用了分层思路虽然不是特别复杂的架构但对于这类规模适中的嵌入式固件来说分层清晰比炫技重要得多。目录结构大致如下app/应用层包含主循环、状态机、保护策略逻辑driver/芯片驱动层主要是SH367309的寄存器读写与初始化port/平台移植层包括I2C底层接口、延时函数、串口打印等middleware/中间层比如滤波算法、数据校验、均衡控制算法config/配置头文件所有阈值参数、使能开关都集中在这里说实话第一次看到这个结构我最满意的一点就是配置隔离做得好。所有需要根据电池规格调整的参数比如过充电压、过放电压、过流阈值、均衡开启压差全部集中在config/battery_config.h里业务代码里几乎没有散落的魔数。这意味着换一个电芯规格只需要改这个头文件而不用动保护逻辑工程实践上能省下大量排查时间。2. 初始化流程与寄存器配置要点2.1 上电初始化顺序SH367309的初始化顺序是有讲究的。源码里第一步是I2C设备地址配置——这颗芯片的地址可以通过外围引脚拉高/拉低来设定所以代码里没有把地址写死而是从配置头文件里读取一个SH367309_I2C_ADDR宏。这里有个小陷阱芯片手册上写的地址通常是7位地址但I2C驱动读写时用的是8位地址左移一位定义宏的时候最好直接定义7位地址在驱动层统一转换否则容易在读写地址上错位。初始化流程简化后大致如下延时等待芯片上电稳定实测至少需要10ms以上太短会读到异常的寄存器值读取芯片ID寄存器确认通信正常配置保护阈值寄存器组配置延时时间寄存器组配置均衡相关参数写入CTRL寄存器使能保护功能回读所有配置数据做一次校验2.2 寄存器配置的方法论SH367309的寄存器配置有个特点很多寄存器是“写1清0”或“按位生效”的直接对整个寄存器赋值容易误操作。源码里专门做了read-modify-write的封装也就是“先读回原始值修改目标位再写回”。这个封装看起来多余实际调试时帮了大忙。我当时就踩过一次坑——直接对整个寄存器写入配置结果把另一个位的保护功能给关了电池过放到2.8V都没触发保护吓得一身冷汗。配置阈值参数时源码里做了一个非常实用的抽象把所有阈值统一换算成ADC原始值来进行配置而不是直接使用电压值。原因是芯片内部的比较器和ADC天然工作在原始采样值域比如过充阈值寄存器里存的不是“3.65V”而是对应的ADC量化值。代码里的换算函数长这样uint16_t voltage_to_adc(uint16_t voltage_mv, uint16_t adc_full_scale_mv, uint16_t adc_resolution) { return (uint32_t)voltage_mv * adc_resolution / adc_full_scale_mv; }这个设计的好处是当ADC参考电压因温度或其他因素发生漂移时只需要调整adc_full_scale_mv这一个参数所有阈值自动跟随校正而不需要逐个寄存器去改。3. 核心逻辑拆解与实现细节3.1 数据采集与滤波处理保护逻辑的判断基础是实时、准确的电压和电流数据。源码在采集部分没有直接使用芯片中断回调的单次采样值而是做了一级滑动滤波。具体做法是维护一个长度为8的环形缓冲区每次采样刷新后去掉最大值和最小值取中间6个数据的平均值。这个算法实现简单但效果非常明显——特别是电动工具这类负载波动大的场景瞬间的电压毛刺很容易造成误保护滤波后才能准确反映电池真实状态。代码实现上这个滤波算法放在middleware/filter.c里接口非常简洁uint16_t filter_get_avg_voltage(uint8_t channel, uint16_t new_sample) { // 将新采样值写入环形缓冲去掉最大最小后取均值 }采样周期的选择也值得说。源码里主循环的采样周期是10ms但电压通道和电流通道交替采样实际每个通道的采样间隔是20ms。对于过压和欠压保护来说20ms的响应速度足够快同时也给了滤波算法足够的数据量来平滑抖动。如果采样过快滤波反而会滞后于真实电压变化这在动态负载下反而可能导致保护不及时。3.2 保护状态机设计过充、过放、过流、短路这四类保护虽然触发条件不同但源码中统一用了一个状态机来管理避免“多个保护逻辑互相打架”的问题。状态机设计如下ST_IDLE正常工作状态所有保护未触发ST_OVP过充保护触发等待恢复条件满足ST_UVP过放保护触发等待恢复条件满足ST_OCP过流保护触发等待恢复条件满足ST_SCP短路保护触发需要硬件复位或手动恢复状态迁移的核心在app/bms_state.c里每次采样完成后执行一次状态评估。这里有一个很关键的细节过充和过放的恢复条件不等同于触发条件的反向而是留了滞回区间。例如过充触发电压是3.65V但恢复电压设置成3.55V避免电池电压在阈值附近来回抖动时保护反复开启关闭损坏MOS管和电芯。滞回参数同样放在配置文件里两个独立宏定义#define CELL_OVP_THRESHOLD_MV 3650 #define CELL_OVP_RELEASE_MV 35503.3 短路保护为什么走硬件中断在四类保护里短路保护最特殊因为它对响应时间的要求是微秒级。MCU就算主频再高走“采样—滤波—判断—控制MOS”这条链路也来不及。所以SH367309内部集成了硬件比较器短路时由芯片直接切断MOS驱动器同时在CTRL寄存器里置一个短路发生标志位。MCU的角色变成了“事后读取标志记录事件等待用户手动复位”。源码里对这个标志位的处理值得学习一旦检测到短路标志置位先读取并保存完整的状态寄存器值再执行一次软件复位清零标志。这样既保住了故障现场又确保下次短路时标志还能正常置位。如果清零顺序反了故障信息就丢了排查问题时会很被动。3.4 均衡策略最简单的才是可靠的均衡逻辑在源码里没有做得很复杂用的是一种“阈值开启、电压差触发”的被动均衡策略。核心思路是每次采样后计算所有电芯电压的最大值和最小值如果最大压差超过BALANCE_START_DELTA_MV默认30mV且最高电压高于BALANCE_START_ABS_MV默认3.4V说明电芯间出现了明显不一致开启均衡均衡方式是对电压最高的那节电芯接通旁路放电电阻降低它的电压当压差回落到BALANCE_STOP_DELTA_MV默认15mV以下关闭均衡这里加一个“绝对电压”条件非常关键。想象一下整组电池都在3.0V以下时电芯间有20mV的压差——这时候开启均衡去放最高节电芯的电不仅没法让电池组容量变高还会加速欠压把好电芯也拖垮。加了这个BALANCE_START_ABS_MV门槛就确保了均衡只在接近满电区域执行这才是实际有效果的均衡窗口。4. 调试阶段踩过的坑与排查实录4.1 I2C通信异常示波器才是最终裁判项目调试初期遇到最头疼的是I2C读取到的电压数据偶发性跳变。有时候读回来的值是合理的有时候突然冒出来一个0V或者满量程值。代码逻辑排查了好几遍滤波算法也没发现问题最后用示波器抓I2C波形才发现——是上拉电阻阻值选大了SCL上升沿特别缓在高速读操作时偶发时钟毛刺导致数据错位。SH367309的数据手册里对I2C时序要求比较严格源码里虽然配置了400kHz速率但实际跑起来发现外部上拉电阻用4.7k不够换成2.2k后波形立刻干净了。这个经验适用于大部分I2C设备如果遇到偶发通信错误先别怀疑代码用示波器看波形重点检查边沿时间和毛刺。4.2 过放保护恢复后的“假死”现象另一个印象深刻的坑是电池组触发过放保护后接上充电器发现设备没有任何反应。代码里过放恢复条件是“检测到充电电压且电芯电压高于恢复阈值”但SH367309在过放保护后内部MOS会被关断此时电池包对外相当于断路充电器插入时根本不会被系统检测到。排查后发现芯片有一个“充电检测唤醒”功能需要MCU在初始化时使能对应寄存器位。这个功能的作用是当充电器插入时通过外部电压唤醒芯片内部的充电检测电路从而重新闭合MOS管让MCU有机会继续运行并完成状态恢复。源码里很早就配置了这个位但默认值是不启用的如果参照手册某个旧版本示例代码很容易漏掉这个配置。4.3 常见问题速查表整理一份我在实际调试中遇到过的问题和对应解法供大家排查时参考现象可能原因排查与解决电压读数偶发跳变I2C上拉电阻过大/布线过长示波器抓波形上拉电阻改为2.2k降低速率到200kHz验证过放后无法充电恢复充电检测唤醒功能未使能检查初始化流程确认CTRL寄存器相应位置位过流保护误触发电流采样滤波不足/延时太短增加滤波窗口、加大过流延时时间寄存器值均衡开启后电芯电压不降反升均衡时间片太短确认均衡MOS驱动配置正确延长均衡运行时间复位后配置丢回读校验失败写入时序不完整使能CRC校验回读比较并重试写入4.4 源码级避坑技巧最后分享几个我在这套代码里沉淀下来的调试技巧常规文档里基本不会写。第一芯片所有阈值寄存器都是16位宽但不同地址的寄存器有效位数和LSB权重不一样。配置前先查手册确认每个寄存器的“步进值”比如过充阈值寄存器每个LSB是10mV过流阈值可能是1A甚至更大。直接用十六进制去推阈值十有八九会出错。我的习惯是在配置文件里把所有参数都用“真实物理值”定义比如3500mV然后通过宏换算成寄存器写入值代码自动保证单位和精度。第二调试保护逻辑时故意用可调电源替代真实电池组。把过充阈值调低到比如4.0V再用可调电源模拟电压上升到4.1V可以快速复现保护动作而不需要真的把电池充到过充状态。这套手段能大幅压缩调试周期也安全得多。第三给所有保护事件打时间戳和状态快照。这套源码里每个保护事件触发时会把当时的电芯电压、电流、温度、状态寄存器值一起写入一个日志结构体。前期调试可能觉得麻烦等整机测试出问题时你才会意识到这是性价比最高的排查手段。5. 源码移植与扩展建议5.1 换芯片型号要注意什么如果你准备把这套代码移植到SH367309同系列的其他型号比如不同串数对应的版本最需要关注的是ADC通道数量和寄存器地址偏移。不同封装和不同串数版本内部ADC通道数会不同寄存器地址也可能整体偏移。移植时先对照新芯片手册重点核对以这几部分I2C设备地址和时序是否一致各通道ADC寄存器地址保护阈值寄存器的LSB权重均衡控制的通道映射5.2 代码风格与工程习惯建议读这套代码的过程中最值得借鉴的是它的“配置集中化”思路。所有与硬件相关的参数、阈值、使能开关都放在配置头文件里业务代码只通过宏来引用不直接出现裸的字面常量。这样做的好处除了换电芯规格时改一个文件就能完成适配外更重要的是当多个工程师协作开发时不会出现“这个阈值是在哪个文件里面改”的沟通成本。另外一点是这套代码对错误处理的态度比较务实——I2C读通信失败时不会中断系统而是连续失败一定次数后才选择重启初始化或置一个错误标志。对于电池保护应用来说一次通信失败完全可能是外部干扰导致直接宕机反而会让系统失去保护能力。做个简单重试机制比费尽精力保证每次通信都成功要实际得多。如果你想把这套代码用在自己的项目里我建议先从config/battery_config.h入手对照你手头电池组的规格书把阈值参数理清楚再跑一遍初始化流程确认寄存器回读值正确。保护逻辑本身代码量不大读懂状态机和采样部分之后基于产品定制改动也就是一两个文件的事。本文还有配套的精品资源点击获取
