做电池供电的小东西,省电永远是第一优先级。我最近用STC8G1K08这颗8脚单片机做了一个10秒周期上报的温湿度节点,要求待机电流足够低,但又不能全靠外部信号来唤醒——设备得自己每隔10秒醒来一次,采完数据再睡回去。折腾了一圈,最后把STOP模式配合定时器唤醒用起来,待机电流做到1μA出头,延时误差通过校准控制在0.1%以内。这篇文章把这套方案从头到尾梳理一遍,包括寄存器配置、完整代码、电流实测对比,以及我在测试过程中踩过的几个坑。
先说结论:STC8G1K08的STOP模式并不是“睡死过去”,芯片内部还有一个约32KHz的低速IRC可以在睡眠状态下继续工作,喂给定时器计数,溢出后把芯片唤醒。这比靠外部中断唤醒可控得多,适合定时采样、定时上报这类需要“自己定闹钟”的场景。文章里所有电流数据都是基于我手头的一块SOP8封装裸板实测的,不同批次芯片会有差异,但量级有参考价值。
1. 低功耗产品的电池焦虑:为什么“睡眠+定时唤醒”是刚需
做电池供电的小设备,最常见的死法就是待机电流压不下去。你费尽心思把工作状态下的电流从10mA优化到5mA,结果设备一天只工作几秒钟,剩下23小时59分都在傻等,待机电流哪怕只是几十μA,电池寿命照样被拖垮。真正的瓶颈从来不是工作时耗电,而是“不工作时还在耗电”。
STC8G1K08这颗芯片的省钱逻辑就在这里。它的Flash有8KB,SRAM 1KB,价格很低,封装又能做得很小(SOP8只有8个引脚),非常适合传感器节点、遥控器、智能家居里的微功耗模块这类场景。但小封装不等于低功耗可以白拿,你必须让它进入睡眠状态,否则主频一跑,电流就是毫安级,电池根本扛不住。
STC8G1K08提供了三种“省电档位”:
- 正常运行模式:CPU全速跑,外设都开着,电流毫安级。
- IDLE空闲模式:CPU停止,外设时钟继续运行,中断可以随时唤醒,电流从毫安降到一毫安左右。
- STOP停止模式:主时钟停振,CPU和外设基本全部停止,电流降到微安级,是真正的深度睡眠。
我自己的项目要求就是:设备绝大多数时间处于STOP模式,每10秒醒来一次,读取传感器,把数据发出去,然后继续睡。这里有个核心问题——STOP模式下怎么定时醒来?
你可能第一反应是外部中断唤醒。外部中断确实能把芯片从STOP模式拉起来,但它的本质是“等人叫”,IO口出个边沿信号才醒。对于“每10秒自己醒一次”这种需求,外部中断根本派不上用场。市面上有些方案会用外部RTC芯片,比如PCF85063这类,靠RTC的闹钟输出去触发单片机的INT引脚,精度确实高,但也多了颗芯片,多了成本,还多了编程量。
STC8G1K08内部其实自带一个解决方案:让定时器在STOP模式下继续跑。这个功能藏得比较深,不少人第一次看手册会忽略掉,但它恰恰是这里最实用的一招——不需要外部RTC,不需要额外的硬件,一颗芯片就能实现“定时闹钟”。
下面从时钟机制开始讲,搞清楚定时器为什么能“在睡眠中工作”,你就理解整个方案了。
2. STOP模式下定时器为什么还能跑?内部低速IRC是幕后功臣
很多人对STOP模式的印象是“芯片全停了,什么都不跑”,这个理解不准确。STC8G1K08进入STOP后停掉的是主时钟(内部高速IRC或者外部晶振),CPU的时钟也随之停了,但芯片内部还有一个低速时钟可以继续工作,这就是约32KHz的内部低速IRC。
这个内部低速IRC本身就是为了低功耗场景准备的。它不需要外部晶振,上电就存在,频率很低,功耗也很低。关键在于:芯片硬件允许把这路32KHz时钟直接喂给定时器,让定时器在STOP模式下继续计数。计数溢出后产生一个中断事件,把MCU从STOP状态唤醒。
这个机制的开关是一个叫TSTOP的寄存器,地址0x86。把TSTOP的bit0(T0STOP)置1,定时器0就允许在STOP模式下运行;bit1和bit2分别对应定时器1和定时器2。如果这个位是0,定时器在STOP模式下就是死的,芯片一旦睡过去就只能靠外部中断拉起来。
TSTOP寄存器(地址0x86)的位定义大致如下:
| 位 | 名称 | 功能 |
|---|---|---|
| bit0 | T0STOP | T0在STOP模式下继续计数/唤醒使能 |
| bit1 | T1STOP | T1在STOP模式下继续计数/唤醒使能 |
| bit2 | T2STOP | T2在STOP模式下继续计数/唤醒使能 |
| bit3-bit7 | - | 保留 |
需要特别说明的是,在STOP模式下,定时器的时钟源被硬件自动切换到内部低速IRC,而不是沿用主时钟。这意味着你进入STOP之前,不需要额外去改定时器的时钟源选择,只要打开TSTOP对应的位就行。也因此,定时器在STOP模式下的计时单位是“32K时钟周期”,不是主频周期。
把IDLE模式和STOP模式放在一起对比,你会更清楚怎么选:
| 对比项 | IDLE模式 | STOP模式 |
|---|---|---|
| CPU | 停止 | 停止 |
| 主时钟(高速IRC) | 继续运行 | 停止 |
| 内部低速IRC(32K) | 运行 | 运行(若TSTOP使能) |
| 外设时钟 | 继续运行 | 停止 |
| 电流量级(3.3V实测) | 约0.8mA | 约0.5μA(纯STOP) |
| 唤醒方式 | 任意中断 | 外部中断/定时器溢出 |
| 适用场景 | 短时等待,UART接收等待 | 长时间定时睡眠 |
看到区别了吧?IDLE模式下外设还在跑,所以电流下不去,但响应快;STOP模式下外设全停,电流直接少了三个数量级,代价是唤醒后主时钟要重新建立,需要等几个微秒。对于10秒级别的定时上报场景,这个代价完全可以忽略。
原理清楚了,下面就是寄存器配置和最小代码,先把“1秒醒一次”跑通。
3. 寄存器配置与最小代码:先把“1秒醒一次”跑起来
整个方案的核心思路是:用定时器0做1秒溢出一次的低速定时(每溢出一次就唤醒一次),然后靠软件变量累加,攒够10次再执行真正的任务。为什么不直接让定时器一次睡10秒?因为16位定时器在32KHz时钟下,最大计数是65536,对应时间只有65536/32768=2秒。想要10秒,必须用软件计数器做二级累加。
配置步骤拆开来看,其实就四件事:
- 把定时器0配置为16位自动重装载模式,初值设为0x8000(即65536-32768),这样它从初值数到65536溢出,正好经历32768个时钟周期,在32KHz下就是1秒。
- 打开T0中断使能(ET0=1)和总中断(EA=1),因为唤醒的本质是中断事件。
- 把TSTOP的bit0置1,允许T0在STOP模式下继续运行。
- 启动定时器(TR0=1),然后进入STOP模式。
这里有一个容易忽略的细节:进入STOP模式的指令PCON |= 0x02执行之后,芯片并不一定马上停,因为Keil C51编译器在开启优化时可能会把后面的指令预取到流水线里。稳妥的做法是在设置STOP位之后加两个_nop_(),确保芯片真正睡下去。唤醒后,程序从PCON|=0x02这一行的下一条指令继续执行,那些NOP也充当了“缓冲垫”。
最小可跑代码长这样:
#include "STC8G.h" #include <intrins.h> #define TSTOP 0x86 volatile unsigned char wake_count = 0; void t0_isr(void) interrupt 1 { wake_count++; // 每溢出一次,说明定时器走过了1秒 } void goto_sleep(void) { PCON |= 0x02; // 进入STOP模式 _nop_(); _nop_(); } void main(void) { // T0配置:16位自动重装载,初值0x8000,对应32768个计数 TMOD &= 0xF0; TMOD |= 0x00; // 16位自动重装载 AUXR &= 0x7F; // T0x12 = 0,保持12T模式 TL0 = 0x00; // 初值低字节 TH0 = 0x80; // 初值高字节,0x8000 = 32768 TSTOP = 0x01; // 允许T0在STOP模式下运行 TR0 = 1; // 启动定时器0 ET0 = 1; // 开T0中断 EA = 1; // 开总中断 while (1) { goto_sleep(); // 睡眠,等待T0溢出唤醒 if (wake_count >= 10) { wake_count = 0; // 在这里执行你的实际任务:采样、发送等 } } }这里解释一下为什么初值是0x8000:16位定时器从0x8000数到0xFFFF再溢出,计数个数是65536-32768=32768,正好对应内部低速IRC的32768Hz,所以理论时间是1秒。注意,这只是“理论”,因为内部低速IRC的实际频率会有偏差,具体怎么校准,下一节专门讲。
跑通这段代码后,用逻辑分析仪或者示波器抓任何一个翻转引脚,你应该能看到大约1秒一次的电平变化。我实测的情况是:第一次跑的时候间隔是1.01秒左右,偏差不大,但确实存在。
4. 让“精准延时”名副其实:误差来源、校准方法与唤醒延迟补偿
标题里的“精准延时”不是随便写的,但要把“精准”做实,得先搞清楚误差从哪来。
这是我在实测中总结出的误差来源,按影响从大到小排列:
- 内部低速IRC的频率偏差。这是最大的误差源。内部32KHz并不保证每颗芯片都是精确的32768Hz,出厂校准值会有1%左右偏差,不同温度下也会漂。测下来10秒间隔误差可能在±100ms量级。
- 唤醒后的恢复延迟。Timer溢出后,芯片要从STOP状态把主时钟拉起来,这需要时间;再加上中断响应和主循环的判断,整体可能有几十微秒到上百微秒。
- 指令执行时间和中断服务程序长度。虽然很短,但在毫秒级延时时就不能忽略。
校准方法是反向推算实际频率。我是这么做的:先把wake_count阈值设成1,让LED每次唤醒都翻转一次。用逻辑分析仪抓LED引脚,测出方波的周期。假如测出来是2.02秒,说明两次唤醒间隔是1.01秒,也就是实际频率比32768Hz高了1%。反推实际32K频率:
实际频率 = 32768 × 1.01 = 33095.68 ≈ 33096Hz
要得到真正的1秒间隔,计数个数应该是33096,那初值就是65536-33096=32440。把初值从0x8000改成0x7EB8,重新烧录实测,误差马上就下来了。我实测校准后的10秒间隔误差在10ms以内,大约是0.1%级别。
再提一个别人不太会告诉你的点:唤醒延迟的补偿。定时器溢出后,芯片不会立刻执行到你的业务代码,中间有主时钟恢复、中断响应、中断返回、主循环判断这几道工序。用示波器抓GPIO翻转,能测出这个“延迟尾巴”大概是多少。比如你原本想要1000ms唤醒一次,实测是1000.08ms,多出来的0.08ms就是唤醒延迟。如果要做毫秒级精准延时,就把这个值从定时器计数里扣掉:计数个数 = 目标时间 × 实际频率 - 延迟时间 × 实际频率。但做秒级延时时,这个补偿只有几个32K计数,基本可以忽略,我建议秒级场景不用管,毫秒级场景才需要考虑。
校准过程中还有一个容易被“精度”迷惑的地方:内部IRC的频率会随温度漂移。你在25℃校准到0.1%,到了50℃可能又漂了0.5%。如果产品工作温度范围很大,又要求很高的计时精度,那就只能外接32.768K晶振或者外部RTC芯片,内部低速IRC解决不了这个问题。但对大多数传感器上报类应用,校准后的精度足够用。
下面是校准前后的实测对比,用的同一块板子,同一个芯片,10秒唤醒间隔:
| 状态 | 定时器初值 | 实测间隔 | 误差 |
|---|---|---|---|
| 未校准 | 0x8000 | 10.11秒 | +1.1% |
| 校准后 | 0x7EB8 | 10.01秒 | +0.1% |
校准的代价只是一次额外测试,性价比很高,强烈建议每次批产前至少抽几颗做频率统计,看看分布,再决定要不要在产线上做个体校准。
5. 电流实测对比与测量中的几个大坑
电流数据是很多人关心的。我用的测量方法是:3.3V稳压电源供电,万用表串联进电源回路测平均电流;动态电流用10Ω采样电阻加示波器来看唤醒瞬间的波形。测的是裸板,下载器拔掉,板上没有任何指示灯。
实测数据整理如下(3.3V供电,主频11.0592MHz):
| 工作状态 | 实测电流 | 备注 |
|---|---|---|
| 正常全速运行,while(1)空转 | 约2.1mA | 未开外设 |
| IDLE模式 | 约0.8mA | 外设时钟还在跑 |
| STOP模式,未使能定时器 | 约0.5μA | IO口全部处理好 |
| STOP模式 + T0定时唤醒 | 约1.1μA | 32K低速IRC在运行 |
| 10秒周期唤醒(含采样任务) | 约2.5μA | 平均电流 |
几个关键结论:
- STOP模式比IDLE模式省电约三个数量级,差距极其明显。
- 纯STOP和STOP+定时唤醒之间,多出来的大约0.6μA就是内部32K振荡器的工作电流。这个代价换来自动唤醒能力,非常划算。
- 如果整机平均电流能做到5μA以下,用一颗CR2032纽扣电池(约220mAh容量)理论续航在5年左右,已经能满足绝大多数小产品的电池寿命预期。
数据好看,但如果你照着测,很可能测不出这么低的数字。原因在于测量和硬件处理上有几个坑,我一个个说。
第一个坑:IO口悬空漏电。这是最容易被忽视的。STOP模式下如果某个IO是悬空的,电平不确定,输入缓冲电路会持续产生漏电,几μA甚至十几μA就这么没了,比你唤醒定时器多耗的电还多。处理办法是进入STOP前把所有不用的IO设置成推挽输出低电平,或者高阻输入,反正不能让引脚悬在空中。
第二个坑:万用表内阻。很多万用表的μA档内阻有几百欧甚至1kΩ,当电流只有几个μA时压降很小没问题,但设备醒来工作的那几毫秒电流可能有几mA,压降瞬间变大,可能导致MCU供电电压跌破复位阈值,直接复位重启。我吃过这个亏:用某款万用表μA档测10秒唤醒节点,死活测不到唤醒后的传感器数据,最后发现是万用表内阻在唤醒瞬间把电压拉掉了。换个mA档或者用电流探头就好了。测低功耗动态电流,更推荐串联小电阻配合示波器看波形,而不是只信万用表的数字。
第三个坑:板子上的隐性耗电。开发板上的电源指示灯、LDO静态电流、下载器芯片的漏电,全都会算进你的“待机电流”里。我一开始直接在开发板上测,死活压不到2μA以下,后来查出来是板载LED在耗电。真正做低功耗测量,必须用最小系统,或者至少把跳线帽、指示灯处理掉。
第四个坑:外设没关干净。片上的ADC、比较器、串口如果处于工作状态,在STOP模式下也可能漏电。进入STOP前最好显式关闭这些外设的电源位。以ADC为例,ADC_CONTR &= 0x7F可以关掉ADC电源。串口如果没用到,相关引脚也设成普通IO处理。
第五个坑:看门狗的问题。STC8G的看门狗在STOP模式下一般是停止计数的,但我建议你在自己的板子上验证一下手册上的说法,因为不同型号行为可能不一样。如果看门狗在睡眠期间继续跑,而你睡眠周期比看门狗溢出周期长,那醒来之前芯片就已经复位了,整个定时逻辑就乱了。睡眠前把看门狗处理干净,长周期(比如分钟级)唤醒才稳妥。
6. 完整例程:10秒周期唤醒的采样节点与平均电流推算
最后给出一个可以直接改来用的完整例程。硬件连接很简单:STC8G1K08的最小系统,VCC接3.3V,GND接GND,P3.3接一个LED(串个限流电阻),用来指示任务执行。传感器部分可以用ADC或者IO读取,这里用LED翻转代替任务执行,重点展示睡眠框架。
完整代码如下:
#include "STC8G.h" #include <intrins.h> #ifndef TSTOP #define TSTOP 0x86 #endif sbit LED = P3^3; volatile unsigned char wake_count = 0; void t0_isr(void) interrupt 1 { wake_count++; } void goto_sleep(void) { PCON |= 0x02; _nop_(); _nop_(); } void main(void) { // IO初始化:P3.3推挽输出,其他引脚输出低电平,避免悬空漏电 P3M0 = 0x08; P3M1 = 0x00; P3 = 0x00; // 关闭不用的模拟外设,降低睡眠漏电 ADC_CONTR &= 0x7F; // 关闭ADC电源 // 如果有比较器、串口,同样显式关闭 // T0初始化:16位自动重装载 TMOD &= 0xF0; TMOD |= 0x00; AUXR &= 0x7F; // 校准后的初值:0x7EB8对应1秒(实际32K频率约33096Hz) TL0 = 0xB8; TH0 = 0x7E; TSTOP = 0x01; // T0在STOP模式下继续运行 TR0 = 1; ET0 = 1; EA = 1; while (1) { goto_sleep(); // 每隔约10秒执行一次 if (wake_count >= 10) { wake_count = 0; LED = ~LED; // 在这里读取传感器、发送数据,处理完自动回到睡眠 } } }代码本身不复杂,但有几个点值得说明:
wake_count用volatile声明,因为它在中断里被修改,编译器优化时不会把它搬到寄存器里导致判断失效。- 中断服务程序里只做
wake_count++,不要放复杂业务。唤醒后停留在中断里越久,平均电流就越高。实际业务放到主循环判断后执行。 - 初值改成校准后的0x7EB8之后,10秒间隔误差已经很小。未校准的板子直接用0x8000也能工作,但延时精度会差一个百分点左右。
现在来算平均电流,验证这套方案到底能不能撑起一颗纽扣电池。
假设工作电流3mA,每次醒来后执行采样和发送需要10ms;睡眠电流1.1μA,睡眠周期10秒。平均电流大概是:3mA×0.01s/10s + 1.1μA = 3μA + 1.1μA = 4.1μA。用CR2032的220mAh容量来算,理论寿命是220mAh/4.1μA,大约是5.4万小时,折合6年左右,还没算电池自放电。如果任务更短,比如2ms,平均电流可以压到2μA以下。这个量级对很多电池供电产品来说已经非常舒服了。
如果你还想继续压电流,优先级最高的是检查IO漏电和外设漏电,把睡眠电流从1.5μA压到0.7μA是可能的;其次是把醒来后的工作时间尽量压缩,因为那10ms的3mA虽然短,但在平均电流里占比不小。另外,如果多个唤醒周期可以合并成更长的睡眠周期(比如从10秒改成60秒),平均电流还能更低,但实时性会变差,需要按业务取舍。
这套方案我已经在一个温湿度上报节点上跑了一个多月,每次唤醒都很稳定,没有丢过定时。比例程更关键的是睡眠前的IO状态检查和测量方法,这两块做不好,代码再正确数据也难看。如果你也想在STC8G1K08上做低功耗,建议先按这篇文章的最小代码跑通定时唤醒,再拿万用表和示波器量一轮电流,把睡眠电流压到1μA级别,后面的产品化就顺了。