在储能系统里,绝缘检测是少数几个「平时没人注意,出事就是大事故」的功能。电池堆的正负极母线动辄几百伏,一旦对机壳的绝缘性能下降,轻则漏电报警,重则电弧、短路、起火。所以几乎每一套正经的储能 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_Init=0,/* 初始化 */eIsoVpgnd_SeleV1=1,/* 片选 V1 */eIsoVpgnd_SampV1=2,/* 采集 V1 */eIsoVpgnd_SeleV2=3,/* 片选 V2 */eIsoVpgnd_SampV2=4,/* 采集 V2 */eIsoVpgnd_SampOK=5,/* 采集完成 */eIsoVpgnd_DiagOK=6,/* 诊断完成 */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.SampVbat>=ISO_MIN_BAT_V){/* 计算正极对地绝缘阻抗 */IsoRp=(IsoCalcMsg.CalcCoeB-IsoCalcMsg.CalcCoeA)*ISO_R2*2/(2+IsoCalcMsg.CalcCoeA);/* 计算负极对地绝缘阻抗(拆成三个中间量,避免中间结果溢出) */IsoAuxValueA=2*(CalcCoeB-CalcCoeA)*(2*ISO_R2+ISO_R1)*ISO_R2;IsoAuxValueB=2*CalcCoeA*(3*ISO_R2+ISO_R1);IsoAuxValueC=2*ISO_R2-2*ISO_R2*CalcCoeA-ISO_R1*CalcCoeA;IsoRn=IsoAuxValueA/(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_V,ISO_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()>=(sHisInsuTime+25)){SetInsuVpgndSampFlagAPI(eIsoVpgnd_SampOK);/* 超时强制置为采样完成 */}超时后强制推进状态机,保证不会永远卡死在某个采样步骤上。这种「任何等待都配超时」的做法,是实时系统的基本素养。
八、报警分级:把绝缘电阻翻译成安全等级
算出 Rp、Rn 之后,系统还要把它们翻译成「该不该报警、报什么级别的警」。这一步在InsuErrStateCheck()里完成,输出四级状态:
typedefenum{/* 0:正常 1:一级报警(严重) 2:二级报警(轻微) 3:绝缘检测异常(失效) */}...判断逻辑是这样的:
/* 绝缘值为失效值 */if((0xffff==isoRp)||(0xffff==isoRn)){sInsuErrStateFlag=3;/* 检测失效 */}/* 绝缘值在一级(严重)范围 */elseif((isoRp*1000)<(Leak1Lim*Vbat)||(isoRn*1000)<(Leak1Lim*Vbat)){sInsuErrStateFlag=1;/* 一级报警 */}/* 绝缘值在二级(轻微)范围 */elseif((isoRp*1000)<(Leak2Lim*Vbat)||(isoRn*1000)<(Leak2Lim*Vbat)){sInsuErrStateFlag=2;/* 二级报警 */}else{sInsuErrStateFlag=0;/* 正常 */}注意那个判断式: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 相关的面试,我的建议是别只记那两个公式,去把这段代码从头到尾读一遍。读代码的时候问自己三个问题:为什么两个电阻要差两个数量级?为什么报警阈值要乘电压?为什么很多判断要等接触器断开?这三个「为什么」能答上来,绝缘检测这块你就真正通了。