两轮车换电 BMS 的模拟前端中颖 SH367306 AFE 芯片调试实录:四个关键问题与解决方案

SH367306 AFE 调试实录:四个关键问题与解决方案

面试场景:你调试 SH367306/SH367309 期间遇到了哪些最难搞的问题?怎么解决的?

本文基于量产 60V 20 串 BMS 代码,还原四个真实工程问题,每个都附代码级解决路径。


问题一:I2C 寄存器被 EMI 打翻 ------ 为什么不能用普通的 I2C 驱动?

现象

BMS 在做大电流放电测试(30A PWM 斩波)时,AFE 的配置寄存器会随机跳变。最常见的表现是:均衡开关莫名全关、短路保护阈值被改、充放电 MOS 意外关断。用示波器抓 I2C 波形,能看到偶尔的毛刺,但不是每次故障都能复现。

根因

BMS 板上的 MOSFET 在 30A 电流下以 303Hz 的 PWM 频率开关,di/dt 可达 30A/100μs = 300A/ms。这个瞬态在 PCB 走线上感应的噪声通过两个途径影响 AFE:

  1. I2C 数据线(SDA/SCL)耦合:直接导致读写数据错误
  2. AFE 寄存器被静电/EMI 直接打翻:即使不通信,寄存器值也会跳变

第二个途径尤其隐蔽------程序没有主动写寄存器,但寄存器的值自己变了。这说明不是 I2C 通信的问题,而是 AFE 芯片内部的寄存器单元在强 EMI 下发生了 bit-flip

解决方案:三层容错架构

第一层:I2C 读写重试

c 复制代码
// Sh367306_1_Drv.c --- 每次读写失败重试 5 次
static uint8_t AfeIf_Read(uint8_t RdAddr, uint8_t Length, uint8_t *RdBuf) {
    uint8_t times = 0;
    while (times++ < TRY_TIMES) {  // TRY_TIMES = 5
        result = Twi1Drv_Read(SlaveAdd << 1, RdAddr, Length, RdBuf);
        if (result == 1) return result;  // 成功就返回
        // 失败继续重试
    }
    return result;
}

这一层解决的是通信链路上的偶发错误------I2C 波形被干扰导致 ACK 丢失或数据位翻转。

第二层:写入后回读验证

c 复制代码
// Sh367306_1_Drv.c --- 写寄存器后立即回读,确认值确实写入了
static uint8_t AfeIf_RegWrite(uint8_t RdAddr, uint8_t Data) {
    uint8_t RegWriteFlg = 1;
    uint8_t Buffer[2] = {0};

    while (RegWriteFlg) {
        AfeIf_Write(RdAddr, Data);       // 写入
        AfeIf_Read(RdAddr, 2, Buffer);   // 立即回读

        if (Buffer[0] == Data) break;    // 一致 → OK
        if (++RegWriteFlg > 20) return 0; // 20 次都不一致 → 报错
    }
    return 1;
}

这一层解决的是写入不生效的问题------AFE 内部正在做 ADC 转换时收到 I2C 写命令,可能导致写入被忽略。回读验证确保每个寄存器写入都是"实打实"的。

第三层:运行时定时回读校验

c 复制代码
// Sh367306_1_Drv.c:1041-1049 --- 每 4s 检测 SCONF1 是否被电磁干扰打翻
if (s_usCTLDPinChek % 100 == 1) {  // 100 × 40ms = 4s
    AfeIf_Read(SCONF1, 2, &DataBuff);
    if (DataBuff[0] != g_Afe1Sconfig1.Data) {
        AfeIf_RegWrite(SCONF1, g_Afe1Sconfig1.Data); // 被改了,回写修复
    }
}

这一层是最关键的一层。它不依赖任何通信触发------即使程序完全没有操作 AFE,AFE 的寄存器也可能被 EMI 打翻。每 4 秒一次的巡检就是"安保巡逻",发现异常立刻修复。

面试要点

