一、瞬时掉电与真掉电:先分清两种掉电
现场说的“掉电”,其实是两种完全不同的物理事件,只是它们在检测脚上看起来一模一样—— 都是“电平变低了”。
| 对比项 | 瞬时掉电(电压跌落) | 真掉电(输入断开) |
|---|---|---|
| 成因 | 大负载启动、接触器切换、雷击浪涌、接线端子接触不良 | 断电、拔插头、保险丝熔断、上游开关跳闸 |
| 持续时间 | 短,通常是毫秒到百毫秒量级 | 长,直到人为恢复 |
| 能量 | 输入还在,只是暂时不够 | 输入侧已经没有能量来源 |
| 要不要复位 | 不需要,重启反而打断正在进行的动作 | 需要,但要先把该存的数据存下来 |
| 该做什么 | 什么都不做,或者只做一次预警 | 抢在电容放完电之前保存关键数据,然后安全停止 |
这里有一个容易被忽略的物理事实:输入消失之后,MCU 并不会立刻停止工作。 电源轨上并联的储能电容会继续供电,轨电压是慢慢往下掉的, 从“输入消失”掉到“MCU 低于工作下限”,中间有一段时间。 这段时间就是掉电处理能用的全部预算,它由电容容量、负载电流和允许的电压跌落范围决定, 是可以估算的,不是“应该来得及”。
而“一掉电就复位”错在哪里?错在它把上面两种事件当成了一种:
- 把不需要重启的事件也重启了。设备正在加注、正在结算,一次几十毫秒的跌落就把它打断, 操作者看到的是“设备自己重启了,量没加完”。
- 频繁重启会把日志刷爆。每次重启都记一笔异常,真正的故障记录被这些重复记录冲掉, 回头看日志只剩一堆“掉电重启”。
- 最危险的是在电源不稳时反复重启。复位到一半电源又掉,可能出现“半初始化”状态: 外设还没配好、输出口的电平是随机的。对执行机构来说,这是不可接受的状态。
想明白这一层之后,我把掉电处理拆成了两件事,它们的快慢要求正好相反: 预警要快(抢时间做保存和安全动作), 确认要慢(判定这确实是真掉电,然后才允许复位)。 后面几节都是围绕这个拆分展开的。
二、检测点选在哪里:越靠上游,预警越早
我最初只有一个电压来源:ADC 采回来的那一路。它其实并不适合做掉电检测,原因有三个:
- 位置太靠后。它采的是稳压之后的轨电压,而那已经是电源链的末端, 等它开始明显下降,留给你的时间已经不多了。
- 速度太慢。它是按采样周期取回来的,还要经过滤波、换算, 响应速度取决于采样节拍,而不是事件本身。
- 它是“测量”而不是“预告”。带小数、带换算、带标定, 任何一个环节出问题,掉电检测这个最不该失效的功能就跟着失效。
所以我加了一路专门的数字检测脚,接在电源链更上游的位置。 它不承担测量任务,只回答一个问题:输入还在不在。 因为是数字信号,它只有两个状态,不受标定和换算影响; 因为位置靠上游,它比 MCU 自己的轨电压更早变低。
这里的关键认识是:电源链是有顺序的—— 输入进来,经过保护与滤波,再到稳压,最后才是 MCU 的供电轨。 链条上每一级都有自己的储能,上游先掉、下游靠电容撑着。 检测点每往上游移一级,你就多拿到一段预警时间, 而且这段时间的代价只是“多一根线、多一个分压”,非常划算。
| 检测方式 | 位置 | 响应速度 | 适合做什么 | 局限 |
|---|---|---|---|---|
| 专用数字检测脚 | 电源链上游(稳压之前) | 快,事件发生即变化 | 掉电预警、抢时间保存、复位决策 | 只知道“在不在”,不知道电压是多少 |
| ADC 采集轨电压 | 电源链末端(MCU 自己的轨) | 慢,受采样节拍与滤波影响 | 运行期的欠压/过压保护、状态显示与上报 | 换算与标定环节多,不适合当作唯一判据 |
两路信号我最后都保留了,因为它们的职责不同: 数字脚负责“预告”,ADC 负责“保护”。 预告要的是快,保护要的是准,这是两个不同的指标, 用同一路信号去满足两个指标,最后一定有一头是勉强的。 顺带一提,数字检测脚本身也要做去抖, 只不过它的去抖和后面要讲的“保持窗口”是同一件事,不需要单独再加一层延时。
三、保持窗口:给“写一笔记录”留出时间
检测脚变低之后,我不会立刻认定掉电,而是记下变低的时刻, 然后看它会不会持续低下去: 只有持续满足到超过一个窗口,才升级为“掉电确认”。 这个窗口我叫它保持窗口。
窗口长度不是拍出来的,它是两件事的权衡,而且这两件事的方向相反:
- 往长了定:瞬时跌落被忽略得更彻底,误复位更少;
- 往短了定:真掉电被更早确认,留给“保存数据”的时间更多。
那这段时间到底要花在哪?把预算拆开看就很清楚。从检测脚变低开始, 到 MCU 轨电压跌破工作下限为止,总时间记为 T;这笔 T 要分给:
- 响应中断、置起预警标志(几乎不花时间,但不能被长中断挡住);
- 把关键数据整理到一块固定的 RAM 缓冲里(当前累计量、动作状态、未落盘队列); 这一步之所以要在预警时就做,是为了让真正落盘时只做“搬运”,不做“计算”;
- 给执行机构发停止指令(这一步的耗时取决于机构本身,往往比写存储还长);
- 把缓冲里的内容写进存储(这一步的耗时是毫秒量级,而且必须整笔写完)。
如果第 4 步的耗时超过剩余的 T,就会出现写到一半断电—— 这比“没写”更糟:账目里会留下一条半截记录,而设备自己还不知道。 所以我的做法是两条:落盘只写必需字段(能少写一个字节就少写一个), 以及写记录本身要能自证完整(带序号与校验,写坏了下次上电能认出来)。 存储这一侧的设计我在《嵌入式存储的三层分工》里展开过, 参数区与记录区的加固手段(双备份、递增序号、头校验、版本与默认值回退)另见 《Flash 参数区的双备份与原子切换》。
回到开头那个矛盾:抗误判要窗口长,抢时间要窗口短。 我后来是靠“双阈值 + 保持窗口”把它拆开的—— 不是把窗口调到一个折中的长度,而是让两个阈值分别负责两件事。 具体怎么拆,下一节和第五节合起来讲。
/* 伪代码骨架:所有时间量都来自配置项或节拍计数,本文不给具体取值 */
if (检测脚 == 低) {
if (预警位 == 未置位) {
记下起始节拍; /* 从这一刻开始算预算 */
预警位 = 置位;
整理关键数据到固定缓冲(); /* 快,先做不花时间的准备 */
通知执行机构停止();
}
if ((当前节拍 - 起始节拍) >= 保持窗口) {
掉电确认位 = 置位; /* 持续满足窗口,才承认是真掉电 */
}
} else {
预警位 = 清零; /* 恢复得比窗口还快 → 当作瞬时跌落,什么都不做 */
起始节拍 = 无效;
}
四、恢复后的延迟复位:等关键动作停下来
如果掉电已经被确认了,电源又回来了,能不能立刻复位? 不能。我现在要求三个条件同时成立,而且必须持续成立:
- 电源已经恢复并稳定了一段时间。输入回来不等于稳定: 负载一压上去可能又掉,立刻复位就会掉进“重启—又掉—再重启”的循环。
- 系统不在运行态。正在执行动作时复位,等于把一个进行到一半的流程硬砍掉, 状态机重启后面对的是一堆“上次没做完的事”。对计量通道来说, 这还会让“已经累加的脉冲计数”和“已经落盘的记录”之间出现一个缺口, 补都补不回来(判向、计数与口径的细节见 《计量脉冲的判向、倍频与消抖》)。 我的做法是:只要还在运行态,就先按正常流程把它收尾或安全中止,再谈复位。
- 所有执行机构都已停止。这一条最容易被跳过,但它最要命。 掉电瞬间机构停在哪里是未知的,复位会让输出口回到默认电平; 如果这时机构还带着负载,就可能停在中间位置、或者让输出在不该动作的时候动作。 所以要有一个明确的“全部停止”的确认,而不是“应该停了吧”。
三个条件的写法上有一个共性:任何一个中途不成立,就要重新计时。 这也是我把这段逻辑叫“抑制”而不是“检测”的原因—— 它不是在检测什么,而是在压住那个想立刻复位的冲动, 直到确认安全。
/* 伪代码骨架:常量用符号表示,稳定时间来自配置项 */
if (掉电确认位 == 置位) {
if (电源已恢复 && 系统不在运行态 && 执行机构全部停止) {
if (恢复后计时 >= 稳定时间) {
受控复位(); /* 三个条件都持续成立,才允许复位 */
}
} else {
恢复后计时 = 0; /* 任何一条不成立,就重新计时 */
}
}这套逻辑带来一个副作用,需要提前想好:复位被推迟了,那这段时间系统在干什么? 我的答案是“什么都不干”——所有输出保持停止状态,显示给出明确的提示, 不接受新的操作指令。宁可让设备安静几秒钟,也不要带着不确定的机构状态去重启。 复位之后要做的第一件事是读回掉电前留下的那笔记录,把状态机恢复到一致的起点, 这部分在《从能跑到能出厂》里 归到了“出厂状态与可观测性”那一节。
五、一个真实的三次迭代:问题—改法—又出问题—再改
下面这个过程是我在加注计量设备上真实经历的,前后跨了几周。 我把它完整写出来,因为中间那次“继续加长滤波”的思路,是这类问题最典型的错误方向。
| 轮次 | 现象 | 当时的改法 | 结果 |
|---|---|---|---|
| 第一轮 | 现场电源不稳,设备频繁重启,正在加注的动作被打断 | 对掉电逻辑增加滤波,屏蔽瞬时掉电就重启 | 重启频次明显下降,但没有消失 |
| 第二轮 | 现场仍反馈“偶尔自己重启一下”;继续优化判定逻辑后仍有 | 继续加长滤波时间 | 误判更少了,但真掉电的响应也一起被推后 |
| 第三轮 | 误判与“来不及保存”这两个问题开始互相牵制,参数怎么调都不舒服 | 改成双阈值 + 保持窗口,把“抗误判”和“抢时间”交给不同环节 | 两个目标不再互相拉扯,现场调参也有章法了 |
前两轮之所以走弯路,是因为我把“滤波时间”当成了一个可以一直往上加的旋钮。 现在回头看,单纯加长滤波时间有三个绕不过去的问题:
- 它把真掉电的确认也一起推后了。 滤波是作用在“所有掉电事件”上的,不分瞬时还是真掉。 而真掉电时能用的总时间 T 是固定的(由电容储能决定), 确认晚了,留给“整理数据 + 停止机构 + 写记录”的时间就少了, 严重时会出现记录写到一半断电。也就是说:为了少几次误判,付出的代价是数据可能丢, 这两件事根本不在一个量级上。
- 它用一个旋钮去满足两个相反的目标。 抗误判希望这个时间越长越好,抢时间希望它越短越好。 一个参数同时被两个相反的方向拉扯,最后只能折中, 而折中的结果往往是两头都不达标——误判没根治,保存时间也变紧了。
- 它没有触及根因。 根因是我在用同一个条件(“检测脚为低”)描述两种本质不同的事件。 这两件事的差别不只是“低电平持续多久”,还包括“电压是在恢复还是在继续掉”、 “MCU 自己这一侧还撑不撑得住”。只调一个时间参数,等于拒绝看其它维度。
/* 错误示范:只把滤波时间加长。这个写法本身没有价值,
放在这里只为了说明“往哪个方向改是错的”。 */
if (检测脚持续为低 超过 很长的滤波时间) {
复位(); /* 真掉电也被一起推后,保存时间被吃掉 */
}
/* 结果:误复位少了,但“来不及写记录”的概率上去了 */第三轮我换成双阈值 + 保持窗口,结构是这样:
- 早阈值 = 上游的数字检测脚。 它一变低,立刻做“预备”动作:把关键数据整理进固定缓冲、通知执行机构停止、置预警位。 这一路完全不参与“要不要复位”的判断,它只负责快。
- 晚阈值 = 自己这一侧的欠压判定(也就是 MCU 轨电压跌破工作下限)。 它更接近“我还能不能正常工作”这个事实, 所以不会因为输入侧一次短暂的跌落而误触发。
- 保持窗口 = 加在早阈值上的持续时间要求。 它只负责滤掉窄脉冲,长度只需要覆盖“输入侧最长可信跌落”, 不必为了抗误判无限加长——因为误判的最终把关已经交给晚阈值了。
- 三者是“与”的关系:早阈值持续满足窗口 且 晚阈值命中, 才认定为真掉电。只要有一个不成立,就继续观望。
这样拆开之后,两个目标终于各自有了归属: 抗误判由“与”逻辑和晚阈值负责,抢时间由早阈值负责, 窗口长度只需要管“多窄的脉冲算噪声”,不再背负“要不要复位”这个重任。 现场调参也从“猜一个滤波时间”变成了“分别问三个问题”: 最长可信跌落有多宽(定窗口)、 MCU 轨电压跌到多少就撑不住(定晚阈值)、 预警之后要做完哪些动作(定预备阶段的内容)。
六、判定值与显示值为什么要分开
还有一次是纯硬件侧的发现:用标准表比对时发现, 我采集到的电压比实际输入电压低一个固定的量, 而且这个偏差在不同设备上基本一致,是这版硬件(分压网络与参考)带来的系统偏差, 不是随机误差。修正的办法很朴素:在换算结果上加一个固定修正量, 显示和上报的值立刻就和标准表对上了。
真正值得写下来的是接下来这个决定:判定用未修正的值,显示用修正后的值。 当时的代码形状就是两条并行的线:
/* 伪代码骨架:修正量与判定阈值都来自参数表,取值不在这里给出 */
raw = adc_read_filtered(); /* 只做滤波,不加任何偏移 */
/* 线一:保护判定只用未修正值 */
if (raw 越过 欠压判定值) {
连续命中次数 += 1;
if (连续命中次数 >= 设定次数) { /* 软件去抖:连续命中才算数 */
置位欠压故障();
}
} else {
连续命中次数 = 0;
}
/* 线二:显示与上报走修正后的值 */
display_voltage = raw + 修正量;为什么必须这么分?我的理由有四条,按重要性排:
- 否则修正量会静默地改变保护阈值。 欠压值是用户设定、存在参数表里的。一旦判定改用修正后的值, “加修正量”这件事就等价于在没有任何人知情的情况下,把欠压阈值移动了同样的量。 用户设的值不再是他以为的那个值——这比显示不准严重得多。
- 两者语义不同,寿命也不同。 修正量描述的是“我这块板子的采样偏差”, 它会随分压电阻批次、温漂、老化而变化; 阈值描述的是“这台设备的保护策略”,由使用者决定。 把两个语义不同的量加在一起,以后任何一个变了都会影响另一个,排查时无法区分 “是硬件漂了”还是“阈值被改了”。
- 一次硬件批次变更不能悄悄改掉保护行为。 如果修正量进了判定侧,那么换一批分压电阻(修正量要重标)就会改变保护动作点。 硬件变更影响保护策略,这在设备上是不可接受的。
- 显示必须和万用表一致。 现场核对电压时,如果显示值总是比标准表低一点, 每次都要解释“这是正常的”,久而久之没人再相信这个显示, 真出问题时也看不出来。
| 值 | 用途 | 是否含修正量 | 它变了会怎样 |
|---|---|---|---|
| 未修正的采样值 | 欠压/过压保护判定、掉电确认 | 不含 | 保护动作点跟着变,属于硬件层面的变化,需要重新标定 |
| 修正后的值 | 显示、上报、日志记录 | 含 | 只影响人看到的数字,不影响任何保护行为 |
| 修正量本身 | 标定用,存在参数表里可现场改写 | — | 只影响显示与上报;改它不会动保护阈值 |
顺序上还有一个小前提:先做滤波,再加修正,不要反过来。 反过来的话,滤波会去平均一个已经被平移过的序列, 虽然数值上差别不大,但会让“这段数据到底经没经过修正”变得说不清, 而这类说不清正是后面排查时最耗时间的地方。 滤波本身的取舍(排序去极值、中值、滑动平均)我在 《采样数据的处理》里写过。
最后补一句关于软件去抖的:判定不是命中一次就置故障, 而是连续命中达到设定次数才算。 这样做的原因和脉冲消抖一样——单次采样完全可能是干扰造成的孤立尖峰, 但连续多次都命中,就说明电压是真的在那里。 这个“设定次数”以及“判定值”我都是做成参数项的, 因为不同现场对保护动作的敏感度不一样。
七、几条我反复用到的经验
- 先分清瞬时掉电与真掉电。它们的处理方式相反,混在一起处理必然两头不讨好。
- 检测点尽量往电源链上游放。多一根线、多一个分压,就能多买到一段预警时间。
- 数字脚负责“预告”,ADC 负责“保护”。预告要快,保护要准,别让一路信号干两件事。
- 预警和确认分开。预警要快(整理数据、停止机构),确认要慢(持续满足窗口才算数)。
- 提前把数据整理进固定缓冲。真正落盘时只搬运、不计算,才不会把预算花在意外的地方。
- 写记录要能自证完整。带序号和校验,写坏了下次上电能认出来,比“希望它写完”可靠得多。
- 恢复之后不要立刻复位。电源稳定、系统不在运行态、执行机构全停,三条都持续成立才允许复位。
- 判定值与显示值分开。修正量只影响人看到的数字,绝不能静默地移动保护阈值。
- 先滤波后修正,顺序固定下来,别让数据“经没经过修正”变成需要考证的事。
- 一个参数不要干两件事。调来调去都不对的时候,先怀疑是不是判据少了一个。
参考资料与说明
- MCU 参考手册中电源管理、欠压复位(BOR/POR)与 ADC 章节:工作电压范围、复位门限与采样保持时间说明,属于公开文档。
- 线性稳压器与 DC-DC 数据手册中关于输入跌落、输出保持时间与负载瞬态响应的说明,属于公开文档。
- 关于“掉电检测与数据保存”的通用应用笔记,以及超级电容/储能电容持电时间的估算方法,属于公开技术资料。
- 本文涉及的时间与单位换算(节拍、毫秒、秒之间的转换)属于同类问题的集中来源,我另做了一篇整理,见《定时器与单位:五个反复出现的坑》。
- 出于对个人项目实现细节的保护,本文只公开方法与思路;涉及核心实现的部分以步骤与字段说明代替代码, 示例代码为按个人理解重写的通用片段。文中的电压阈值、修正量、滤波时间、保持窗口与延时数值等产品特定参数已全部省略; 第五节里的第二段代码是错误示范,仅用于说明问题。
- 文中出现的芯片型号仅用于说明技术方案,与相关厂商无隶属或授权关系。