← 返回文章归档

电量管理可迁移技术详解:架构、库仑账本、状态机与工程难点

以 N32G455 与 DS2740 裸机工程为例,深入剖析库仑账本、事务级样本提交、安全窗口通信、显示预测、满充校准与跨复位恢复的系统设计与实战经验。

典型应用场景:高可靠便携仪器 / Cortex-M 裸机系统 / 脉冲动态负载 / 外部单总线库仑计 核心议题:当电池系统面临动态脉冲负载、端电压畸变、软硬件复位及单总线通信冲突时,如何构建一套可解释、能自愈且边界清晰的电量管理模块?

在嵌入式项目初期,电量估算往往容易被简化为一个“读电压查表”或“读芯片寄存器算百分比”的单一函数。然而在真实的便携设备中,工况远比静态模型复杂:高频刺激脉冲或加热负载会导致端电压剧烈抖动;单总线微秒级时序与高优先级 PWM 存在实时冲突;设备深度休眠唤醒后面临上下文丢失;充电插拔瞬间信号存在机械反弹。

如果在设计之初没有理顺边界,这些现实因素会迅速演变成一系列难以捉摸的产品缺陷:开机跳电、带载误报关机、充不满电、通信稍有扰动界面就卡死或归零。

真正健壮的电量系统,其核心不在于堆砌多复杂的拟合算法,而在于明确测量、账本、预测、显示与安全保护各自的职责边界:

  • 物理测量只采集带有效性标记的硬件样本,不掺杂任何业务猜测;
  • 核心账本作为电荷流动的单一事实来源(Single Source of Truth),严格按事务提交;
  • 显示预测在总线受限或不可观测的窗口期维持平滑体验,绝不反向污染物理账本;
  • 安全保护直连原始硬件判据,绝不等待软件滤波或显示排队;
  • 异常自愈在通信中断或意外复位时最大限度保留有效事实,并在新证据建立后平滑对齐。

本文结合在 N32G455 裸机平台上的落地经验,全面复盘从底层 1-Wire 驱动、库仑积分、状态机解耦到跨复位持久化的完整架构体系。

阅读导航


1. 哪些技术真正值得迁移

1.1 从“算法函数”演进为“带证据的状态系统”

许多工程师初期习惯写一个简单的电压百分比计算函数。但在实际产品落地时,系统必须严谨回答以下工程问题:

  1. 当前电压的物理意义:该读数是在设备完全空闲、大功率脉冲放电,还是在充电恒压阶段采集的?
  2. 电量基线的可溯源性:当前的百分比究竟基于完整的满充校准,还是基于开机时的粗略查表?
  3. 故障恢复能力:单总线偶尔出现一次 CRC 校验失败,此前的容量账本是否应立即作废?
  4. 上下文连续性:MCU 发生看门狗复位或从 STOP2 低功耗唤醒时,库仑计是否保持供电?期间的电荷变化能否精准补算?
  5. 用户体验与真实计量:屏幕上的电量数字是否必须实时等于内部化学估计?怎样避免数字忽高忽低?
  6. 保护机制的权威性:低电关机决策应当依据瞬时电压、平滑后的估算 SOC,还是独立硬件比较器?

显然,这些问题无法仅靠优化一条拟合曲线来解决。系统必须把数据源、时标、设备身份、置信度以及状态转移权限完整纳入架构模型。

1.2 可迁移核心资产清单

核心设计 解决的工程痛点 可直接迁移的架构思想 需要结合具体平台适配的内容
单一账本所有者 多任务、多页面并发读写覆盖电量 设立唯一的状态写者,外部仅能获取不可变只读快照 具体全局上下文定义与模块组织结构
ACR 模差与速率校验 计数器溢出回绕、偶发通信故障补算 16 位有符号模差算法、物理电流速率上界防跳变 寄存器位宽、量纲缩放、硬件采样周期
多维置信度体系 把粗糙估计误当实测,或未知状态误判 0% 长期基线来源与瞬态通信状态彻底解耦 具体业务枚举定义与告警策略
总线安全窗口机制 软件模拟单总线临界区干扰高优先级输出 “业务意图允许”与“底层硬件静默”双重闭锁调度 具体的定时器比较通道与 GPIO 宏
展示预测与真实账本隔离 强干扰时段总线挂起,UI 失去电量反馈 独立预测模型推演显示目标,通信恢复后硬件补算 负载工况电流特征表与多通道叠加算法
跨复位持久化恢复 重启后电量骤跳,丢失历史累计基线 版本控制、芯片身份核验、CRC 校验与最小提交协议 目标 MCU 的备份域(BKP)或持久存储驱动
满充事件权威锚定 长期积分误差累积,恒压期电压判断失真 专用充电完成信号去抖判定,一次性幂等刷新基准 充电 IC 状态引脚电气特性与逻辑极性
展示平滑变换层 电量频繁跳变、数字反弹引发用户困惑 内部客观核算,展示层执行单向受控步进滤波 步进间隔(如 1%/5s)与单向约束规则
最终执行点硬件互锁 充电时主循环软件关热,却被 ISR 定时器踩踏 在最靠近硬件寄存器的执行汇合处加装安全栅栏 PWM 占空比刷新入口与互锁引脚电平
基于不变量的主机回归 缺少硬件时无法高效验证复杂分支边界 脱离目标板的纯 C 逻辑验证、硬件桩与时序回放 目标环境独有的编译告警与电气特性

1.3 适用场景与应用边界

本文沉淀的架构思想特别适用于:

  • 具备段码屏、OLED 或 LCD 的各类便携式仪器与手持医疗/健康设备;
  • 包含脉冲负载(电刺激、微型电机、高频通信模组、超声探头等)的设备;
  • 采用裸机架构、软件模拟微秒总线且无法持续轮询传感器的低功耗系统;
  • 具备深度休眠唤醒流程,要求电量连续不跳变的嵌入式系统。

