☰
GD32三种低功耗模式详解:从睡眠到待机,续航从月变年
2026/10/3 19:00:39 网站建设 项目流程

前阵子帮朋友改一个温湿度记录仪,主控是GD32F303。原来装3节AAA电池,一个月出头就得换,朋友怀疑单片机漏电,拿万用表一测,正常运行电流确实只有十几毫安,问题在于它几乎从不睡觉,偶尔进一下低功耗,也是那种“掩耳盗铃”式的假睡——核心停了,外设还在跑,电流一点没降下来。后来我把GD32的睡眠、深度睡眠、待机三种低功耗模式完整梳理了一遍,产品重新设计后换成CR2032纽扣电池,续航从“月”变成了“年”。这篇就把三种模式讲透:每个模式到底关了哪些东西、代码怎么写、唤醒源怎么选、实测功耗怎么量,以及从STM32迁过来容易踩的坑。正在做电池供电设备、或者打算用GD32替代STM32做低功耗项目的朋友,可以参考。

1. 三种模式到底关了什么、留了什么

GD32的低功耗模式不是简单地把主频降下来,而是从内核、时钟、外设、SRAM、电源调节器这几个维度分别做取舍。想用好它,先得搞明白每个模式的动作范围。

1.1 睡眠模式:内核休息,外设继续上班

睡眠模式对应的是Cortex-M3内核的WFI/WFE指令。执行之后,CPU停止取指、停止执行指令,但系统时钟仍然在跑,所有外设都在正常运转。也就是说,USART收到数据能产生中断,定时器到点能触发中断,DMA照常搬运数据,SysTick也还在一毫秒一毫秒地数。

这个模式的好处是唤醒速度极快。因为时钟没有停,中断来了之后CPU几乎不需要恢复时间,直接进中断服务程序,处理完回到WFI之后的下一行代码继续跑。

它的代价是功耗降低有限。内核停了,但外设时钟和Flash电路都还在耗电。GD32F303在8MHz主频下,睡眠模式实测电流大约在1mA上下,比正常运行十几毫安低了一个数量级,但对纽扣电池来说还是太高,只能在短时间等待场景里用。

适用场景很明确:CPU在等一件很快发生的事,比如等待DMA搬运完成、等待串口发完数据、等待用户按键去抖。如果这个等待时间超过10ms,就值得考虑深度睡眠。

1.2 深度睡眠模式:时钟全停,SRAM保住

深度睡眠模式在GD32里对应的是Cortex-M3的SLEEPDEEP位加电源控制寄存器配合。进入之后,HSE、HSI、PLL全部被关闭,所有外设的时钟都停了,CPU自然也没法执行代码。但SRAM内容保留,寄存器状态也保留,代码中定义的全局变量、当前程序运行到哪一步,这些信息都不会丢。

这个“状态保留”非常关键。唤醒之后不需要像复位那样重新初始化一切,你可以把现场数据继续用,甚至可以在中断服务程序里判断一下醒来原因,再决定下一步干什么。

深度睡眠的功耗比睡眠低了一个量级。我实测GD32F303在深度睡眠、LDO低功耗模式下,整机电流大概在9μA左右。如果外设引脚处理得当,还能再低一点。这个量级已经适合纽扣电池做定时唤醒的数据采集了。

需要注意一点,深度睡眠模式并不是只有一个固定配置。GD32库函数的第二个参数可以选择稳压器工作在正常模式还是低功耗模式。低功耗模式下省电,但唤醒后稳压器需要时间恢复,不能立刻支撑大电流负载。如果唤醒后要马上跑无线发射或者点亮大屏,建议用正常模式,或者唤醒后先加延时再上高负载。

1.3 待机模式:除了唤醒逻辑,全部断电

待机模式是GD32最狠的一档。进入待机后,内核、SRAM、外设寄存器、绝大部分时钟全部断电。整个芯片只剩下备份域、待机唤醒电路、以及复位移除相关逻辑还在工作。SRAM里的数据会丢,所有全局变量重新上电后都是初值。

唤醒后的行为等效于一次复位。芯片会从启动向量重新开始执行,SystemInit会重新跑一遍,main函数会重新进入。你想知道自己是“第一次上电”还是“从待机唤醒”,只能通过复位标志和备份寄存器来判断。

