简介:一份针对STM32F103低功耗应用开发的学习资料,围绕Stop模式与RTC、中断组合唤醒展开,适合嵌入式入门及中级开发者用于电池供电设备、物联网节点等低功耗场景。资料包含完整工程源码,可基于HAL库直接移植验证,涉及LSE/LSI时钟选择、RTC闹钟配置、EXTI外部中断唤醒、PWR模块进入与退出Stop模式等关键环节,并附带调试排错思路与文件组织结构说明。压缩包共212个文件,以h和c源码、o目标文件、d依赖文件、crf编译输出、uvprojx工程文件为主,另有hex、axf等可执行与映像文件,整体约5.25MB,便于直接打开工程或核对编译流程。目前已有8254人学习下载,内容紧凑、针对性强,是一份兼顾理论讲解与工程实践的参考资源。 做嵌入式项目的朋友迟早会遇到这么一个问题:设备靠电池供电,功耗压不下来,产品就活不了几天。这次要聊的STM32 Stop模式低功耗及唤醒,就是把MCU在不干活的时候睡过去,需要的时候再叫醒。核心思路是Stop模式下内核时钟停摆、SRAM内容不丢,配合RTC闹钟和外部中断两种唤醒源,让设备既能待机省电,又能按计划或者按需响应。这篇文章面向打算用STM32做低功耗产品的开发者,不管你是刚从标准库转HAL库,还是第一次碰电源管理,都可以参考这套方案。
1. 方案选型:为什么是Stop模式,唤醒源怎么选
1.1 三种低功耗模式的对比与取舍
ST的STM32产品线里,低功耗模式大致分三个档位:睡眠(Sleep)、停止(Stop)、待机(Standby)。很多人一开始容易在这几个模式之间选错,导致要么功耗压不下去,要么关键数据莫名其妙丢光。
睡眠模式只停CPU时钟,外设还在跑,中断随时可以进,但电流基本还在mA级别,对电池产品来说省了个寂寞。Stop模式则把1.8V内核电源关掉,整个系统时钟全部停摆,但SRAM和寄存器内容完整保留,典型电流能到20μA上下。待机模式最狠,除了备份域和唤醒电路,整个芯片几乎全掉电,电流能到2μA以下,但代价是RAM内容全部丢失,唤醒后等同于复位。
选哪个模式,核心就看一个问题:设备睡着的时候,RAM里的数据还要不要?比如传感器校准值、协议栈状态、临时采集结果,这些如果不想重新来一遍,Stop模式就是首选。如果产品就是一台上报完数据就睡觉的传感器,无状态可保留,直接上Standby更省电。我做这类项目时一般建议客户,先别急着写代码,把"睡着时保留什么数据"这个问题想清楚,模式自然就定了。
标题里的"STM32 Stop模式低功耗及唤醒"说的就是Stop这个档位,它也是实际低功耗项目里用得最多、平衡点最好的一种模式。
1.2 RTC闹钟唤醒与外部中断唤醒的分工
唤醒源的选择,直接决定产品体验。标题里写了"RTC+中断",实际上是两类互补的唤醒路径:
- RTC闹钟唤醒:设备按固定时间或周期醒来。典型场景是环境监测节点每小时上报一次数据,或者电表每天固定时刻结算一次。
- 外部中断唤醒:通过按键、传感器信号、充电器插入等事件实时唤醒。典型场景是用户按一下按键,设备立刻响应进入配置模式。
这两种唤醒源可以同时使能,互不干扰。比如设备平时深度休眠,每天早上8点RTC闹钟唤醒上报数据,用户随时按按键也能立刻唤醒调整参数,两者各有各的中断标志,代码里很容易区分。
需要说明的是,STM32F1系列只有RTC闹钟(Alarm A),没有独立的唤醒定时器(WakeUpTimer),所以F1上做RTC唤醒就是靠闹钟中断。而F0/F4/L4这些系列额外带了一个RTC WakeUpTimer,配置起来更直白。我这次以最经典的STM32F103C8T6为例讲完整流程,最后会补充其他系列的区别。
2. 硬件准备与CubeMX初始化
2.1 最小硬件清单与引脚规划
硬件其实很简单,列一下我调试用的清单:
- STM32F103C8T6最小系统板,或者自制的板子
- 32.768kHz LSE晶振,两个6~12pF负载电容,RTC要准全靠它
- 一个按键接PA0,用于EXTI唤醒测试
- USB转串口模块,打印调试信息
- 台式电源或万用表,用于功耗测量
这里有个容易掉坑的点:不少STM32F103开发板为了省成本,板上没焊LSE晶振。RTC如果走LSI内部RC时钟,虽然也能跑,但误差非常大,一天下来偏个几十秒很正常。低功耗应用对时间精度稍有要求,必须用LSE,硬件上要确认晶振真的焊了、起振正常。
引脚规划方面,PA0配置成外部中断输入,下降沿触发,内部上拉,平时读高电平,按键按下接地产生下降沿。RTC的时钟源走LSE,对应PB0/PB1(OSC32_IN/OSC32_OUT)。其他闲置GPIO在进Stop前全部切成模拟输入模式,这一步对功耗影响极大,后面实测数据会说明。
2.2 CubeMX关键配置项与避坑
用CubeMX初始化工程,有几个配置项必须盯紧:
- RCC设置里,Low Speed Clock选择LSE为Crystal/Ceramic Resonator。
- RTC设置里勾选Activate Clock Source,再在Alarm A处打勾,打开闹钟中断。
- PA0配置为External Interrupt Mode with Falling edge trigger detection,开启内部上拉。
- NVIC设置里,RTC Alarm interrupt和EXTI line0 interrupt两个中断都要使能。
- 时钟树:HSE 8MHz经PLL倍频到72MHz作为系统时钟,LSE 32.768kHz单独给RTC。
- 工程生成时选HAL库,Toolchain选你用的IDE。
我在这里提醒一个新的坑:CubeMX生成的RTC初始化代码会启用备份域访问,如果你之前烧过其他程序、备份域里寄存器状态残留,第一次启动可能出现RTC配置不生效的情况。稳妥的做法是在初始化RTC前,先把备份域复位一次:
__HAL_RCC_BKP_CLK_ENABLE(); HAL_PWR_EnableBkUpAccess(); __HAL_RCC_BACKUPRESET_FORCE(); __HAL_RCC_BACKUPRESET_RELEASE();这套操作放在RTC初始化之前,确保备份域是干净的。
3. 代码实现:进入Stop、唤醒恢复与完整流程
3.1 进入Stop模式的完整代码
进Stop的代码说起来就几步,但每一步都有讲究。先关掉不用的外设时钟,再把空闲GPIO全部配成模拟输入,最后调用HAL库函数睡过去:
void Enter_Stop_Mode(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; // 1. 关闭外设时钟 __HAL_RCC_USART1_CLK_DISABLE(); __HAL_RCC_ADC1_CLK_DISABLE(); __HAL_RCC_TIM2_CLK_DISABLE(); // 2. 空闲GPIO全部配成ANALOG,降低IO漏电 GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; GPIO_InitStruct.Pin = GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // GPIOB、GPIOC同样处理,保留PA0(EXTI唤醒引脚)和RTC晶振引脚不动 // 3. 暂停SysTick,防止唤醒后HAL_Delay异常 HAL_SuspendTick(); // 4. 进入STOP模式,WFI等待中断唤醒 HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); // 唤醒后会从这里继续往下执行 SystemClock_Config(); HAL_ResumeTick(); // 重新初始化需要恢复的外设 MX_USART1_UART_Init(); }HAL_PWR_EnterSTOPMode的第一个参数选择了PWR_LOWPOWERREGULATOR_ON,意思是内部稳压器切到低功耗模式,比默认的普通稳压器模式更省电。第二个参数PWR_STOPENTRY_WFI表示用WFI指令等待中断唤醒。如果用了WFE,要注意事件标志是否被提前置位,否则会出现刚睡下就被唤醒的"假休眠"问题,功耗根本降不下去。
3.2 唤醒后的时钟恢复与外设重初始化
Stop模式唤醒后,系统时钟自动回到HSI,HSE、PLL全部处于关闭状态。如果不重新配置时钟树,串口波特率直接乱掉,各种外设的时序全都不对。所以唤醒后第一件事就是重新调用SystemClock_Config()。
F103的经典配置如下:
void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE | RCC_OSCILLATORTYPE_LSE; RCC_OscInitStruct.HSEState = RCC_HSE_ON; RCC_OscInitStruct.LSEState = RCC_LSE_ON; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { Error_Handler(); } RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK) { Error_Handler(); } }注意F103在72MHz主频下Flash等待周期必须是FLASH_LATENCY_2,不要想当然用1,否则程序跑飞了都不知道原因。
3.3 RTC闹钟+外部中断组合唤醒的完整流程
两个唤醒源同时开启时,代码结构需要清晰地区分唤醒来源。先看RTC闹钟的初始化,以每10秒定时唤醒为例:
RTC_AlarmTypeDef sAlarm = {0}; sAlarm.AlarmTime.Hours = 0; sAlarm.AlarmTime.Minutes = 0; sAlarm.AlarmTime.Seconds = 10; sAlarm.AlarmMask = RTC_ALARMMASK_DATEWEEKDAY; HAL_RTC_SetAlarm_IT(&hrtc, &sAlarm, RTC_ALARM_A);AlarmMask设置成RTC_ALARMMASK_DATEWEEKDAY,意思是闹钟匹配时分秒,日期和星期不参与匹配,这样每天(或者每个小时)都能周期触发,非常灵活。
两个中断回调各司其职:
volatile uint8_t wakeup_source = 0; #define WAKEUP_NONE 0 #define WAKEUP_RTC_ALARM 1 #define WAKEUP_EXTI_LINE0 2 void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc) { wakeup_source = WAKEUP_RTC_ALARM; } void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == GPIO_PIN_0) { wakeup_source = WAKEUP_EXTI_LINE0; } }主循环的结构就是休眠、醒来、判断来源、干活:
while (1) { // 业务代码... // 进入休眠 Enter_Stop_Mode(); // 唤醒后的处理 if (wakeup_source == WAKEUP_RTC_ALARM) { Handle_Timed_Task(); } else if (wakeup_source == WAKEUP_EXTI_LINE0) { Handle_Key_Press(); } // 清状态、清标志 wakeup_source = WAKEUP_NONE; __HAL_RTC_ALARM_EXTI_CLEAR_FLAG(); __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0); }中断回调里千万不要做重活儿,比如重配时钟、打印、延时,这些全部放回到main里执行。中断里只做置标志位,这是嵌入式开发的老规矩,低功耗场景下尤其重要,因为Stop唤醒后的系统状态还没完全恢复。
4. 实测功耗数据与优化技巧
4.1 功耗测试方法与实测结果
低功耗做得好不好,不能光看代码,要拿数据说话。最常用的测量方法是把万用表串进电源正极,用uA档测整机电流。但这里有个问题:很多万用表的uA档内阻偏大,串进电路后相当于给MCU供电加了限流电阻,会导致测量值失真甚至MCU无法正常工作。
我的实操习惯是:先用台式电源的电流显示功能粗测,再用万用表uA档串入确认。有条件的话,用高精度电流探头或源表最好。另外强烈建议在电源输入处预留一个10欧姆的采样电阻位,产线上用示波器测压降换算电流,这比万用表串表稳定得多。
实测数据基于STM32F103C8T6、LSE给RTC计时、PA0内部上拉:
| 设备状态 | 实测电流 |
|---|---|
| 正常运行,72MHz跑while循环 | 18~25mA |
| Stop模式,默认配置,GPIO未处理 | 80~150μA |
| Stop模式,空闲GPIO全部模拟输入 | 15~25μA |
| Stop模式,关闭外设时钟+GPIO处理+低功耗稳压器 | 8~15μA |
从数据能明显看出来,GPIO漏电是Stop功耗最大的敌人。很多人配置完Stop模式,一测还有好几百微安,90%的情况都是GPIO没处理干净。这里提供一个判断方向:如果Stop电流超过100μA,基本可以断定有引脚漏电或者外设没关。
4.2 六个让功耗更低的实操技巧
在我做过的低功耗项目里,以下几个技巧对降低功耗最有效,按性价比排序:
- 空闲GPIO全部配置成模拟输入。这招见效最快,能把几十上百微安的漏电流直接压到个位数。
- 关闭所有不用的外设时钟。APB1和APB2上的定时器、USART、SPI、ADC,哪个不用关哪个。
- 使用低功耗稳压器模式,也就是
HAL_PWR_EnterSTOPMode的第一个参数用PWR_LOWPOWERREGULATOR_ON。 - 禁用调试接口。SWD在调试时好用,但睡眠状态下调试接口相关电路会增加功耗,量产程序里可以调用
DBGMCU->CR = 0;禁止调试访问。 - 检查外部上拉和下拉电阻。一个10k的外部下拉在3.3V下就有330μA的电流,这在低功耗设计里是致命的,硬件电路设计时必须算清楚每个电阻的静态功耗。
- 对时间精度要求不高的场景,RTC改用LSI时钟源,能再省下LSE振荡器的工作电流。但LSI误差大,走时一天偏几十秒是常事,如何取舍看产品需求。
关于第5点多说一句,很多团队软件调得非常好,结果整机功耗还是下不去,最后发现是硬件上某个LED指示灯的限流电阻、某个上拉电阻在偷偷耗电。低功耗从来不只是软件的事,软硬件要一起看。
5. 常见问题与排查技巧汇总
5.1 唤醒后死机、乱码、外设无响应的排查路径
这是我被问得最多的现象。代码明明按照流程写了,结果唤醒后LCD没显示、串口输出乱码、程序像卡死了一样。按以下顺序排查,大多数问题5分钟内能定位:
- 先确认唤醒后有没有重新调用
SystemClock_Config()。忘了恢复系统时钟是头号原因,唤醒后系统跑在HSI,外设时序全乱。 - 检查
HAL_SuspendTick()和HAL_ResumeTick()是否配对。SysTick不恢复的话,HAL库里所有基于tick的延时和超时机制全部失效。 - 检查中断标志是否清除。RTC闹钟中断标志没有清除干净,会反复触发中断,程序看起来就是行为怪异、执行顺序不对。
- 确认外设时钟重新使能。比如你在睡眠前关了USART时钟,唤醒后要先
__HAL_RCC_USART1_CLK_ENABLE()再初始化串口。 - 如果唤醒后进不了中断,检查NVIC配置。某些低功耗模式下,如果外部中断的抢占优先级和子优先级配置不合规,中断会被悬起但不执行。
5.2 功耗偏高和唤醒偶发失效的深层原因
功耗比预期高,我一般按这个思路查:
- 先在最小系统上跑裸机Stop测试,不加载任何业务代码,排除自己业务逻辑干扰。
- 逐个恢复外设模块,看电流在哪一步出现跳变,基本能定位到漏电单元。
- 用万用表mA档逐个测每个GPIO引脚的电流,找出异常漏电引脚。
- 确认没有在休眠前启动DMA、ADC连续转换这类"隐形"外设,它们会阻止进入真正的Stop状态。
唤醒偶发失效,重点查这几个方面:
- EXTI触发的边沿与按键实际电平变化不匹配。按键按下过程中有机械抖动,如果电平在边沿附近反复跳变,第一次触发唤醒后,第二次的下降沿可能会被忽略。
- PA0引脚如果有较大容性负载,按键释放后电平恢复慢,会导致唤醒事件不完整。
- GPIO内部上下拉和外部电路冲突。比如内部上拉+外部上拉同时存在,按键接地时电平下降不明显。
- 代码里中断标志清理时机不对。如果在进入Stop前把中断标志清了,但WakeUp事件已经在NVIC里挂起,睡眠指令一执行立刻被唤醒,看起来就是"没睡觉"。
按键消抖这块,我的做法是:唤醒后延时20~30ms再读一次引脚电平确认。为什么是这个时间?机械按键的抖动时间一般在5~15ms,留出30ms足够避开抖动区间,又不至于影响响应速度。这比硬件RC滤波更省事,也更容易调整参数。
补充:其他STM32系列的唤醒配置差异
如果你的主控不是F103,比如用的是F0、F4、L4或者G0系列,唤醒配置上有个明显的区别:这些系列带RTC WakeUpTimer(唤醒定时器),配置逻辑比闹钟更直白,对应CubeMX里的WakeUp项,设置一个重装载值定时唤醒即可,不用像F1那样在闹钟结构体里对齐时分秒。
L4、G0这些低功耗系列的Stop模式还有更细分的子模式(Stop1、Stop2),Stop2模式下大部分SRAM都掉电,电流能做到更低,但唤醒后恢复的东西也更多。如果产品切换平台,建议查阅对应型号的参考手册,重点看低功耗模式特性表里的"唤醒源"和"唤醒后状态"这两栏,这两个信息直接决定你的代码框架怎么写。
另外,HAL库的HAL_PWR_EnterSTOPMode函数在不同系列上参数语义有细微差别,比如F4系列还有PWR_STOPENTRY_WFI和PWR_STOPENTRY_WFE的选择,L4系列则多了稳压器档位的选择。移植代码时,建议直接用对应系列的标准库例程对照修改,不要盲目硬搬。
我个人实际做项目的体会是,低功耗这事的坑不在概念,而在细节。同一个Sleep/Stop流程,不同人写出来功耗可能差一个数量级。最核心的是先建立一套可靠的测量方法和固化的代码框架,把GPIO、时钟、外设状态这些基础项管好,功耗自然就下去了。STM32 Stop模式+RTC+中断这套组合,覆盖了绝大多数电池供电产品的需求,跑通这套流程之后,后续换平台、调优化都是有章可循的。
本文还有配套的精品资源,点击获取