需要注意,本文方案针对单节锂电池体系。对于多串多并电池包,还需增加单体过欠压、均衡状态与整包有效容量管理;换用镍氢或磷酸铁锂等平坦放电曲线电池时,电压初值表及容量估算模型也必须重新标定。


2. 物理量语义与能力边界

2.1 区分电压、电流、剩余电荷与可用能量

在电化学系统中,四个关键物理量各自承担不同的数学与工程意义:

  • 端电压(Voltage):反映电池端口在当前瞬间的宏观电势,受负载、内阻及极化现象强烈调制;
  • 电流(Current):电荷在某一时刻进出电池的瞬时速率,定义充电为正、放电为负;
  • 剩余电荷(Remaining Charge):电流在时域上的积分累计结果,是电荷守恒意义上的绝对电量;
  • 可用能量(Usable Energy):放电全过程中瞬时功率 V(t) × I(t) 的积分,受终止电压门限及负载大小严重影响。

本架构的核心职责是维护高精度的电荷账本:

Q_remaining(t) = Q_baseline + ∫ I_battery(t) dt
SOC_charge     = Q_remaining / Q_capacity

若产品需要精准预估剩余可用能量(Wh)或续航剩余时间(Time-to-Empty),必须建立未来放电功率假定和端电压轨迹模型,这超出了纯库仑账本的能力范围。

2.2 为什么脉冲负载下端电压会严重失真

在直流负载模型中,端电压可作如下简化分解:

V_cell(t) ≈ OCV(SOC, T) - I(t) × R_internal - V_polarization(t)

其中电化学极化电压 $V_{polarization}$ 具有显著的时间滞后特性。当数百毫安的大电流脉冲接通时,端电压会因内阻瞬间产生明显压降,并在脉冲持续期间随极化加剧进一步下滑;脉冲切断后,端电压又需要数秒乃至数分钟才能缓慢回弹。

在实际仪器运行测试中可以清晰观察到:若在脉冲输出期间直接通过 ADC 读取端电压并查表计算 SOC,电量读数会瞬间深跌;而停机后通过库仑计核算,电池实际消耗的电荷仅占极小比例。因此,在动态带载工况下,绝不能让瞬时端电压主导电量展示。

2.3 库仑积分对绝对基线与校准的刚性需求

库仑计的核心优势在于高频精确测量“电荷进出的相对变化量 $\Delta Q$”,但它本身并不天然知晓接入电池的绝对初始电量。

如果初始粗估存在 300 mAh 的偏差,在没有权威事件重新校准前,整个充放电过程都将持续背负这 300 mAh 的系统误差。此外,毫欧级采样电阻的温漂、ADC 零点漂移以及 MCU 未计入的微小静态旁路功耗,都会随时间推移转化为微小的积分漂移。

因此,一套完整的库仑计方案必须明确回答以下逻辑链条:

  • 开机第一笔电量基线从何而来?
  • 什么级别的事件具备重新校准绝对原点的权威性?
  • 校准后如何保证坐标系在软硬件复位中持续平滑有效?
  • 一旦发生何种物理异常,必须果断宣布基线失效?

2.4 容量假设与 SOH 学习的边界

在许多基础工程中,额定容量往往硬编码为固定值(例如 4000 mAh)。此时 SOC = 100% 仅代表当前电荷账本达到了标称设定值,并不意味着电池实际仍能完整放出 4000 mAh。

电芯健康度(SOH)涉及循环老化、低温衰减以及放电截止条件。如果在缺乏高精度完整充放电闭环的前提下贸然编写自适应学习逻辑,往往会因瞬态异常数据把标称容量越学越偏,最终固化为永久性系统误差。因此在工程实现中,首要任务是确保物理计量的确定性与连续性,切忌在基础未扎实时盲目引入容量学习。


3. 总体架构:五条链路与一个状态所有者

3.1 跨层次数据流与控制流

系统在物理硬件、核心调度与展示保护之间建立了清晰的分级链路:

[电池电流] ──> [采样电阻 / DS2740] ──> [1-Wire 驱动] ──> [候选样本校验]
                                                               ↓
[BATT/REF ADC] ──> [有界批次采样] ──> [初始基线准入] ──> [真实容量账本] <──> [BKP 寄存器]
                                                               ↑
[充电 IC 硬件引脚] ──> [500ms 硬件去抖 / FULL 锚定判定] ────────┘

[真实容量账本] ────────────┐
[多通道负载 / 加热模型] ──┴─> [显示目标抉择] ──> [UI 限速/单向滤波] ──> [快照发布 / 屏幕刷新]

[BATT/REF ADC] ────────────> [独立硬件欠压保护](直接控制系统生死)
[充电 IC 硬件引脚] ──────────> [底层 PWM 执行点互锁](周期性硬切断加热输出)

注意:欠压关机与电量展示虽然在物理上共用同一组 ADC 转换通道,但逻辑判据完全独立,低电保护绝不受电量算法中间状态的干扰。

3.2 模块职责分工

  • onewire.c:底层物理层。负责 GPIO 开漏配置、微秒级时基延时、Reset/Presence 握手、字节级位操作与时钟活性检测。
  • ds2740.c:芯片驱动层。负责 ROM 码读取校验、CRC8 校验、Status 寄存器配置及双读一致性校验。
  • battery_manager.c:业务核心层。负责维护主电荷账本、多维可信度流转、安全窗口仲裁、显示平滑与跨复位持久化。
  • main.c(展示模块):负责将对外发布的结构体快照映射为 LCD 段码或图符。
  • main.c(保护模块):负责开机瞬态电压核验与运行期 9 批中位值投票欠压关机。
  • n32g45x_it.c / ChargPrg():在最底层定时器中断及充电状态机中,硬性关断加热等功率输出。