因为断电范围最大,功耗自然也最低。GD32F303待机模式实测整机电流在1.8μA左右。这个数字非常接近一块CR2032电池的自放电水平。

代价是唤醒后的恢复成本高。深度睡眠醒来后是“接着往下走”,待机醒来后是“从头再来”。所有外设要重新初始化,所有现场数据要不就得存在备份寄存器里,要不就得从Flash重新加载。

如果你做的是智能门锁、烟雾报警器、电子标签这种“平时完全休眠,来事件或定时器触发后才干活”的产品,待机模式是首选。如果产品需要频繁唤醒,比如每秒钟都要采样一次传感器,待机模式反而不合适,因为每次都从头初始化,这段时间的电流和工作量反而会拉高平均功耗。

1.4 一张表看穿GD32的三档功耗

项目睡眠模式深度睡眠模式待机模式
CPU内核停止停止掉电
系统时钟继续运行全部关闭全部关闭
外设时钟继续运行全部关闭掉电
SRAM内容保留保留丢失
寄存器状态保留保留丢失
唤醒源任意中断/事件EXTI、RTC等WKUP引脚、RTC闹钟、NRST
唤醒后行为继续执行继续执行等效复位
典型电流(实测参考)1mA级9μA级1.8μA级
典型应用短时等待定时采集按键唤醒、长期待机

这张表基于GD32F303实测,不同系列和不同主频会有差异,但相对关系是一致的。搞清楚了这层,就能避免一个常见错误:不是所有低功耗需求都得用待机,有时候深度睡眠反而更合适。

2. 从库函数到底层寄存器:进入低功耗的动作拆解

GD32标准外设库把低功耗模式封装得很简单,一个函数就能进去。但只背函数名永远调不准,因为不同系列的库函数参数有差异,而且进入前后要处理的事情远比函数本身多。

2.1 睡眠模式为什么一行代码就够了

睡眠模式的标准写法:

pmu_to_sleepmode(WFI_CMD);

有的系列库函数里叫pmu_to_sleepmode,有的叫pmu_to_sleep,但核心动作就是执行CPU的WFI指令。所谓WFI,就是Wait For Interrupt,CPU停在这里,直到一个中断到来。

这里有个很多人第一次用会理解错的点:WFI被中断唤醒后,CPU会先执行中断服务程序,执行完毕再返回WFI的下一条指令。也就是说,你不能在WFI后面紧跟一句“唤醒后处理逻辑”的代码,指望它立刻执行。正确的做法是把“唤醒后干什么”写在中断服务程序里,或者用标志位,在WFI返回后再处理。

如果你希望中断返回后不回到原来的执行流,而是直接再睡过去,可以设置Cortex-M3的SLEEPONEXIT位。这在纯中断驱动的程序里很常见:主循环只做一件事——进睡眠,所有工作都在中断里完成。

2.2 深度睡眠:SLEEPDEEP、LDO参数和WFI的关系

深度睡眠的标准写法:

pmu_to_deepsleepmode(PMU_LDO_LOWPOWER, WFI_CMD);

这句函数内部做了三件事。

第一,设置SCB系统控制寄存器里的SLEEPDEEP位。这一位是决定WFI/WFE指令执行后到底进入普通睡眠还是深度睡眠的关键。睡眠模式不设这一位,深度睡眠设置这一位。

第二,配置电源控制寄存器,把模式指向深度睡眠而不是待机。GD32的电源控制寄存器里有一个模式选择位,这位置0就是深度睡眠,置1就是待机。库函数帮你区分好了。

第三,选择LDO的工作模式。PMU_LDO_LOWPOWER表示稳压器进入低功耗模式,进一步省电;如果选择PMU_LDO_NORMAL,稳压器保持正常输出,唤醒后负载能力更强。

执行WFI后,芯片才会真正睡过去。所以库函数的形式是“配置模式参数 + 然后WFI”,不是单独一个模式切换指令。

唤醒后要特别注意:深度睡眠把HSE、HSI、PLL都关了,即使你原来跑的是内部HSI,唤醒后时钟树也不会自动恢复原样。正确做法是唤醒后调用一次系统时钟初始化函数,把时钟重新配置好,再操作外设。这一步漏掉,最常见的现象就是串口波特率变乱、定时器时间不对、ADC采样值跳动。

