充电 IC 外围设计实战:样机”差一点点满”,量产 5% 报”充不满”——我把截止电压、充电电流和 NTC 位置从头复盘了一遍

2026年10月8日 0 By admin

上一期写 USB-C PD 协商,结尾提了一句”充电 IC 外围的坑准备另开一篇”。这一周返修件正好先到了,那就先把这篇写了。

起因是一个带 18650 的手持终端。样机阶段我做了三台,充电都正常,绿灯一亮拔下来基本都是 4.19~4.21V,我当时就放心了。量产 300 台,客户那边反馈回来一堆”充不满”——不是不充,是”灯变绿了,但电量显示差一格”,还有几台干脆报温度故障,插着不充。

我把样机、量产板、客户退回的板子各拿几台摆在桌上,量了半天,最后归到三件事上:截止电压的检测点在哪儿、充电电流设了多少、NTC 贴在什么位置。

这三个都是”设计阶段一句话、量产阶段一堆事”的东西。这篇就把整条链子按能测量的顺序捋一遍。


一、先把”充电”拆成四个可测量的量

我以前调充电的时候容易一上来就怀疑 IC 坏了、寄存器写错了。后来强迫自己按顺序看这四个量,八成的问题能当场分开:

量 怎么测 说明什么
V(BAT) 电池端电压 万用表直接顶在电池焊盘上(不是 IC 的 BAT 脚) 电池真实的终端电压
I(CHG) 实际充电电流 串一个电流表,或读 IC 寄存器 有没有被限流/降流
V(TS)/T 电池温度 读 ADC 或 IC 寄存器 + 一支独立温度计对照 NTC 准不准、位置对不对
V(IN) 输入端电压 顶在 IC 的 VIN 脚上(不是适配器输出端) 有没有触发输入电压跌落限流

第一件事永远是同时记前三个量。因为”充不满”至少有三种截然不同的成因:

  • V(BAT) 到了 4.2V 但系统显示没满 → 软件侧的满充判定或 SOC 重锚有问题(见那篇《电池管理与 SOC 估算》);
  • V(BAT) 只到 4.06V、I(CHG) 在 CV 段正常衰减 → 回路压降/截止电压精度问题;
  • V(BAT) 只到 4.06V、I(CHG) 明显被拉低 → 热调节或输入限流在动作。

三种成因的处置手段完全不同。不先把量测清楚,就是在盲改原理图。


二、拓扑选型:线性还是开关,先算热再谈其他

线性充电器(LDO 型)和开关充电器(Buck 型)的选择,很多项目是”顺手用哪个就用哪个”。但这件事本质上是个热学问题,先算一下就有答案了。

线性充电器的耗散账

耗散就是压差乘电流,没有别的项:

P = (Vin − Vbat) × Ichg

我用一个常见场景算一下:适配器 5V 输入、电池 3.7V、充 1A。

$ python charger_calc.py lin --vin 5 --vbat 3.7 --ichg 1.0 --rthja 50 --tamb 40

  耗散   P     = (Vin-Vbat)×Ichg = 1.300 W
  封装热阻 θJA = 50.0 °C/W
  温升   ΔT    = 65.0 °C
  环境   Tamb  = 40.0 °C  ->  结温 Tj ≈ 105.0 °C

  Tj≤125°C 时:允许耗散 1.700 W -> 最大 Ichg 1.308 A
  Tj≤110°C 时:允许耗散 1.400 W -> 最大 Ichg 1.077 A
  Tj≤ 85°C 时:允许耗散 0.900 W -> 最大 Ichg 0.692 A

1.3W、θJA 50°C/W、40°C 环境,结温就到 105°C 了。而绝大多数线性充电器的热调节环门槛就在 110~125°C(各家不同)。也就是说这个设计从出厂第一天起,就是踩着降流线在跑。

这就解释了一个我遇到过很多次的现象:“充电电流自己掉下来了”不是 bug。它是在自我保护。

反过来说:用 PD 取 9V/12V 再喂线性充电器,是主动制造灾难。

9V 输入、3.7V 电池、1A → 5.3W 耗散,QFN 封装几秒钟就进热调节,实际电流可能掉到 300mA 以下。