3.3 单一所有权与对外接口规范

系统内部定义唯一的私有上下文静态变量 s_bm,真实剩余量 remaining_counts 由其统一管辖。外部模块只能通过规范的对外 API 触发操作或读取不可变快照:

/* 初始化与周期轮询 */
void BatteryManager_Init(void);
void BatteryManager_Process(void);

/* 事件驱动请求与低功耗准备 */
void BatteryManager_RequestSample(void);
void BatteryManager_PrepareForStop(void);

/* 读取系统只读快照 */
void BatteryManager_GetSnapshot(BatterySnapshot *snapshot);

其中 RequestSample() 仅发出调度标记,不在当前调用栈执行阻塞通信;PrepareForStop() 在进入低功耗前完成最后一笔合规数据的保存和状态冻结。

3.4 公共快照字段语义

对外发布的 BatterySnapshot 结构体提供完整、明确的数据视图:

typedef struct
{
    uint32_t remaining_counts;       /* 核心账本绝对剩余量 (counts) */
    uint16_t estimated_soc_percent;  /* 内部计算目标 SOC (0~100) */
    uint16_t soc_percent;            /* 经过 UI 限速与单向滤波后的显示 SOC */
    int32_t  current_ua;             /* 最近一次硬件测得的物理电流 (μA) */
    uint16_t voltage_mv;             /* 最近有效滤波端电压 (mV),0 代表无效 */
    BatteryConfidence confidence;    /* 当前对外部展示数据的置信度 */
    BatteryState state;              /* 核心状态机枚举 */
    ChargeState charge_state;        /* 经去抖后的业务充电状态 */
    uint16_t fault_flags;            /* 硬件故障分类位图 */
} BatterySnapshot;

4. 状态模型:解耦正交维度

4.1 五大正交维度的分离

许多电量模块的 Bug 源于试图用一个统一的 State 枚举表达所有现象。事实上,系统在任意时刻均由五个互不冲突的正交维度决定:

  1. 绝对基线是否建立:内部是否拥有经过有效事件确认的电荷原点;
  2. 基线的权威程度:该原点是通过粗估电压查表建立,还是通过充满电硬件锚定建立;
  3. 物理总线健康度:芯片在线、通信超时、CRC 损坏还是时序异常;
  4. 外部充电语义:空闲未充、充电恒流中、充满断开还是状态引脚悬空;
  5. 显示层展示模式:直随物理账本、跟随模型预测还是故障平滑冻结。

将这五个维度拆开后,诸如“通信偶发超时但历史满充基线仍然完全可信”的场景便能得到清晰的边界解耦,避免一报错就引发全局变量清零。

4.2 账本状态机流转

                  [上电复位 / BKP 校验通过]
                             │
                             ▼
[未初始化 UNINITIALIZED] ──> [正常跟踪 TRACKING] <──┐
           │                         │              │
           │ (充满信号有效且经确认)   │ (重新校准)   │ (通信恢复且连续)
           ▼                         ▼              │
    [满充锚定 FULL_ANCHORED] ───────────────────────┤
                                     │              │
                           (连续 3 次通信严重故障)   │
                                     ▼              │
                             [计量故障 GAUGE_FAULT] ─┘
  • UNINITIALIZED:系统开机且无有效持久化记录,等待电压稳定或充满事件;
  • TRACKING:已建立基准,依据每笔通过校验的物理增量驱动账本;
  • FULL_ANCHORED:满充事件已完成权威基线校准,正处于最高精度坐标系;
  • GAUGE_FAULT:总线连续失败达到门限。此时保留历史基准,可信度降级,等待总线自愈。

4.3 必须严格守护的八大系统不变量

在进行任何代码修改或平台移植时,必须保证以下八条核心不变量(Invariants)绝不破损:

  1. 样本准入铁律:未通过物理速率校验与双读一致性的 ACR 数据,严禁推进核心账本和参考时间戳;
  2. 单向隔离原则:显示平滑滤波与负载预测消耗,绝不能反写进真实硬件电荷账本;
  3. 权威优先原则:一旦建立了有效连续账本,动态带载电压与充电端电压严禁强行覆盖账本;
  4. 故障语义分级:单总线偶发物理通信错误,绝不等同于计量芯片掉电;
  5. 未知不等于零:未建立初始基准时对外报 NONE,绝不能向 UI 伪造 0% 导致误关机;
  6. 校准条件严谨:满充校准依赖持续稳定的物理引脚电平与合法硬件 ACR,绝不依赖“UI 是否已显示 100%”;
  7. 安全关断最高优先级:低电关机与加热切断必须直连底层硬件判据,绝不等待显示层或主循环状态机慢慢排队;
  8. 跨复位自愈前提:只有在芯片身份(ROM)与复位标志(RNAOP)双重确认连续时,才允许基于历史 ACR 补算。

5. 库仑账本:单位推导、定点计算与模差处理

5.1 原始数据拒绝过早截断

DS2740 采样寄存器返回 16 位补码数据。在将物理寄存器换算为对外展示的百分比前,必须在内部保持高精度定点计数(counts)。

以 4000 mAh 电池为例,1% 的电量相当于 40 mAh。如果驱动层过早把每次采样转换为整数百分比,每次只有零点几毫安时的消耗都会在整除中被抹去,累积数小时后将造成严重的漏计。

5.2 换算基准与参数推导

在案例工程中,硬件采用 $R_{sns} = 20\text{ m}\Omega$ 采样电阻,芯片配置为 DS2740U。

根据官方数据手册,各物理量单位换算如下:

ACR 分辨率 = 6.25 / R_sns(mΩ) = 6.25 / 20 = 0.3125 mAh / count
配置额定容量 = 4000 mAh
总容量对应 counts = 4000 / 0.3125 = 12800 counts

DS2740U 瞬时电流分辨率 = 1562.5 / R_sns(mΩ) = 1562.5 / 20 = 78.125 μA / LSB

在 C99 源码中,通过定点整数避免浮点开销:

#define BM_CAPACITY_COUNTS ((BM_CAPACITY_MAH * BM_RSNS_MOHM * 4) / 25)

/* 瞬时电流定点换算 (μA) */
int32_t current_ua = ((int32_t)raw_current * 15625) / (10 * BM_RSNS_MOHM);

5.3 16 位有符号模差算法解析 ACR 回绕

库仑计的累积电荷寄存器(ACR)会在 16 位边界(0x0000 与 0xFFFF 之间)发生自然的自然溢出回绕。如果直接用 32 位无符号减法,溢出时会算出数万 counts 的虚假跳变。

系统采用严格的 16 位有符号模差算法:

int16_t delta = (int16_t)(uint16_t)(new_acr - old_acr);
remaining = clamp(remaining + POLARITY * delta, 0, CAPACITY_COUNTS);

通过两个典型工况验证补码回绕的正确性:

  • 放电越界回绕:上次采样 old = 0x0004,本次读到 new = 0xFFFE。 new - old = 0xFFFE - 0x0004 = 0xFFFA(16位无符号)。 强制转为 int16_t 后精确得到 $-6\text{ counts}$,准确反映放出 6 counts 电荷。
  • 充电越界回绕:上次采样 old = 0xFFFC,本次读到 new = 0x0003。 new - old = 0x0003 - 0xFFFC = 0x0007。 强制转为 int16_t 精确得到 $+7\text{ counts}$,准确反映充入 7 counts 电荷。

该算法成立的前提是:两次采样之间真实的净电荷变动必须落在有符号半量程($\pm 32767\text{ counts}$)之内。在 20 mΩ 下,32767 counts 对应超过 10240 mAh,远大于整包电池容量,因此在常规轮询周期下具备坚不可摧的数学可靠性。


6. 事务级样本接收机制

6.1 候选缓冲与两阶段提交

总线通信不可避免地会受到电磁干扰或接触抖动。驱动设计严守“两阶段提交”模式:

[读取硬件寄存器] ──> [局部 candidate 临时结构体]
                              │
                    [严格完整性与速率校验]
                              │
                    ┌─────────┴─────────┐
                 [合格]               [异常]
                    │                   │
                    ▼                   ▼
           [正式吸收入账]          [果断丢弃 candidate]
           [推进 last_good_tick]   [保留历史账本与参考点]

如果粗暴地直接将原始数据读入全局上下文再去判断合法性,一旦校验失败,系统将被迫处于半更新的污染状态。

6.2 速率校验的时间窗口锚定

物理世界中电池电流存在物理上限。系统通过校验单次增量是否超越物理极限,有效拦截异常干扰帧:

允许最大增量 limit = I_max × (当前时间 - 上次有效样本时间) + 余量

这里的关键工程细节是:计算时间差必须使用 last_good_tick(上次成功被系统接受的样本时间),绝不能使用 last_attempt_tick(上次尝试通信的时间)。

如果使用尝试时间,当总线因外部干扰连续失败两次并在第 15 秒成功恢复时,时间差会被错误算成 5 秒,导致累计了 15 秒物理电荷的合规数据被当成“超速大跳变”错误拒绝,最终陷入“通信越重试、恢复越失败”的死锁。


7. ADC 采样:成对校验与有效性优先

7.1 成对比值滤波

电压采集采用芯片内部精确基准 REF(1.20V / 2.048V 等)与分压后的电池端通道 BATT 联动测量:

V_cell(mV) = (BATT_raw × V_ref_mv) / REF_raw

系统按批次采集 8~12 组样本。每采入一组立即计算比值,最终对这批比值进行排序,取中心均值滤波。

为什么不能分别对 BATT 数组和 REF 数组取中位数后再相除? 在数学上,比值的中位数并不等价于中位数的比值:

\[\text{Median}\left(\frac{A_i}{B_i}\right) \neq \frac{\text{Median}(A_i)}{\text{Median}(B_i)}\]

当电源轨在采样瞬间出现纹波抖动时,单独排序可能会把第 1 次的尖峰 BATT 与第 8 次的低谷 REF 强行拼在一起,凭空捏造出一个从未真实存在过的离谱电压。

7.2 测量失效与电池欠压的严格区分

当 ADC 轮询超时、硬件分压偏置未开启或基准通道异常时,系统严格将数据标记为 voltage_valid = 0,向对外接口发布 0 mV 作为无效哨兵值。

上层业务逻辑绝不能因为“读数失效”就顺理成章地当成“0V 电池放空”执行紧急关机,否则任何底层的偶发采样时序抖动都会导致整机意外死机。


8. 无基线初始化:审慎使用开路电压

8.1 开路电压查表的苛刻准入条件

当系统彻底断电冷启动、且 BKP 中没有任何历史记录时,系统只能借助静态电压估算初始 SOC。但该入口受到极其严格的互锁保护:

bool BM_IdleVoltageAllowed(void)
{
    return (!have_baseline) &&                     /* 1. 仅限无基线状态 */
           (voltage_valid) &&                      /* 2. 电压采样有效 */
           (raw_charge_state == CHARGE_IDLE) &&    /* 3. 硬件未充电 */
           (Pwr1 == 0 && Pwr2 == 0) &&             /* 4. 脉冲输出关闭 */
           (TIM1_CH1_Disabled && TIM8_CH1N_Disabled); /* 5. 硬件底层输出真正静默 */
}