如果用的是GD32标准外设库的工程模板,时钟初始化的函数一般在system_gd32f10x.c里,叫SystemInit。你可以在唤醒后重新调用它,再把外设的时钟使能和分频重新做一遍。

2.3 待机模式的额外几步与唤醒后的复位行为

待机模式的标准写法:

pmu_backup_write_enable(); pmu_flag_clear(PMU_FLAG_STANDBY); pmu_wakeup_pin_enable(PMU_WAKEUP_PIN0); pmu_to_standbymode(PMU_LDO_LOWPOWER, WFI_CMD);

进入待机前必须做三件事。

第一,使能备份域写访问。因为待机模式下备份寄存器是唯一能保存用户数据的地方,而备份域默认写保护。调用pmu_backup_write_enable()取消写保护后,才能操作备份寄存器和RTC。

第二,清掉待机标志位。GD32的电源控制寄存器里有个Standby标志,记录芯片是否发生过待机唤醒。如果不清除它,可能影响下一次进入待机的判断逻辑,甚至在某些系列里导致意外行为。养成进入前清标志、唤醒后读标志并清除的习惯,能少很多坑。

第三,使能WKUP唤醒引脚。待机模式的唤醒源有限,常用的引脚唤醒必须提前配置。WKUP引脚通常是PA0,上升沿触发。你需要在硬件上把PA0接一个按键到高电平,或者接一个默认低电平、事件到来时拉高的信号源。引脚如果悬空,噪声导致的反复上升沿会把芯片从待机里频繁叫醒,整机功耗直接报废。

执行pmu_to_standbymode后,芯片立刻断电,所有代码执行流到此为止。唤醒后,芯片从启动向量重新跑,你可以把它理解为一次“没有按键复位的上电过程”。

2.4 唤醒之后的第一件事:重新喂饱时钟

这个建议针对深度睡眠,因为待机唤醒本身就是复位,会自动走SystemInit。

很多人在深度睡眠唤醒后只做外设重新初始化,忘了时钟。看起来代码没问题,但实测就是不对。原因在于深度睡眠把PLL关掉了,如果你原来主频来自PLL,唤醒后CPU可能还在用低速时钟,或者时钟没恢复彻底。

我的习惯是写一个system_clock_recover()函数,里面包含:

  • 调用SystemInit()重新配置系统时钟
  • 重新使能需要用到的外设时钟
  • 重新配置串口波特率相关寄存器(如果外设初始化里没做)

把这个函数放在所有唤醒处理的最前面,然后再判断唤醒源、恢复外设。这样做之后,深度睡眠和睡眠的切换就非常稳定了。

3. 唤醒源设计:什么时候该醒来,用哪个源

低功耗模式选得再好,唤醒源设计不对,整个系统还是白搭。唤醒源决定了你能不能在正确的时间醒来,以及醒来后能不能快速定位自己为什么醒了。

3.1 RTC闹钟定时唤醒:数据采集场景首选

定时唤醒最常用的是RTC闹钟。比如温湿度记录仪每5分钟醒一次,采集数据、存Flash、继续睡。这种场景用RTC最合适,因为深度睡眠和待机模式下,只要备份域还在供电,RTC就能靠32.768kHz晶振继续跑。

RTC配置一般分几步:

  • 使能备份域访问和RTC时钟
  • 选择RTC时钟源,通常是LXTAL外部低速晶振
  • 配置预分频,得到1Hz的计数时钟
  • 设置闹钟值
  • 使能RTC闹钟中断或事件唤醒

伪代码如下:

rtc_configuration(); rtc_alarm_config(rtc_counter_get() + 300); /* 5分钟 */ nvic_irq_enable(RTC_IRQn, 2, 0); pmu_to_deepsleepmode(PMU_LDO_LOWPOWER, WFI_CMD);

要注意的是,RTC承载了时间基准,进入低功耗前千万不要再把RTC相关寄存器重新初始化一遍,否则时间会跳变。正确做法是初始化一次后,以后每次唤醒都不动RTC配置,只读计数或设置下次闹钟。