高压取电只适合开关充电器——这是我把上一期的 PD 协商和这一期的充电 IC 连起来看之后,最想强调的一条。

什么时候必须上开关充电器

判据 阈值(经验)
充电电流 > 1.5A
输入电压与电池压差 > 2V(或输入固定 9V/12V)
环境温度 > 45°C 且要满流充
体积 板子小、散热铜皮给不出来
效率 要求 > 85%

开关充电器当然也有代价:需要一个电感、SW 节点辐射、环路补偿、更高的 BOM。而且布局要求更严:

$ python charger_calc.py sw --vin 5 --vbat 3.8 --ichg 2.0

  输出功率 Pout = 7.600 W(Vbat 3.80 × 2.000 A)
  按效率 90.0% -> Pin = 8.444 W,发热 0.844 W
  占空比 D ≈ Vbat/Vin = 0.760
  电感纹波 ΔIL = (Vin−Vbat)·D/(fsw·L) = 276.4 mA
  峰值电流 Ipk ≈ Ichg + ΔIL/2 = 2138.2 mA

电感按 Ipk 选饱和电流并留 30% 余量。注意”MOS 内阻压降”那一项:开关充电器内部上下管的 Rds(on) 会随温度上升 40~60%,如果把电流设得贴着 IC 的额定值上限,热天就是”充电变慢”。留 20% 余量。


三、截止电压:真正决定续航的那个参数

这是我这篇最想纠正的一个认知偏差。

选充电 IC 的时候,大家普遍在意的是效率、静态电流、封装、价格,但真正决定设备续航的,是截止电压(VFLOAT)的精度。

精度换算成容量

4.2V 体系,IC 标称精度 ±1%,也就是 ±42mV。换算到容量上大概是这么个量级:

$ python charger_calc.py vfloat --vn 4.20 --tol 1.0

  标称 VFLOAT = 4.200 V,精度 ±1.00% = ±42.0 mV
  区间:4.158 V ~ 4.242 V

  经验对照(4.2V 体系,厂商口径差异大,仅作量级参考):
    4.20 V  ->  容量 额定容量 100%     循环寿命基准
    4.15 V  ->  容量 约 −3~4%       寿命约 ×1.3
    4.10 V  ->  容量 约 −8~10%      寿命约 ×2
    4.05 V  ->  容量 约 −12~15%     寿命约 ×2.5~3
    4.00 V  ->  容量 约 −18~22%     寿命约 ×4

(这张表的第二列和第三列来自电池厂的技术手册,各家的百分比差异不小,只能当量级参考。但”每降 50mV,寿命大致翻一倍”这个趋势是普遍认同的。)

所以 ±1% 的 IC,落在 4.16~4.24V 区间,容量差异是 ±2~4%。而同样是这颗 IC,”效率从 92% 提到 93%”带来的收益远没有这个数大。

结论:在 4.2V 体系里,±0.5% 精度的 IC 贵一点是值的,除非你打算做单板校准(后面会讲)。

但比精度更狠的,是检测点

这是量产那批板子翻车的真正原因。

IC 说”我输出到 4.2V 了”,这句话的前提是IC 在自己的检测脚上量到了 4.2V。而检测脚到电池之间,还隔着走线、连接器、以及电池保护板上的两颗 MOS。

$ python charger_calc.py drop --iwire 1.0 --trace-res 49 --conn 30 --prot 60

  充电电流 Ichg = 1.000 A

  PCB 走线(正+负合计)    49.0 mΩ
  连接器(正+负合计)      30.0 mΩ
  电池保护板 MOS 等        60.0 mΩ
  ---------------------------------
  回路总阻                139.0 mΩ
  压降                    139.0 mV

  若 IC 按自己的检测点判满,电池端实际只到 4.061 V(标称 4.20 V)

139mV。 而 IC 的 ±1% 只有 42mV。也就是说,回路压降比 IC 精度大了三倍还多,而且它是个系统性偏差——所有板子都偏低,校准都救不回来(因为不同板子的走线长度、连接器接触电阻差异会叠加)。

样机为什么没事?因为我做样机的时候电池是用短线直接焊在焊盘上的,回路总阻不到 20mΩ,压降只有 20mV,看起来完美。量产用的是带引线的电池组 + 弹片连接器,一下就变成 139mΩ。