"这个问题让我意识到,在电力电子环境中,不能假设芯片的寄存器是稳定的。普通 I2C 驱动的思维是'我写了,所以值就是对的'------这在实验室是对的,在 30A PWM 的 PCB 上不是。三层容错分别解决三个不同层面的问题:通信链路错误、写入不生效、寄存器被动翻转。"


问题二:电流零点温漂 ------ 为什么静止状态下的电流读数是 -1.5A?

现象

BMS 在常温(25°C)下校准完毕后电流精度很好(±50mA)。但样品放在高低温箱里测试时:

  • -20°C 时 :0A 静止状态,电流读数约 -1.2A
  • 60°C 时 :0A 静止状态,电流读数约 +0.8A

这意味着低温下电池实际在休息,BMS 却判定"有 1.2A 放电电流",触发模式切换和 SOC 累积误差。更糟的是,这个误差在不同板子上还不一样------有的偏 0.5A,有的偏 2A。

根因

SH367306 的 CADC(电流 ADC)链路中有三个温漂源:

  1. 0.5mΩ 采样电阻的 TCR:锰铜电阻的 TCR 约 ±50ppm/°C,在全温范围内电阻值变化约 0.4%
  2. AFE 内部 PGA 的输入失调电压温漂:这是主因。PGA 的输入失调电压约几个 μV,但温漂系数约 50nV/°C。经过 PGA 放大后,-20°C→60°C 的 80°C 温差对应几百 μV 的偏移,折算到 0.5mΩ 采样电阻就是几百 mA 的等效电流
  3. AFE 内部基准电压温漂

关键问题是:零点偏移不是固定的,而是随芯片温度变化的。上电时做的零点自学习只在那个温度点有效。

解决方案:零点自学习 + 温漂补偿表

第一步:上电零点自学习

c 复制代码
// Sh367306_1_Drv.c:880-933 --- 上电时自动学习当前温度下的零点
while (CadcSample_Check == 1) {
    if (FLAG2.CADC == 1) {
        CadcValue = ReadReg(REGCUR);

        // 判断充放电方向,取偏移量
        if (CadcValue >> 12 == 1)  // 放电方向
            Offset = -((0x10000 - CadcValue) & 0x1FFF);
        else
            Offset = CadcValue & 0x1FFF;

        learnCadcOffsetOver++;

        // 取两次不同值的均值(不是所有采样的均值!)
        if (learnCadcOffsetOver == 2)
            learnCadcOffsetBuffer[0] = Offset;

        if (learnCadcOffsetOver > 2 && Offset != learnCadcOffsetBuffer[0]) {
            learnCadcOffsetBuffer[1] = Offset;
            Offset = (learnCadcOffsetBuffer[0] + learnCadcOffsetBuffer[1]) / 2;
            CadcSample_Check = 0;  // 学习完成
        }

        // 限制 ±10 LSB(约 ±240mA)
        if (Offset >= 10) Offset = 10;
        if (Offset <= -10) Offset = -10;
    }
}

第二步:芯片温度补偿表

c 复制代码
// Sh367306_1_Drv.c:679-728 --- 根据芯片内部温度查表补偿温漂
int8_t GetTheTempOffset(int16_t ChipTemp) {
    // 低温 → 负偏移(AFE 读到的电流偏充电方向)
    if      (ChipTemp > -44 && ChipTemp <= -40) return -6;
    else if (ChipTemp <= -30) return -5;
    else if (ChipTemp <= -20) return -4;
    else if (ChipTemp <= -10) return -3;
    else if (ChipTemp <=   0) return -2;
    else if (ChipTemp <=  10) return -1;

    // 常温 → 不补偿
    else if (ChipTemp >  10 && ChipTemp < 35) return 0;

    // 高温 → 正偏移(AFE 读到的电流偏放电方向)
    else if (ChipTemp >=  75) return +4;
    else if (ChipTemp >=  65) return +3;
    else if (ChipTemp >=  55) return +2;
    else if (ChipTemp >=  35) return +1;

    return 0;
}

第三步:最终电流计算