还有一点,备用电池的设计。深度睡眠和待机模式下如果主电源完全断开,但RTC还要继续走,必须有VBAT备用电源。很多开发板直接把VBAT接主电源,这样整机断电后RTC时间会丢。如果是做需要长期走时的产品,VBAT必须接纽扣电池或超级电容。

3.2 EXTI与WKUP引脚:按键唤醒的门道

睡眠模式和深度睡眠模式可以被EXTI外部中断唤醒。你可以把GPIO配置成下降沿或上升沿触发,比如门磁传感器报警、人体红外触发、按键按下,都存在EXTI线上,从而唤醒芯片。

待机模式的引脚唤醒不一样。它不能走EXTI中断,只能靠专用的WKUP引脚上升沿。这是因为待机模式下NVIC都已经掉电了,中断机制不工作,唤醒只能靠独立的电源管理逻辑。

具体到GD32F103系列,WKUP引脚是PA0。如果你的产品上电默认PA0是低电平,按键按下拉高,就可以用待机模式做到“按一下,起来工作,再按一下,继续睡”的效果。注意WKUP引脚唤醒后不需要清中断标志,因为它本质是事件唤醒。但如果你想区分唤醒原因,还是要读电源控制寄存器的标志位。

按键唤醒有个硬件细节:按键两端最好并联一个100nF电容,滤除机械抖动。虽然WKUP唤醒不经过CPU,毛刺也可能直接触发唤醒,造成设备“没按却醒了”。加电容后能明显减少误唤醒。

3.3 用复位标志判断“我是怎么醒的”

待机唤醒后代码会从main重新跑,如果你不在main开头判断唤醒原因,就不知道系统是上电还是被唤醒。好在GD32提供了标志位。

if (RESET != pmu_flag_get(PMU_FLAG_STANDBY)) { /* 从待机模式唤醒 */ pmu_flag_clear(PMU_FLAG_STANDBY); /* 从备份寄存器恢复现场 */ }

对于深度睡眠,由于不经过复位,你可以在唤醒函数里直接判断当前走到哪一步,不需要标志位。但为了排查问题,我建议每次唤醒后记录一个递增计数,存到备份寄存器里,方便后期确认唤醒次数和异常复位次数。

不要把关键现场数据只存在普通全局变量里。待机模式SRAM会丢,只有备份寄存器能保留数据。GD32的备份寄存器数量足够存几个关键状态,比如设备编号、唤醒次数、上次采集时间。每次进入待机前写备份寄存器,唤醒后读出来,比存在Flash快得多,也不存在擦写寿命问题。

4. 实测功耗与电池寿命:别只看数据手册

数据手册上的低功耗电流都是在理想条件下测的,没有外设、引脚不浮空、LDO配置最优。实际产品里,一个LED下拉电阻就可能吃掉你几十微安。想判断一个方案能不能用,必须自己测,自己算。

4.1 功耗测量:万用表、采样电阻、功耗分析仪

最入门的方法是万用表串联电流档。但低功耗测量有个很阴的坑:万用表在mA档和μA档之间切换时内阻不一样。你用μA档测低功耗电流,内阻可能高达几百欧,设备一醒来,电流瞬间拉高,在这几百欧上产生的压降就足以让芯片欠压复位。结果就是:你越认真地测低功耗,板子越容易反复复位。

我的做法是两种:

  • 用一台支持自动量程的数字万用表,测平均电流,大概判断量级。
  • 在电源回路串联一个10Ω精密采样电阻,用示波器测电阻两端电压,再除以10得到电流波形。示波器可以看到睡眠、唤醒、工作的完整电流变化过程,定位哪个阶段耗电。

如果条件允许,强烈建议用功耗分析仪或者J-Link配套的能量分析工具,能直接给出电量积分,算出每个阶段的电荷消耗。

测量时还有两个细节。第一,测试前把所有串口打印关掉。调试串口连着USB转串口模块时,模块会从目标板取电,测出来的电流根本不是单片机的。第二,程序进入低功耗后,把调试器断开。J-Link、ST-Link这些调试器会维持内核调试接口的时钟,导致芯片无法进入最低功耗。

4.2 从μA到年:电池寿命估算全过程