这块板子的解决方案,按性价比排序:

  1. 加宽电池走线并缩短长度——1oz 铜、1mm 宽、100mm 长、2A,往返就是 196mV:
$ python charger_calc.py trace --width 1.0 --len 100 --oz 1 --cur 1

  电阻 = ρ·L/(W·t) = 49.14 mΩ
  往返(正+负)= 98.3 mV

  对照表(1oz 铜、往返、1A 电流)
    宽度       20mm     50mm     100mm
    0.3 mm     65.5 mV  163.8 mV  327.6 mV
    0.5 mm     39.3 mV   98.3 mV  196.6 mV
    1.0 mm     19.7 mV   49.1 mV   98.3 mV
    1.5 mm     13.1 mV   32.8 mV   65.5 mV
    2.0 mm      9.8 mV   24.6 mV   49.1 mV

电池回路不要靠”铺铜宽度够用就行”的直觉,它是整块板子上电流最大的路径之一。

  1. 换低接触电阻的连接器。弹片/板对板连接器接触电阻通常在 10~20mΩ/触点;杜邦头、排针这类随手用的,20~50mΩ 起步,而且用久了会氧化、会松。我在客户那批板子上就发现,同一型号不同批次的弹片,接触电阻从 8mΩ 到 35mΩ 都有。
  1. 做 Kelvin(四线)检测。如果 IC 有独立的 BAT_SENSE / GND_SENSE 脚,把采样点引到电池焊盘上,压降就不在检测回路里了。这是最彻底的解法。
  1. 软件侧补偿(没有独立检测脚时的退路):用实测充电电流乘以已知回路电阻做补偿。但回路电阻本身散差大,只能补平均项,补不了个体差异。

一个实操经验:排查”充不满”的时候,第一件事就是把万用表顶在电池焊盘上量,而不是读 IC 寄存器或者量 IC 的 BAT 脚。这两个数往往差 100mV 以上,而 100mV 在 4.2V 附近就是 10% 的容量。


四、充电电流:设定电阻、公差、以及被误解的”降流”

设定电阻

绝大多数充电 IC 用一个电阻设定充电电流,形式是 I = K / R。K 是各家自己的常数,TP4056 这一类是 1200(A·Ω),开关充电器常见几百到两千量级。

$ python charger_calc.py iset --k 1200 --r 1.2

  关系式  I_BAT = K / R_SET[Ω]
  本算例  K = 1200 A·Ω , R_SET = 1.200 kΩ = 1200 Ω
  -> 充电电流 I = 1000.0 mA
     R_SET =  0.50 kΩ -> I =  2400.0 mA
     R_SET =  0.68 kΩ -> I =  1764.7 mA
     R_SET =  0.82 kΩ -> I =  1463.4 mA
     R_SET =  1.00 kΩ -> I =  1200.0 mA
     R_SET =  1.20 kΩ -> I =  1000.0 mA
     R_SET =  1.50 kΩ -> I =   800.0 mA

K 值必须以手册为准,不要照着别人板子上的电阻抄。同一家的不同型号 K 都不一样,有的用电流镜、有的用电压比较,形式看着像但系数差好几倍。

两个容易漏的点:

  • 电阻用 1% 的。1% 电阻 → 电流误差 ±1%;如果图省事用了 5% 的,电流就有 ±5% 散差,再叠加 IC 内部基准公差。电阻还要看温漂,100ppm/°C 在 60°C 温升下再贡献 ±0.6%。
  • ISET 是模拟小信号脚。走线贴着 SW 节点或者电感,会读到开关噪声,症状是”充电电流比算出来的大一截/小一截,且随负载变化”。开关充电器的 ISET 脚一般建议并 100pF 量级的滤波电容——但别太大,会影响环路和软启动。

热调节与输入限流:两种”降流”要分清

实测电流比设定值低的时候,别急着改电阻,先分清是哪一种:

现象 成因 判据
电流随时间缓慢下降 热调节环(Die 到 110~125°C 自我降流) 手摸 IC 烫、持续充 10 分钟以上才出现、环境温度高时更明显
电流一插上就偏低,且随适配器/线材变化 输入限流或 VINDPM 量 IC 的 VIN 脚电压,比适配器输出端低了一截
电流在某个值上下跳 环路不稳 / ISET 被干扰 示波器看 ISET 脚和 SW 节点

