一、项目概述与需求解析
先说背景。去年接了个电池供电的采集终端项目,主控用的GD32F303,整机靠一颗CR2032纽扣电池供电,客户要求续航不少于一年。拿到需求的时候我大概估算了一下:如果让MCU满负荷跑,CR2032大概撑不过一周,可一旦把GD32的低功耗模式用起来,睡眠、深度睡眠、待机这三种模式配合着用,把平均电流压到几十微安级别,一年不但没问题,甚至还有余量。项目做完之后,我顺手把GD32F10x和F30x系列的低功耗模式彻底梳理了一遍,今天这篇就把三种模式的原理、寄存器配置、代码实现和踩坑记录一次性讲清楚。
这篇文章适合谁看?两类人最需要:一类是正在做电池供电设备、传感器节点、手持仪器、便携医疗设备这类对功耗敏感的产品,另一类是刚接触GD32、想搞明白低功耗模式到底怎么用的单片机开发者。哪怕你之前只点过灯、跑过串口,只要照着文章里的思路走一遍,也能把三种模式玩明白。
二、三种低功耗模式到底差在哪
刚接触低功耗的人最容易犯的错,就是把“睡眠”“深度睡眠”“待机”当成同一件事。实际上GD32这三种模式在CPU状态、时钟状态、外设状态、唤醒方式、唤醒后执行路径上差别非常大,用错了地方,轻则功耗压不下去,重则设备睡死过去叫不醒。
2.1 核心差异:CPU停没停、时钟停没停、内容保不保
先把结论放在前面。GD32的三种低功耗模式,本质上是在回答三个问题:CPU还跑不跑?系统时钟还转不转?RAM里的数据还在不在?
- 睡眠模式(Sleep Mode):CPU停止取指执行,但系统时钟照常运行,所有外设照常工作,RAM数据完全保留。唤醒源就是普通的中断或事件,唤醒后CPU从停止的位置继续往下执行,几乎不需要额外恢复时间。它的功耗降低有限,但响应最快。
- 深度睡眠模式(Deep-Sleep Mode):CPU停止,系统时钟停止,大部分外设的时钟也被关掉,但SRAM内容保留,部分外设(比如RTC、备份域)可以继续工作。唤醒后需要重新配置系统时钟,因为原来的时钟树已经断了。功耗可以降到微安级别。
- 待机模式(Standby Mode):这是最狠的一档,整个芯片几乎全部断电,主SRAM内容丢失,只有备份寄存器和备份SRAM还能保留数据,唤醒后芯片相当于经历了一次复位,程序从main函数重新开始执行。功耗可以做到几个微安,甚至更低。
2.2 一张表看清三种模式的定位
| 对比项 | 睡眠模式 | 深度睡眠模式 | 待机模式 |
|---|---|---|---|
| CPU | 停止 | 停止 | 掉电 |
| 系统时钟 | 运行 | 停止 | 停止 |
| 外设时钟 | 运行 | 大部分停止 | 全部停止 |
| SRAM内容 | 保留 | 保留 | 丢失 |
| 备份寄存器 | 保留 | 保留 | 保留 |
| 唤醒源 | 任何中断/事件 | EXTI、RTC等 | WKUP引脚、RTC、NRST |
| 唤醒方式 | 中断/事件 | 中断/事件 | 事件/复位 |
| 唤醒后执行 | 继续执行 | 复位或继续 | 复位从头执行 |
| 功耗水平 | 毫安级 | 微安级 | 亚微安级 |
实际产品里怎么选,我的经验是:需要频繁唤醒响应、对外设功耗不敏感的场景用睡眠模式;大部分时间在睡觉、偶尔醒来干活的传感器节点用深度睡眠;需要极低功耗、能接受掉电重启的场景用待机模式。比如采集终端,休眠期间用深度睡眠,每隔几分钟用RTC唤醒采一次数据,采完继续睡,这一套组合拳下来平均功耗就能压得很低。
2.3 低功耗设计前必须做的两件事
开始折腾模式之前,先做两件准备工作。
第一,把用不到的外设时钟全部关掉。GD32的外设时钟默认是关闭的,但很多人开了外设之后忘了关,进入低功耗模式时外设还在偷偷耗电。建议在进入低功耗之前,用rcu_periph_clock_disable()把所有不用外设的时钟停掉,尤其是GPIO、USART、SPI、I2C这些,每个外设的时钟电流加在一起非常可观。
第二,确认电源管理单元(PMU)已经在工程里配置好了。GD32的低功耗控制核心是PMU模块,它负责管理LVD(低电压检测)和低功耗模式的进入与唤醒。在初始化代码里,要把PMU时钟使能,否则后面调pmu_*系列函数根本没有反应。
另外提醒一句:GD32系列不同型号的库函数名称可能略有差异,但底层寄存器逻辑是一致的。下面以GD32F10x和GD32F30x的标准外设库为例,这两个系列在嵌入式项目里用得最多。
三、睡眠模式:让CPU停下来,外设继续干活
3.1 睡眠模式的工作原理
睡眠模式是所有低功耗模式里门槛最低、行为最好理解的一种。它实际上只做了一件事:让CPU停止执行指令。系统的时钟树、外设、中断控制器、DMA这些都还活着。
为什么这样也能省电?因为处理器核心本身是芯片里的耗电大户。以GD32F303为例,主频跑到120MHz时,CPU核心功耗占整颗芯片功耗的比例相当可观,把CPU停掉,系统电流可以从几十毫安立刻降到几毫安。如果再把一些用不到外设的时钟关掉,还能进一步压低。
在Cortex-M内核里,进入睡眠模式靠的是WFI(Wait For Interrupt)或WFE(Wait For Event)指令。执行WFI后,CPU会挂起,等待中断到来;执行WFE后,CPU会等待事件到来。GD32的标准外设库已经把这些封装好了,调用pmu_to_sleepmode()就能进入睡眠。
3.2 睡眠模式的进入方式与代码
来看代码。使用GD32标准外设库时的最小实现:
/* 使能PMU时钟 */ rcu_periph_clock_enable(RCU_PMU); /* 关闭不需要的外设时钟,减少漏电流 */ rcu_periph_clock_disable(RCU_GPIOA); rcu_periph_clock_disable(RCU_USART0); // ... 根据实际需要关闭其他外设 /* 进入睡眠模式 */ pmu_to_sleepmode(PMU_LDO_NORMAL);pmu_to_sleepmode这个函数,库函数内部会判断当前是否处于中断上下文,然后执行WFI指令。有一点要注意:WFI指令在ICSR寄存器里有一个PENDINGSET机制,如果进入睡眠之前已经有一个中断挂起但没有被处理,WFI会立刻被这个挂起的中断唤醒,导致睡眠“秒醒”。我遇到过好几次这种“睡不下去”的情况,排查到最后都是某个外设中断标志没有清干净。
3.3 睡眠模式的唤醒源与注意事项
睡眠模式的唤醒源非常宽泛,任何一个使能的中断或事件都能把CPU叫醒。它的优点是完全不挑唤醒源,配置起来几乎不用动脑子,缺点是功耗降不下去,因为整个系统的时钟还活着,外设中断一直开着,电流怎么都压不到微安级别。
我一般只在两个场景用睡眠模式。一个是MCU在等待某个很快就来的事件,比如等DMA传输完成、等外设数据准备好,中间只有几十微秒到几毫秒的空档,这种情况下睡一下能省一点功耗,又不需要复杂的唤醒配置。另一个是多外设协作场景,CPU不需要全程盯梢,但外设要一直工作,数据准备好了再通过中断把CPU叫醒。
睡眠模式最大的坑在于中断优先级和中断屏蔽。如果你在进睡眠之前把PRIMASK置1(屏蔽了所有可屏蔽中断),那么WFI只能被不可屏蔽中断(NMI)唤醒,普通中断来了也没用,设备会一直睡下去。另外,如果一个高优先级中断正在服务中,退出的瞬间执行WFI,有些内核会立刻再次进入睡眠,导致后续代码无法执行,这就是SLEEPONEXIT位的影响。解决方法是把SLEEPONEXIT清0,或者在主循环里检查执行路径。
我用睡眠模式时还会刻意避开一个陷阱:外设中断函数里做了重量级处理。因为睡眠模式下CPU是“秒醒”的,如果中断服务函数里跑了一大堆浮点运算、Flash擦写之类的耗时操作,睡眠省下来的电又被中断处理吃回去了。轻量级中断服务函数是睡眠模式的基本素养。
四、深度睡眠模式:功耗和响应速度的平衡点
4.1 深度睡眠模式的电源与时钟行为
深度睡眠模式才是真正意义上“低功耗”的开始。在这个模式下,不仅CPU停了,整个系统时钟也停了,大部分外设的时钟被关闭,芯片只剩下SRAM、备份域、以及少数经过特殊配置仍能工作的外设还在供电。
GD32的深度睡眠模式需要两个条件同时满足:Cortex-M内核的SCR寄存器里SLEEPDEEP位置1,同时PMU控制寄存器里STBMOD位为0。这两个条件组合起来,内核执行WFI指令时才会进入深度睡眠,而不是普通睡眠。
这里有个容易混淆的地方:很多GD32手册里会把深度睡眠模式称为“停止模式”(Stop Mode),因为它的行为本质上和STM32的Stop模式一样——所有时钟停摆,SRAM数据保留。
4.2 深度睡眠的进入流程与代码实现
进入深度睡眠模式的代码,在用标准外设库时是这样写的:
/* 使能PMU时钟 */ rcu_periph_clock_enable(RCU_PMU); /* 清除之前遗留的唤醒标志 */ pmu_flag_clear(PMU_FLAG_RESET_WAKEUP); /* 配置RTC作为唤醒源,使能RTC闹钟中断或唤醒事件 */ rtc_interrupt_enable(RTC_INT_ALARM); nvic_irq_enable(RTC_IRQn, 2, 0); /* 进入深度睡眠,LDO进入低功耗模式 */ pmu_to_deepsleepmode(PMU_LDO_LOWPOWER, PMU_LDO_NORMAL);执行完这行函数后,芯片会立刻睡过去。库函数内部会完成SCR的SLEEPDEEP配置、PMU_CTL寄存器的设置,然后执行WFI。等唤醒事件到来,系统从WFI之后的指令继续执行。
4.3 深度睡眠唤醒后最重要的事:重建时钟树
我从第一次用深度睡眠到现在,踩过最深的坑就是唤醒后的时钟恢复。很多人都没意识到,深度睡眠模式下系统时钟已经停了,唤醒后HXTAL(外部高速晶振)不会自动恢复,IRC16M(内部16MHz RC振荡器)也需要重新等待稳定。
正确的唤醒后处理顺序是这样的:
/* 深度睡眠唤醒后,首先重新配置系统时钟 */ system_clock_80m_hxtal(); /* 或者根据你的硬件设计选择对应的时钟源 */我习惯把SystemInit()或自己写的时钟初始化函数在唤醒后重新调用一遍。但要注意一点:如果唤醒事件来自RTC闹钟,RTC在深度睡眠模式下是靠备份域供电继续走时的,唤醒后RTC还在正常工作,不要重复初始化RTC,否则会把闹钟配置冲掉。
唤醒后的时钟切换过程中还有一个坑:HXTAL起振需要时间。GD32的HXTAL起振时间通常在几百微秒到几毫秒之间,如果唤醒后立即操作需要高速时钟的外设,会发现寄存器操作没反应,或者数据错乱。解决办法是等待HXTAL稳定标志,或者在时钟初始化里加入超时判断。
4.4 深度睡眠下哪些外设还能继续工作
深度睡眠模式并不是所有外设都断电。根据GD32的电源架构,备份域(Backup Domain)在深度睡眠和待机模式下都保持供电,所以RTC、备份寄存器和备份SRAM是可以工作的。另外,某些特定引脚(比如WKUP引脚)的唤醒检测电路也是独立供电的。
这就引出一个很关键的设计思路:深度睡眠模式下,如果想让某个外部传感器继续工作,最可靠的办法是给传感器独立供电,传感器的输出信号接到支持唤醒的GPIO或EXTI线上。MCU在深度睡眠期间几乎不耗电,传感器的功耗单独控制,等传感器把数据准备好,一个电平跳变就把MCU唤醒,MCU醒来后通过SPI或I2C去读取数据。
还有一点很多人不知道:深度睡眠模式下,LDO可以进入低功耗模式。GD32的PMU库里pmu_to_deepsleepmode的第一个参数就是控制LDO状态的:PMU_LDO_LOWPOWER让LDO节能运行,适合深度睡眠这种长时间待机场景;PMU_LDO_NORMAL则保持LDO全功率,适合需要快速唤醒的场景。实测下来,LDO低功耗模式能让深度睡眠电流再降一个档次,但代价是从LDO低功耗模式切回正常模式需要额外的时间。
五、待机模式:整个芯片几乎全断电
5.1 待机模式的本质:一次可编程的复位
待机模式是GD32三种低功耗模式里功耗最低、但代价也最大的一种。进入待机模式后,除了备份域和唤醒电路,整个芯片几乎全部断电,主SRAM内容全部丢失,所有寄存器回到复位值。当唤醒事件到来,芯片的唤醒过程等同于一次系统复位,程序从main函数的第一行重新开始执行。
所以用待机模式,首先要接受一个事实:程序没法“接着睡之前的地方继续跑”,所有现场数据都需要提前保存到备份寄存器或备份SRAM里,唤醒后通过标志位来判断自己是“冷启动”还是“从待机唤醒”。
5.2 待机模式的进入流程与代码实现
进入待机模式的代码,看似简单,但每一步都有讲究:
/* 使能PMU时钟 */ rcu_periph_clock_enable(RCU_PMU); /* 清除待机模式唤醒标志,否则第一次进待机可能被残留标志干扰 */ pmu_flag_clear(PMU_FLAG_RESET_WAKEUP | PMU_FLAG_STANDBY); /* 使能WKUP引脚唤醒功能,上升沿唤醒 */ pmu_wakeup_pin_enable(ENABLE); /* 进入待机模式 */ pmu_to_standbymode();代码执行完之后,芯片在一两个时钟周期内就会掉电进入待机状态。这里我强调一下为什么很多人第一次用待机模式会失败:如果不清除之前遗留的唤醒标志(WUF),上一次唤醒事件可能仍然挂着,执行pmu_to_standbymode后,芯片会立刻被这个残留标志唤醒,看起来就像“进不了待机模式”。这在开发板上尤其常见,因为调试器或者RC复位电路可能已经产生过唤醒事件。
5.3 待机模式的唤醒源配置
GD32待机模式的唤醒源主要有三类:
第一是WKUP引脚。GD32F10x系列一般有WKUP0、WKUP1、WKUP2等引脚,这些引脚在待机模式下具有上升沿唤醒功能。用法是把外部信号接到WKUP引脚上,当信号从低电平变成高电平时,芯片被唤醒。我用这个方法做过一个开门感应器,门磁开关一动作,MCU就被唤醒去发一条告警,发完继续睡。
第二是RTC唤醒。RTC模块在待机模式下继续走时,可以配置RTC闹钟定时唤醒。这是最常用的待机模式唤醒方式,相当于给MCU上了一个“定时闹钟”,每隔几秒、几分钟、甚至几天唤醒一次。
第三是NRST复位引脚。对NRST引脚拉低会产生外部复位,芯片也会从待机模式被唤醒并复位。但这种方式通常用于调试或强制恢复,产品中一般不会拿它做正常唤醒源。
5.4 待机模式下如何保存关键数据
前面提到,待机模式主SRAM会丢数据,但备份域不会。GD32的备份域里有备份寄存器和备份SRAM,这些区域靠VBAT引脚供电,在待机模式下数据依然保留。
我常用的做法是:进入待机之前,把关键数据(状态标志、计数值、传感器校准参数)写入备份寄存器,唤醒后读取这些寄存器判断这次唤醒的原因,再决定执行哪条分支。
/* 进入待机前保存数据到备份寄存器 */ bkp_data_register_write(BKP_DATA_REG_1, 0xA5A5); bkp_data_register_write(BKP_DATA_REG_2, sensor_cal_factor); /* 唤醒后判断是否从待机唤醒 */ if (RESET != pmu_flag_get(PMU_FLAG_STANDBY)) { /* 从待机模式唤醒,读取备份数据 */ sensor_cal_factor = bkp_data_register_read(BKP_DATA_REG_2); /* 清除待机标志,防止影响下一次判断 */ pmu_flag_clear(PMU_FLAG_RESET_STANDBY); }这里提醒一下:备份域访问需要先使能备份域写保护解除。GD32在复位后,备份域的写访问是禁止的,需要通过PWR_CTL寄存器或调用库函数来解除写保护,否则写备份寄存器会没有效果。
六、实操:三种模式的代码实现与功耗测量
6.1 最小代码工程搭建
先解决一个最基础的问题:用什么开发环境。GD32的官方开发环境叫GD32 Embedded Builder,内置了工程模板、外设库和编译调试工具链,新手用这个最省心。如果你和我一样习惯了Keil MDK,也可以直接在Keil里选择对应的GD32芯片型号,把标准外设库里的pmu.c、rcu.c等源文件加进工程。需要留意的是,不论用哪个环境,都要把PMU和备份域的源文件包含进去,因为低功耗相关的API都在这里面。
另外,如果你的项目需要在Linux环境下用命令行编译GD32工程,那就要用到arm-none-eabi-gcc交叉编译工具链。GD32官方和外设库的例程里,有对应的GCC工程模板。编译命令大概长这样:
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -O2 -Wall \ -I./Libraries/CMSIS/Include \ -I./Libraries/GD32F30x_standard_peripheral/Include \ -c ./main.c -o ./build/main.o关键是要把启动文件(startup_gd32f30x.s)换成GCC版本,因为Keil的启动文件用的是armcc语法,GCC不认识。GD32官方仓库里有gcc版本的startup文件,直接用那个就好。
更省事的办法是直接用CMake组织整个工程,把GD32外设库的源码全部编进去,这样不管是在Linux命令行还是在IDE里都能一键编译。我现在好几个项目都是这么干的,跨平台非常方便。
6.2 三种模式的完整代码示例
下面给一份完整的测试代码,把三种模式的进入和唤醒流程串起来,方便你直接在开发板上复现。
#include "gd32f30x.h" volatile uint32_t tick = 0; void delay_ms(uint32_t ms) { while (ms--) { for (volatile uint32_t i = 0; i < 18000; i++); } } void rtc_config(void) { /* 使能PMU时钟,解除备份域写保护 */ rcu_periph_clock_enable(RCU_PMU | RCU_BKPI); pmu_backup_write_enable(); /* 选择LXTAL作为RTC时钟源 */ rcu_bkp_clock_config(RCU_BKPI_SRC_LXTAL); rcu_osci_on(RCU_LXTAL); rcu_osci_stab_wait(RCU_LXTAL); rcu_periph_clock_enable(RCU_RTC); rtc_register_sync_wait(); rtc_interrupt_enable(RTC_INT_ALARM); /* 设置闹钟时间,这里简单配置为10秒后触发 */ rtc_alarm_config(10); } int main(void) { /* 系统时钟初始化,使用外部晶振 */ system_clock_120m_hxtal(); /* 使能PMU时钟 */ rcu_periph_clock_enable(RCU_PMU); /* 初始化RTC作为唤醒源 */ rtc_config(); /* 配置LED引脚或串口,用于指示状态 */ while (1) { /* 先进入深度睡眠,10秒后RTC闹钟唤醒 */ printf("enter deepsleep\r\n"); pmu_flag_clear(PMU_FLAG_RESET_WAKEUP); pmu_to_deepsleepmode(PMU_LDO_LOWPOWER, PMU_LDO_NORMAL); /* 唤醒后重新配置系统时钟 */ system_clock_120m_hxtal(); printf("wakeup from deepsleep\r\n"); /* 待机模式测试,注意进入之前先清除标志 */ delay_ms(1000); printf("enter standby\r\n"); BKP_DATA_REG_1 = 0x1234; /* 写入备份寄存器做标志 */ pmu_flag_clear(PMU_FLAG_RESET_WAKEUP); pmu_to_standbymode(); /* 待机唤醒后,程序会重新从main开始执行 */ } }这份代码在实际开发板上跑通的,关键路径我都加了注释。你测试的时候,把LED和串口初始化加在靠近main开头的位置,这样从待机唤醒后你能直观看到程序是从头开始的,从深度睡眠唤醒后则是接着睡前的代码继续往下跑的。
6.3 三种模式的实测功耗数据
功耗这个东西,光看手册没用,得自己动手量。我常用的工具是串接一个10欧姆的采样电阻,用示波器测电阻两端的压降,再根据I=U/R换算出电流。精度要求高的时候,用带电流探头的高端示波器或者专用的电流分析仪。
我在GD32F303开发板上实测的一组数据(供电3.3V,外设时钟全部关闭):
| 模式 | 实测电流 | 备注 |
|---|---|---|
| 正常运行(120MHz) | 约28mA | 全外设开启 |
| 睡眠模式 | 约3.5mA | 关闭所有外设时钟 |
| 深度睡眠模式(LDO低功耗) | 约8uA | 仅RTC工作 |
| 深度睡眠模式(LDO正常) | 约20uA | 仅RTC工作 |
| 待机模式 | 约2.5uA | 仅WKUP唤醒电路 |
| 待机模式(RTC工作) | 约4.5uA | 含RTC和备份域电流 |
不同型号、不同批次、不同温度下的数据会有差异,但趋势是一致的:深度睡眠比睡眠省电约500倍,待机比深度睡眠还要再省3到5倍。如果你的实测数据比这些大一个数量级,先别怪芯片,大概率是某个外设还在跑,或者某个IO引脚配置成了高阻输入而且没有上下拉,导致漏电。
6.4 测量功耗时最容易忽略的IO问题
说到漏电,这是我在多轮低功耗调试中踩过最深的坑。MCU本身的静态电流很低,但GPIO引脚的状态会严重影响整体功耗。如果一个GPIO被配置成浮空输入,引脚悬空时电平不定,内部输入缓冲会反复翻转,产生不小的动态电流。更常见的是引脚对外部电路输出高电平但外部没有相应的负载,电流就顺着引脚流走了。
我的习惯是,进入低功耗之前把所有不用的引脚统一配置成模拟输入或者推挽输出低电平。GD32的GPIO有模拟输入模式(GPIO_MODE_AIN),这种模式下输入缓冲器和上下拉电阻全部断开,漏电流最小。对于需要保持特定电平的引脚,就配置成推挽输出并输出固定电平。
实测中,仅优化GPIO配置这一项,就可以让整个系统的低功耗电流从几百微安降到几十微安,效果立竿见影。
七、常见问题与排查实录
7.1 唤醒失败、秒醒、唤醒后跑飞
这是低功耗开发中最常见的三类问题,我一个个说。
先看唤醒失败。程序调用pmu_to_deepsleepmode或pmu_to_standbymode后,电流表读数显示芯片确实进入了低功耗,但无论怎么给唤醒信号,芯片都没有反应。这个问题九成出在唤醒源配置上。比如你用RTC闹钟唤醒,却忘了使能RTC中断,或者RTC的时钟源没起振。调试时可以先随便用一个外部按键中断做唤醒源,把问题链路缩短,确认系统本身能睡能醒,再换成目标唤醒源。
再看秒醒。特征是程序执行进入低功耗的代码,电流刚降下去,还没等两秒钟又自己上来了。这种问题大概率是唤醒标志残留或者中断挂起。我在深度睡眠里遇到过无数次:EXTI中断服务函数退出后,挂起位没有清掉,WFI一执行就立刻被同一个中断唤醒。排查方法是在进入低功耗之前把相关外设的中断标志全部清一遍。
最后看唤醒后跑飞。这个在待机模式里最常见,表现为程序明明从main从头执行了,但行为不对。原因多半是没有正确区分“冷启动”和“待机唤醒”,程序复用了冷启动的初始化流程,把备份域、RTC又重新初始化了一遍,导致数据被冲掉。解决办法是用PMU的待机标志位做分支,待机唤醒后跳过RTC的完整初始化,只恢复系统时钟。
7.2 待机模式掉进了“下载死区”
用J-Link调试GD32时有个特别坑的现象:程序烧录后第一遍还能跑,但只要设备进入待机模式,第二次再想用J-Link下载程序就报错,提示无法连接到目标芯片。
原因不复杂:待机模式下芯片几乎掉电,调试接口的时钟和电源域也被关闭,J-Link自然找不到芯片。解决方法有两个。一是在下载时让J-Link执行“Connect under Reset”,在芯片被复位引脚拉住的时候连接调试接口,这时芯片还没进入待机模式,可以正常连接。二是在下载前先按住复位键,等J-Link连接成功后再松开,利用复位窗口把Flash烧进去。
另外还有一个从调试器角度解决的办法:如果项目已经进入稳定期,不再需要频繁调试,就把待机模式的触发条件做成“只有按下特定键才进入待机”,避免一上电就掉进待机,给后续的调试和升级留出操作窗口。
7.3 深度睡眠下Flash和Code Flash的微妙影响
很多人在低功耗模式下对GD32的Flash操作有疑惑,尤其是被“Code Flash”和“Flash”这两个概念绕晕。简单讲,Code Flash是GD32内部用于存放代码的执行Flash,也就是通常意义上MCU内部掉电不丢失的主存储区;而日常口语里说的“Flash”,有时候指的就是这片Code Flash,有时候又指外部扩展的NOR Flash或NAND Flash存储芯片。GD32的Code Flash在低功耗模式下如果完全断电,会导致唤醒后从Flash取指失败,所以芯片设计上保证了唤醒后能够迅速恢复对Code Flash的访问,前提是你别在深度睡眠模式里对Flash做擦写之类的操作。
我在一个项目中犯过一个错误:深度睡眠唤醒后,在系统时钟还没恢复稳定的情况下,立即调用了Flash擦除函数,结果Flash操作卡死,程序跑飞。原因是Flash控制器需要稳定的时钟才能正常操作,而深度睡眠唤醒后时钟树还在重建过程中。解决方法是把Flash相关的擦写操作放到唤醒后至少两个时钟稳定周期之后,或者直接让系统时钟初始化完成后再操作Flash。实话说,这个细节手册上不会写得很直白,完全是在实际调试中一次一次试出来的。
另外强调一点,如果你在深度睡眠模式里还期望GD32的以太网接收描述符继续工作,趁早打消这个念头。深度睡眠模式下MAC和DMA的时钟都停了,接收描述符指向的缓冲区根本不会收到数据。但是睡眠模式下CPU暂停时,如果DMA和外设时钟还开着,DMA是可以继续搬运数据到内存的,等CPU被中断唤醒后,再从头到尾处理这些接收描述符,这样反而可以实现“CPU睡大觉、DMA干活”的低功耗接收模式。这个设计我在一个低功耗联网终端里用过,效果很好。
7.4 从待机唤醒后第一个执行的是什么
前面说了待机唤醒会触发复位,但这里有个细节值得展开:从待机模式唤醒后,芯片执行的是“复位向量”还是“从main开始”?答案是复位向量。唤醒后芯片会经历完整的复位流程,包括从Code Flash的0x00000004地址加载初始SP、从0x00000008地址加载复位向量,然后才进入SystemInit、再跳到main。这一点和外部复位行为类似,但和深度睡眠完全不同——深度睡眠唤醒是接着WFI后面的指令继续跑的。
因为待机和复位行为高度一致,产品里就需要一个全局变量或备份寄存器来区分两种启动原因。我常用的做法是,在复位向量之后的初始化代码里,尽早读PMU的待机标志位,判断这次启动是否从待机唤醒,然后把这个标记写到一个全局变量里,供main函数和各个外设初始化函数参考。
八、从实际项目中总结的几点经验
最后分享几个我做低功耗项目时沉淀下来的小习惯,不一定在手册里能看到,但实测非常有用。
第一,低功耗设计中“不必要的外设一个都不开”。GD32的外设种类多,功耗各异,很多人在原型阶段把SPI、I2C、串口、DMA全都开了,调试完忘了关,低功耗模式怎么优化电流都压不下来。写代码时养成习惯:外设用完后立即关闭时钟,进入低功耗之前再做一次全面检查。
第二,引脚电平状态对功耗的影响被严重低估。一个浮空输入引脚可能引入几十微安的漏电流,这在低功耗设计中是不可接受的。我会写一个小小的函数,专门在进入低功耗之前把所有不用的引脚统一配置成模拟输入,退出低功耗后再恢复。这个函数虽然不起眼,但在多个项目中让整机功耗下降了三分之一以上。
第三,唤醒后要“先干活再睡觉”。低功耗系统的平均功耗,取决于“沉睡时间”和“活跃时间”的占比。如果每次唤醒后都磨蹭几十毫秒才重新入睡,平均功耗就会被拉高。我一般会把唤醒后的动作精简成“采集数据、写日志、发消息、立刻再睡”这样的短链流程,把耗时操作放到下一次正常运行的窗口里做。
第四,备份数据不要只放一个地方。待机模式下备份寄存器虽然能保存数据,但CAN和RTC等模块的配置寄存器在待机模式下的状态与深度睡眠不同,我在项目里试过,有些外设配置在待机唤醒后会丢失,如果只依赖单一的数据保存点,很容易在重启后出现配置不一致的问题。我的习惯是除了备份寄存器,还要定期把关键参数写到外部Flash或EEPROM里,双保险才安心。
做低功耗这件事,最难的不是把芯片睡过去,而是把所有细节都收拾利索,让整个系统在“睡”和“醒”之间无缝切换。希望这篇GD32低功耗模式的文章,能让你少走几段弯路。