以我实测过的GD32F303温湿度记录仪为例,设计参数如下:

  • 深度睡眠电流9μA
  • 每5分钟唤醒一次,每次醒来工作50ms,工作电流12mA
  • 其余时间深度睡眠

计算平均电流:

每次循环时间是300秒,工作50ms即0.05秒,睡眠299.95秒。

工作阶段占空比:0.05 / 300 = 0.0001667

平均电流 = 12mA × 0.0001667 + 0.009mA × (1 - 0.0001667)

这一步算下来大约是0.002mA + 0.0089985mA,也就是0.011mA左右,约11μA。

如果改用睡眠模式,背景电流1mA,平均电流会变成大约1.002mA,直接差了90倍。

电池容量用CR2032,额定容量210mAh,考虑实际可用打八折,约168mAh。

寿命估算:168mAh / 0.011mA ≈ 15272小时 ≈ 636天,接近1.7年。

如果换成待机模式,背景电流降到1.8μA,但唤醒后要重新初始化,等工作时间可能从50ms变成150ms,平均电流大约是12mA × 0.0005 + 0.0018mA × 0.9995 ≈ 0.0078mA,寿命可以拉到2.5年以上。

这个对比说明一个道理:决定电池寿命的不只是睡眠电流,还有唤醒频率和工作时间。唤醒越频繁,越要权衡“省电模式”和“唤醒恢复开销”之间的关系。

4.3 那些偷偷吃掉微安的“隐形漏洞”

只优化单片机不优化整板,低功耗设计就失败了一半。常见偷电点有几个。

GPIO浮空排第一。芯片进入低功耗前,没被初始化、或者被配置成浮空输入的引脚,会通过输入缓冲产生漏电流,一个引脚几百纳安到几微安不等,几十个引脚加起来非常可观。进入低功耗前,把所有用不到的GPIO统一配置成模拟输入,或者输出低电平,能显著降低整板电流。

LED限流电阻是第二号元凶。很多人只关了LED,但限流电阻一端连着GPIO,GPIO输出低电平时电阻上的电流确实是零。问题是如果GPIO配置成高电平或者浮空,电阻上就有电流。更隐蔽的是LED的漏电流很小,但串联的1k电阻如果接在3.3V电源和地之间,哪怕没有LED,电阻本身就有毫安级电流。所以不用LED时,应该把对应GPIO配置成模拟输入,而不是只输出低电平。

板上稳压器是第三号。很多LDO静态电流5μA起步,你单片机再怎么降到2μA,整机还是有七八微安。如果做长期待机,选静态电流低于1μA的LDO才有意义,或者用负载开关在待机时把不用的电路断电。

滤波电容漏电和电阻分压电路也是隐藏开销。特别是有电池电压检测分压电阻的产品,两个100k电阻直接挂在电池两端,就是33μA的持续电流,比你单片机待机还高一截。正确做法是用MOS管或者单片机GPIO控制分压电阻的接地端,只在检测时接通。

5. 移植和调试阶段的坑:从STM32过来尤其注意

很多团队用GD32是因为它可以兼容STM32。但这个“兼容”主要体现在引脚和软件框架上,低功耗部分还是有不少差异。坑踩多了,总结成下面几条。

5.1 库函数版本和寄存器命名不是ST的翻版

GD32早期对外设库的设计参考了STM32标准外设库,但函数名、参数定义和寄存器位命名都改过。直接拿STM32的低功耗例程往GD32上套,编译都过不了。

比如STM32F1的PWR_EnterSTOPMode,GD32对应的是pmu_to_deepsleepmode;STM32的PWR_EnterStandbyMode,GD32对应的是pmu_to_standbymode。参数也从“模式枚举”变成了“LDO模式 + WFI/WFE命令”。

更重要的一点,不同系列的GD32库函数也有差异。GD32F10x、GD32F30x、GD32E230、GD32L233,它们的PMU外设设计不完全一样,库函数接口也不完全一样。项目里一定要以你所用系列对应版本的固件库为准。我见过有人把GD32F30x的例程直接拷到E230上,函数名碰巧一样,但底层寄存器偏移都对不上,调试了半天。