只有当系统确认既无大功率放电、又无充电电流注入时,才允许调用 12 点分段线性插值表建立第一个粗略的 TRACKING 基准。

8.2 12 点参考曲线与插值规范

当前采用的 12 点典型常温开路电压(OCV)参考映射表如下:

端电压 (mV) 3000 3400 3550 3660 3710 3750 3790 3840 3900 3970 4060 4180
初始 SOC (%) 0 5 10 20 30 40 50 60 70 80 90 100

相邻电压点之间采用标准的整数定点线性插值。需要明确,该曲线仅作为冷机上电时的粗略落脚点,一旦库仑计开始正常工作,该表即刻退出舞台,绝不允许在后续运行中周期性强行查表修正。


9. 实时冲突:总线安全窗口机制

9.1 微秒级延时与高优先级脉冲输出的冲突

软件模拟 1-Wire 协议要求严苛的微秒级时序:主机拉低 500 μs 复位、并在特定 15~60 μs 采样窗口内读取从机电平。在此期间,MCU 必须临时关闭全局中断(__disable_irq())。

然而,工程中的高优先级任务(例如驱动高频功率开关管或脉冲变压器的 TIM 互补 PWM、以及负责实时安全监控的定时器中断)要求微秒级确定性响应。如果在高占空比输出过程中强行执行 1-Wire 通信,长达数百微秒的关中断会直接导致硬件输出波形失真;反之,若在 1-Wire 期间不关中断,中断抢占又会摧毁总线时序,导致通信频繁超时。

9.2 软件意图与底层硬件双重闭锁

解决该冲突的核心方案是总线安全窗口机制(Safe Bus Window):

bool BM_GaugeBusSafe(void)
{
    /* 软件业务层认为已停止 */
    bool soft_idle = (Pwr1 == 0 && Pwr2 == 0);

    /* 底层硬件定时器比较输出引脚已真正物理关闭 */
    bool hard_idle = ((TIM1->CCER & (TIM_CCER_CC1E | TIM_CCER_CC2E)) == 0) &&
                     ((TIM8->CCER & (TIM_CCER_CC1NE | TIM_CCER_CC2NE)) == 0);

    return soft_idle && hard_idle;
}

只有当上层意图和底层硬件输出同时处于静默状态时,才允许启动 1-Wire 事务。若窗口不满足,主循环平滑推迟总线采样,绝不强行打断实时输出。

9.3 通信挂起期间硬件独立积分

软件暂停通信并不等同于硬件暂停计量。

在脉冲输出期间,虽然 MCU 停止读取 DS2740,但库仑计芯片本身由电池持续供电,其内部独立的模拟积分器和计数器依然在以极高精度默默记录流经采样电阻的每一微库伦电荷。当脉冲输出结束、安全窗口重新开启时,MCU 仅需读取一次最新的 ACR 寄存器,即可通过 16 位模差一次性补全整个输出期间消耗的全部电荷。


10. 1-Wire 驱动:时序、临界区与容错退出

10.1 独立硬件微秒时基

驱动摒弃了不可靠的 for 循环空指令延时,采用独立定时器 TIM2 构建确定性时基:

  • 定时器主频 64 MHz,预分频系数设为 63,得到标准的 1 MHz 自由运行向上计数器(1 tick = 1 μs);
  • 自动重装载寄存器设为 0xFFFF,支持自然的 16 位溢出回绕;
  • 超时计算全部采用模差:(uint16_t)(TIM2->CNT - start_us) < timeout_us。

10.2 严格规范的临界区保护

在关键读写时序中,关中断必须遵循“保存现场—恢复现场”规范,严禁粗暴调用全局开中断:

/* 正确写法:保存调用者的原始中断屏蔽状态 */
uint32_t primask = __get_PRIMASK();
__disable_irq();

/* 执行极微秒级总线位操作 */

__set_PRIMASK(primask); /* 精确恢复原有状态,不踩踏上层嵌套 */

10.3 双重有限循环 Guard 防御

在等待总线电平翻转时,若硬件总线因外部短路持续锁死在低电平,单纯依赖时钟判决可能会因时钟异常陷入死循环。

系统在所有等待宏中加入了有限循环计次门限(Guard Count):

#define OW_WAIT_LEVEL(level, max_us, guard) do { \
    uint16_t start = (uint16_t)TIM2->CNT; \
    uint32_t cnt = 0; \
    while (GPIO_ReadInputDataBit(...) != (level)) { \
        if ((uint16_t)(TIM2->CNT - start) >= (max_us)) return OW_TIMEOUT; \
        if (++cnt >= (guard)) return OW_BUS_ERROR; \
    } \
} while(0)

通过“时基超时”与“最大指令循环上限”双重防护,彻底杜绝外设硬件故障导致 CPU 主循环假死。


11. DS2740 驱动与主循环协作调度

11.1 芯片复位侦测技巧(RNAOP 位利用)

DS2740 具备一个专用状态寄存器(Status Register)。其中的 RNAOP(Read Net Address Opcode)位在上电时的硬件默认值为 0。

驱动在每次成功完成初始化后,主动将 RNAOP 置为 1。在后续的周期性巡检中,若发现该位意外变回了 0,即可作为库仑计自身发生过掉电复位的决定性铁证,从而果断废除旧坐标系,防止按错误的参考点累计。

11.2 主循环非阻塞协作状态机

为了将耗时较长的单总线事务平摊到各个调度周期中,BatteryManager_Process() 被设计为一个分步推进的状态机:

[IDLE 周期等待] ──> [STEP_ADC 单组采集] ──> [STEP_GAUGE 安全读芯片] ──> [STEP_FUSE 融合发布]
       ▲                                                                       │
       └───────────────────────────────────────────────────────────────────────┘

