在储能系统里绝缘检测是少数几个「平时没人注意出事就是大事故」的功能。电池堆的正负极母线动辄几百伏一旦对机壳的绝缘性能下降轻则漏电报警重则电弧、短路、起火。所以几乎每一套正经的储能 BMS 都会把绝缘检测做进安全保护逻辑里而且它的优先级往往排在所有故障的最前面——绝缘失效意味着系统已经不适合继续上高压了。这篇文章我想讲清楚一件事一套储能 BMS 是怎么通过不平衡电桥法用两次电压测量同时把「正极对地绝缘电阻」和「负极对地绝缘电阻」两个未知数都解出来的。我会结合一套真实的量产代码来讲一套基于 NXP LTC2949 的储能 BMU 固件绝缘检测的完整实现就落在InsuCheck.c里一共一千五百多行。一、先搞清楚要测的到底是什么一个电池堆接到系统里正极母线和负极母线对机壳chassis也就是「地」之间天然存在两条漏电路径。我们把它们抽象成两个电阻Rp正极对地的绝缘电阻Rn负极对地的绝缘电阻理想情况下这两个电阻都无穷大正负极和机壳之间完全隔离。但现实里因为灰尘、凝露、绝缘材料老化、走线破损它们会变成有限值而且往往不是同步下降的——可能正极这边先漏也可能负极那边先漏。这就引出了绝缘检测的第一个难点你得把 Rp 和 Rn 分开测不能只测「总绝缘」。为什么因为如果一个故障只发生在正极侧你测出「总绝缘偏低」但不知道是正极还是负极漏保护策略就没法做——正极漏和负极漏对系统安全的影响、对后续排查的方向是完全不同的两回事。国标对储能和电动汽车的绝缘电阻都有明确要求而且这个要求不是固定阻值而是按电压折算的通常表述成「每伏特多少欧姆」比如常见的 100 Ω/V 或者 500 Ω/V。为什么按电压折算因为绝缘电阻的安全意义本质上是限制漏电流。漏电流等于电压除以绝缘电阻I V / R。电压越高同样的电阻产生的漏电流就越大、越危险。所以「安全」的判据应该是漏电流不超过某个安全值也就是 R ≥ V / I_safe等价于 R ≥ V × K其中 K 是常数单位 Ω/V。这个「按电压折算」的思路会直接体现在后面要讲的报警阈值判断里。二、平衡电桥法以及它的致命盲区测绝缘电阻最朴素的思路是电桥法。在正极和机壳地之间、负极和机壳地之间各接一个已知阻值的电阻通常是相同阻值所以叫「平衡桥」然后测机壳地对电池的电位。根据分压关系就能反推出绝缘电阻。平衡电桥的优点是电路简单、一次测量就能出结果。但它有一个教科书里反复强调的盲区当正极和负极的绝缘电阻同时、等比例地下降时平衡桥测不出来。理解这个盲区并不难。平衡电桥的两个桥臂电阻对称如果 Rp 和 Rn 也对称地下降比如都从 10MΩ 掉到 1MΩ那么机壳地的电位根本不会变——因为分压比例没变。测量系统看到的是「一切正常」实际上两侧绝缘都已经劣化了。对一个以安全为底线的系统来说这种「故障了但仪表显示正常」的情况比直接报故障更危险。所以真正量产的系统几乎都绕不开不平衡电桥法。三、不平衡电桥法的核心思想两次测量两个方程两个未知数不平衡电桥的「不平衡」指的是投切进去的已知电阻不对称。它的核心思路一句话就能概括用两个不同的已知电阻分别做一次测量得到两个独立的方程就能解出 Rp 和 Rn 两个未知数。这是整个方法论的骨架值得停下来把它想透。一个未知数需要一个方程两个未知数就需要两个方程。而「两个方程」从哪来靠的是改变电路条件——投切不同的已知电阻让电路进入两种不同的状态各测一次电压。下面我把这个过程用电路方程走一遍。这部分是理解后面代码的关键。设电池总电压为 Vbat正极对地电压为 Up负极对地电压为 Un这里 Un 取「地到负极」的电压幅值是个正数那么 Up Un Vbat。电路里有三个电阻正极对地的绝缘电阻 Rp、负极对地的绝缘电阻 Rn以及一个通过继电器投切的已知电阻 R一会儿是 20kΩ一会儿是 2.2MΩ。这个已知电阻 R 接在正极和地之间。根据基尔霍夫电流定律在「地」这个节点上流入的电流等于流出的电流正极经 Rp 流入地的电流Up/Rp加上正极经已知电阻 R 流入地的电流Up/R等于地经 Rn 流入负极的电流Un/Rn。写成式子Up/Rp Up/R Un/Rn把 Up/Un 这个比值单独拎出来记作系数 k它其实就是「正极对地电压」和「负极对地电压」的比值是可以直接测出来的量k Up/Un代入上面的方程整理一下k × (1/Rp 1/R) 1/Rn现在关键来了R 有两个取值。投切 R1 时测一次得到 k1投切 R2 时再测一次得到 k2。两个方程k1 × (1/Rp 1/R1) 1/Rn k2 × (1/Rp 1/R2) 1/Rn两个方程、两个未知数 Rp 和 Rn联立求解。用第二个减第一个消掉 Rn就能先解出 Rp再代回任意一个方程解出 Rn。到这里不平衡电桥法的「道」就讲完了。你会注意到推导过程里没有任何神奇的东西就是一个基尔霍夫电流定律加上「改变电路条件做两次测量」这个朴素的思路。真正体现工程功力的是怎么把这个思路落到具体的芯片和电路上。四、这套系统是怎么落地的LTC2949 两个电阻 一个继电器回到这套 BMU。它的绝缘检测硬件方案是这样搭的采样芯片是 LTC2949。这颗芯片大家更熟悉它的身份是「高压侧电流/能量监控」但它其实带了不少辅助测量通道本系统就复用了它来做绝缘检测的电压采样。两个已知电阻R1 20kΩR2 2.2MΩ2200kΩ。这两个阻值在InsuCheck.h里定义了#defineISO_R2(2200.0)/* 绝缘检测电阻值 R2, (KΩ) */#defineISO_R1(20.0)/* 绝缘检测电阻值 R1, (KΩ) */一个继电器负责在这两个电阻之间切换也就是前面说的「投切」。代码里它对应 GPIO 端口ePOut_ISO_PGND检测开始时闭合通过 LTC2949 内部的 RLY9 通道在 R1 和 R2 之间切换。这里有一个值得注意的细节R1 和 R2 差了整整两个数量级20kΩ 对 2.2MΩ。这不是随手选的。小电阻 R1 投进去会把正极「拉低」得比较明显让分压比变化大、测量更灵敏适合用来测那一侧的绝缘大电阻 R2 投进去对电路几乎没什么扰动接近开路状态适合测另一侧。两个数量级的差距保证了两次测量之间有足够大的「差异」方程才解得稳。五、采样流程一个五步的状态机绝缘检测不是一个瞬间完成的操作它在代码里被拆成了一个清晰的状态机由InsuAdhesMeasurTask()这个 100ms 周期任务驱动。状态定义在InsuCheck.h里typedefenum{eIsoVpgnd_Init0,/* 初始化 */eIsoVpgnd_SeleV11,/* 片选 V1 */eIsoVpgnd_SampV12,/* 采集 V1 */eIsoVpgnd_SeleV23,/* 片选 V2 */eIsoVpgnd_SampV24,/* 采集 V2 */eIsoVpgnd_SampOK5,/* 采集完成 */eIsoVpgnd_DiagOK6,/* 诊断完成 */eIsoVpgnd_Fin}e_IsoVpgndSampStep;流程走下来是先片选 V1把继电器拨到 R1 一侧等采样稳定后采集 V1再片选 V2拨到 R2 一侧采集 V2两次都采完进入计算。这里有三个电压量Vbat电池总电压、V1接 R1 时负极对地的电压、V2接 R2 时负极对地的电压。它们都是通过 LTC2949 采样后由InputInsuSampDataAPI()喂进绝缘检测模块的。这个函数里同时做了两件事一是保存采样值二是当场算出分压比系数。看关键几行caseeInsu_V1:IsoCalcMsg.SampV1...;/* 保存 V1 */IsoCalcMsg.CalcCoeA(SampVbat-SampV1)/SampV1;/* 系数 A 正极对地电压 / 负极对地电压 */break;caseeInsu_V2:IsoCalcMsg.SampV2...;/* 保存 V2 */IsoCalcMsg.CalcCoeB(SampVbat-SampV2)/SampV2;/* 系数 B */break;CalcCoeA和CalcCoeB就是我前面推导里的 k1 和 k2——正极对地电压和负极对地电压的比值。(SampVbat - SampV1)是正极对地电压总电压减去负极对地电压SampV1是负极对地电压二者一除就是分压比。注意一个工程细节这些浮点计算里代码反复在做「乘 10000 再除 10000」的操作比如IsoCalcMsg.CalcCoeA(u32)(IsoCalcMsg.CalcCoeA*10000);/* 只保留四位小数 */IsoCalcMsg.CalcCoeA(float)IsoCalcMsg.CalcCoeA/10000;这是在用整数截断来限制浮点精度避免系数保留过多小数位导致后续计算不稳定。没有浮点运算单元的嵌入式平台上这种「先放大、取整、再缩小」的手法很常见写代码的人显然是吃过精度亏的。六、从系数到绝缘电阻两行公式两次采样完成后状态机走到计算这一步。核心就是InsuAdhesMeasurTask()里 case 2 的这段/* 电池电压大于阈值才进行绝缘判断 */if(IsoCalcMsg.SampVbatISO_MIN_BAT_V){/* 计算正极对地绝缘阻抗 */IsoRp(IsoCalcMsg.CalcCoeB-IsoCalcMsg.CalcCoeA)*ISO_R2*2/(2IsoCalcMsg.CalcCoeA);/* 计算负极对地绝缘阻抗拆成三个中间量避免中间结果溢出 */IsoAuxValueA2*(CalcCoeB-CalcCoeA)*(2*ISO_R2ISO_R1)*ISO_R2;IsoAuxValueB2*CalcCoeA*(3*ISO_R2ISO_R1);IsoAuxValueC2*ISO_R2-2*ISO_R2*CalcCoeA-ISO_R1*CalcCoeA;IsoRnIsoAuxValueA/(IsoAuxValueB-CalcCoeB*IsoAuxValueC);}IsoRp就是正极对地绝缘电阻IsoRn是负极对地绝缘电阻单位都是 kΩ。这两个公式就是我前面第三节那组二元方程在这个特定电路拓扑下的解析解。你看IsoRp里出现了(CoeB - CoeA)和(2 CoeA)IsoRn里出现了(2*ISO_R2 ISO_R1)、(3*ISO_R2 ISO_R1)这样的组合——这些整数系数全都来自本系统具体电阻网络20kΩ 和 2.2MΩ 以及它们的并联串联关系的化简结果。换句话说公式长得这么「怪」是因为它已经被针对这个具体电路解好了代码里直接套用不重新解方程。有个细节特别值得提IsoRn 的计算被拆成了IsoAuxValueA、IsoAuxValueB、IsoAuxValueC三个中间量而不是一行写完。原因在注释里——「防止越界」。这三个中间量动辄是2 × 2200 × 2200这种几十万量级的乘积如果强行在一条表达式里连乘中间结果可能超出某些数据类型的范围。拆开算每一步的数值都可控。这是嵌入式浮点/定点计算里一个非常实际、又很容易被忽略的坑。七、边界条件与异常处理比公式更见功力公式只是理想情况。真实系统里采样值会碰到各种异常代码花了不少篇幅处理这些边界。我把它们归纳成三类每一类背后都有一个真实的物理场景。第一类电压太低不测。代码先判断SampVbat ISO_MIN_BAT_VISO_MIN_BAT_V定义成 36V。低于这个电压绝缘检测没有意义电压太低漏电流本来就测不准而且低压下绝缘问题不构成安全威胁直接把 Rp、Rn 置成无效值3000kΩ3MΩ跳过判断。这里有个值得注意的小细节头文件里ISO_MIN_BAT_V定义的是 36.0但代码注释里写的是「电池电压大于 48V 才进行绝缘判断」。注释和实际值对不上——这大概率是历史版本改过阈值、注释没同步更新。这种「注释与代码不一致」的地方恰恰是阅读存量代码时要留心的点面试里能主动指出来是很加分的。第二类结果出现负值或无意义值。理想情况下 Rp、Rn 都该是正数。但采样误差、噪声、以及公式在极端情况下的数值行为都可能算出负值或者溢出。代码对这些情况做了兜底如果IsoRp 3000超过量程上限 3MΩ就把它钳到 3MΩ——超过这个值说明绝缘已经好到超出测量范围直接按「良好」处理。如果IsoRp 0说明算出了负值。这时再分情况如果 V1 接近 Vbat差值小于 5V说明正极对地几乎短路Rp 判定为 0否则判定为量程外的无效值。类似的逻辑对IsoRn也走一遍如果 V1 小于 1V说明负极对地几乎短路Rn 判定为 0。你看这段边界处理本质上是在用「电压的极端取值」来反推「绝缘电阻的物理状态」。V1 接近 Vbat意味着地电位被抬到了接近正极正极对地自然就是短路的V1 接近 0意味着地电位被拉到了接近负极负极对地短路。这个对应关系是理解整套检测物理意义的关键。第三类超时保护。绝缘检测靠 LTC2949 的连续采样如果芯片通信故障或者采样一直没完成状态机就会卡住。代码在等待采样完成时设了 25 秒超时if(GetSysSecondTimeAPI()(sHisInsuTime25)){SetInsuVpgndSampFlagAPI(eIsoVpgnd_SampOK);/* 超时强制置为采样完成 */}超时后强制推进状态机保证不会永远卡死在某个采样步骤上。这种「任何等待都配超时」的做法是实时系统的基本素养。八、报警分级把绝缘电阻翻译成安全等级算出 Rp、Rn 之后系统还要把它们翻译成「该不该报警、报什么级别的警」。这一步在InsuErrStateCheck()里完成输出四级状态typedefenum{/* 0:正常 1:一级报警(严重) 2:二级报警(轻微) 3:绝缘检测异常(失效) */}...判断逻辑是这样的/* 绝缘值为失效值 */if((0xffffisoRp)||(0xffffisoRn)){sInsuErrStateFlag3;/* 检测失效 */}/* 绝缘值在一级(严重)范围 */elseif((isoRp*1000)(Leak1Lim*Vbat)||(isoRn*1000)(Leak1Lim*Vbat)){sInsuErrStateFlag1;/* 一级报警 */}/* 绝缘值在二级(轻微)范围 */elseif((isoRp*1000)(Leak2Lim*Vbat)||(isoRn*1000)(Leak2Lim*Vbat)){sInsuErrStateFlag2;/* 二级报警 */}else{sInsuErrStateFlag0;/* 正常 */}注意那个判断式isoRp * 1000 Leak1Lim * Vbat。isoRp单位是 kΩ乘 1000 变成 ΩLeak1Lim单位是 Ω/V乘 Vbat 得到「当前电压下允许的最低绝缘电阻」。这正好呼应了第一节讲的「绝缘阈值按电压折算」——电压越高允许的最低绝缘电阻就越高因为要保证漏电流不超过安全值。两级阈值Leak1Lim和Leak2Lim是参数表里的可配置项存在gGBmuGenPara_102里一级对应更低的电阻值更严重的漏电二级对应略高的电阻值轻微劣化。从严重到轻微逐级判断符合故障分级的基本逻辑先看是不是最严重的不是再看是不是次严重的。这套分级最终会接到系统的诊断流水线上转化成具体的保护动作——比如二级报警可能只做提示一级报警就要限制功率甚至断开高压回路。绝缘检测在整个 BMS 里扮演的就是「第一道安全闸门」的角色。九、什么时候测触发条件的设计绝缘检测不是一直跑它有明确的触发时机这部分的逻辑在SysNeedInsuCheckState()里写得很细致。挑几个关键的讲上电早期要测系统上电后的头几秒会做一次检测确认初始状态是安全的。接触器断开时才测这是很关键的一个约束。接触器闭合时电池已经和母线、负载连通此时测到的「绝缘」会受到负载电路的干扰测不准。所以很多工作模式下系统要求接触器全部断开后才做绝缘检测。对应代码里的LeakMonitType参数1/2 表示「只在接触器断开时检测」。电压变化触发如果总电压或者负母线电压发生了明显跳变比如变化超过 10V说明系统状态变了需要重新检测。代码里有专门的分支判断ABS(GetGSampSumVoltAPI(), sSumVolt) 100。周期性复检正常运行一段时间后按参数设定的周期重新检测防止绝缘性能在运行中慢慢劣化。外部请求上位机或调试口可以主动下发检测命令。这套触发逻辑体现了一个原则绝缘检测有成本要断开接触器、要占用采样时间所以只在「需要」的时候做而「需要」的定义覆盖了上电、状态跳变、周期性复检、外部请求这几类场景。这个「何时测、测多频」的权衡是绝缘检测工程化里很容易被忽略、但其实很见功力的一环。十、写到最后把整个链路串起来就是LTC2949 通过继电器在 20kΩ 和 2.2MΩ 两个已知电阻之间切换各采一次负极对地电压算出两个分压比系数套入预先解好的公式同时得到正极和负极的绝缘电阻再按电压折算的阈值分成正常/轻微/严重/失效四级送入诊断系统驱动保护动作。拆开看每一步都不复杂。真正有价值的是看到一套理论方法不平衡电桥是怎么在真实芯片、真实电路、真实代码约束下被一步步落地的——从电阻阻值的选取、采样状态机的设计到浮点精度控制、超时保护、边界异常兜底、按电压折算的阈值、以及触发时机的权衡。这些细节才是区分「背过绝缘检测原理」和「真正做过绝缘检测」的分界线。如果你也在做 BMS 或者准备 BMS 相关的面试我的建议是别只记那两个公式去把这段代码从头到尾读一遍。读代码的时候问自己三个问题为什么两个电阻要差两个数量级为什么报警阈值要乘电压为什么很多判断要等接触器断开这三个「为什么」能答上来绝缘检测这块你就真正通了。