VINDPM(输入电压动态管理)是个经常被忽略的保护:输入电压跌到阈值以下,IC 就主动降流,免得把适配器拉到欠压复位。典型阈值在 4.4~4.7V 之间(可配)。它触发的时候,整机看起来像”充电变慢”,实际是适配器/线材供不上。

这时候要查的不是充电 IC,是上一期讲的那条链路:线材线阻、连接器、适配器实际能力、Type-C 的电流协商档位。

$ python charger_calc.py table

  现象                                原因
  ────────────────────────────────────────────────────────────────────────────────────
  一直充不到 100%,卡在 9x%                 终止电流设太大(C/10 且大电流);VFLOAT 公差;充电回路压降未补偿
  显示已满,拔下走一圈就掉 10%                 终止过早 + SOC 未在满充点重锚
  绿黄灯来回跳                             重充阈值太窄 + 负载把电池电压拉到阈值附近;或 VINDPM 反复触发
  插上就烫,充得还慢                          线性充电器压差大(如 9V 输入);θJA 太大
  充电电流比设定值小一截                       热调节环在自我保护;VINDPM/输入限流触发;ISET 脚被 SW 噪声干扰
  温度报错、完全不充                           NTC 开路/短路;分压电阻装错;JEITA 阈值设反;NTC 贴到了板子上
  低温环境不充电                             JEITA 冷区禁止充电——这是设计行为,别当故障
  换一根细线充电电流掉一半                      线损导致输入电压跌落 -> VINDPM 降流
  同批板子截止电压散差大                         IC 基准公差 + 走线/连接器差异叠加;先量电池端,别量 IC 端
  电池鼓包 / 循环寿命差                         充电环境温度高;高温区未降 VFLOAT;长期满电浮充

五、终止判定:显示 100% 到底是不是 100%

这一条是我在后来的项目里才真正重视起来的。

锂电充电进 CV 段之后,电流是指数衰减的,永远不会真的到零。所以必须定一个”终止电流”阈值。最常见的默认值是 C/10。

问题在于:C/10 是个相对值。小电流充电(0.2C)的时候 C/10 = 0.02C,很苛刻,能充得很满;但大电流充电(0.5C~1C)的时候 C/10 = 0.05C~0.1C,此时电池其实只充到 92~95%。

$ python charger_calc.py term --cap 3000 --ichg 1500

  电池容量 3000 mAh,充电电流 1500 mA
  充电倍率 = 0.50 C

  终止电流阈值           相当于     典型截止时 SOC(经验值)
    10.0% C =    300 mA   C/10(最常见)          ≈ 92%
     7.0% C =    210 mA   约 C/14             ≈ 95%
     5.0% C =    150 mA   C/20(较严)           ≈ 97%
     3.0% C =     90 mA   C/33(很严,耗时明显变长)    ≈ 99%

  大电流充电 + C/10 终止是常见坑:1.5A 充 3000mAh 时 C/10=150mA,
  相对电池已经很粗,会在真实 SOC 92~95% 就判满。
  症状:显示 100%,拔下来走一圈就掉到 9x%。

再叠加第三节讲的回路压降(系统性偏低),就会出现客户反馈的那种效果:“灯绿了、显示 100%,但一拔下来走两分钟就掉到 92%”。

三条实操建议:

  1. 终止电流别只跟着 C/10 走。如果目标就是充满,把终止电流设成绝对值(比如 50mA)或 max(C/20, 50mA)。代价是 CV 段时间变长——从 92% 到 99% 可能要多花 20~30 分钟,这部分时间要算进”整机充满时间”的验收指标里。
  2. 终止判定要加”已进 CV 且电压达到 VFLOAT”的前提。否则恒流段如果碰到输入电压波动导致电流短暂掉到阈值以下,会被误判终止。
  3. 重充阈值(recharge threshold)不要设太窄。通常在 VFLOAT − 100~200mV(4.0~4.1V)。设太窄(比如只差 50mV),负载一拉压就会反复触发重充,表现就是”绿黄灯来回跳”。设太宽,则电池自放电很久都不补电。

