
最近又折腾了一遍GD32的低功耗起因是一个做智能门锁的朋友跑来问我同样是省电模式为什么深度睡眠和待机唤醒之后表现完全不一样这个问题确实问住过不少人。GD32的电源管理在Cortex-M3/M4内核基础上做了不少封装睡眠、深度睡眠、待机三套机制各有各的脾气寄存器配置、唤醒方式、代码写法和排错思路都不一样。这篇文章就把这三种模式从头到尾捋一遍包含原理、代码、唤醒配置、实测电流和踩坑记录适合正在用GD32做电池供电产品的工程师也适合刚接触GD32低功耗、想搞明白该选哪种模式的同学。1. 三种模式的设计思路与选型逻辑1.1 从内核机制看三种模式的差异GD32的内核是Cortex-M3/M4它自带两类低功耗机制一类是内核层面的普通睡眠通过WFI或WFE指令让CPU暂停另一类是通过系统控制寄存器SCB_SCR里的SLEEPDEEP位进一步切到深度睡眠。GD32的PMU外设在这个基础上增加了电源域管理能力从而把“睡眠”扩展成了三个档位睡眠模式、深度睡眠模式、待机模式。理解这三个档位的核心在于搞清楚到底什么东西被关了睡眠模式CPU停外设不停。VDD电源域、SRAM、内部时钟全部保持外设如定时器、串口、SPI照常工作。CPU只是暂停执行任何中断或事件都能把它叫醒。深度睡眠模式内核进入深度睡眠态GD32会关掉大部分系统时钟。外设时钟被切断只有始终保持供电的区域和部分唤醒源还在工作。SRAM内容保留寄存器保留唤醒后可以从原断点继续跑。待机模式整个PMU的LDO都关闭几乎全部数字逻辑掉电。SRAM内容丢失寄存器复位唤醒后等于重新上电复位程序从启动代码开始执行。把这三个档位比作人的睡眠状态就好理解了。睡眠模式像打盹有点动静就能醒醒过来马上能干活深度睡眠像深睡周围的事全停了得靠闹钟喊待机模式像关机再启动要走完整的开机流程之前内存里的东西全没了。这个底层差异直接决定了三种模式的功耗量级、唤醒源种类和唤醒后的行为也是选型时最先要考虑的因素。1.2 一张表看清三种模式对比项睡眠模式深度睡眠模式待机模式CPU状态暂停时钟保持停止掉电外设时钟继续运行全部关闭全部关闭SRAM内容保持保持丢失寄存器内容保持保持复位唤醒源任意中断/事件RTC闹钟、外部中断EXTI等WKUP引脚上升沿、RTC闹钟、NRST复位唤醒后行为从WFI/WFE下一条继续执行从原断点继续执行从0地址重新启动典型电流3~5mA视外设而定2~5uALDO低功耗模式2uA左右官方典型值唤醒时间数个时钟周期数百us到ms级ms级这里的电流数值是基于GD32F103平台、外设全部关闭且LDO处于低功耗模式下的实测参考值。你的板子电源设计、引脚状态、外部电路都会直接影响最终数字后面第五部分我专门讲测量和排查。1.3 选型的核心唤醒后要不要保留现场选深度睡眠还是待机关键就看一点唤醒后需不需要保留RAM里的数据和原执行状态。如果是做数据采集设备采集到一半进入低功耗唤醒后要继续断点执行那就必须用深度睡眠。如果在低功耗期间只需要把少量关键数据保存下来也可以先把数据写进备份寄存器BKP然后进待机模式唤醒后从备份寄存器里把现场恢复出来。备份寄存器在待机模式下由VBAT供电继续保留这个方案在做极低功耗产品时非常常见。如果是做门锁、遥控器、传感器节点这类唤醒后直接“重新干活”的设备待机模式更合适。因为唤醒等于复位所有初始化流程重新走一遍逻辑干净出错概率低。2. 睡眠模式轻量省电响应最快2.1 睡眠模式的原理与正确打开方式睡眠模式操作起来最简单。把SCB_SCR里的SLEEPDEEP位清0然后执行WFI或WFE指令即可。GD32标准外设库也提供了封装函数pmu_to_sleep_mode(WFI_CMD)但对于这个模式直接用CMSIS里定义的__WFI()更直接。这里要注意WFI和WFE的区别。WFI是等待中断唤醒如果中断使能唤醒后会先进入中断服务程序执行完再返回到WFI指令的下一条继续运行。WFE是等待事件唤醒如果对应事件寄存器里已经有事件执行WFE会直接跳过睡眠状态被事件唤醒时不会进中断服务程序直接从WFE下一条继续跑。不少初学者会在这两个指令上栽跟头以为是代码问题其实是唤醒机制理解错了。睡眠模式一个很实用的特性是SysTick仍然在跑。因为SysTick的时钟源来自HCLK而睡眠模式下HCLK没有停。这意味着可以用SysTick做定时唤醒实现一个低功耗版本的软件定时器。很多RTOS的空闲钩子就是这么干的没事的时候WFI等下一个定时事件到来再唤醒。2.2 睡眠模式的代码示例与SysTick定时唤醒下面这个例子演示了按键中断唤醒睡眠模式唤醒后翻转LED#include gd32f10x.h void sleep_mode_test(void) { // 确保SLEEPDEEP0这决定是普通睡眠而不是深度睡眠 SCB-SCR ~SCB_SCR_SLEEPDEEP_Msk; // 配置PA0作为外部中断唤醒源上升沿触发 gpio_init(GPIOA, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_0); gpio_exti_source_select(GPIO_PORT_SOURCE_GPIOA, GPIO_PIN_SOURCE_0); exti_init(EXTI_0, EXTI_INTERRUPT, EXTI_TRIG_RISING); nvic_irq_enable(EXTI0_IRQn, 0, 2); while (1) { __WFI(); // 进入睡眠等待中断唤醒 gpio_bit_toggle(GPIOA, GPIO_PIN_1); // 唤醒后翻转LED } } void EXTI0_IRQHandler(void) { exti_interrupt_flag_clear(EXTI_0); }如果你需要定时唤醒用SysTick配合WFI非常方便。思路是维护一个递减计数器在SysTick中断里减一主循环里不断WFI直到计数器归零volatile uint32_t sleep_tick 0; void SysTick_Handler(void) { if (sleep_tick 0) { sleep_tick--; } } void sleep_until(uint32_t ms) { sleep_tick ms; SCB-SCR ~SCB_SCR_SLEEPDEEP_Msk; while (sleep_tick 0) { __WFI(); } }这样主循环里调用sleep_until(1000)就能精确延时1秒期间大部分时间处于WFI睡眠态。比单纯空转省电得多MCU基本没有发热实测在GD32F103上电流能从一个LED闪烁的十几毫安降到5毫安左右代价仅仅是代码多几行。2.3 睡眠模式的坑调试器会“稳住”芯片用JLink调试睡眠代码时有一个很典型的坑程序卡在WFI位置半天不往下走你以为死机了其实大概率是调试器在作怪。调试器连接状态下SWD接口时钟还在工作内核状态被调试器“稳住”WFI指令的行为会和断开调试器时不一样。具体表现是连上JLink后可以正常唤醒但烧录完程序、断开调试线按键唤醒才恢复正常。更麻烦的是有些调试器即使断开连接也会持续给芯片供电导致功耗根本压不下来。所以我的习惯是测低功耗前把调试器整个拔掉或者用带硬件断点的J-Flash烧录后直接断开。如果必须在调试状态下观察睡眠流程可以先用调试器跑到WFI前然后断开调试器等硬件触发唤醒后再重新连接。这个方法麻烦但能救急。睡眠模式虽然省电有限但它的意义在于响应速度极快、实现简单、不丢现场。在做需要频繁短暂休眠、又要求外设继续工作的场景比如通信模块等待数据、按键扫描防抖用它正合适。3. 深度睡眠模式低功耗交互设备的首选3.1 深度睡眠的硬件机制深度睡眠模式是实际项目里用得最多的一档。它跟睡眠模式的本质区别是所有系统时钟全部停止包括HCLK、PCLK1、PCLK2以及所有外设的时钟。这等于把芯片的大部分动态功耗直接砍掉只剩下静态功耗和必须保持供电的逻辑单元。GD32进入深度睡眠比STM32多一步PMU操作。需要同时做三件事把SCB_SCR的SLEEPDEEP位置1往PMU_CTL寄存器写入DPSLP位再执行WFI或WFE指令。GD32标准外设库把这套流程封装成了pmu_to_deepsleep_mode函数。PMU_CTL里的LDOLP位是另一个关键点。置位表示进入LDO低功耗模式此时内部线性稳压器输出降低进一步减小静态电流清零则保持LDO正常输出。用LDO低功耗模式能省下大约一半的深度睡眠电流代价是唤醒时间变长而且唤醒后LDO需要额外时间恢复到高功率输出。如果应用对唤醒时间敏感比如需要在毫秒级响应外部事件就选LDO正常模式如果追求极限续航选LDO低功耗模式更划算。深度睡眠模式下SRAM内容完整保留所有寄存器值也保留。唤醒后不需要像待机模式那样从头跑启动代码直接从调用进入函数的下一行继续执行。这是它跟待机模式最本质的区别。3.2 深度睡眠的进入与唤醒实现以GD32F103为例写一个RTC闹钟唤醒的完整流程。首先配置RTC用LSI作为时钟源。GD32F10x的LSI典型频率是40kHz左右设置预分频器为40000-1就能得到大约每秒一次的RTC计数实际上LSI精度有限想精确到秒级需要外接32.768kHz晶振或做校准这个后面讲坑的时候细说。void rtc_setup_alarm(void) { rcu_periph_clock_enable(RCU_PMU); rcu_periph_clock_enable(RCU_BKP); rcu_bkp_reset_enable(); rcu_bkp_reset_disable(); // 使能写备份域寄存器 rtc_parameter_struct rtc_init; rtc_init.rtc_prescaler 40000 - 1; // LSI约40kHz得到1Hz rtc_init.rtc_division_factor 0; rtc_init.rtc_amor 0; rtc_init.rtc_year 21; rtc_init.rtc_month 1; rtc_init.rtc_day 1; rtc_init.rtc_hour 0; rtc_init.rtc_minute 0; rtc_init.rtc_second 0; rtc_init_config(rtc_init); rtc_alarm_struct rtc_alarm; rtc_alarm.alarm_mask RTC_ALARM_MASK_NONE; rtc_alarm.week_mask RTC_ALARM_MASK_NONE; rtc_alarm.alarm 10; // 10秒后触发闹钟 rtc_alarm_config(rtc_alarm); rtc_flag_clear(RTC_FLAG_ALARM0); rtc_interrupt_enable(RTC_INT_ALARM0); }然后进入深度睡眠void enter_deepsleep_mode(void) { rtc_setup_alarm(); // 标准库函数LDO低功耗模式WFI指令触发 pmu_to_deepsleep_mode(PMU_LDO_LOWPOWER, WFI_CMD); // 唤醒后从这一行继续执行 system_clock_config(); // 重新配置系统时钟 // 重新初始化所有外设... }如果要用外部中断唤醒配置方式跟睡眠模式类似只不过中断源要选择能在深度睡眠下工作的唤醒源。GD32F10x上PA0对应的EXTI0是标准的WKUP引脚唤醒路径把它配置成上升沿或者下降沿触发中断NVIC使能EXTI0_IRQn然后在深度睡眠模式下就能唤醒。唤醒后同样需要重新配置系统时钟。3.3 深度睡眠的注意事项深度睡眠唤醒后第一件事永远是恢复系统时钟。因为HXTAL在深度睡眠期间停振唤醒后HSI倒是立刻就能用但如果你原本用的是HXTALPLL跑到108MHz必须等待HXTAL稳定后再重新配置PLL否则主频会不对。配套的坑是Flash等待周期。GD32跑高主频时Flash读取需要配置合理的等待周期。唤醒后如果系统时钟恢复到108MHz而Flash等待周期还是低主频的配置程序取指就会出错表现为唤醒后跑飞、进HardFault或者乱码。所以恢复时钟的代码一定要包含Flash等待周期的设置。深度睡眠状态下的“code flash”和“flash”话题也经常有人混。GD32的存储架构里Code Flash是芯片内部用来存放并执行程序的Flash区域而“flash”通常指整个Flash存储空间。在低功耗工程里Code Flash一直保持上电状态因为唤醒后CPU要从里面取指令执行。有些型号还支持Code Flash在低功耗下关电来进一步省电但进入前必须确保不再从Code Flash取指否则直接宕机。这个特性一般用不到但有印象总比踩坑再查好。另一个容易忽略的点是中断标志。深度睡眠被RTC闹钟唤醒后RTC闹钟中断标志如果不清除会导致唤醒后立刻再次进入中断甚至反复唤醒。同样的道理适用于所有EXTI中断。唤醒流程里务必要在中断服务函数里把对应标志清干净。4. 待机模式极限功耗方案4.1 待机模式的机制与唤醒流程待机模式是GD32功耗最低的模式。进入待机后PMU的LDO直接关闭内核和大部分外设的电源域全部掉电SRAM数据全部丢失所有寄存器复位。整个主控芯片除了备份域和唤醒电路还在供电其余部分跟断电没什么两样。唤醒后的行为本质上等价于一次上电复位CPU从0地址开始重新执行启动代码所有初始化流程全部重来。所以你不必纠结“从哪个断点继续”你的程序在待机唤醒后就是一次完整的重启。这带来两个好处代码逻辑简单启动流程干净同时也带来两个代价唤醒时间长RAM数据全没了。GD32待机模式可用的唤醒源有这几个WKUP引脚上升沿。GD32F10x上这个引脚就是PA0。RTC闹钟唤醒。前提是RTC还在备份域运行时钟源用LSI或LSE且VBAT有供电。NRST复位引脚。严格说这是复位不是唤醒但也能从待机状态拉回来。进入待机有一个容易栽的细节必须先把PMU_CS寄存器里的WUF唤醒标志清掉。如果不清待机模式可能进不去或者进去后马上又醒来。所以调用进入待机函数前的标准动作是pmu_flag_clear(PMU_FLAG_RESET_WAKEUP)。4.2 待机模式的代码实现以RTC闹钟唤醒为例代码比深度睡眠少一截void enter_standby_mode(void) { rcu_periph_clock_enable(RCU_PMU); rcu_periph_clock_enable(RCU_BKP); rtc_setup_alarm(); // 同样用RTC闹钟做唤醒源 // 清除唤醒标志否则待机可能进不去 pmu_flag_clear(PMU_FLAG_RESET_WAKEUP); // 进入待机执行WFI触发 pmu_to_standby_mode(WFI_CMD); // 代码永远不会执行到这里 // 唤醒后芯片直接复位从main入口重新跑 }如果只想用PA0引脚唤醒不需要RTC代码更简洁void enter_standby_mode_wkup(void) { rcu_periph_clock_enable(RCU_PMU); pmu_flag_clear(PMU_FLAG_RESET_WAKEUP); pmu_to_standby_mode(WFI_CMD); // 唤醒后同样直接复位 }这里pmu_to_standby_mode函数内部会把STBMOD位置1然后执行WFI指令。如果你的库函数版本里没有这个封装手动操作时记得把PMU_CTL的STBMOD位置位再执行__WFI()。4.3 待机模式的实测与选型建议待机模式功耗实测跟板子设计的关系极大。芯片自身在待机模式下能做到2uA左右但整板电流经常是几十甚至上百uA问题几乎都出在外部电路上。最典型的是PA0/WKUP引脚的悬空或外部上拉电阻。WKUP引脚本身有内部上拉如果外部再接一个10K上拉到VDD待机状态下这个电阻就会产生约0.33uA的电流看着不大但如果是一排引脚都这么处理累计起来不可小视。更夸张的情况是某颗LED限流电阻没断开或者某个传感器的电源始终没关那种情况下待机电流能直接飙到毫安级。另外待机模式下如果JTAG/SWD引脚保持连接状态调试器供电也会把电流拉起来。测量待机功耗前必须把所有调试线、串口线全部拔掉只保留电源线。选择深度睡眠还是待机我的判断准则如下判断维度选深度睡眠选待机RAM数据必须保留可以丢唤醒后恢复时间越快越好可接受ms级重启产品逻辑需要断点续传重启后重新初始化极低功耗也能做到极致代码复杂度稍复杂简单这里多提一句如果你的产品在待机期间不需要保存任何数据待机模式永远是更优解。反过来需要保留RAM里大量缓冲数据的场景即使待机能做到2uA你也只能选深度睡眠。5. 实战经验把低功耗跑稳的6个细节5.1 GPIO处理是最大的坑很多人在低功耗模式上栽跟头不是因为模式选错而是GPIO没处理好。芯片进入深度睡眠或待机后所有GPIO维持进入前的状态。如果某个引脚是浮空输入引脚电平不定内部施密特触发器的漏电就会变大如果某个引脚被外部电路拉到一半电压漏电路径更明显。处理方法是进入低功耗前把所有不用的GPIO配置为模拟输入模式GPIO_MODE_AIN。模拟输入会关闭数字输入缓冲漏电最小。这个动作看似简单实际效果非常显著。我做过一次对比同样一块板子在深度睡眠模式下GPIO保持原配置时整板电流20uA全部配置成模拟输入后降到4uA。同时别忘了关闭GPIO对应的RCU时钟。GPIO配置成模拟输入后如果GPIO时钟还开着动态功耗依然存在。进入低功耗前应该把RCU里能关的外设时钟全部关掉只保留必要的中断唤醒源。5.2 测量功耗的正确姿势低功耗项目的成败最终要靠数据说话所以测量方法是必学的技能。最基础的方式是把万用表串到电源回路里用电流档测整板静态电流。但很多人不知道串联万用表时会有额外压降而且数字万用表的电流档在低量程时内阻也不小对低功耗MCU的供电电压会有轻微影响。如果MCU工作电压接近最小值这个压降可能导致芯片工作不正常。更实用的方法是串一个10欧电阻到供电回路上用示波器观察电阻两端的电压差换算成电流波形。这样能抓到唤醒瞬间的动态电流尖峰也能看到不同阶段的电流变化比万用表记录平均值直观得多。实测中我经常看到唤醒瞬间电流冲到几十毫安几毫秒后才回落到静态值这种瞬态特性用万用表是看不见的。测量环境要注意测试时把调试器、USB转串口线全部拔掉只留电池或稳压电源供电。很多“待机电流下不来”的查不到原因最后发现是串口线的地线把电流带走了。5.3 常见问题排查表现象可能原因解决方案唤醒后死机/HardFault系统时钟未重配唤醒后先system_clock_config再初始化外设唤醒后串口乱码外设未重新初始化深度睡眠唤醒后所有外设都要重新配置电流压不下来GPIO悬空/时钟未关进入低功耗前统一配置为模拟输入并关闭RCU时钟无法进入待机WUF标志未清除调用pmu_flag_clear(PMU_FLAG_RESET_WAKEUP)睡眠被频繁唤醒EXTI触发配置错误/中断标志未清检查触发沿中断里清标志RTC唤醒不生效RTC时钟源未起振检查BKP域使能LSI/LSE是否就绪闹钟中断是否使能JLink连上后测功耗奇高调试器供电断开调试器再测LSI计时不准LSI精度有限使用外部LSE晶振或软件校准预分频器5.4 工程工具链的注意事项写低功耗代码的时候你用什么编译环境其实不重要。GD32标准外设库在Keil、GD32 Embedded Builder和Linux下用arm-none-eabi-gcc编译都能正常工作低功耗相关寄存器操作没有区别。不过有几个细节要留意。在Keil里要先确认Device Pack选对GD32的pack跟STM32不是同一个选错型号会导致启动文件不对低功耗代码可能根本跑不起来。GD32 Embedded Builder向导生成的工程里SystemInit和时钟配置是现成的深度睡眠唤醒后要记得重新调用system_clock_config。Linux下用GCC编译时链接脚本里Code Flash区对应的是实际Flash容量如果你在烧录时用JLink/J-Flash选错芯片型号程序可能烧进去但跑飞这个跟低功耗本身无关但排查时会分散注意力。JLink烧录GD32时还有个小讲究烧录完成后默认会保持调试连接这时候如果程序立刻进入待机模式JLink可能因为目标掉电而报错。我一般烧录后直接拔掉调试线或者把J-Flash设置成烧录结束自动断开省得每次都要手动复位。最后再分享一个小技巧说了这么多最后补充一个自己用过很多次的方案。产品里如果同时用到深度睡眠和待机两个档位可以把PA0同时当作待机唤醒引脚和低功耗外部中断源。通过外部电路设计短按触发EXTI中断进入深度睡眠长按触发WKUP功能进入待机硬件上只需要一个按键加一个简单的RC延时电路就能把一个引脚玩出两个唤醒通道。这样既能保留深度睡眠的快速响应能力又能在长期休眠时切到待机模式获取极限续航。低功耗这东西折腾到最后拼的不是你会不会调寄存器而是对整个系统漏电路径的敏感度。多测几次、多查几个可疑点总能找到那个让你“原来如此”的瞬间。希望这篇能帮你在GD32低功耗上少走几步弯路。