每个主循环仅推进一个有限子步骤(例如仅采集一组 ADC,或仅执行一次受控读取),单步耗时控制在毫秒级以内,确保主循环对按键、UI 和保护判据保持毫秒级流畅响应。


12. 显示预测:在不可测区间提供平滑体验

12.1 引入独立显示预测的必要性

在脉冲电刺激输出期间,为了规避实时冲突,1-Wire 通信完全挂起。如果界面直接冻结不变,用户会误以为设备卡死;若直接拿带载电压查表,电量又会瞬间跳水。

系统的解决方案是构建轻量级展示预测层(Display Prediction Layer):

真实电荷账本 remaining ──(脉冲期间冻结等待补算)─────────────────> 停机后一次性模差补齐
                                                                   │
负载工况模型推演 ───────> [显示预测器 stim_remaining] ──> [UI 目标] ──> UI 限速展示

预测模型仅负责在总线挂起期间计算一个合理的“假想目标”,指导 UI 平滑走字,该数据绝不反写进底层的硬件真实账本。

12.2 双通道负载电流模型与静态功耗扣除

单通道脉冲在不同档位下的平均等效电流基于实测物理曲线分段插值:

刺激强度档位 10 20 30 40 50 60
单路参考总电流 (mA) 95.3 138.7 216.6 264.4 303.5 342.8

在推导双通道复合工况时,必须扣除整机基础静态功耗 $I_{base}$(约 27 mA),防止基础功耗被重复叠加两次:

\[I_{total} = I_{base} + \sum_{k=1}^{2} \left[ I_{single}(Pwr_k) - I_{base} \right] + I_{heat1} + I_{heat2}\]

12.3 微安·毫秒定点积分与余数保留

预测模块在主循环中累积消耗。定义基本计算单位为:

\[1\text{ count} = 0.3125\text{ mAh} = 11,250,000\text{ } (0.1\text{ mA} \times \text{ms})\]

在每次循环迭代中,必须将不足 1 count 的微小余数精准保存在 fraction 变量中:

uint64_t consumed = (uint64_t)model_current_01ma * elapsed_ms + fraction;
uint64_t delta_counts = consumed / UNITS_PER_COUNT;
fraction = consumed % UNITS_PER_COUNT; /* 余数无损滚入下一轮 */
predicted_remaining -= delta_counts;

如果直接丢弃除法余数,由于主循环调度周期极快(每次仅消耗微小的几分之一 count),截断误差会导致预测模型“跑不动”,即使输出持续数分钟,预测电量也纹丝不动。

12.4 档位切换时先积旧档、再切新档

当检测到用户按键调整了脉冲档位时,计算顺序必须严格遵循因果依赖:

  1. 第一步:使用旧档位电流与流逝时间 $\Delta t$,将上一阶段的实际电荷消耗积分入账;
  2. 第二步:将当前观察到的新档位存入上下文,作为下一阶段的计算基准。

如果颠倒顺序,系统会错误地用崭新的高档位电流去反溯之前低档位区间的消耗,人为放大估算误差。


13. 满充校准:事件权威与幂等控制

13.1 充电信号引脚的业务语义

系统通过硬件上拉检测充电芯片的两只漏极开路指示脚(CHARG 与 STDBY,低电平有效):

CHARG 引脚 STDBY 引脚 业务充电状态 说明
高电平 高电平 IDLE 外部未接充电器,或电源已切断
低电平 高电平 CHARGING 正在恒流/恒压充电中
高电平 低电平 FULL 充电截止,电池已充满
低电平 低电平 INVALID 瞬态抖动或硬件异常组合

13.2 信号去抖与一次性幂等锚定

充满校准绝不能仅凭瞬态脉冲直接触发,必须经过两级可靠性确认:

  1. 时序去抖:FULL 状态在硬件引脚上连续稳定保持至少 500 ms,排除插入和拔出瞬间的机械抖动;
  2. 安全锚定:在总线安全窗口内取得最新合法 ACR 后,执行原子校准:
    • 将账本 remaining_counts 重置为额定最大值(12800 counts);
    • 将当前的 last_acr 设为全新基准参考点;
    • 将基线可信度提升为最高等级 ANCHORED,并持久化到 BKP;
    • 置位 full_done 标志,确保在设备插着线满电静置的数小时内,只执行一次基线重置,绝不周期性反复覆盖正常的静态待机微小损耗。

14. 跨复位持久化:守护上下文连续性

14.1 为什么持久化不能只存一个 SOC 百分比

许多系统习惯在休眠前把电量存为 85%。但在唤醒恢复时,该数值无法告诉系统:

  • 85% 究竟对应多少物理电荷?
  • 库仑计当时的内部累积寄存器停在哪个刻度?
  • 睡眠期间库仑计是否一直在正常走字?
  • 唤醒后的第一笔新电量应该相对哪个原点进行差分?

如果只存百分比,系统在唤醒后将彻底失去连续跟踪能力,只能重新依赖端电压粗估查表,造成频繁的“睡醒电量突变”。

14.2 BKP V3 精确存储布局与校验

系统使用 MCU 内部由备用电源维持的 备份数据寄存器(BKP_DAT1~DAT12),总计 24 字节:

寄存器 存储内容 核心设计意图
DAT1 Magic Word (0x2740) 既是有效特征字,又是两阶段提交的最后 Commit 标记
DAT2 Data Version (0x0003) 结构体版本控制,防止固件升级后结构体错位
DAT3 Baseline Confidence 记录基线等级(TRACKING 或 ANCHORED)
DAT4 Last Valid ACR 保存时的库仑计绝对硬件参考点
DAT5~6 Remaining Counts 32 位剩余电荷绝对计数值(低 16 位在前)
DAT7 Capacity Counts 保存时的电池额定总容量基准
DAT8~11 64-bit DS2740 ROM 库仑计芯片的全球唯一 64 位硬件身份序列号
DAT12 CRC-16 校验码 覆盖前 11 个字的数据完整性校验