顺便一个容易踩的坑:有些 IC 在”充电超时”后会强制终止并报”充满”。如果你把超时设得太短(比如 2 小时),大容量电池就会在真的没充满的情况下被报满。超时值应该按 容量/(充电电流) × 2.5 量级来估,别照默认值抄。


六、NTC:放哪儿比用什么型号重要得多

NTC 温度检测这一块,我的经验是:大部分人纠结的(R25 选 10k 还是 100k、B 值选 3435 还是 3950)都不是主要矛盾;主要矛盾是它贴在哪。

先把换算搞对

标准 NTC 用 B 值公式:

1/T = 1/T0 + (1/B)·ln(R/R25)

T0 = 298.15K(25°C)。分压网络通常是”上拉电阻接 VREF,NTC 到 GND”,电压随温度升高而降低。

$ python charger_calc.py ntc --r25 10 --b 3435 --rtop 10 --vref 3.3 --temp 45

  温度   Rntc(Ω)     V(NTC)     ΔT 引起的 ΔV(每°C)
  ──────────────────────────────────────────────────────────────────
   -20°C      77523    2.9230 V    -18.20 mV/°C
   -10°C      46290    2.7138 V    -24.20 mV/°C
     0°C      28704    2.4474 V    -29.33 mV/°C
     5°C      22897    2.2969 V    -31.15 mV/°C
    10°C      18410    2.1385 V    -32.33 mV/°C
    25°C      10000    1.6500 V    -31.77 mV/°C
    35°C       6881    1.3451 V    -28.63 mV/°C
    45°C       4847    1.0773 V    -24.40 mV/°C
    50°C       4101    0.9598 V    -22.17 mV/°C
    55°C       3488    0.8534 V    -19.97 mV/°C
    60°C       2981    0.7578 V    -17.86 mV/°C
    70°C       2207    0.5967 V    -14.09 mV/°C

10k/10k 配置(R25 = 上拉阻值 = 10kΩ)是行业里最常见的,因为中点(VREF/2)正好落在 25°C,全温区都有不错的灵敏度。

误差账要算清楚(这块 NTC 方案真正的短板):

误差源 贡献
NTC 阻值公差 ±1% 约 ±0.25°C
B 值公差 ±1% 约 ±0.4~0.8°C(离 25°C 越远越大)
分压电阻公差 ±1% 约 ±0.4°C
ADC 基准 + 量化(12 位) 约 ±0.2°C
自热(NTC 长期有电流) 约 ±0.1~0.3°C
合计 约 ±1.5~2°C

±2°C 用来做保护是完全够的;但如果产品界面要显示电池温度,这个精度会被人挑刺(尤其是拿手机温度显示对比)。要做显示的话,至少得用独立的高精度温度传感器(I2C 数字式,±0.5°C),或者接受”宁低勿高”的单向偏差。

关键:NTC 贴哪儿

这是我这批返修件里 5% “报温度故障”的直接原因。

位置 效果 说明
直接贴在电池表面(导热胶/高温胶带 + 少量导热硅脂) 正确 测的是电池本体温度,这才是你要保护的东西
贴在电池附近的 PCB 铜皮上 偏差 5~15°C 测的是板温,与电池温度取决于二者热耦合
贴在电池座/支架上 偏差 10~20°C,滞后大 中间隔了空气和塑料,基本没意义
电路板另一侧、远离电池 完全失效 测的是环境温度,甚至是被其他器件烤热的板温

我那批板子是NTC 贴在电池仓旁边的 PCB 上,离电池本体大约 15mm,中间还隔着结构件的塑料壁。症状就是:室温 25°C 环境下,IC 读到的温度比真实电池温度低 12°C 左右。冷天(比如 5°C)时,IC 读到 −7°C,直接进”冷区禁止充电”;热天边充边跑大电流,电池实际到 50°C 了,IC 还读到 38°C,该降流不降。

