
简介一份针对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/PB1OSC32_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同样处理保留PA0EXTI唤醒引脚和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模式唤醒后系统时钟自动回到HSIHSE、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~25mAStop模式默认配置GPIO未处理80~150μAStop模式空闲GPIO全部模拟输入15~25μAStop模式关闭外设时钟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、Stop2Stop2模式下大部分SRAM都掉电电流能做到更低但唤醒后恢复的东西也更多。如果产品切换平台建议查阅对应型号的参考手册重点看低功耗模式特性表里的唤醒源和唤醒后状态这两栏这两个信息直接决定你的代码框架怎么写。另外HAL库的HAL_PWR_EnterSTOPMode函数在不同系列上参数语义有细微差别比如F4系列还有PWR_STOPENTRY_WFI和PWR_STOPENTRY_WFE的选择L4系列则多了稳压器档位的选择。移植代码时建议直接用对应系列的标准库例程对照修改不要盲目硬搬。我个人实际做项目的体会是低功耗这事的坑不在概念而在细节。同一个Sleep/Stop流程不同人写出来功耗可能差一个数量级。最核心的是先建立一套可靠的测量方法和固化的代码框架把GPIO、时钟、外设状态这些基础项管好功耗自然就下去了。STM32 Stop模式RTC中断这套组合覆盖了绝大多数电池供电产品的需求跑通这套流程之后后续换平台、调优化都是有章可循的。本文还有配套的精品资源点击获取