c 复制代码
// AfeIf_Calculate.c --- 零点 = 自学习值 + 温度补偿
CorrectedValue = RawValue - ZeroOffset + GetTheTempOffset(ChipTemp);
Current_mA = CorrectedValue × (50mV / 4096) / 0.5mΩ;

温漂补偿表是怎么来的?

取 10 块板子,放在高低温箱里,电池断开(确保真·零电流),从 -40°C 到 85°C 每个温度点停留 30 分钟,记录 AFE 的电流读数。10 块板子的数据取中位数,做成表。每个 LSB ≈ 24.4mA,所以 -6 LSB 约 -146mA,+4 LSB 约 +98mA。

面试要点

"这个问题让我认识到,电流采样精度的瓶颈不在 ADC 分辨率,而在模拟链路的温漂。0.5mΩ 采样电阻在 30A 时只有 15mV 的信号,而 AFE 的输入失调电压温漂可以达到几百 μV------信号和误差在同一个数量级。解决方案必须从系统层面考虑:上电自学习解决初始偏差,温度补偿表解决全温漂移,两次采样取均值解决瞬态噪声。"


问题三:均衡时电压读数异常 ------ 为什么满充时电芯电压"跳"了 50mV?

现象

在充电末端(单体 3.55V 附近)开启均衡后,上位机显示的电压趋势图上出现规律的锯齿波:电压先跌 30-50mV,然后恢复正常,再过几秒又跌。这导致 FDR 模块产生误报------把正常的电芯判为"单体电压高保护"或"单体无效故障"。

更诡异的是:电压异常的总是那些相邻的均衡通道。比如 Cell3 开启均衡时,Cell4 的电压读数也偏低。

根因

SH367306 内部只有一个 ADC,10 路电芯电压通过模拟开关切换后顺序采样。均衡开启时,均衡电流(约 50mA)流过电芯的采样线和内部走线,在采样线上产生额外的压降:

复制代码
均衡电流路径:Cell_N+ → 均衡 MOSFET → 均衡电阻 → Cell_N-
采样电流路径:Cell_N+ → 采样线 → AFE → 采样线 → Cell_N-

两条路径共享 Cell_N+ 和 Cell_N- 的引线!
均衡电流 × 引线阻抗 = 额外的压降 → 电压读数偏低

相邻通道的影响是因为均衡电流的回路经过了相邻电芯的采样线------这在 20 串菊花链连接中是不可避免的。

解决方案:分时调度 + 交错均衡

策略 1:电压采样和均衡不同时进行

c 复制代码
// Sh367306_1_Drv.c:1070-1099 --- 11 个周期的调度表
void Sh367306_1_Runable(uint8_t Cnt) {
    switch (Cnt) {
        case 2:
            GetVadcData();        // ← 读电压
            BalanceOpen();        // ← 然后立刻开均衡(奇通道)
            break;
        case 6:
            BalanceOpen();        // ← 只开均衡(偶通道),不读电压
            break;
        case 10:
            BalanceClose();       // ← 关均衡,准备读下一轮电压
            break;
        case 11:
            g_Afe1Flag2.B.VADC = 0;  // 清除标志,触发新一轮电压采样
            break;
    }
}

// 每个周期 40ms,11 个周期 = 440ms 一个完整循环
// 在 GetVadcData() 之前,均衡已经被关闭了(上一轮的 case 10)
// 所以读到的是没有均衡干扰的"干净"电压

关键设计:先关均衡,等 2 个周期(80ms)让采样线稳定,再读电压

策略 2:均衡交错开启

c 复制代码
// Sh367306_1_Drv.c:378-418 --- 相邻电芯不同时均衡
if (i % 2 == 0) {
    SCONF4 = Config & 0x55;  // 只开 bit 0,2,4,6,8 = Cell6,8,10 和 Cell1,3,5
    SCONF5 = Config & 0x55;
} else {
    SCONF4 = Config & 0xAA;  // 只开 bit 1,3,5,7,9 = Cell7,9 和 Cell2,4
    SCONF5 = Config & 0xAA;
}