改进措施(按有效性排序):

  1. NTC 用柔性引线引到电池正上方,贴紧电池外壳。这是最有效的。注意用导热硅脂填充,不要只靠胶带——否则仍然有 5°C 以上的滞后。
  2. 写进装配作业指导书:NTC 位置、贴合方式、用什么胶。这一条比改进硬件更重要——产线上换个工人就贴错位置,是常见事故。
  3. 走线用小电流的独立回线回 IC 的模拟地,不要借用大电流的电池回路当地。NTC 走线上并一个小 RC(比如 1kΩ + 100nF)滤高频,但电容别太大(否则温度响应迟滞明显)。
  4. 验收测试:产线加上”NTC 读数校验”——用手按住电池 30 秒,或轻轻吹热风,看读数是否跟着动。这条测试只花几秒,但能拦住”NTC 虚焊/开路/贴错位置”这一整类问题。

补充一个我踩过的:NTC 开路和”极低温”在分压电路上会产生类似的电压(开路时电压被拉到上拉电平,接近低温端)。如果不做区分,NTC 掉了会被界面显示成”电池太冷,暂不充电”,用户永远猜不到是硬件故障。稳妥做法是开路/短路都单独报故障码,别混进温度区间判断里。


七、JEITA 温区:冷热两端才是事故高发区

JEITA 是一套按温度分区间调整充电行为的规范(各家实现细节不同,但框架一致,通常还会遵循电池厂自己的推荐值)。

$ python charger_calc.py jeita --t1 0 --t2 10 --t3 45 --t4 60

  区间              区名    行为                                          常见限值
  ──────────────────────────────────────────────────────────────────────────────────
  < 0°C           冷     禁止充电(低温析锂,硬伤不可逆)                            0
  0~10°C          低温    限流充电(常见 0.2C / 20% 额定)                      ≤0.25C
  10~45°C         正常    全额充电,VFLOAT 不降                              全额
  45~60°C         高温    降流 或 降 VFLOAT(常见 −100mV)/ 二者兼有              50% 或 4.10V
  > 60°C          过热    禁止充电(多数还禁放电)                                0

几个实践里容易做错的地方:

  • 0°C 以下禁止充电是硬性的。低温充电会在负极析出金属锂(锂枝晶),这是不可逆的物理损伤,还可能刺穿隔膜。不要为了”北方客户抱怨冬天不能充”就放宽到 −10°C。真要支持低温充电,需要电池自带加热膜 + 硬件互锁。
  • 45°C 之后怎么办,各家推荐差别很大:有的要求只降电流,有的只降 VFLOAT(常见 −100mV),有的两者都做。按电池厂的技术手册来,别按 IC 的默认值来。
  • 降 VFLOAT 需要写寄存器。很多 IC 只能降电流,VFLOAT 的高温档必须由固件在温度越限时改写。这意味着这个逻辑在 MCU 里,要测试、要写文档,不能漏。
  • 冷/热区”禁止充电”时通常仍允许放电。别把整机也停了——用户看到的是”插上充电器就关机”,非常难解释。
  • 如果电池自带保护板(PCM)也有 OTP,两侧阈值别设成贴着的(比如 IC 60°C、PCM 62°C)。否则会互相”打架”:IC 觉得还能充,PCM 已经切断充放电回路,症状是”充一会儿断一会儿”。

八、布局:五条不省钱的

充电回路的布局要求,本质上都是”大电流 + 小信号混在一起”这个矛盾。

  1. 输入电容紧贴 VIN 与 GND 引脚,回流环路面积最小。通常是一颗 10µF 陶瓷(X5R/X7R,注意直流偏压下容量衰减可能到 50%)+ 一颗 100nF。放远了等于没放。
  2. SW 节点铜皮尽量小。它是辐射源,面积越大辐射越强(见《PCB 布局与 EMC》《EMC 传导发射》两篇)。够过电流就行。
  3. ISET / TS / FB 这些模拟脚走线远离 SW 和电感。至少隔开,不要平行长距离走线。ISET 被干扰的直接后果是充电电流不准;TS 被干扰的直接后果是温度误判。
  4. 热焊盘必须打接地过孔阵列。9~16 个 0.2~0.3mm 过孔,贯通到内层地平面。θJA 从单层的 100~150°C/W 降到 40~55°C/W,靠的就是这个。过孔别只打两三个——那是心理安慰。
  5. 电池回路走线按 3~5A 余量算宽度,并且和 NTC 走线分开。NTC 如果和大电流回路共用地的走线,采样点的地电位差会直接叠加到温度读数上。

