简介:这份资料包面向正在学习STC15F408AS增强型8051单片机的嵌入式初学者与进阶开发者,聚焦LVD低压检测这一实用功能,分别用汇编与C语言两种查询方式给出完整实现,帮助读者理解外设状态轮询的编程思路与低电压保护机制。压缩包共18个文件,约13KB,以Keil工程文件(uv2、opt、lnp、plg)和编译产物(hex、bak)为主,另含main.asm与main.c两份核心源码,工程结构清晰,可直接打开对照学习。已有272人学习下载,说明该例程在同类核心板资料中具有一定参考价值。读者可从中获得汇编与C两种查询方式的完整代码框架、初始化与状态判断的写法对照,以及LVD触发保护的实际配置思路,便于快速移植到自己的低功耗或电池供电项目中。
1. 32-LVD低压检测:一个压缩包标题背后的真实工程问题
第一次看到「32-LVD低压检测.zip」这个标题,多数人第一反应是去找源码,但真正在一线做过电池供电设备的人会先问三个问题:32 指的是 32 位平台还是 32 路通道?LVD 是 Low Voltage Detect 还是 Low Voltage Disconnect?检测对象是单节锂电、多节串包,还是整机母线?这三个问题答错一个,后面所有参数都是白调。LVD 在嵌入式语境里通常指低压检测/低压保护逻辑,核心诉求是让系统在电源跌到危险区之前做出确定动作——报警、降功耗、保存现场、切断负载。它看起来只是几个 ADC 采样加阈值比较,实际是采样精度、迟滞设计、去抖策略、掉电时序四件事叠在一起。这篇笔记面向正在做电池供电产品、需要把低压检测从「能跑」做到「敢量产」的固件和硬件工程师,也适合刚接手一个来历不明的检测压缩包、想先判断它值不值得复现的人。
2. LVD 低压检测到底在检测什么:从分压采样到阈值判定
2.1 先分清 LVD 的三种语义,别把保护和解算混在一起
LVD 这个词在不同芯片手册里指的东西并不一样,落地前必须先对齐语义,否则你会拿着一个「检测中断」的配置去实现「切断负载」的需求。常见做法是把它拆成三层:第一层是检测,只负责把电源电压采回来并和阈值比较,输出一个状态位;第二层是判定,在检测基础上加迟滞、去抖、多通道一致性校验,输出一个可信的告警事件;第三层是动作,根据告警事件执行降频、关外设、写 Flash、断负载。很多翻车案例的根因就是把三层揉成一个 if 判断,结果电压在阈值附近抖动时系统反复进出保护态,现场表现为「设备自己重启」。
从选型角度看,如果 MCU 自带硬件 LVD(多数 Cortex-M 和部分 8 位机都有),优先用硬件比较器做第一层,因为它不依赖 ADC 采样调度,响应是微秒级;但硬件 LVD 的阈值档位通常是固定的几档,精度和可调性差,适合做「最后一道保险」。需要精确阈值、需要多路监测、需要记录电压曲线的场景,还是得用 ADC 加分压电阻自己算。我的习惯是两者都留:硬件 LVD 设一个略低于软件阈值的兜底档,软件 ADC 负责正常判定和日志,这样即使软件跑飞,硬件还能兜住。
标题里的「32」如果指 32 路通道,那本质是一个多通道扫描问题,重点在采样时序和通道间串扰;如果指 32 位平台,重点在定点运算和 Flash 写入时序。两种解读对应的代码结构差别很大,复现前先看压缩包里有没有通道配置表或引脚映射,没有的话基本可以判断它只是个演示级 Demo。
2.2 分压采样链路:电阻取值、参考电压和 ADC 有效位怎么定
低压检测的第一颗坑往往不在代码,而在分压电阻。假设母线最高 16.8V(四节锂电满电),ADC 参考 3.3V,分压比至少要 16.8/3.3≈5.1,取 6 倍留余量,常见取法是上臂 100k、下臂 20k,分压比 6。这里有两个容易被忽略的点:一是电阻精度,1% 电阻带来的分压比误差约 2%,直接吃掉阈值余量;二是下臂电阻不能太大,否则 ADC 采样保持电容充电不足,读数偏低且随通道切换漂移。经验值是下臂不超过 20k,采样时间给到 5 微秒以上。
ADC 有效位也要算清楚。12 位 ADC 在 3.3V 参考下理论分辨率 0.8mV,但实际有效位受噪声影响通常只有 10 位左右,折算到母线上是 6×3.3/1024≈19mV 每 LSB。如果你的保护阈值要求 ±50mV 精度,单次采样是不够的,必须做多次平均或中值滤波。下面这段是常见的采样与换算骨架,重点看注释里的参数含义:
/* 单通道低压采样:多次采样取中值,再按分压比换算母线电压 */ #define ADC_FULL_SCALE 4095.0f /* 12 位 ADC 满量程 */ #define VREF_MV 3300.0f /* 参考电压,单位 mV */ #define DIV_RATIO 6.0f /* 分压比 = (R_top + R_bottom) / R_bottom */ #define SAMPLE_NUM 9 /* 取奇数便于取中值 */ static uint16_t adc_read_once(uint8_t ch); float lvd_read_bus_mv(uint8_t ch) { uint16_t buf[SAMPLE_NUM]; for (int i = 0; i < SAMPLE_NUM; i++) { buf[i] = adc_read_once(ch); } /* 简单插入排序取中值,9 个元素开销可接受 */ for (int i = 1; i < SAMPLE_NUM; i++) { uint16_t key = buf[i]; int j = i - 1; while (j >= 0 && buf[j] > key) { buf[j + 1] = buf[j]; j--; } buf[j + 1] = key; } float code = buf[SAMPLE_NUM / 2]; /* 先算引脚电压,再乘分压比还原母线电压 */ return code / ADC_FULL_SCALE * VREF_MV * DIV_RATIO; }这段代码的逻辑是:单次采样容易被开关电源纹波和通道切换干扰,取中值比取平均更能抵抗偶发尖峰;DIV_RATIO必须和实际焊接的电阻一致,改硬件不改这个宏是最常见的「读数对不上」原因;VREF_MV如果用的是内部参考而非 VDDA,要按实际参考源填,不能想当然写 3300。参数调整上,SAMPLE_NUM增大能提升稳定性但增加采样时间,9 到 15 是常见区间;如果 ADC 时钟较慢,采样间隔要留够,否则中值也救不回来。
2.3 阈值、迟滞和去抖:三个参数决定保护逻辑稳不稳
阈值不是拍脑袋定的。以单节锂电为例,工作下限通常取 3.3V,保护阈值取 3.0V,恢复阈值取 3.2V,中间 200mV 就是迟滞带。迟滞太小,负载瞬间电流把电压拉低就触发保护,负载一撤又恢复,形成振荡;迟滞太大,电池已经该保护了系统还在硬撑。经验做法是迟滞带取「最大负载阶跃引起的电压跌落」的 1.5 倍左右,这个值要实测:接上最大负载,用示波器看母线跌落幅度,再定迟滞。
去抖是时间维度的迟滞。电压低于阈值后不能立刻动作,要连续 N 次采样都低于阈值才确认,N 次乘以采样周期就是确认时间。确认时间太短抗不住瞬态,太长则真正掉电时来不及保存现场。常见配置是采样周期 10ms、连续 5 次确认,即 50ms 确认窗。下面是一个带迟滞和计数的判定函数:
/* 带迟滞与去抖的低压判定:返回 1 表示确认低压 */ #define TH_LOW_MV 3000.0f /* 触发阈值 */ #define TH_RECOVER_MV 3200.0f /* 恢复阈值,必须高于触发阈值 */ #define DEBOUNCE_CNT 5 /* 连续确认次数 */ static uint8_t low_cnt = 0; static uint8_t is_low = 0; uint8_t lvd_check(float bus_mv) { if (!is_low) { if (bus_mv < TH_LOW_MV) { if (++low_cnt >= DEBOUNCE_CNT) { is_low = 1; low_cnt = 0; } } else { low_cnt = 0; /* 未达阈值则清零,避免累积误判 */ } } else { if (bus_mv > TH_RECOVER_MV) { is_low = 0; /* 只有回到恢复阈值以上才解除 */ } } return is_low; }逻辑说明:is_low是状态机而不是单次比较结果,保证进入和退出走不同阈值;low_cnt在电压回升时清零,防止「断续低压」被累加成误触发。参数上,TH_RECOVER_MV必须严格大于TH_LOW_MV,写反了会锁死在保护态;DEBOUNCE_CNT与采样周期共同决定确认时间,改采样周期时这个计数要同步复核,这是血泪经验里最常见的一处遗漏。
3. 把 32-LVD 检测跑起来:从通道配置到动作执行的落地路径
3.1 通道与引脚配置:32 路扫描的时序和串扰控制
如果标题里的 32 指通道数,核心矛盾是「扫描一遍要多久」和「通道之间会不会互相污染」。多通道 ADC 扫描的常见问题是前一通道的高电压残留在采样保持电容上,导致后一通道读数偏高,尤其是高低压通道相邻时。解决办法有两个:一是通道顺序上把高压通道和低压通道交错开,中间插入一个接地通道做放电;二是每次切换通道后丢弃第一次采样,用第二次的结果。扫描周期要按最坏情况算:32 通道 ×(切换时间 + 采样时间 + 转换时间),如果单通道 20 微秒,一轮就是 640 微秒,保护确认窗 50ms 内能扫 78 轮,够用;但如果采样时间给到 100 微秒,一轮 3.2ms,50ms 只能扫 15 轮,去抖计数就要相应调整。
配置层面,我一般会维护一张通道表,把通道号、分压比、用途、阈值档位写在一起,代码里按表遍历,避免 32 个 if 散落各处:
typedef struct { uint8_t ch; /* ADC 通道号 */ float div_ratio; /* 该通道分压比 */ float th_low_mv; /* 该通道低压阈值 */ uint8_t action_id; /* 触发后执行的动作编号 */ } lvd_channel_t; static const lvd_channel_t lvd_table[] = { { 0, 6.0f, 3000.0f, 1 }, /* 主母线 */ { 1, 1.0f, 1800.0f, 2 }, /* 内部 1.8V 轨 */ /* ... 其余通道按实际硬件填写 ... */ };这张表的价值在于把「硬件参数」和「判定逻辑」解耦,换板子只改表不改算法。div_ratio为 1.0 表示直采,适合监测芯片自身电源轨;action_id让不同通道触发不同动作,比如主母线低压先降功耗,内部轨低压直接复位。注意表里的阈值必须和 2.3 节的迟滞逻辑配套,每个通道独立维护自己的状态和计数,不能共用一个全局low_cnt,否则通道之间会互相干扰。
3.2 低压动作序列:保存现场、降功耗、断负载的先后顺序
检测到确认低压之后做什么,比检测本身更容易出事。正确的顺序是:先冻结非关键任务并停止写 Flash 之外的操作,再把关键现场写入非易失存储,然后关闭大电流外设(射频、电机、屏幕背光),最后根据策略决定是否切断负载或进入休眠。顺序反了会怎样?先关外设再存现场,可能存到一半电压已经跌到 Flash 写入下限以下,写进去的是脏数据;先断负载再存现场,如果负载就是系统自身供电路径,直接把自己断电了。
Flash 写入是掉电时序里最脆弱的一环。多数 MCU 的 Flash 在 2.7V 以下写入不可靠,所以低压动作的第一优先级是「在电压还够的时候把现场写完」。实操上会把现场数据压缩到最小(几十字节),用单次页写入完成,避免多页擦写拉长窗口。如果硬件有掉电中断(PVD 或比较器中断),把它配在比软件阈值高 100~200mV 的位置,作为「最后窗口」的触发源,软件低压判定负责提前预警,硬件中断负责兜底保存。这个双阈值结构是我做电池设备时的固定套路,能显著降低「电量低时配置丢失」的返修率。
3.3 用串口日志和电压曲线验证检测逻辑
检测逻辑写完不等于对。验证的第一步是打日志:每次采样输出原始码值、换算电压、当前状态机状态和计数,采样周期固定,连续跑够一个完整放电周期。第二步是把日志导出来画电压曲线,重点看三个位置:阈值穿越点是否干净、迟滞带内是否有反复跳变、动作执行时刻电压是否还在安全区。如果条件允许,用可编程电源做阶梯下降和叠加纹波的测试,比真实电池放电更容易复现边界。
一个容易被忽略的验证项是「上电瞬间」。电池插入瞬间电压会有跌落和反弹,如果检测在系统初始化完成前就跑,可能误判一次低压并触发保存,把正常上电变成一次异常写入。常见做法是上电后延时 200~500ms 再使能低压判定,或者要求连续确认次数在上电阶段加倍。验证清单里我会固定加一条:冷启动 50 次,统计误触发次数,非零就要回去查上电时序。
4. 32-LVD 低压检测的避坑清单:五条现场踩出来的经验
4.1 读数整体偏低:先查参考电压和采样时间,别急着改代码
现象是万用表量母线 12.0V,固件算出来 11.4V,偏差稳定在 5% 左右。原因通常有两个:一是VREF_MV填的是标称 3300 而实际内部参考是 3.0V 或 VDDA 只有 3.2V;二是采样时间不足,采样保持电容没充满,读数系统性偏低。解决方法是先用一个已知电压(比如稳压源 3.000V 直接进 ADC 引脚)标定参考,再按标定值改宏;采样时间按数据手册要求的下限乘 1.5 倍设置。稳定偏差优先怀疑参考和分压比,随机偏差才怀疑噪声。
4.2 阈值附近反复重启:迟滞和去抖只做了一个
现象是电池放到 3.0V 附近,设备每隔几秒重启一次。原因是只做了阈值比较没做迟滞,或者做了迟滞但去抖计数在恢复时没清零。解决方法是按 2.3 节的状态机结构,进入和退出用不同阈值,且计数在反向条件出现时立即清零。另一个隐蔽原因是低压动作里关了某个外设,恢复时没重新初始化,表现为「恢复了但功能不全」,这类问题要在动作序列里成对写「关」和「开」。
4.3 多通道互相污染:相邻通道读数跟着前一个通道跑
现象是 32 路扫描时,某个低压通道的读数会随前一个高压通道的电压变化。原因是采样保持电容残留。解决方法是在通道表里插入放电通道、丢弃切换后的第一次采样、或者把高压和低压通道在扫描顺序上隔开。如果通道数多,还可以分组扫描,每组之间加一次接地采样。这个坑在通道数少的时候不明显,一到 32 路就集中爆发。
4.4 掉电保存写坏:Flash 写入窗口没算够
现象是低电量时保存的配置偶尔变成全 0 或全 FF。原因是写入过程中电压跌破 Flash 最低写入电压,或者写入被复位打断。解决方法是把保存数据压到最小、用单次页写、把硬件掉电中断阈值设在软件阈值之上留出窗口,并在写入前校验电压仍高于安全线。写入后加校验和,读回不一致就标记为无效,下次上电用默认值兜底,这比写坏后静默使用危险数据安全得多。
4.5 上电误触发:初始化顺序埋的雷
现象是冷启动时偶发进入低压保护。原因是 ADC 和参考还没稳定、或者外设上电顺序导致母线瞬时跌落,检测逻辑已经开始判定。解决方法是在初始化完成并延时稳定后再使能判定,上电阶段确认次数加倍,必要时上电后第一次采样直接丢弃。这个问题的排查成本高,因为复现率低,建议在验证阶段就用冷启动循环压测。
5. 把低压检测做成可复用的模块:标定、回归与参数固化
做到能跑之后,真正决定这套东西能不能长期用的是三件事:标定流程、回归测试、参数固化。标定上,我会给每个板型留一组标定系数(参考电压实测值、分压比实测值),存在非易失区,固件启动时读取,而不是把标称值硬编码。这样换电阻批次或换参考源时不用改代码,产线也能逐板校准。标定方法很简单:产线夹具给两个已知电压点,固件算出增益和偏移写回,两点标定就能消掉大部分系统误差。
回归测试上,把 3.3 节的验证清单固化成脚本能跑的流程:阶梯降压、纹波叠加、冷启动循环、最大负载阶跃,每改一次判定逻辑就跑一遍,记录误触发次数和动作时刻电压。参数固化上,阈值、迟滞、去抖计数、动作序列这些不要散落在代码各处,集中到一个配置结构体或配置区,版本号和硬件版本绑定,出问题时能一眼看出用的是哪套参数。下面是一个配置固化的最小结构:
typedef struct { uint16_t cfg_version; /* 配置版本,与硬件版本对应 */ float vref_mv; /* 实测参考电压 */ float th_low_mv; /* 触发阈值 */ float th_recover_mv; /* 恢复阈值 */ uint8_t debounce_cnt; /* 确认次数 */ uint16_t sample_period_ms;/* 采样周期 */ uint16_t crc; /* 整块校验 */ } lvd_config_t;读取时先校验crc和cfg_version,不匹配就回落到编译期默认值并置一个标志位,方便现场判断是配置丢了还是逻辑错了。sample_period_ms和debounce_cnt放在一起,是因为它们共同决定确认时间,分开存容易改一个忘一个。
最后说个我自己的习惯:任何低压检测方案,我都会先做一个「只记录不动作」的观察版本,在真实设备上跑够两周,把电压曲线和误触发统计拿回来,再决定阈值和动作序列。直接上保护逻辑的版本,往往在实验室里一切正常,到了现场因为负载差异和温度漂移开始玄学重启。低压检测这件事,参数是算出来的,稳定性是跑出来的。希望帮到你。
本文还有配套的精品资源,点击获取