交错不只是为了防止相邻电芯串扰------更重要的是防止局部过热。5 个均衡 MOSFET 同时导通时,芯片内部的热量集中在一侧,会导致那几路的 ADC 读数同时偏移。

完整的 440ms 调度循环:

复制代码
Cnt=0   空闲(均衡全关,等待 VADC 采样完成)
Cnt=1   空闲
Cnt=2   GetVadcData() → 立即 BalanceOpen(奇通道)
Cnt=3   均衡开着(奇通道),不读电压
Cnt=4   均衡开着(奇通道),不读电压
Cnt=5   均衡开着(奇通道),不读电压
Cnt=6   BalanceOpen(偶通道),不读电压
Cnt=7   均衡开着(偶通道),不读电压
Cnt=8   均衡开着(偶通道),不读电压
Cnt=9   均衡开着(偶通道),不读电压
Cnt=10  BalanceClose() → 全关均衡
Cnt=11  清除 VADC 标志 → AFE 开始新一轮电压采样

面试要点

"这个问题让我理解了芯片内部资源冲突 的概念。SH367306 用单颗 ADC 做 10 路电压 + 2 路温度 + 1 路电流 + 1 路芯片温度共 14 路------它不是一个并行的数据采集系统。均衡 MOSFET 开启时,流过采样线的电流会干扰 ADC 的测量结果。解决方案不是换芯片,而是从时序调度上保证均衡和采样永远不同时发生。这种'时间分片'的思路在资源受限的嵌入式系统中非常普遍。"


问题四:Cell1 电压系统性偏低 ------ B- 线上的"隐形电阻"

现象

同一电池包,用高精度万用表(Fluke 289,四线法)直接测量 Cell1 正负极,读数是 3.521V。但 BMS 显示的 Cell1 只有 3.498V,偏差 23mV。

而 Cell2~Cell20 的读数与万用表一致(偏差 < 5mV)。

只有 Cell1 偏低。换了板子也一样。换了电池包也复现。

根因

Cell1 是电池组的最负端电芯 。它的负极直接连到 BMS 板的 B- 端子,中间经过了一段较长的导线(约 15-20cm,从电池包内部到 BMS 板)。

复制代码
电池 Cell1 负极 ──── 长导线(≈20cm, ≈3mΩ) ──── BMS B- 端子 ──── 采样电阻(0.5mΩ) ──── GND
                                                    │
                                               AFE Cell1 采样点

问题出在 B- 线是电流的必经之路 。30A 放电时,B- 线上 3mΩ 的阻抗产生 3mΩ × 30A = 90mV 的压降。这个压降被"算"进了 Cell1------即 AFE 采到的 Cell1 电压 = 真实 Cell1 电压 - B-线压降。

Cell2 不受影响,因为 Cell2 的负极是 Cell1 的正极------这条线不过大电流。

为什么只有 Cell1? 这是开尔文连接的局限。Cell2~Cell20 的采样线只流过 μA 级的 ADC 输入电流,几乎无压降。但 Cell1 的 B- 线是所有 20 串电芯的电流回路------30A 的电流在这条线的阻抗上产生的压降,Cell1 的采样线无法区分"这是电芯的真实电压差"还是"这是导线的压降"。

解决方案:工厂模式在线校准 + 运营模式固定补偿

工厂模式AfeIf_Calculate.c:199-216):

c 复制代码
if (FactoryMode == 1) {
    // 利用其余 19 节电芯的平均值来校准 Cell1
    // 前提:电流 > 6A(确保有足够的信号用于计算)

    uint16_t AvgOfOther19 = (PackVoltage - CellVoltage[0]) / 19;

    if (abs(Current) > 6000) {
        if (abs(CellVoltage[0] - AvgOfOther19) < 150mV) {
            CellVoltage[0] = AvgOfOther19;  // 在线自动校准
        }
    }
}

这个方法的巧妙之处:假设 20 节电芯在工厂下线检测时是均衡的(生产线上会先做一次满充均衡),那么每节电芯的电压应该相等。19 节电芯的平均值 ≈ Cell1 的真实电压。

