做单片机开发,很多人把注意力都放在功能逻辑上——通信协议、显示刷新、电机控制、传感器采集——电源只要一上电能点亮,就觉得万事大吉。直到产品在市场里碰上电压跌落,设备随机重启、EEPROM数据错乱、继电器乱跳,客户当场退货,这时候才回头研究电源问题。我在这个坑里栽过跟头,所以专门写一篇关于单片机欠压保护、欠压复位的博文,把这套机制彻底说透:它到底是什么、阈值怎么定、硬件和软件分别怎么实现、实际项目里有哪些防不胜防的坑。
欠压保护解决的是“电源电压跌到某个危险值”时,系统能否安全退出运行状态的问题;欠压复位解决的是“电压已经低到无法维持稳定运行”时,系统能否干净地重启的问题。两者是兄弟,但做的事情不一样。这篇文章的内容,从原理到选型,从电路计算到代码实现,都按实际工程的逻辑来,适合正在做产品设计的工程师,也适合做课设、毕设的在校学生。
1. 欠压到底是什么:掉电瞬间系统里发生的连锁反应
欠压,或者说电源电压跌落,并不只是“电压数字变小”这么简单。它背后是一连串的连锁反应,而且每个环节都可能导致产品表现异常。
1.1 电压不会“突然没了”,而是逐级跌破
很多人误以为欠压就是停电或者电池没电,系统直接从正常状态跳到关机状态。真实情况远没有那么干净。市电供电的设备,电网波动、大功率负载启动都会让整流后的母线电压瞬间拉低;电池供电的设备,随着电量下降,电压是缓慢下滑的;即便用DC-DC或LDO做二次稳压,当输入侧电压垮掉时,输出侧也会跟着垮——只不过是延迟一点、幅度小一点而已。
更要命的是,同一个系统里有好几种电源轨。比如单片机用3.3V,继电器驱动用5V,传感器用12V。欠压不是“单片机3.3V先掉”,往往是12V或者5V先撑不住。12V掉到9V,继电器可能还能勉强吸合,但吸合力下降,触点抖动;5V掉到4.5V,逻辑芯片已经开始超出工作范围了。所以做欠压保护,一定要站在整个系统的角度去看“谁的电压先顶不住”,而不是只盯着单片机那一路。
1.2 到达单片机之前,欠压先击穿哪些环节
我在项目里总结过,电压下降的过程里,最先出问题的往往不是MCU,而是外部电路。
第一梯队是继电器、电磁阀、接触器这类电磁驱动器件。它们是纯机械结构,吸合力跟线圈电压直接相关。电压偏低时,吸合力不足,触点接触电阻变大,通流能力下降,严重的会出现触点反复通断,电火花直接烧蚀触点。
第二梯队是模拟前端和通信芯片。比如RS485收发器,工作电压偏低时输出驱动能力下降,总线电平摆幅不够,远距离通信就开始误码;运放和ADC如果供电不足,采集到的电压信号会直接偏掉,悬空或接近参考电压时尤其明显。
第三梯队才是单片机本身。MCU虽然标称工作电压范围宽,比如STM32F103的供电范围是2.0V到3.6V,但在电压偏低时,Flash读取可能出错、内部RC振荡器频率会漂、GPIO输出高电平不达标、复位电路可能误触发。而且不同寄存器的保持电压并不完全一样,有些外设先失效,CPU还在跑,这种“半脑死亡”状态最难受——程序看着还在运行,但逻辑已经全乱了。
1.3 项目里哪些场景最容易踩中欠压
结合我做过的几个具体项目,下面这些场景是欠压问题的高发区:
- 电机驱动板:电机启动瞬间的堵转电流或启动电流可以达到额定电流的5到7倍,如果电源余量不够,母线电压会被直接拉下去一大截。
- 电磁炉等强电控制板:可控硅调功、IGBT开关瞬间的浪涌电流很大,电网电压稍微不稳,控制板电源就容易出现跌落。
- 车载电子:汽车启动时电瓶电压可能从12V跌到8V甚至更低,车载设备如果直接从电瓶取电,必须做宽压输入和欠压保护。
- 电池供电设备:锂电池放电到截止电压附近时,电量曲线会突然掉得很快,电量显示往往还没到底,设备就罢工了。
- 大电容储能设备:断电后靠电容维持供电的电路,电压下降速度其实很快,如果软件没有在电压尚可时完成关键数据保存,就会出现数据丢失或Flash损坏。
2. 保护阈值与回差:先搞清楚“以谁为准”再定参数
很多新手一上来就问“欠压阈值设多少合适”,这其实是一个伪命题。阈值不取决于某一颗芯片的标称值,而是取决于整个系统里对电压最低可接受的要求。这个要求得从“最脆弱的那个器件”倒推出来。
2.1 阈值不能拍脑袋:从系统最脆弱器件倒推
拿一个典型的5V系统举例:单片机最低能跑到2.7V,但继电器在3.5V以下吸合力就明显不足,RS485收发器低于4.5V就开始误码。那这个系统的欠压保护阈值就不能定在3.0V——那时候单片机还能跑,但继电器已经乱跳了,通信已经乱套了。阈值应该以RS485收发器的4.5V为基准,再留一点余量,定在4.0V到4.2V比较合理。
设计逻辑是这样一个顺序:
- 枚举系统里所有用电模块的最低可靠工作电压。
- 取其中的最大值,作为欠压保护阈值的下限基准。
- 加上设计余量(通常0.2V到0.5V,取决于电源纹波大小)。
- 确保正常工作时电源电压的最低点不会下探到这个阈值。
举个例子,一个5V系统,如果知道纹波峰峰值有200mV,那阈值不能设成4.9V,否则正常工作时也会频繁触发欠压。合理的做法是:正常电压范围5.0V±5%,最恶劣情况下低到4.75V,那欠压阈值设在4.2V到4.5V之间,既躲开正常波动,又能在真正出问题前报警并停机。
2.2 回差是怎么算的?以4.2V阈值为例
回差,也叫滞回宽度,是欠压保护里最容易忽略的细节。如果保护阈值和恢复阈值完全一样,就会出现临界抖动:电压刚低于阈值,系统保护停机;电压稍微恢复一丁点,保护解除又开始工作;大负载一启动,电压又掉下去……系统就会在保护和不保护之间反复横跳,继电器噼里啪啦响,接触器疯狂通断,非常伤硬件。
正确做法是设置两个阈值:一个欠压触发阈值,一个恢复阈值,中间留出滞后区间。比如欠压阈值4.2V,恢复阈值4.4V,回差就是200mV。这200mV的回差,实际含义是“电压跌到4.2V以下触发保护,但一定要升到4.4V以上才解除保护”。
回差大小的工程经验值,一般是系统最大纹波峰峰值的2倍左右。纹波100mV,回差就取200mV。太小,无法滤掉纹波带来的反复触发;太大,恢复门槛太高,电网上来之后系统迟迟不启动,用户体验很差。
3. 硬件实现:比较器方案、专用芯片和单片机内置检测
欠压保护可以用硬件实现,也可以用软件实现,实际项目里经常是混合着来。硬件的核心优势是响应快、不依赖程序状态,哪怕CPU已经跑飞了,它也能兜底。这一节把三种主流硬件方案讲清楚,包括参数计算和选型细节。
3.1 电阻分压+比较器方案的设计与计算
最经典的电路是用一颗电压比较器,比如LM393、LM339,加上两个分压电阻和一个基准电压源。我的做法是这样的:
把被测电源电压(比如5V)用电阻分压,分压点接到比较器的负输入端;正输入端接一个基准电压(用TL431或稳压二极管产生);当分压点电压低于基准电压时,比较器翻转,输出端从高电平变成低电平。这个低电平接到单片机的外部中断引脚,或者直接接到复位引脚上。
分压电阻的大小需要算一下。假如要检测阈值4.2V,基准电压选2.5V(TL431很常用),那分压比就是2.5 / 4.2 ≈ 0.595。取R2 = 100kΩ,R1 = 68kΩ,实际分压比100 / (100 + 68) ≈ 0.595,正好匹配。电阻一定要选1%精度的,普通5%误差的电阻,在批量生产时会导致阈值偏差很大——同一批板子,有的4.1V就触发,有的4.3V才触发,非常头疼。
这里有几个容易被忽视的细节:
- LM393的输出级是开集电极,必须加一个上拉电阻到MCU的IO电压域,10kΩ就够。
- 分压点可以加一个10nF的小电容到地,滤掉高频毛刺,但不要加大电容,否则响应速度会变慢。我用68kΩ的源电阻加10nF电容,时间常数约0.68ms,还可以接受。
- 要让比较器有回差,可以在正输入端和输出端之间接一个正反馈电阻,让输出电平反过来改变参考电压。算回差宽度时,要用正输入端等效电阻和反馈电阻的比例关系来算,工程上直接用可调电阻调试更方便。
3.2 专用电压检测芯片的选型要点
如果不想自己算电阻,也不想折腾比较器,直接用电压检测芯片最省心。这类芯片内部集成了比较器、基准源和迟滞,体积小、精度高、温漂小,一颗就能搞定。
常见的型号包括:TPS3839、MAX809、TCM809、STM809、APX803等。选型时主要看这几个参数:
| 参数 | 含义 | 选型建议 |
|---|---|---|
| 检测阈值 | 触发电平 | 选择最接近你算出来的系统阈值的型号 |
| 滞回电压 | 恢复阈值与触发阈值之差 | 一般在20mV~100mV之间,满足要求即可 |
| 输出类型 | 推挽或开漏 | 开漏型号适用于不同电压域的电平转换 |
| 复位延时 | 电压恢复后延时释放复位的时间 | 常用140ms,用于等待电源稳定 |
| 静态电流 | 芯片自身功耗 | 电池供电产品要特别关注,选1μA以下 |
比如TPS3839系列有3.08V、2.93V、2.63V、1.67V等多种阈值可选;STM809支持5V、3.3V、3.0V等常见电压。这类芯片另一个好处是输出可以直接接到MCU的NRST引脚,实现硬件级的欠压复位,接线非常简洁,PCBA面积也省。
3.3 单片机内置掉电检测的用法(STC/STM32)
很多单片机内部就集成了掉电检测电路,不需要额外加芯片。51家族里STC系列的低压检测(LVD)就很实用,比如STC89/STC12/STC8系列,通过配置低压检测寄存器,可以设置成在电源电压跌到某个阈值时产生中断或复位。
STC的低压检测用法大概是:初始化时设置LVD阈值(有3.7V、4.2V等档位可选),使能低压检测中断。电压掉到阈值以下时,内部比较器触发,程序进入中断服务函数,在里面把需要保存的关键数据存到EEPROM,然后等待电压进一步跌落或主动关闭负载。这个方案成本为零,缺点是阈值档位有限,不如外部芯片灵活。
STM32系列对应的功能叫PVD(Programmable Voltage Detector,可编程电压检测器)。PVD的阈值不是随便选的,寄存器里有一组固定档位,比如2.2V、2.3V、2.4V……一直到2.9V。配置方法是在PWR_CR寄存器里设置PLS,再使能EXTI16中断。PVD复位或中断的机制是:当VDD电压从高于阈值降到低于阈值时产生事件;反过来说,从低于阈值升到高于阈值时也会产生事件。所以用PVD还能检测“电压恢复正常”这个状态。
STM32 PVD的初始化代码看起来类似这样:
void pvd_init(void) { // 选择PVD阈值,例如2.9V PWR_CR |= PWR_CR_PLS_LEV7; // 使能PVD PWR_CR |= PWR_CR_PVDE; // PVD连接到EXTI线16,使能上升沿和下降沿中断 EXTI_InitTypeDef exti; exti.EXTI_Line = EXTI_Line16; exti.EXTI_Mode = EXTI_Mode_Interrupt; exti.EXTI_Trigger = EXTI_Trigger_Rising_Falling; exti.EXTI_LineCmd = ENABLE; EXTI_Init(&exti); } void EXTI15_10_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line16) != RESET) { if (PWR_GetFlagStatus(PWR_FLAG_PVDO)) { // VDD低于PVD阈值:执行欠压保护动作 SaveCriticalData(); DisableHeavyLoad(); } else { // VDD恢复:清理欠压标志,准备恢复工作 ClearUnderVoltageFlag(); } EXTI_ClearITPendingBit(EXTI_Line16); } }4. 软件实现:ADC采样、状态机与受控复位
硬件方案响应快,但欠压保护如果全交给硬件,会有两个问题:一是只知道“电压低了”,不知道“低了多少”;二是没有机会在断电前完成数据保存和负载关闭。所以实际产品里,我一般用“软件为主、硬件兜底”的策略。
4.1 什么时候必须用软件方案
软件做欠压检测,最大的价值是能做到分级保护。比如电池供电设备,电压降到3.5V时,先降低屏幕亮度、关掉震动电机,提醒用户充电;降到3.3V时,停止大功率操作,保存现场数据;降到3.1V时,彻底进入低功耗模式,只留一颗外部唤醒芯片在等待。这种多级管理,硬件比较器很难做到。
另外,有些单片机没有内置LVD,也不想额外加电路,那就只能用ADC采样电源电压。做课设、毕设的同学经常遇到这种情况,比如51单片机,自己做个电阻分压,把分压点接到ADC引脚,程序里判断电压值就够了。
4.2 ADC采样与滤波的工程细节
用ADC测电压,第一步是确定分压电阻和参考电压的关系。以5V系统、单片机ADC参考电压5V为例,直接给一个电阻分压比例1:1,把VCC分压到ADC输入范围的一半,留出测量余量。
软件里要做滤波。我常用的方法是中值滤波加滑动平均:连续采样5次,去掉最大值和最小值,剩下的取平均。这个操作能滤掉大部分随机噪声,但不适合滤掉“电压真的突然掉了”这种情况——如果电压在快速跌落,滤波反而会“平滑”掉真实变化。所以采样周期要短,比如每1ms采一次,连续判断3到5次再确认欠压。响应慢几毫秒没问题,但抗干扰能力会强很多。
一个容易被忽略的点是ADC采样时机。电机PWM开关瞬间、继电器吸合瞬间、射频模块发射瞬间,电源上都会有大毛刺。如果刚好在这个时刻采样,测到的电压可能比真实值低很多。我现在的做法是:把欠压检测放到主循环固定位置执行,避开中断密集区;如果负载开关是可编程的,在开关瞬间前后各跳过几个采样周期。
4.3 欠压检测状态机:消抖逻辑与动作分发
不要在主循环里用delay等待电压恢复,那样会把整个系统卡死。我建议用状态机来实现欠压检测的消抖和动作分发,下面的代码是一个可以参考的框架:
typedef enum { VOLT_NORMAL, VOLT_CHECK, VOLT_LOW, VOLT_RECOVER } VoltState; #define LOW_CONFIRM_CNT 5 #define HIGH_CONFIRM_CNT 5 #define ADC_LOW_TH 2810 // 对应4.0V #define ADC_RECOVER_TH 2960 // 对应4.25V,回差约0.25V static VoltState volt_state = VOLT_NORMAL; static uint8_t confirm_cnt = 0; void voltage_monitor_task(void) { uint16_t adc_val; adc_val = read_avg_adc(); switch (volt_state) { case VOLT_NORMAL: if (adc_val < ADC_LOW_TH) { volt_state = VOLT_CHECK; confirm_cnt = 0; } break; case VOLT_CHECK: if (adc_val < ADC_LOW_TH) { confirm_cnt++; } else { volt_state = VOLT_NORMAL; break; } if (confirm_cnt >= LOW_CONFIRM_CNT) { // 确定进入欠压状态 save_critical_data(); disable_heavy_load(); volt_state = VOLT_LOW; } break; case VOLT_LOW: // 电压必须回升到恢复阈值以上,才允许恢复流程 if (adc_val > ADC_RECOVER_TH) { volt_state = VOLT_RECOVER; confirm_cnt = 0; } break; case VOLT_RECOVER: if (adc_val > ADC_RECOVER_TH) { confirm_cnt++; } else { volt_state = VOLT_LOW; break; } if (confirm_cnt >= HIGH_CONFIRM_CNT) { // 电压稳定恢复,执行系统复位 system_reset(); } break; } }这套状态机的设计思想是:正常状态不能直接进入欠压处理,必须经过一段确认时间;欠压状态也不能直接回到正常,必须升到更高的恢复阈值并且持续稳定一段时间。这样处理之后,那些瞬间的电压毛刺根本不会引发误保护。
4.4 欠压后的处理顺序:保存、关负载、受控复位
确认欠压之后,处理顺序是有讲究的。我的推荐顺序是:
- 先保存最关键的数据。把系统状态、本次运行参数、错误码写入EEPROM或备份寄存器。注意,Flash和EEPROM写入本身需要时间,如果电压已经极低,可能写一半就断了。这种情况下,要评估写入时间,必要时用一个大电容储能,维持几十毫秒供电,保证写入完成。
- 关闭大负载。继电器、电机、加热丝这些,能关就关。这样做的目的是降低系统电流消耗,让电源电压不要继续快速下跌,给后续操作争取时间。
- 设置欠压标志。标志写到RAM和EEPROM各一份。下次上电时,程序先读这个标志,决定是直接进入正常运行,还是先提示用户“上次是异常断电”。
- 受控复位或者进入低功耗。如果电压低到接近MCU的最低工作电压,与其让程序在“半脑死亡”状态下乱跑,不如主动复位。这就是欠压复位的意义:与其等电压低到硬件复位门限,不如在电压还没彻底垮掉的时候,干净地把系统重置到初始状态。
关于软复位,51单片机没有统一的软复位指令,常见的办法是ISP_CONTR |= 0x60(针对STC系列),或者直接把一个软件看门狗喂不上让它超时复位。STM32直接用NVIC_SystemReset(),干净利落。
5. 欠压复位和其他“看起来像”的复位有什么区别
复位有很多种,合在一起很容易晕:上电复位、欠压复位、看门狗复位、软件复位……它们触发条件不同、后果不同,不要混为一谈。搞清楚区别,排查问题时才能少走弯路。
5.1 四类复位的触发条件与实现对比
| 复位类型 | 触发条件 | 特点 | 典型实现 |
|---|---|---|---|
| 上电复位(POR) | 电源从0开始上升 | 每次上电必触发,清除所有寄存器 | 片内上电复位电路 |
| 欠压复位(BOD/LVD) | 电源电压低于MCU最低工作电压 | 电压跌落过程中触发,防止MCU乱跑 | 专用电压检测芯片、片内BOD电路 |
| 看门狗复位 | 程序跑飞,未及时喂狗 | 由软件超时触发,硬件自动执行 | 独立看门狗或窗口看门狗 |
| 软件复位 | 程序主动调用复位指令 | 完全受控,用于执行重启流程 | NVIC_SystemReset、软复位指令 |
上电复位和欠压复位的区别在于:上电复位发生在电压上升阶段,欠压复位发生在电压下降阶段。它们在很多芯片内部用的是同一套检测电路,只是触发条件不同。
5.2 欠压保护与欠压复位的配合关系
这两个概念经常被放在一起说,但物理意义不一样。欠压保护是“发现电压危险之后,系统采取的保护动作”,比如保存数据、关闭负载、进入安全状态;欠压复位是“电压实在太低,必须重启系统”这个最终处置动作。我用一个比喻来解释:欠压保护就像是人在身体出现异常信号时主动停下休息、吃药;欠压复位则是已经晕倒被送进急诊室抢救,强制重启。
一个设计良好的系统,欠压保护和欠压复位是配合使用的:软件检测到电压接近危险区,执行保护动作,把现场数据保存好;如果电压还在继续掉,突破了MCU的复位门限,硬件自动复位——此时关键数据已经保存完,损失被降到最低;电压恢复后,MCU重新上电启动,读取欠压标志,恢复现场或者按安全模式运行。
6. 实测踩坑:三个案例还原完整排查链路
写了这么多原理,最后分享几个我实际调过的故障案例。这些案例不是个例,几乎每个项目里都能找到类似的影子。
6.1 案例1:电机启动瞬间系统反复重启
现象:一块电机驱动板,主控是STM32F103,电源是24V转5V再转3.3V。电机一启动,板子就反复重启,但用万用表量5V电压,只看到4.8V左右,并没有低到离谱。
排查过程:一开始怀疑是看门狗的问题,把看门狗关了,现象依旧;后来怀疑是软件逻辑问题,把主循环里所有任务都屏蔽,现象反而更频繁了。用示波器直接看3.3V引脚,才发现电机启动瞬间有约300ms的连续毛刺,其中若干尖峰已经低于2.9V,而我在程序里把PVD阈值设到了2.9V。虽然平均电压看着没事,但毛刺已经频繁触发PVD中断,中断里我又执行了保存数据和延时,导致主循环没法正常运行。
修复方案:第一步把PVD阈值从2.9V降到2.7V,避开毛刺;第二步在电机驱动IO上增加了软启动逻辑,把PWM占空比从0逐渐升高,避免启动瞬间电流尖峰;第三步在3.3V输出端加了一个100μF的钽电容,把瞬态压降进一步拉低。三层处理之后,重启问题彻底消失。
这个案例的教训是:不要用万用表测电压来判断瞬态问题,要用示波器抓毛刺;欠压检测的阈值设置,必须把瞬态纹波考虑进去。
6.2 案例2:比较器回差太大导致恢复失败
现象:一块用硬件比较器做欠压保护的板子,电源电压恢复正常后,设备不启动,必须手动断电再上电才能恢复。
排查过程:查电路,发现当初为了图省事,直接在比较器正输入端和输出端之间接了一个100kΩ的正反馈电阻,没仔细算回差。实测回差高达800mV,保护阈值是4.2V,恢复阈值要等到5.0V才能解除——而实际电源电压恢复之后稳定在4.9V,刚好卡在恢复阈值下面,于是保护一直锁死。
修复方案:把正反馈电阻换成470kΩ,回差缩到约150mV,问题解决。这个坑说明:回差不是越大越好,过了头就会造成欠压恢复困难。
6.3 案例3:掉电瞬间EEPROM数据丢写
现象:电池供电的产品,客户反馈偶尔出现参数被清零、配置丢失的情况。查日志发现,都是发生在电池电压极低、设备即将关机的那一段时间。
排查过程:掉电瞬间,软件检测到电压偏低,开始往EEPROM里写当前参数。问题是EEPROM写入需要时间,比如常见的AT24C02在5V下页写需要5ms,而电压从触发欠压保护到彻底断电可能只有两三毫秒。写入还没完成,电压已经没了,EEPROM里就留下了一个半写状态的坏数据,下次上电读出来校验不过,直接被当成无效数据。
修复方案:硬件上加了一个470μF的储能电容,加大掉电后的维持时间;软件上调整逻辑,在欠压触发后先关闭所有大负载,再执行EEPROM写入;写入前先读回校验,如果写入失败就重试一次,再失败就标记为“未完成写入”,保证不会用坏数据顶替好数据。
6.4 其他容易踩的细节:检测点、钳位、上电时序
最后再列几个我经常提醒自己的细节:
- 检测点位置要选对。不要只检测LDO输出端,还要看单片机的实际供电引脚。如果电源路径上有二极管或者MOS管做防反接,压降可能不小,检测点和MCU实际电压之间会有差异。
- 电机等感性负载关断瞬间会产生反电动势,可能导致电源轨出现负压。负压灌进比较器或者ADC引脚,轻则读数不对,重则烧芯片。解决办法是在电源轨到地之间加一个肖特基二极管做钳位。
- 上电时序也要注意。有些系统的欠压保护阈值设得比较高,上电时电源电压从0爬升,会先处于“欠压”区间。如果保护电路在上电瞬间被触发,并且因为是电平锁存型导致无法自动恢复,设备就永远启动不了。设计时必须允许上电过程中有一段“欠压容忍时间”,或者在检测到电压恢复后自动解除保护。
做欠压保护和欠压复位,说到底就是在跟时间赛跑。电压掉落的过程通常只有几毫秒到几十毫秒,系统要在这段时间里完成判断、确认、保存、关机这一整套动作。我的经验是,先搞清楚系统里谁最怕电压低,再把阈值倒推出来;硬件负责兜底快速响应,软件负责分级处理和现场保护;回差和消抖一定要做,不然就是给自己埋雷。调完所有逻辑,记得拿示波器在阈值附近多抓几次瞬态波形,确认没有临界抖动再批量出货。这个功能看着不起眼,但真出问题时,它往往是让产品在严酷现场活下来的最后一道防线。