简介:面向BMS开发者的 SH367306+SH79F6441 方案资源包,聚焦锂电池组高精度采集与主控协同设计,适合电动车、储能及电池保护板研发工程师学习参考。压缩包共274个文件、约7.12MB,含C/H源码、obj与lst中间文件、hex固件,以及schdoc、pcbdoc、pcblib等原理图与PCB设计文件,还附带SH36730X APPTool调试工具,覆盖从芯片配置、数据采集到硬件布局的完整开发链路。已有2811人学习下载。通过DEMO代码可快速掌握SH367306多通道电压电流采集初始化、采样率设置与校准流程,了解SH79F6441如何通过I2C/SPI等接口读取数据并完成SOC、SOH估算;结合硬件图纸与STARTUP.A51启动代码,便于在真实项目中移植和排错。
1. SH367306+SH79F6441 BMS 采集方案:为什么这套组合值得做
进 BMS 这一行,你会发现一个现实:刚把代码跑通,硬件就给你脸色看。很多做电池管理系统(BMS)的工程师,最初都试过用 MCU 的 ADC 直接采电芯电压,结果不是共模电压扛不住,就是噪声大到手抖。SH367306+SH79F6441 这套组合的思路很直接:高压侧采集交给专业模拟前端芯片,主控只负责读数据、跑策略、做保护和通信。这个 DEMO 的价值不是给你看一个能点亮的板子,而是帮你在一周内跑通 "读电压、读温度、触发保护、看反馈" 这条完整链路。我按自己做过的方案拆给你看,哪些参数必须调,哪些坑绕不开。
2. SH367306 采集侧:电压、温度、均衡的数据链路怎么打通
2.1 SH367306 的定位与选型理由
先说清楚,SH367306 不是一颗 MCU,它是一颗电池模拟前端(AFE )芯片,放在电芯组那一侧,通过采样引脚直接接每一节电池的正负极。为什么要用专用芯片而不是 MCU 内置 ADC?因为串联电池组的共模电压可能到几十伏,MCU 的 ADC 输入范围通常只有 3.3V 或 5V,即使做电阻分压,温漂和精度也很难看。SH367306 内部是差分输入结构,天生为高压侧采样设计,还带温度采样通道、均衡开关控制、以及硬件保护比较器。对主控而言,高压节点被挡在外面,这是一层安全隔离,也是整个 BMS 硬件设计的底气。
选型的时候,我一般只看三件事:支持的串数范围是否覆盖你的电池组、通信接口是 I2C 还是 SPI 、均衡通道能不能逐通道开关。以我手上的 DEMO 为例,7 串锂电池组,SH367306 的采样通道数和均衡通道刚好对应,SH79F6441 按增强型 8051 内核来写,外设够用。和常见的 STM32 BMS 方案对比:STM32 做策略和通信确实强,但采样前端一样要外挂 AFE,整套物料和调试成本并不低。SH79F6441 的优势是便宜、外设匹配、开发方式传统,很多老工程师拿起来就能写。
2.2 通信基线与最小读取操作
SH367306 和 SH79F6441 之间的通信,常见做法是 I2C 或 SPI。DEMO 里我用的是模拟 I2C,理由很简单:引脚占用少,代码可以直接从一个芯片移植到另一个芯片。你要确认两件事:SH367306 的 7 位从机地址,以及寄存器是 8 位还是 16 位数据宽度。这两个信息决定了 I2C 起始地址和读写顺序,拿不准就去查数据手册里的通信章节。
// 引脚定义:SCL=P1.0, SDA=P1.1,外部上拉4.7k
#define SCL_PIN P10
#define SDA_PIN P11
#define SDA_IN P1IN & 0x02 // 读SDA电平
static void i2c_delay(void) {
// 标准模式100kHz,半周期约5us,按主频调整
_nop_(); _nop_();
}
static void i2c_start(void) {
SDA_PIN = 1; SCL_PIN = 1; i2c_delay();
SDA_PIN = 0; i2c_delay();
SCL_PIN = 0; i2c_delay();
}
static void i2c_stop(void) {
SDA_PIN = 0; SCL_PIN = 1; i2c_delay();
SDA_PIN = 1; i2c_delay();
}
static unsigned char i2c_write_byte(unsigned char dat) {
unsigned char i, ack;
for (i = 0; i < 8; i++) {
SDA_PIN = (dat & 0x80) ? 1 : 0;
dat <<= 1;
i2c_delay();
SCL_PIN = 1; i2c_delay();
SCL_PIN = 0; i2c_delay();
}
SDA_PIN = 1; // 释放总线,等从机ACK
SCL_PIN = 1; i2c_delay();
ack = SDA_IN ? 1 : 0; // 0表示从机应答
SCL_PIN = 0; i2c_delay();
return ack;
}
uint16_t sh367306_read_reg(unsigned char reg) {
unsigned char hi, lo;
i2c_start();
i2c_write_byte(SH367306_ADDR << 1); // 写方向
i2c_write_byte(reg);
i2c_start();
i2c_write_byte((SH367306_ADDR << 1) | 1); // 读方向
hi = i2c_read_byte(1); // 最后一字节回NACK
lo = i2c_read_byte(0);
i2c_stop();
return ((uint16_t)hi << 8) | lo;
}
逻辑说明:主机先发从机地址加写位,把寄存器地址写进去,然后再发一次起始信号,切换到读方向。读回来的两个字节按高字节在前拼成一个 16 位值。这个 "先写地址再重新起始读" 是 I2C 读寄存器的标准套路,很多新手在这里只发一次起始,结果读出的一直是同一个位置的数据。
参数说明:SH367306_ADDR 按数据手册填写;i2c_delay 的延迟决定总线速率,100kHz 比较稳。如果你的主频跑得快,不要一味缩短延迟,超过 400kHz 就需要查 SH367306 是否支持快速模式。SDA 一定要配置成开漏并加上拉,否则通信会偶发出错。
注意:如果 SDA 被配置成推挽输出,从机拉低总线时会和主机打架,现象就是读回来 0xFF 和正常值随机交替,这种问题在示波器上很难看,直接把引脚改成开漏输出就好。
2.3 三个必调参数:过充、过放、均衡阈值
拿到电压数据之后,最先要设的是保护参数。DEMO 里通常把过充保护电压、过放保护电压和均衡开启电压写在一组配置结构体里,由主控在初始化时写入 SH367306 的寄存器。
typedef struct {
uint16_t over_volt_mv; // 过充保护阈值,单位mV
uint16_t under_volt_mv; // 过放保护阈值,单位mV
uint16_t balance_start_mv; // 均衡开启阈值,单位mV
uint8_t cell_count; // 串联电芯数
} bms_prot_param_t;
const bms_prot_param_t demo_param = {
4200, // 4.2V 磷酸铁锂常见值
2800, // 2.8V
3600, // 高于3.6V才允许均衡
7 // 7串电池组
};
写入时需要把毫伏值换算成 SH367306 内部的电压分辨率,也就是 LSB 值。不同型号的 LSB 不一样,常见在 0.382mV/LSB 到 1mV/LSB 之间,换算公式是 reg = (uint16_t)(mv / lsb_mv)。这个换算最容易出错,我一般会在代码里留一个宏,方便按实际手册改。
均衡开启阈值这个参数容易被忽略,它的作用不是保护,而是维持一致性:只有电芯电压高于该阈值时才允许开启均衡,防止低电量时把电放空。DEMO 里如果用电阻均衡,要注意均衡电流不能超过芯片手册给的推荐范围。下面这张表是我常用的一组初始值,量产前还要配合电芯厂家参数再调。
写完参数后要回读校验,防止写寄存器时被噪声打断。回读校验这个小动作,是 DEMO 到量产之间最简单也最值钱的一步,很多现场问题都是参数没写进去导致的。
3. SH79F6441 主控侧:从原始值到保护动作
3.1 采集与管理如何分工:这也是 BMS 和 BMU 的区别
很多人在 DEMO 阶段就把 "采集" 和 "管理" 混在一句话里讲,其实这两件事在工程上能分开,这也是 BMS 和 BMU 的区别所在。BMU 通常指电池管理单元,负责贴电池最近的那部分:采样电压、温度、做底层保护、控制均衡;BMS 是完整系统,还要包含 SOC 估算、充放电策略、上位机通信和故障上报。你用 SH367306 做采集,SH79F6441 做主控,实际上已经天然划出了这两层:SH367306 是 BMU 的采集前端,SH79F6441 跑 BMS 的策略部分。
把这个边界想清楚,代码组织就不会乱。DEMO 里最容易出现的翻车写法,是主控每 10ms 把所有寄存器读一遍再立刻处理,逻辑全堆在一个死循环里,后来加一个故障、加一个通信,就改不动了。常见做法是分层维护:底层只负责 "读寄存器→换算成工程值",中间层维护 "电芯状态表",上层根据状态表决定动作。
typedef struct {
uint16_t cell_mv[7]; // 电压工程值
int16_t cell_temp[3]; // 温度工程值
uint8_t balance_mask; // 均衡开关位图
} cell_status_t;
cell_status_t g_cell = {0};
逻辑说明:这个结构体是中间层的数据核心,底层代码只负责更新它,上层逻辑只读它。位图 balance_mask 每一位对应一个均衡通道,1 表示开启。这样调试的时候直接看全局变量,就能知道当前系统在想什么。
3.2 原始值换算:LSB 缩放与 NTC 查表
SH367306 返回的电压寄存器值是裸的 ADC 编码,不能直接拿去和 4200mV 比较。换算公式很简单,问题在于很多人只在初始化时乘一次系数,后来换了电芯串数或换了采样电阻也不改。
// 电压换算:raw 是寄存器读回的16位值
// lsb_mv 按数据手册填写,例如0.382mV
float cell_voltage_mv(uint16_t raw) {
return (float)raw * lsb_mv;
}
温度通道是另一个坑。SH367306 的典型做法是给 NTC 电阻供电,通过 ADC 读回分压点,温度靠查表得到。直接把 ADC 值除一个系数当温度,会在低温段翻车,因为 NTC 曲线是非线性的。
// NTC查表简化示例,表项为 ADC值->温度,-20度到80度每5度一档
const int16_t ntc_table[][2] = {
{3900, -20}, {3800, -15}, {3650, -10}, {3400, -5}, {3100, 0},
{2800, 5}, {2500, 10}, {2200, 15}, {1900, 20}, {1600, 25},
{1350, 30}, {1150, 35}, {1000, 40}, {880, 45}, {780, 50}
};
int16_t ntc_get_temp(uint16_t adc) {
uint8_t i;
for (i = 0; i < 14; i++) {
if (adc > ntc_table[0]) {
// 线性插值取两点之间
return ntc_table[1] + (adc - ntc_table[0]) *
(ntc_table[i + 1][1] - ntc_table[1]) /
(ntc_table[i + 1][0] - ntc_table[0]);
}
}
return ntc_table[14][1];
}
这段代码的逻辑是先找到 ADC 值落在哪两个温度点之间,再做线性插值。参数说明:表项选择取决于 NTC 型号和分压电阻,不要在 DEMO 里直接抄别人的表,同一颗 NTC 换一个分压电阻,曲线就变了。NTC 分压电路的上拉电阻我一般选 10k,电芯温度范围 -20 到 80 度,这样 ADC 分辨率在常温段表现最好。
采集数据不能拿来就用,我建议加一层简单滤波,避免电压突变导致保护误动作。
// 滑动平均,权重偏向当前值
g_cell.cell_mv = (g_cell.cell_mv * 7 + raw_mv) / 8;
逻辑说明:这一行代码让输出电压只跟最近 8 次采集相关,既保留了突变响应能力,又不会因为一次毛刺误触发保护。参数说明:系数 7 和 8 不是固定的,想要更快响应就把 7 改成 3,想要更平滑就改成 15,建议在具体项目里试几组再定。
3.3 保护状态机 与故障处理
采集数据之后,主控要做的不是 "每毫秒比较一次电压",而是维护一个保护状态机。状态机的好处是能够处理回差、恢复条件和故障保持,不会一有扰动就反复跳变。
typedef enum {
BMS_NORMAL,
BMS_OVERVOLTAGE, // 过压保护
BMS_UNDERVOLTAGE, // 欠压保护
BMS_OVERTEMP, // 过温保护
BMS_FAULT // 硬件故障
} bms_state_t;
bms_state_t bms_state = BMS_NORMAL;
void bms_state_machine(uint16_t *cell_mv, uint8_t cell_count) {
switch (bms_state) {
case BMS_NORMAL:
for (uint8_t i = 0; i < cell_count; i++) {
if (cell_mv > demo_param.over_volt_mv) {
bms_state = BMS_OVERVOLTAGE;
}
}
break;
case BMS_OVERVOLTAGE:
// 必须低于恢复值才退出保护
uint8_t all_clear = 1;
for (uint8_t i = 0; i < cell_count; i++) {
if (cell_mv > demo_param.over_volt_mv - 100) {
all_clear = 0;
}
}
if (all_clear) bms_state = BMS_NORMAL;
break;
// 其它状态类似处理
}
}
这段代码里最关键的是过压状态退出的回差逻辑:保护阈值 4200mV,恢复阈值 4100mV,防止在临界点反复触发。再配合上一章写入 SH367306 的硬件保护,等于做了两层保护:第一层是芯片硬件反应,第二层是主控策略。DEMO 里两层要独立验证,不要同时关掉调试。
状态机的执行周期我一般放在 100ms 循环里跑,电压换算放在 10ms 任务里,两个频率分开。这样可以避免 I2C 总线被频繁占用导致通信出错。很多初学者把所有事都塞进一个循环里,等加上显示和通讯后,时序就乱了。驱动 MOS 的动作放在状态机之后,用两个 IO 分别控制充电 MOS 和放电 MOS:
#define MOS_CHARGE P2_0
#define MOS_DISCHARGE P2_1
void bms_apply_action(void) {
switch (bms_state) {
case BMS_NORMAL:
MOS_CHARGE = 1; MOS_DISCHARGE = 1;
break;
case BMS_OVERVOLTAGE:
MOS_CHARGE = 0; MOS_DISCHARGE = 1;
break;
case BMS_UNDERVOLTAGE:
MOS_CHARGE = 1; MOS_DISCHARGE = 0;
break;
default:
MOS_CHARGE = 0; MOS_DISCHARGE = 0;
break;
}
}
逻辑说明:过压只关充电 MOS,放电还能继续;欠压只关放电 MOS,充电还能继续;硬件故障则两个一起关。这是 BMS 保护逻辑的基本动作语义,DEMO 阶段就要按这个语义来,不要一个状态关断所有 MOS。
4. DEMO 工程从零到跑通:接线、初始化和验证
4.1 BMS 硬件最小系统与接线清单
做 DEMO 不要一上来就搭整板,先搭建最小硬件系统。这套组合的接线可以分三块:电源、采样、通信。我给出常用的接线清单,按这个查不漏线。整个过程涉及到的 BMS 硬件问题,基本都集中在这几个信号上。
注意 VCC_IN 不要直接接电池总正,常见做法是加 RC 滤波和瞬态二极管。GND 一定要共地,这一条放到避坑章节单独说,因为它是很多奇怪故障的头号来源。
主控这边要留出串口,DEMO 阶段你有没有串口直接决定调试验证效率。SH79F6441 的串口我一般拿来做调试通道,波特率 115200,输出一帧格式化后的设备状态。上拉电阻的选择也有讲究,I2C 的 SDA 和 SCL 各放一个 4.7k 上拉到 3.3V,不要省,也不要为了追求快速模式换成 1k,上拉太强会让边沿变差。
4.2 工程初始化顺序与启动序列
DEMO 程序能不能跑起来,很大程度看初始化顺序。很多人的演示程序在上电时读寄存器全是 0xFF,原因不是硬件坏了,而是初始化顺序不对。常见做法是把初始化分成几个固定步骤,按依赖关系排序。
void bms_demo_init(void) {
mcu_clock_init(); // 1. 先配主频和时钟源
gpio_init(); // 2. 配好I2C、串口引脚方向
uart_init(115200); // 3. 调试串口先起来
sh367306_sw_reset(); // 4. 复位AFE,清掉上电乱态
delay_10ms(); // 5. 等模拟前端上电稳定
bms_prot_param_write(); // 6. 写入保护参数
bms_state = BMS_NORMAL; // 7. 状态机复位
bms_apply_action(); // 8. MOS初始化为关闭
}
逻辑说明:先把主控自己的时钟和外设配好,再动 SH367306。AFE 上电需要时间,不同批次可能差几十毫秒,复位后加一个 10ms 延时,不要省。参数写在复位之后,确保内部寄存器处于默认状态再写入,避免残留值影响判断。
参数说明:SH367306 的软件复位时序必须在数据手册里找到精确时序图,有的芯片要求先拉低某个引脚再发命令,有的只需要写复位寄存器。如果你拿到的 DEMO 源码里有一段时间敏感的循环,先跑一遍看是否满足了最小复位时间要求。启动序列最后一步把 MOS 全部关闭,是为了防止初始化过程中出现意外。等串口打印出第一帧状态后再开启通道,这样万用表比对时心里有数。
4.3 验证 DEMO 程序是否跑通:串口、万用表和强制保护
验证不是看一眼灯亮了就算过,我常用三个手段验证 DEMO 程序是否跑通。第一个是串口打印电压和温度,代码逻辑很直接,就是每 500ms 把各通道电压打出来:
void demo_report(void) {
printf("state=%d\r\n", bms_state);
for (uint8_t i = 0; i < g_cell.cell_count; i++) {
printf("cell%d=%dmV\r\n", i, (uint16_t)g_cell.cell_mv);
}
}
逻辑说明:打印格式带状态编号和电压值,方便后续用脚本解析。参数说明:500ms 的间隔是折中选择,太快刷屏,太慢看不出问题。量产版本里可以把打印放到调试固件里,正式固件只保留逻辑分析和事件记录。
第二个手段是万用表比对:用万用表实测每节电芯电压,和串口打印值比对,偏差应在个位数毫伏以内。如果偏差大,先检查分压电容和采样电阻。第三个手段是强制保护:用一个可调电源代替其中一节电芯,把电压缓慢调到保护阈值以上,观察 SH79F6441 是否发出关断信号。这一步能同时验证 AFE 的中断输出和主控的状态机,是 DEMO 和原型的区别中最关键的分水岭——DEMO 只证明能读,原型要证明能保护。
强制保护测试时要盯着 MOS 控制引脚,逻辑分析仪接上更好。没有逻辑分析仪时,可以用 LED 拉一个指示灯,直观看到动作。建议把测试步骤写成表格,电压值、串口读数、MOS 状态各列一列,现场比对方便很多。
5. 联调避坑记录:SH367306+SH79F6441 的五条血泪经验
5.1 读回来的数据全是 0xFF,通信根本没通
现象:上电后 I2C 读任何寄存器都返回 0xFF,串口打印一屏的 F。
原因:SH79F6441 和 SH367306 没有共地。调试的时候我给主控单独供电,AFE 用电池组供电,两个电源的地不通,I2C 数据线没有回流路径。
解决:把两边的 GND 直接短接,再量一下 SDA 上拉波形。这类问题常被归类为玄学,其实就是检查地线连接。DEMO 阶段尽量用同一路电源给两个芯片供电,或者把两个 GND 在排针上就近短接。
5.2 唤醒失败,主控喊不醒 AFE
现象:芯片进入低功耗后,主控发 I2C 命令无响应,按复位键也没用。
原因:SH367306 休眠后不会响应普通命令,必须先满足唤醒条件,常见是拉低唤醒引脚一段时间,或者先执行唤醒序列。直接发命令就像对着睡着的人说话。
解决:查数据手册里的低功耗章节,在初始化前加一个唤醒脉冲。我把这个逻辑放在 bms_demo_init 的第四步,而不是等到通信超时再处理。注意不要在唤醒序列中间插别的 I2C 操作,时序要一气呵成。
5.3 电压读数整体偏高,且随温度漂
现象:所有通道电压比万用表高 20-50mV,温度升高后偏差变大。
原因:ADC 的参考源选成了内部参考,内部参考温漂大;另一个可能是采样 RC 组合不匹配,导致采样时间不足。
解决:检查参考电压配置寄存器,改成外部高精度基准。同时在代码里把 ADC 采样时间参数加大一倍试试,很多 AFE 有采样时间配置位,默认值是 "足够快" 而不是 "足够准"。改完之后,把板子拿去吹风机加热,看电压读数是否稳定。
5.4 均衡电阻烫手,均衡控制关不掉
现象:均衡开启后芯片发热,均衡电阻烫到不能摸。
原因:均衡电流由外部均衡电阻决定,DEMO 板上的均衡电阻选太小,比如 10Ω,压差 100mV 就有 10mA 电流,串数多就发热明显。还有可能是主控没有按状态机关闭均衡通道,只关了总开关。
解决:均衡电阻按数据手册推荐范围选,常见 100Ω 到 330Ω。软件上每个均衡通道都要单独记录开关状态,不要只靠一个全局标志位。加一个超时强制关闭逻辑,均衡持续超过 30 分钟自动停止,避免长期发热。
5.5 偶发死机,看门狗喂狗不到位
现象:跑几个小时出现一次假死机,串口停止输出,复位后恢复。
原因:主控的状态机或 I2C 在某一步陷入死循环等待。SH367306 可能把 SDA 拉死,也有可能是主控 while 等 ACK 超时条件写死,导致一直卡住。
解决:所有 I2C 等待循环都要加超时计数,超时后主动复位 AFE 再重新初始化。这把问题归类为程序健壮性,在 DEMO 阶段就要加,不然到量产测试时一天能翻车七八次。看门狗要喂在主循环最高层,不要在中断里喂,否则死循环照样能喂狗。
6. 进阶:从 DEMO 到量产前,必做的三个验证动作
DEMO 跑通只是起点。按我的习惯,下一步做三件事:一是校准产线流程,二是把策略拿到 HIL 台架上模拟故障,三是固化看门狗和日志。校准这件事,DEMO 阶段可以不做,但走量绕不开。产线需要一个基准电源,主控通过串口进入校准模式后读基准值,然后计算每通道的增益修正系数,写进 EEPROM 或 Flash。SH367306 本身精度不差,但外围电阻和 PCB 走线会带来通道差异,校准表能挽回其中一大半。
HIL 测试是我比较推荐的方向,特别是用 simulink 搭电池模型做闭环仿真。真实电池组很难做的故障注入,在 HIL 上可以轻松制造:单节断开、温度传感器短路、通信线半断开。整套 BMS 策略在这些故障下的行为,直接决定客户现场的返修率。搭建方式不复杂:SH79F6441 跑策略,通过串口或 GPIO 与仿真机通信,电池模型在 simulink 里跑,再用信号线把故障输入到主控采集口。
日志和看门狗是一对搭档。量产程序里我会留一块环形日志区,记录最近几十条保护触发和寄存器快照,客户返修时靠它定位问题。看门狗一定要喂,但喂在最高层循环,而不是某个中断里,这是从 DEMO 转量产最容易漏的细节。最后说一个我的习惯:每次改完参数都固定一个版本号,打印在串口和日志里。没有版本号,排查现场问题靠猜是猜不出结果的。希望这篇 SH367306+SH79F6441 的 DEMO 笔记能帮到你,减少几个可以提前避开的坑。
————————————————
版权声明:本文为CSDN博主「任云舒」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_32349093/article/details/169725775
|
|