ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STC8低功耗改造:用WKT掉电唤醒替代循环延时,续航提升30倍

STC8低功耗改造:用WKT掉电唤醒替代循环延时,续航提升30倍 做电池供电的小设备时我见过太多次这种情况样机功能全部跑通代码里到处是delay(500)、delay(1000)逻辑上一点毛病没有一装箱待机电流一测几毫安客户那边直接一句“电池撑不了几天”。问题往往不是MCU不够低功耗而是你把一个最应该省电的动作——延时做成了最费电的方式让CPU全速空转死等。这篇文章就拿STC8做例子把项目里最常见的循环延时改成“STOP模式掉电唤醒定时器”的“睡到点再醒”顺便把实测电流、精度差异以及唤醒之后那堆容易让人掉头发的坑都讲清楚。如果你手头的设备正好是电池供电、对功耗敏感或者你在用STC8G/STC8H系列做低功耗改造这篇可以直接照着操作。1. 循环延时不是“慢”是“耗”先给delay算一笔账1.1 你以为MCU在“等一下”其实它在全速烧电先看最常见的延时写法。传统的51单片机教程里都会有这么一段void delay_ms(unsigned int ms) { unsigned int i, j; for (i 0; i ms; i) for (j 0; j 1800; j) ; }这段代码本身的意图很简单CPU 什么都不干空转那么多个周期凑够时间。问题在于STC8 是增强型 8051 内核指令周期是 1T也就是一个时钟周期执行一条指令运行速度比传统 8051 快很多。你以为它在“等一下”实际上它满负荷工作内核里所有逻辑都在翻转电流往 2~5mA 这个量级走。我在一个温湿度采集终端上实测过MCU 跑 24MHz 内部时钟只做简单的 IO 翻转和 ADC 采集全速功耗大约 3.2mA。系统每 5 秒上报一次数据上报只需要 50ms剩下 4.95 秒本来可以睡大觉结果程序里写了一堆延时用来等传感器稳定、等通信时序CPU 一直处于全速运行状态。整机平均电流算下来也有 2mA 以上。按一颗 200mAh 的 CR2032 电池算理论上 100 小时就空了也就是四天多。这还没算传感器和无线模块的消耗。问题显然不是 MCU 省不省电而是系统根本没给 MCU 睡觉的机会。1.2 三张方案的功耗画像循环、定时器、睡醒再干常见的延时方案有至少三种循环忙等、定时器中断配合空闲模式、以及本篇文章重点讲的掉电唤醒定时器配合掉电模式。方案实现方式延时期间电流量级CPU占用时间精度循环忙等for 空转2~5mA全速运行100%受编译优化、主频漂移影响大定时器中断 IDLE定时器计数CPU进空闲模式中断唤醒0.8~1.5mA只在中断时短暂占用取决于晶振精度偏高WKT STOP掉电唤醒定时器计时CPU进掉电模式1~3uA基本为0取决于内部低速IRC漂移需校准表格里的数值只是量级参考不同型号、不同供电电压差异不小但数量级的差距一看就明白。循环延时和掉电唤醒之间差了大约一千倍。所以低功耗优化的第一步不是换低功耗MCU而是先把那些让CPU“没事也要跑”的延时给处理掉。有一点需要特别提醒循环延时在高优化等级下还有可能被编译器直接优化掉。Keil C251 里如果开高优化空循环变量没有外部效应编译器会认为这段代码没有实际操作直接给你删掉延时函数变成了“秒醒”整个时序乱掉。这种问题排查起来特别隐蔽因为代码看起来没有任何问题。2. STC8的停电与待机IDLE、STOP和WKT唤醒机制拆解2.1 IDLE和STOP一个停一半一个全停STC8 系列提供了两种低功耗模式对应用户常说的“空闲模式”和“掉电模式”。空闲模式通过 PCON 寄存器置 IDL 位进入也就是执行PCON | 0x01;。进入之后CPU 内核时钟停止程序不跑但外设时钟还继续工作。定时器、串口、ADC这些外设该干活还是干活。如果外设全都还在运行功耗自然降不下来大概只有全速运行时的三分之一到二分之一。这种模式适合那种“CPU想休息但外设不能断”的场景。唤醒也简单任何已使能的中断都可以把它唤醒。需要注意一个细节IDLE 唤醒之后 PCON 里的 IDL 位不一定自动清零如果在中断服务程序里没有把它清掉中断返回之后又会继续睡过去。掉电模式则是通过 PCON 寄存器置 PD 位进入也就是执行PCON | 0x02;。进入掉电模式后主时钟停振内外设时钟全部停止CPU 彻底断电进入静止状态只有指定的唤醒源还在待命。STC8 系列的掉电电流典型值在微安级别部分型号能做到 1uA 以下。唤醒之后CPU 从进入掉电模式的下一句指令开始恢复执行过程非常像一个阻塞函数返回这也是我们能用它替代延时函数的核心原因。这里我用一个不太恰当的比喻循环延时是守在灶台前不关火等水烧开IDLE 是厨房灯开着人睡了STOP 是整个厨房断电只留一个手环闹钟到时间把你震醒。功耗差异自然一目了然。2.2 WKT这个“睡眠闹钟”怎么用STC8G、STC8H 系列里通常带一个专用的掉电唤醒定时器叫 WKTWake-Up Timer这不是普通定时器而是专门为了在掉电模式下保持计数、定时唤醒而设计的。WKT 有独立的时钟源可以选择内部低速 IRC标称大约 32kHz也可以选择外部 32.768kHz 晶振。如果在掉电模式下想用 WKT 唤醒就不能依赖高速主时钟了因为主时钟已经停了。这一点和普通定时器在 STOP 模式下还能不能跑是两回事普通定时器在掉电模式下绝大多数都停工了只有 WKT 这类专用外设还在数数。WKT 的计数装载值宽度是 15 位也就是单次最大装载 32767。以 32kHz 时钟来算单次最长唤醒周期大约 1 秒左右。想延时超过 1 秒需要分多次进掉电、多次重装 WKT。这个细节后面写代码的时候会用到。WKT 控制寄存器分成两个字节WKTCL 和 WKTCH。写装载值时必须先写低 8 位 WKTCL再写高 7 位和使能位到 WKTCH顺序不能反。我第一次用的时候就栽在这上面先写了 WKTCH 再写 WKTCL结果每次唤醒时间都不对后来翻手册才反应过来是写入顺序的问题。3. 把delay_ms替换成“睡到点再醒”完整改造代码3.1 调用方式不变内部天翻地覆做低功耗改造时最理想的状态是业务代码尽量少改底层实现偷偷换掉。WKTSTOP 的方案刚好可以做到这一点——我原来的代码里写delay_ms(1000);改造后还是写delay_ms(1000);但函数内部变成了“进掉电模式等 WKT 溢出唤醒继续往下走”。好处是显而易见的业务逻辑不会因为改造而产生大量变动风险面被控制在一个函数内部。坏处是你得把函数内部实现吃透别只看个大概就直接搬上去STC8 不同系列在寄存器细节上还是有一些差异的。3.2 完整代码WKT装载值计算与STOP入口下面这段代码以 STC8G 系列为例实现一个毫秒级的低功耗延时函数。它不依赖普通定时器中断也不需要 CPU 在延时期间运行。#include STC8G.h #include intrins.h void lowpower_delay_ms(unsigned int ms) { unsigned long remain; unsigned int load; // 按 WKT 内部低速 IRC 标称 32kHz 换算 // 延时 ms 毫秒需要计数多少下 remain (unsigned long)ms * 32u; while (remain) { // WKT 是 15 位计数器单次最大装载 32767 if (remain 32768u) load 32767u; else load (unsigned int)remain; remain - load; // WKT 溢出一个周期需要“装载值 1”个计数时钟 // 所以装载值要减 1 load load - 1u; // 先写低 8 位再写高 7 位 WKTEN 使能位 WKTCL (unsigned char)(load 0xFF); WKTCH (unsigned char)((load 8) | 0x80); // 根据具体型号清掉 WKT 唤醒标志 // 不同型号清标志方式有差异以手册为准 // 进入掉电模式PD 1 PCON | 0x02; _nop_(); _nop_(); // 唤醒后从这里继续执行 } }这段代码里有几个细节值得展开讲。第一remain的计算用 32 这个近似值。WKT 内部低速 IRC 并不是十分精确的 32kHz它受制造偏差和温度影响所以这个函数适合对时间精度要求不高的场景。如果要精确计时要么用外部 32.768kHz 晶振喂给 WKT要么在代码里做频率校准。第二循环中的拆段逻辑。如果ms小于 1000 左右一次 STOP 就够如果大于 1000比如延时 5 秒那么循环会跑 5 次每次进掉电模式大约 1 秒。每次从 STOP 唤醒后CPU 继续执行循环重新装载 WKT再睡过去。因为唤醒后到再次睡下的执行时间非常短毫秒量级的误差可以接受。第三执行PCON | 0x02;之后CPU 立刻停止下一条_nop_()要等唤醒之后才会执行。所以从调用方的角度这个函数和原来的delay_ms表现得一模一样都是“卡在这里一阵子然后继续”。3.3 进入STOP前的那几十行“清洁工作”代码核心部分很简单但我不建议直接把这套函数塞进项目里就跑。进入 STOP 模式之前最好把片内外设“打扫”一遍否则功耗会偷偷上涨。我一般做四件事关闭 ADC 电源。STC8 的 ADC 模块在掉电模式如果有电还在跑会平白增加功耗。关闭 PWM、比较器等模拟外设不需要的模块全部断电。把未使用的 GPIO 设置成确定的电平别让引脚浮空。浮空引脚相当于一个悬空的输入端会通过寄生二极管漏电对微安级功耗来说是灾难。把高速外设的时钟关掉比如没有用到的定时器、串口、SPI、I2C 模块能关就关。如果它们还带着时钟运行掉电模式就白进了。进入掉电模式前的 GPIO 处理有一半的人会踩坑。最简单的做法是把不用的引脚设置成推挽输出低电平前提是外部电路不会因此过流。如果引脚外接的是 LED 阳极到电源、阴极到引脚这种结构输出低会把 LED 点亮那就不能这么干。要根据外部电路选安全电平原则只有一个让每一个引脚都处于确定状态既不悬空也不带重负载。4. 实测数据同一个延时三种方案的电流账单4.1 测量uA级电流不能拿万用表乱怼想拿到可靠的低功耗数据测量方法非常重要。很多人直接把万用表串进电源线里读电流结果发现数字跳来跳去或者干脆系统复位重启于是怀疑功耗优化没用。问题出在万用表的电流档内阻上。普通的便携万用表电流档内阻从零点几欧到几欧不等当系统工作电流是毫安级时这个压降还不明显可一旦系统进入微安级电池电压又被内阻分走一部分MCU 的供电电压掉到复位阈值以下系统就开始反复复位永远测不到真实睡眠电流。我自己测试时用的是采样电阻法在电源回路上串一个 1 欧姆的精密采样电阻用示波表测电阻两端的电压波形电流 电压 / 电阻。这样既能看到稳定电流也能捕捉唤醒瞬间的电流尖峰。如果只想测平均电流也可以串一台高位台式万用表用它的微安档连续记录十几分钟再取平均这样比手持表靠谱得多。4.2 实测数据与估算以 STC8G 在 3.3V 供电、内部 24MHz 主频下的一组数据为例我做了三种实测方案延时期间实测电流500ms周期平均电流估算200mAh电池理论续航循环忙等2.8mA约2.8mA约71小时定时器IDLE0.9mA约0.9mA约222小时WKTSTOP2.1uA约0.09mA约2222小时这里的“平均电流估算”我按一个典型场景算系统每 500ms 执行一次任务每次任务运行 20ms运行电流 2mA其余 480ms 处于不同延时状态。循环忙等方案下因为延时阶段也在 2mA 左右平均电流接近 2.8mA改用 WKTSTOP 之后任务运行时间和电流不变睡眠阶段压到 2uA平均电流算下来大约 90uA 左右。续航从 3 天变成 3 个月这就是低功耗延时的价值。有朋友可能会杠“你只把 MCU 睡眠了外围传感器和无线模块还醒着呢有什么用”这话对了一半。MCU 睡眠只解决 MCU 自己的开销但很多项目里 MCU 恰恰是功耗大头之一。而且把 MCU 睡好了你才有余力去梳理外围器件的供电时序比如让传感器只在测量前才上电无线模块只在发送时才唤醒。低功耗是一个逐项抠出来的工程第一刀切在 MCU 延时上是最容易、风险最低的。4.3 唤醒瞬间的电流尖峰怎么抑制测量时还会注意到一个现象每次从 STOP 唤醒的那一刻电流会出现一个尖峰有时达到几十毫安持续几十微秒到几百微秒。尖峰有两个来源一是主时钟重新起振时需要瞬间拉起振荡器电流二是唤醒后外设模块同时上电。这个尖峰该怎么处理首先别慌持续时间很短平均能量占比很低。真正的风险在于如果你的供电是纽扣电池或者小容量 LDO尖峰可能导致瞬间电压跌落让系统复位。我遇到过一回系统一唤醒就重启查了很久才发现是电源路径上的 ESR 太高尖峰直接把电压拉穿了。解决办法有几种在电源输入端加一个较大的储能电容比如 100uF 或更大或者降低主频恢复的时间唤醒后不要立刻开启所有外设再或者在唤醒后的头几十微秒只做必要状态恢复把高功耗外设推迟到主体任务开始前再逐个打开。对绝大多数 STC8 项目来说加一颗 100uF 电容就能压住尖峰这也是成本最低的方案。5. 唤醒之后才是坑主频、看门狗、IO和调试器的连锁反应5.1 串口乱码的幕后黑手唤醒后主频没切回来实测中我遇到过最经典的场景把延时函数换成 WKTSTOP 之后功能看起来正常但串口输出的日志开始乱码。一开始以为是波特率配置被改了查了一圈才发现问题出在时钟源切换上。STC8 掉电唤醒之后会默认先从内部高速 IRC 恢复运行而不是回到你进入 STOP 之前用的外部晶振。如果系统本来用的是外部 11.0592MHz 晶振串口波特率也是按外部晶振算的唤醒后 MCU 却跑在内部 IRC 上两边频率对不上串口自然乱码。解决办法是在唤醒后、外设初始化之前把系统时钟源切回外部晶振。STC8 系列的时钟控制寄存器不同型号命名不同常见的有 CLKSEL、CLKDIV 之类。代码里需要显式地设置时钟源和分频不能假设硬件会记住你睡前的状态。我是把“时钟恢复”写成了一个函数在每次唤醒后第一个调用然后再恢复需要高精度时钟的外设。5.2 看门狗在低功耗模式下会“复活”你的MCU第二个坑是看门狗。如果你开了硬件看门狗又想在 STOP 模式里待几百毫秒一定要先确认 STC8 的看门狗在掉电模式下是否还在计数。不同系列、不同使能配置下表现不一致有些数据手册明确写看门狗在掉电状态下依然运行溢出会直接复位。如果看门狗确实还在跑那低功耗延时期间就会面临“还没睡够就被看门狗踹醒”的尴尬。处理思路有三条要么在进入 STOP 前把看门狗暂停但要看硬件支不支持要么保证单次 STOP 时间明显小于看门狗溢出周期并且每次唤醒后重新喂狗要么干脆调整整体设计用外部低功耗看门狗芯片MCU 只负责睡觉和干活。我个人的建议是在低功耗产品的早期原型阶段看门狗先不要着急开等功耗数据和唤醒流程稳定后再把看门狗加回来并且要专门写测试用例去验证“看门狗会不会干扰长睡眠”。5.3 仿真调试会让芯片“永远醒不来”这个坑非常隐蔽坑过我整整一个下午。用 STC 的仿真器调试程序代码执行到进入掉电模式那一步Keil 里程序就“卡死”了打断点完全没反应看寄存器也正常但就是不往下走。原因其实不神秘仿真器为了维持调试通道不让目标芯片真正脱离管控。当 CPU 执行 STOP 指令后仿真器会通过调试接口把系统“拉”在一个可控状态结果你的芯片根本没有真正掉电唤醒条件当然无法触发。所以凡是要验证低功耗数据或者掉电唤醒逻辑的时候我都是先拔掉仿真器用串口打印结合 IO 翻转的方式观察时序或者干脆用逻辑分析仪看唤醒引脚。这类问题不是代码 bug是调试工具和低功耗模式天然不兼容。5.4 内部32K时钟不准长延时如何校准WKT 用内部低速 IRC 时标称 32kHz实际可能落在 30kHz 到 34kHz 之间。按 32kHz 算的延时和真实时间之间会有几个百分点的误差。短延时无所谓长延时就离谱了。我测试过 10 秒延时实际走了 10.7 秒换算下来误差接近 7%。如果项目对延时精度有要求有几个办法可以做改用外部 32.768kHz 晶振作为 WKT 时钟源精度提升最明显。在产线校准时用高精度晶振作为参考让 MCU 计算出实际内部低速 IRC 频率然后把修正系数写进 EEPROM运行时不使用固定值 32而是用校准后的频率值换算。延时较长时在代码里分段每一段用一个相对精确的外部时间基准去校正比如利用硬件 RTC 或者串口收到的时间戳。按我的经验大部分低功耗场景对延时的要求是“差不多就行”没必要为了百分之几的误差去加外部晶振。但如果你的设备涉及时序协议比如 Modbus 帧间隔、通信超时那延时精度就要认真对待不能指望内部 IRC 精确。6. 再往下抠动态主频、分片唤醒和整体低功耗策略6.1 进入STOP前主动降频醒来再拉回去STC8 支持运行时调整系统主频这给低功耗优化多了一个维度。一个典型动作正常通信时跑 24MHz保证时序和处理速度在进入 STOP 之前把主频切到较低的值比如 6MHz 或更低再执行后续指令进入休眠。因为进入 STOP 前那几十条指令也需要消耗功耗虽然持续时间短但在高频和低压差环境下切换分频可以让整个尾部的电流斜坡更平缓。唤醒之后先把时钟恢复函数调好再把主频拉回 24MHz然后继续干活。切忌一醒来就先开串口发数据此时时钟还没来得及切回准确源发出去的帧很可能就是乱码。这里的顺序是恢复时钟、恢复外设、执行任务、再次睡眠。6.2 超过1秒的延时怎么分片前面提到 WKT 单次最大装载值 32767按 32kHz 算约等于 1 秒。实际项目中常有“休眠 10 秒后再测一次”这样的需求。我的做法就是循环多次每次睡 1 秒左右中间用几行代码重新装载 WKT再次进入 STOP。上面的lowpower_delay_ms函数已经覆盖了这个逻辑调用lowpower_delay_ms(10000);会自动拆成大约 10 段。需要留意的是每一段醒来后 CPU 都要跑一小段代码去重装 WKT这段时间消耗的电流比睡眠大得多但也就是几十微秒到几百微秒对整体平均功耗影响很小。不过如果拆得很碎比如 10 秒拆成 1000 段每一段 10ms那唤醒频率就太高了电流尖峰反而会拉高平均功耗。现实中尽量让每段睡眠时间不低于 100ms。6.3 低功耗是产品级设计不是换一个延时函数最后说点心得。用 WKTSTOP 替换循环延时是低功耗优化的第一颗钉子但绝不是终点。真正把功耗做到满意需要把眼光放到整个系统上传感器怎么供电无线模块怎么调度通信协议能不能让设备尽快回到睡眠按键是否需要定期扫描LED 要不要降低占空比甚至连 PCB 上漏电流和去耦电容的选择都有影响。STC8 把 MCU 自身功耗压到微安级很容易但如果外围器件一直在耗电整机功耗还是下不来。我现在接到低功耗项目第一天不是去翻芯片数据手册而是先把项目里所有delay都拉个清单逐个问它是否必须存在是否能换成事件驱动是否能睡过去再醒。光是这个动作往往就能让整机平均功耗掉两个数量级。等你把这一层做扎实了再回过头去抠外设工作时长、电源转换效率每一个步骤都会有很清晰的方向。低功耗不是靠某一条魔法指令实现的是靠一层一层把“不该醒的时间”压缩到极致实现的。
返回列表