如果板子上还有 Type-C 口,接口处的 ESD/浪涌防护是另一件事(见《ESD/浪涌防护实战》),充电 IC 本身的 TVS 挡不住 IEC 61000-4-2 的能量。


九、固件侧:读寄存器、判满充、异常上报

硬件改完,软件侧还有几件事必须做,否则问题只是从”充不满”变成”不知道充得怎么样”。

9.1 状态与故障读取

开关充电器基本都有 I2C/SPI 接口,至少能读:充电状态、故障标志、实际充电电流、NTC 换算温度。把这些值定期采下来上报,比出问题时去现场连调试器便宜一万倍。

/* 充电状态快照结构。全部整型,零 malloc、无 %f(本站铁律) */
typedef struct {
    uint8_t  chg_stat;      /* 0=未充电 1=预充 2=恒流 3=恒压 4=已满 5=完成 */
    uint8_t  fault;         /* 位域:bit0 输入过压 bit1 过温 bit2 电池过温
                               bit3 安全定时器超时 bit4 NTC 异常 */
    uint16_t i_chg_ma;      /* 实际充电电流 (mA) */
    uint16_t vbat_mv;       /* IC 检测到的电池电压 (mV) */
    int16_t  t_bat_c10;     /* 电池温度 ×10 (°C),-32768 = 无效 */
    uint16_t t_chg_s;       /* 本次充电已持续秒数(溢出按 65535 截断) */
} chg_snapshot_t;

9.2 满充判定不能只看”绿灯”

IC 的”充满”标志是它自己的判据(通常是终止电流门限)。固件应该做交叉校验:

/* 满充判定:三个条件同时满足才算真满 */
static bool chg_is_fully_charged(const chg_snapshot_t *s)
{
    /* 1) IC 报"已满/完成" */
    if (s->chg_stat != CHG_STAT_DONE && s->chg_stat != CHG_STAT_FULL)
        return false;
    /* 2) IC 检测到的电压确实到了阈值附近(防止回路压降导致的假满) */
    if (s->vbat_mv < CHG_VFULL_MIN_MV)          /* 例如 4150 mV */
        return false;
    /* 3) 充电总时长不为零,排除"插上瞬间就是满电"的读数异常 */
    if (s->t_chg_s == 0)
        return false;
    return true;
}

第 2 条是我加上去的。因为回路压降的存在,IC 可能在自己检测到 4.2V 时就判满,而电池端其实只有 4.06V。加个”电压下限 + 时长非零”的双条件,至少能把明显异常拦下来。

9.3 未满就停的看门狗

最实用的一招:充电状态看门狗。如果进入充电状态后,N 分钟内既没到”已满”,充电电流又没有明显变化(说明卡住了),就上报一条异常。这条逻辑能自动抓到:热调节卡死、输入限流、NTC 误判、电池异常。

/* 每 10s 调一次。连续 12 次(2 分钟)电流变化 < 5mA 且未满 -> 上报 */
#define CHG_STALL_WIN   12
#define CHG_STALL_DELTA 5   /* mA */

static void chg_watchdog(chg_snapshot_t *s)
{
    static uint16_t last_ma = 0;
    static uint8_t  stall_cnt = 0;

    if (s->chg_stat != CHG_STAT_CC && s->chg_stat != CHG_STAT_CV) {
        stall_cnt = 0;
        last_ma = s->i_chg_ma;
        return;
    }
    int16_t d = (int16_t)s->i_chg_ma - (int16_t)last_ma;
    if (d < 0) d = -d;
    if (d < CHG_STALL_DELTA) {
        if (++stall_cnt >= CHG_STALL_WIN) {
            chg_report_anomaly(s);   /* 组帧上报,见 9.4 */
            stall_cnt = 0;
        }
    } else {
        stall_cnt = 0;
    }
    last_ma = s->i_chg_ma;
}

9.4 上报组帧:整数 + 定点,不用 %f