我的做法是尽量用官方提供的GD32 Embedded Builder或官方固件库模板,而不是自己从零拼。官方模板里的system_gd32系列文件已经把时钟初始化和启动文件配好了,低功耗相关的PMU驱动也在库里,省很多事。

5.2 调试器一接,芯片就睡不着

这是低功耗调试里最挫败的一种情况:代码看起来完全正确,传感器电流就是降不下来。折腾半天,发现是J-Link还插着。调试器为了维持调试会话,会强制芯片保持调试时钟,低功耗模式根本进不去,或者进去后立刻被调试请求唤醒。

不只是在线调试,哪怕程序已经烧录完毕,但J-Link的线还连着目标板的SWD引脚,也可能有影响。低功耗调试建议按下面的流程:

  • 先把程序烧录到Flash
  • 拔掉调试器
  • 用外部电源或电池给板子供电
  • 用万用表或功耗分析仪测电流

如果在Keil里必须在线调试,可以在进入低功耗前设置一个断点,等芯片睡过去后,调试器就会报“Cannot access target”,这时候基本可以确定睡眠代码执行到了。不要试图在这种状态下继续单步,只会把芯片踩醒。

GD32在Linux下的开发环境也一样,用gcc工具链交叉编译,J-Link的JLinkExe烧录,流程没问题,但低功耗测量必须断开调试器,这是硬规则。

5.3 唤醒后外设时钟没恢复,串口乱码、ADC乱跳

深度睡眠唤醒后,系统时钟可能还处于未恢复状态。如果你直接发串口数据,波特率是按错误的时钟算的,出来就是乱码。ADC采样也一样,采样时间、转换时间全部依赖ADC时钟,时钟不对结果就不稳定。

这个问题的排查方法很简单:在唤醒处理函数最开始重新调用系统时钟初始化,然后再初始化外设。如果你用的是HSE + PLL倍频配置,尤其要注意,因为PLL在深度睡眠期间是关断的,唤醒后不会自动重新锁定。

还有一个容易忽略的点:如果深度睡眠前用了低功耗LDO模式,唤醒后刚开始给外部负载供电的能力有限。如果你一醒来就开始执行Flash写入或者RF发射,可能把电压拉低导致复位。稳妥的做法是唤醒后先做51微秒级延时,或者把LDO切回PMU_LDO_NORMAL,等稳压器稳定再干重活。

5.4 GPIO浮空不只是费电,还可能直接唤醒不成功

GPIO浮空的第一个危害是漏电流,第二危害更隐蔽:它可能让唤醒源无法正常工作。

比如WKUP引脚PA0,如果你在硬件上外接了一个按键到GND,按下后拉低,但单片机上没配内部上拉,那待机模式下引脚状态不确定,系统可能反复唤醒,也可能完全唤不醒。GD32的WKUP唤醒方向是上升沿,要求默认状态是确定的低电平,事件来时拉高。如果引脚悬空,上升沿的触发条件不成立。

建议硬件设计时给WKUP引脚加一个外部下拉电阻(10k),或者芯片支持内部下拉就配置内部下拉,确保默认电平确定。

对于其他GPIO,进入低功耗前的统一处理方式:

gpio_init(GPIOA, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, GPIO_PIN_ALL); gpio_init(GPIOB, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, GPIO_PIN_ALL); gpio_init(GPIOC, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, GPIO_PIN_ALL);

把用不到的引脚全部设成模拟输入,既避免了漏电流,也不影响芯片状态。唤醒后再恢复成实际需要的功能。

但要注意,有些引脚承担特殊功能,比如WKUP、RTC晶振引脚、复位引脚,不能一股脑配置成模拟输入。做批量配置前,先过一遍原理图,把特殊引脚摘出来。

最后分享一个我调试低功耗的习惯:永远先测“最小系统的底电流”。也就是只保留MCU、最小复位电路、电源电路,跑一个空循环进入深度睡眠的测试程序,测出芯片本身的底电流。然后一层层加代码、加外设、加中断,每一步重新测一次电流。这样一旦电流异常,立刻能锁定是哪个环节引入的。别一开始就整套代码直接睡眠,那样出了问题根本找不到元凶。

低功耗设计没有太多玄学,就是把模式选对、唤醒源接对、引脚管好、电流测准。做到这四步,电池供电的设备就能稳定跑上一年以上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询