14.3 最小提交协议(Commit Protocol)

在写入备份域时,系统采用严格的写入时序保证原子性:

BKP_Write(DAT1, 0x0000); /* 第一步:先清空 Magic,将旧记录显式作废 */

/* 第二步:按序写入版本号、物理参数、身份码和计算出的全新 CRC16 */
BKP_Write(DAT2, VERSION);
...
BKP_Write(DAT12, crc16_value);

BKP_Write(DAT1, 0x2740); /* 第三步:最后一步回写 Magic,完成正式提交 */

如果在写入中途 MCU 发生断电或意外复位,重启时由于 DAT1 != 0x2740,系统能准确识别出这是一条“未完成的半截废数据”,果断放弃恢复并重新初始化,绝不加载被破坏的数据。

14.4 校验恢复流水线与补算

在开机恢复时,BM_TryRestore() 顺序通过七重安全门禁:

  1. DAT1 Magic 匹配且 CRC16 严格通过;
  2. 存储版本号完全一致;
  3. 容量配置与系统宏定义一致;
  4. 现场读出的 64 位芯片 ROM 码与存储的一致(防止电池包或从板被热替换);
  5. 现场读出的芯片 RNAOP 标志位仍为 1(证明库仑计自身未发生过掉电复位);
  6. 存储的 ACR 与现场读到的 ACR 模差在合理容量边界内;
  7. 恢复出的剩余电量落在 $0 \sim \text{Capacity}$ 合法区间。

门禁全部通过后,系统通过模差算法补全休眠期间的全部电量变动:

\[\text{Restored Remaining} = \text{Saved Remaining} + \text{Polarity} \times \left( \text{Current ACR} - \text{Saved ACR} \right)\]

系统随后将当前 ACR 刷新为最新起点。整个恢复过程完全不需要读取电压,实现了平滑无缝的电量延续。


15. UI 展示层:平滑滤波与单向约束

15.1 三层数值解耦

[物理账本 SOC]  ──> 依据真实电荷积分,毫秒级反映微小进出
       │
[内部目标 SOC]  ──> 融合刺激预测与故障保持后的计算目标
       │
[展示层 UI SOC] ──> 面向用户的最终平滑视图,严格遵守步进与单向约束

三层解耦保证了:内部对物理世界了如指掌,而对外展现给用户的是一个稳定、合乎直觉的仪表盘。

15.2 核心展示平滑规则

  1. 步进限速:UI 显示每 5000 ms 内最多只能变动 1 个百分点,消除任何因算法切换导致的数字跳动;
  2. 放电单向约束:在未接入充电器的正常使用期(CHARGE_IDLE),UI 电量绝对不允许向上反弹;
  3. 充电单向约束:在充电状态(CHARGING / FULL)下,UI 电量绝对不允许向下倒退;
  4. 异常保持:通信偶发故障或信号抖动时,UI 坚定冻结显示上一次的合法百分比,避免闪烁;
  5. 未知与真零分离:未建立有效基准时,屏幕隐藏百分比或显示破折号;只有真实电荷耗尽时才显示“0%”。

16. 安全保护与硬件互锁

16.1 低电关机严禁依赖 UI 估算值

显示层为了用户体验添加的限速和单向滤波,在功能安全领域统称为“延迟与失真”。

如果将低电关机的判据绑定在“UI 变为 0%”上,当大电流突发负载将端电压拉低到保护板即将截断的临界点时,UI 可能还在以每 5 秒 1% 的速度慢吞吞递减。这将导致设备尚未提示用户保存就直接被硬件保护板猝死切断,甚至引发 Flash 写入崩溃。

因此,低电关机必须直连底层的独立硬件 ADC 判据:

  • 采集真实的 BATT 端电压;
  • 采用 8 组中位值滤波形成批次;
  • 采用 9 批连续滑动投票机制(超过 5 批低于 3.309V 门限即刻触发紧急关机流程);
  • 保护逻辑完全独立于任何电量估算代码。

16.2 经典 Bug 复盘:主循环单次关热为什么会被中断踩踏

在包含加热功能的设备中,曾出现过一个极为隐蔽的软硬件竞态缺陷: 当用户插入充电器时,主循环虽然检测到了充电状态,并调用函数将加热引脚拉低(HOTEN_OFF);但此时用户之前设定的温度档位全局变量仍停留在“中档”。

负责产生加热 PWM 调制信号的底层定时器中断(TIM5 ISR)每隔 100 毫秒执行一次:中断一触发,代码按当前的“中档”重新计算占空比,并再度将加热引脚拉高(HOTEN_ON)。结果导致主循环的单次关热指令被底层中断反复践踏,使得设备在充电时依然偷偷发热。

16.3 最终执行点硬件互锁(Defensive Sink)

正确的工程解法是在最终硬件输出汇合处建立不可逾越的安全互锁:

/* TIM5 中断服务函数中,最靠近硬件输出寄存器的唯一出口 */
void TIM5_IRQHandler(void)
{
    /* 1. 硬件级首要互锁:直接读取充电引脚的原始电平 */
    if (GPIO_ReadInputDataBit(CHARG_PORT, CHARG_PIN) == Bit_RESET ||
        GPIO_ReadInputDataBit(STDBY_PORT, STDBY_PIN) == Bit_RESET)
    {
        /* 只要插着线,底层无条件切断加热开关管,直接忽略任何上层软件档位 */
        HOTEN_OFF();
        return;
    }

    /* 2. 正常工况下,才允许根据上层档位配置 PWM 占空比 */
    Apply_Heater_PWM();
}