和本站一贯的规矩一致:业务路径不用 %f(newlib-nano 的 %f 会走 malloc,而且在 Release 配置关掉 nanoprintffloat 时会静默输出空字符串,JSON 变成 "ua":, 被服务端整帧丢弃)。

温度、电流这些量直接在整数域上报,单位写进字段名:

/* 用 snprintf 的整数格式,绝不出现 %f */
static int chg_build_json(char *buf, size_t n, const chg_snapshot_t *s)
{
    return snprintf(buf, n,
        "{\"stat\":%u,\"fault\":%u,\"ichg_ma\":%u,\"vbat_mv\":%u,"
        "\"tbat_c10\":%d,\"dur_s\":%u}",
        (unsigned)s->chg_stat, (unsigned)s->fault,
        (unsigned)s->i_chg_ma, (unsigned)s->vbat_mv,
        (int)s->t_bat_c10, (unsigned)s->t_chg_s);
}

字段名里带单位(ichg_ma、vbat_mv、tbat_c10)是我坚持的一个习惯:服务端和前端都不用猜单位,也就不会有人把 mV 当 V 用、把 ×10 当成原值。


十、量产测试与现场分诊

10.1 量产阶段加三项(加起来不到一分钟)

测试项 方法 判据
截止电压分布 满充后量电池焊盘(不是 IC 的 BAT 脚) 4.15~4.23V,且批次内散差 < 30mV
NTC 读数校验 手按住电池 30s,看读数是否上升 变化 ≥ 2°C 且方向正确
充电电流实测 串电流表,或读 IC 寄存器 + 对比设定值 与设定值偏差 < 15%

第二项特别值——它拦住的正是我这次出的那 5%。

10.2 现场分诊的三步

遇到”充电相关”的返修件,我的固定顺序是:

  1. 万用表顶在电池焊盘上量 V(BAT),同时记 IC 寄存器读数。两个数差多少 = 回路压降。
  2. 串电流表量 I(CHG),对比设定值。低了就是被限流/降流。
  3. 独立温度计贴电池外壳,对比 IC 读到的温度。差多少 = NTC 位置问题。

三步走完,问题基本就归到三类之一了,不用猜。


十一、设计自查清单(12 项)

  • ☐ 先算了线性充电器的耗散与结温,确认全程不会长时间压在热调节门槛上
  • ☐ 输入电压与电池压差 > 2V、或充电电流 > 1.5A 时,已改用开关充电器
  • ☐ VFLOAT 精度(±0.5% / ±1%)与”续航损失”做了取舍,不是随手选的
  • ☐ 电池回路做了压降账:走线 + 连接器 + 保护板 MOS,总压降低于 IC 精度的一半
  • ☐ 有独立检测脚的话做了 Kelvin 检测;没有的话在固件里做了压降补偿
  • ☐ 电池走线宽度按 3~5A 余量算过,不是”看着够宽”
  • ☐ ISET 电阻用 1% 精度,且 K 值查的是本型号手册
  • ☐ ISET / TS 走线远离 SW 与电感,开关充电器的 ISET 有合适的小电容滤波
  • ☐ 终止电流没有盲目用 C/10:大电流场景改成绝对门限或 C/20
  • ☐ 终止判定有”已进 CV 且达到 VFLOAT”的前提条件
  • ☐ NTC 用引线贴到电池本体上,有导热硅脂,位置与做法写进了作业指导书
  • ☐ 冷热区(≤0°C / ≥60°C)行为明确,且与电池保护板的 OTP 阈值错开
  • ☐ 有充电看门狗:未满 + 电流停滞 → 自动上报异常
  • ☐ 上报数据全整数、字段名带单位、零 malloc 且无 %f(Release 版验过)

相关阅读


本文配套脚本:_tools/charger_calc.py —— 线性充电器耗散与结温、开关充电器效率与电感纹波、充电电流设定电阻公差、截止电压公差对容量的影响、充电回路压降账、终止电流与”实际充到几成”、NTC 分压与全温区灵敏度表、JEITA 温区行为表、电池走线宽度与压降、现场症状分诊表。纯标准库,全离线,不联网。

下次打算写:Type-C 口在工业现场的 ESD/浪涌防护(上一期和这一期的预告都还欠着),或者是 LIN 总线主从调度那一篇。看这周的返修件先到哪个。