运营模式AfeIf_Calculate.c:219-226):

c 复制代码
else {
    // 工厂模式校准完毕后,运营模式用固定系数补偿
    // 不同电池包的 B- 线阻抗不同,系数也不同

    #ifdef BMP6020S20
    CellVoltage[0] -= Current_mA × 50 / 100000;  // 0.50mV/A
    #endif

    #ifdef BMP6030S20
    CellVoltage[0] -= Current_mA × 33 / 100000;  // 0.33mV/A
    #endif
}

6020 电池包的 B- 线更细/更长(20Ah 容量,电流较小,线径可以细一些,但阻抗更大),所以补偿系数 0.50mV/A。6030 电池包线更粗,只有 0.33mV/A。

B- 线阻抗系数是怎么确定的?

在工厂模式下,让电池以不同的电流放电(0A、5A、10A、15A、20A),记录每个电流下的 Cell1 读数和其余 19 节的平均值。然后拟合:

复制代码
Cell1_误差(mV) = k × Current(A)

对 6020:k ≈ 0.50mV/A  →  等效 B- 线阻抗 = 0.5mΩ
对 6030:k ≈ 0.33mV/A  →  等效 B- 线阻抗 = 0.33mΩ

面试要点

"这个问题让我认识到,采样精度不只取决于 ADC 的位数,更取决于物理连接的质量 。在 μV 级别的测量中,一根 20cm 的导线就是一个'电阻'。12-bit ADC 有 1.47mV 的分辨率,但 B- 线的 90mV 压降(30A 时)相当于 60 个 LSB------它淹没了 ADC 的精度的 60 倍。解决方案的核心是:找到那个'差'的系统性来源,用数学建模把它补偿掉。工厂模式的在线校准就是这个思路------利用已知条件(19 节电芯的均值)求解未知量(Cell1 的真实值)。"


总结:四个问题的共同线索

问题 表象 根因 解决思路
I2C 寄存器翻转 均衡/保护参数随机跳变 EMI 通过辐射打翻寄存器 三层容错:重试 → 回读验证 → 定时巡检修复
电流零点温漂 -20°C 时静止电流显示 -1.2A PGA 输入失调电压的温度系数 上电自学习 + 芯片温度查表补偿
均衡时电压跳变 充电末端电压锯齿波 ±50mV 均衡电流经采样线产生压降 分时调度 + 相邻通道交错均衡
Cell1 系统性偏低 Cell1 读数偏低 23mV B- 线电流回路阻抗压降 工厂模式在线校准 + 运营模式固定补偿

四个问题的共同点:都不是芯片本身的 bug,而是物理世界的约束在数字域的投影

  • EMI 是物理的
  • 温漂是物理的
  • 均衡电流的采样线压降是物理的
  • B- 线的铜阻抗是物理的

量产代码和开发板 demo 的区别就在于------是否处理好了这些物理约束。


本文基于 STM32F072 + SH367306 × 2 两轮车换电 BMS 量产代码(APP-V02.01.20)分析,所有代码引用均可追溯到源文件对应行号。

相关推荐
kebidaixu3 小时前
两轮车换电 BMS 的模拟前端中颖 SH367306 AFE 芯片详解
stm32
wuyk55517 小时前
24. C 语言模块化:不是拆几个.c 文件那么简单
c语言·开发语言·stm32·单片机
hyhsandy18031 天前
STM32F103 TIM学习笔记
笔记·stm32·学习
GuHenryCheng1 天前
【ESP32】ESP-IDF开发环境搭建(cursor)
git·stm32·单片机·学习
小心亦新1 天前
STM32学习10---串口
stm32·嵌入式硬件·学习
wuyk5551 天前
25. 函数指针表:用查表替代 switch-case,打造高效可维护的嵌入式状态机
c语言·stm32·单片机·mcu·51单片机
灯厂码农1 天前
STM32三大通信协议详解——UART、I2C、SPI
stm32·单片机·嵌入式硬件
逐步前行1 天前
HAL_IIC (EEPROM)
stm32·单片机