将安全互锁从脆弱的上层业务页面下沉到物理执行的最终汇合点,无论上层逻辑发生多严重的调度错乱、任务阻塞或变量残留,底层硬件安全都能得到绝对保证。


17. 完整生命周期事件演进

为了清晰呈现各模块的协同,下表梳理系统在关键生命周期节点中的行为:

[冷机上电] ──> 探测 1-Wire ──> BKP 校验失败 ──> 满足空闲条件 ──> 电压查表建初值 ──> 启动跟踪
     │
[正常运行] ──> 周期 ADC 滤波 ──> 安全窗口读 ACR ──> 16位模差入账 ──> UI 限速 1%/5s 刷新
     │
[启动脉冲] ──> 硬件输出开启 ──> 挂起 1-Wire ──> 模型定点积分 ──> UI 预测走字
     │
[停止脉冲] ──> 硬件输出关闭 ──> 窗口恢复 ──> 读最新 ACR ──> 一次性模差补齐 ──> 清除预测态
     │
[插入充电] ──> 底层 ISR 硬切断加热 ──> 充电去抖 500ms ──> UI 转为单向递增
     │
[满充锚定] ──> STDBY 稳定 500ms ──> 读取有效 ACR ──> 重置 100% 基线 ──> 写入 BKP ──> 保持幂等
     │
[准备休眠] ──> 调用 PrepareForStop ──> 封存最后有效数据 ──> 提交 BKP ──> 进入 STOP2
     │
[唤醒重启] ──> 读 BKP ──> 七重门禁全过 ──> 模差补算休眠损耗 ──> 零跳变恢复运行

18. 跨平台移植与最小抽象层设计

当需要将本套电量架构移植到 STM32、GD32、ESP32 或搭载 RTOS 的新项目中时,切忌全盘复制代码。应当优先为核心模块实现以下六个最小硬件抽象层(HAL)接口:

/* 最小硬件适配接口草案 */
typedef struct
{
    uint32_t (*get_now_ms)(void);                       /* 单调递增毫秒时钟 */
    bool     (*is_hardware_safe)(void);                 /* 硬件是否处于安全访问窗口 */
    bool     (*read_battery_mv)(uint16_t *voltage_mv);  /* 读取有效端电压 */
    bool     (*read_gauge_sample)(Ds2740Sample *sample);/* 读取底层硬件库仑计 */
    ChargeRawState (*read_charger_pins)(void);          /* 读取充电引脚瞬时物理电平 */
    bool     (*bkp_save_record)(const BatteryRecord *r);/* 持久化写入 */
    bool     (*bkp_load_record)(BatteryRecord *r);      /* 持久化读取 */
} BatteryPlatformOps;

通过将具体的时钟树配置、定时器寄存器操作以及 BKP 驱动收敛到底层接口中,battery_manager.c 中的状态机、模差计算、显示限速和预测逻辑可以实现 100% 纯逻辑无缝跨平台复用。


19. 工程踩坑排查清单

在调试便携电量管理系统时,以下是最容易踩中的技术隐患:

  1. 电阻单位混淆:代码中的 BM_RSNS_MOHM 宏使用的是毫欧(如 20)。若误写成欧姆(0.02)或微欧(20000),将彻底摧毁整个定点换算体系;
  2. 假死在关中断区:1-Wire 等待循环务必加入 Guard Count,防止因上拉断开导致 MCU 永久死锁在临界区;
  3. 把尝试时间当有效时间:速率校验如果绑定了重试时间戳,通信故障恢复后必定引发虚假超速报警;
  4. 充满后 UI 滞后不是 Bug:满充锚定瞬时将内部账本重置为 100%,而 UI 需遵守 1%/5s 限速。从 85% 追赶到 100% 约需 75 秒,这是为了兼顾用户体验的刻意设计;
  5. 休眠期间 BKP 供电:MCU 备份域若依赖电池直供,必须确保在整机主 LDO 关断时,VBAT 引脚仍有可靠备电,否则无法实现跨睡眠连续恢复;
  6. 边充边用场景极性相反:若仪器支持插线运行,必须仔细核对采样电阻的位置是串在“纯电池支路”还是“系统总电源支路”,否则充电时开启大功率输出会被诊断逻辑误判为“极性接反故障”。

20. 核心源码锚点与参考资料

20.1 关键逻辑速查

核心功能逻辑 对应函数 / 结构体 核心设计关注点
状态机与生命周期 BatteryManager_Process / PrepareForStop 非阻塞分步推进与休眠前状态封存
ACR 模差与安全入账 BM_FuseSample 16 位有符号差分,防止溢出回绕
物理速率防跳变校验 BM_DeltaLimit / BM_DeltaValid 基于 last_good_tick 的物理电流上界判断
总线安全窗口判断 BM_GaugeBusSafe 软件意图与底层比较输出双重校验
跨复位持久化管理 BM_BkpSave / BM_TryRestore 24 字节 BKP 存储、CRC16 校验与提交协议
展示层限速与平滑 BM_UpdateDisplay 5000ms 步进、单向递增/递减与异常保持
开路电压准入与插值 BM_IdleVoltageAllowed / BM_VoltageToSoc 纯空闲严格准入与 12 点定点分段插值
输出端硬件关热互锁 TIM5_IRQHandler (中断底层) 最底层硬件出口直接闭锁加热开关管

20.2 外部资料与规范

  • Analog Devices DS2740 官方数据手册:用于核对 16 位补码物理意义、RNAOP 与 SMOD 状态寄存器位定义;
  • ARM Cortex-M4 编程手册:深入理解 PRIMASK 临界区保护规范与硬件单周期指令行为。
← 返回文章归档

讨论交流

支持免注册访客评论,填写真实邮箱可接收回复提醒