STM32F103 Stop模式低功耗实战:从原理到例程的完整避坑指南
发布时间:2026/9/8 12:50:56 锦皓数字建站

简介STM32F103停止模式低功耗测试例程基于金龙107开发板面向嵌入式开发者及低功耗产品设计者。例程演示通过电源控制寄存器配置进入停止模式并用外部中断或定时器事件唤醒涵盖时钟配置、外设时钟关闭、IO口低功耗处理及唤醒后系统恢复等完整流程保留静态存储器和寄存器内容以便快速恢复。压缩包共143个文件大小2.44MB以C语言源码和头文件为主搭配工程文件、可执行文件及映射文件等调试信息可直接编译烧录并按需修改。已有1254人学习该资源。代码内包含循环进出停止模式的测试逻辑便于测量电流消耗和验证唤醒可靠性同时提供调试打印接口可跟踪模式切换过程。整体模块划分清晰初始化、低功耗处理、唤醒服务相互独立易于移植到电池供电等功耗敏感场景帮助开发者快速掌握低功耗设计要点也可作为学习STM32电源管理的实用参考。 最近在做一款电池供电的低功耗产品stm32f103的stop模式测试例程来回折腾了不少时间。stop模式算是实际项目里用得最多的一种低功耗模式——CPU停转、全部时钟关断但SRAM和寄存器内容还在功耗能从正常运行时的几十毫安直接掉到微安级别对依赖电池供电的设备来说这是最关键的设计手段之一。如果你正在做低功耗仪表、便携设备或者需要“休眠-唤醒”工作流的项目这篇例程实测总结会帮你少走不少弯路。我会从stop模式的基本原理讲起给出完整的测试代码和实测数据再把我踩过的坑全部摊开来说包括唤醒后时钟恢复、EXTI电平选择、调试器对电流测量的干扰这些平时文档里不会写的细节。1. stop模式在低功耗设计里的定位1.1 为什么是stop模式而不是睡眠或待机先用一张表把STM32F103的三种低功耗模式看清楚选型的时候就不会纠结。模式时钟状态SRAM/寄存器典型功耗唤醒源适用场景睡眠CPU停止外设时钟照常保留毫安级任意中断短时等待、需要外设继续工作停止所有时钟停止内部1.8V区域供电保留微安级EXTI、RTC、USB等需要快速唤醒、保持现场数据待机全部断电全部丢失亚微安级WKUP引脚、RTC超低功耗、允许重新上电初始化睡眠模式对省电来说基本是“洒洒水”因为外设还在跑电流根本压不下去。待机模式虽然最省电但SRAM内容全没唤醒后等于冷启动如果设备需要保留运行状态比如计数值、通信配置、传感器校准参数待机就直接不适用了。stop模式正好卡在中间电流能到微安级RAM和寄存器数据还在唤醒时间也快。设备从stop模式恢复后能继续执行进入stop之前的代码不需要重新走一遍初始化逻辑这在很多需要“接着干”的场景里太方便了。这也是我这次测试例程选择stop模式的核心原因。1.2 stop模式内部到底发生了什么官方参考手册里对stop模式的描述很简单所有时钟都被停止包括HSE、HSI和PLL但是SRAM、寄存器内容和1.8V区域的模拟部分保持供电。这里有个常被忽略的点——PWR_EnterSTOPMode函数参数里可以选择电压调节器的工作模式。PWR_Regulator_ON电压调节器保持正常模式唤醒速度快但静态电流稍微高一点。PWR_Regulator_LowPower电压调节器进入低功耗模式电流更低但唤醒后需要一段稳定时间时钟恢复稍慢。我的测试例程里用的是LowPower模式因为目标是压功耗。如果设备对唤醒后的响应速度非常敏感比如要在微秒级内处理紧急事件我建议用ON模式实测对比一下差距通常在几十微秒之内但电流差异可能有好几微安。另外一个关键点是stop模式唤醒后系统时钟不会自动恢复到进入前的状态。因为HSE和PLL都停了唤醒后如果不重新配置时钟MCU会一直跑在HSI 8MHz下波特率、定时器周期全都错乱这也是后面要着重处理的问题。2. 测试前的软硬件准备2.1 开发板与最小系统设计要点我用的是一块很常见的STM32F103C8T6最小系统板蓝色那种。板上的晶振是8MHz复位电路、boot跳线都有了做低功耗测试不需要额外改造。如果你是自己画的板子有几点必须注意。首先是VDD和VDDA的滤波电容一定要靠近芯片引脚放否则进入stop模式后电源纹波可能造成异常唤醒或者电流偏高。其次是NRST引脚不要悬空最近测试发现NRST受干扰时会导致MCU随机复位低功耗模式下这个问题会被放大因为系统对复位更敏感。最后是BOOT0引脚必须用10kΩ左右电阻下拉到GND不能直接悬空。测量工具方面我建议准备一个能测微安级的万用表我在用的是一块四位半的台式万用表串联到电源回路里测电流。如果没有高精度表也可以在回路里串一个1Ω的精密采样电阻用示波器或万用表mV档测压降换算成电流这个方法在观察睡眠-唤醒的瞬态波形时尤其好用。2.2 开发环境与标准外设库配置这版例程基于标准外设库V3.5STM32F10x_StdPeriph_LibIDE用的是Keil MDK 5。虽然现在ST主推HAL库和STM32CubeMX但F103的标准库资料最多、参考代码最好找而且低功耗寄存器操作逻辑一目了然做这种底层功耗测试反而更直观。新建工程的时候记得在预定义符号里加上STM32F10X_MD中等容量和USE_STDPERIPH_DRIVER不然编译会报错。文件结构上我习惯把低功耗相关的代码单独放到一个lowpower.c里主程序留一个入口就行这样后续移植其他项目也比较干净。工程里需要引入几个核心外设库文件misc.cNVIC配置、stm32f10x_rcc.c时钟管理、stm32f10x_gpio.c引脚控制、stm32f10x_usart.c串口调试、stm32f10x_exti.c外部中断唤醒、stm32f10x_pwr.c低功耗模式函数。STOP模式本身只需要PWR和RCC的支持但测试例程里为了实现完整的“按键进入-中断唤醒-串口打印状态”链路这几个外设库都跑不了。2.3 测试例程的功能设计这个例程想验证三件事能不能正常进入stop模式、功耗是否达到预期、外部中断能不能可靠唤醒。所以整个流程设计成上电后LED点亮串口打印“System Started”。按一次按键LED熄灭系统进入stop模式。再按同一个按键EXTI触发唤醒LED重新点亮串口打印“Wakeup from STOP Mode”。循环执行每次进入前重新初始化时钟和外设验证唤醒后系统工作的稳定性。硬件连接上按键接PA0对应EXTI0LED接PB12串口1的TX/RX接到USB转串口工具。PA0默认要接上拉电阻到3.3V按键另一端接地这样按下时引脚拉低产生下降沿触发的EXTI中断。3. 测试例程的完整实现3.1 时钟与GPIO初始化进入stop模式之前的所有外设初始化和普通程序是一样的。但有一个细节需要注意在进入stop前要把不用的外设时钟全部关闭否则这些外设会继续消耗电流导致实测功耗偏高。void RCC_Configuration(void) { ErrorStatus HSEStartUpStatus; RCC_DeInit(); RCC_HSEConfig(RCC_HSE_ON); HSEStartUpStatus RCC_WaitForHSEStartUp(); if (HSEStartUpStatus SUCCESS) { RCC_HCLKConfig(RCC_SYSCLK_Div1); RCC_PCLK1Config(RCC_HCLK_Div2); RCC_PCLK2Config(RCC_HCLK_Div1); FLASH_SetLatency(FLASH_Latency_2); FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable); RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); RCC_PLLCmd(ENABLE); while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) RESET) {} RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); while (RCC_GetSYSCLKSource() ! 0x08) {} } RCC_APB2PeriphClockCmd(LED_GPIO_CLK | KEY_GPIO_CLK | RCC_APB2Periph_AFIO | RCC_APB2Periph_USART1, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); }GPIO初始化时要重点处理按键引脚。PA0作为EXTI输入需要配置成上拉输入同时打开AFIO时钟并复用EXTI映射。LED引脚直接配置成推挽输出就行。void GPIO_Configuration(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin LED_GPIO_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(LED_GPIO_PORT, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin KEY_GPIO_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(KEY_GPIO_PORT, GPIO_InitStructure); }注意配置EXIT之前必须调用GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0)不然外部中断不会触发。这个函数依赖AFIO时钟所以RCC配置时别漏了RCC_APB2Periph_AFIO。3.2 EXTI唤醒源配置外部中断是这个例程的唤醒触发器。EXTI0对应的中断通道是EXTI0_IRQn中断服务函数是EXTI0_IRQHandler。配置时要把优先级分组、占先优先级、子优先级都设置好同时保证中断是在进入stop模式之前就配置完成的。void EXTI_Configuration(void) { EXTI_InitTypeDef EXTI_InitStructure; GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); EXTI_InitStructure.EXTI_Line EXTI_Line0; EXTI_InitStructure.EXTI_Mode EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger EXTI_Trigger_Falling; EXTI_InitStructure.EXTI_LineCmd ENABLE; EXTI_Init(EXTI_InitStructure); EXTI_ClearITPendingBit(EXTI_Line0); } void NVIC_Configuration(void) { NVIC_InitTypeDef NVIC_InitStructure; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitStructure.NVIC_IRQChannel EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); } void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) ! RESET) { EXTI_ClearITPendingBit(EXTI_Line0); } }关于下降沿还是上升沿触发这是一个小考点。如果按键默认接上拉到高、按下接地进stop前按键是松开状态按下时产生下降沿所以用Falling。如果换成了低电平触发模式会在按下期间一直触发中断可能导致系统反复唤醒所以实际项目里我喜欢用边沿触发一个沿只唤醒一次后面在软件里再做消抖处理。3.3 进入stop模式与唤醒后的恢复流程核心操作就一句PWR_EnterSTOPMode但前后要做的准备工作直接影响整个测试的成败。void Enter_StopTest(void) { printf(Entering STOP Mode...\r\n); Delay(10); // 进入stop前关闭LED避免额外电流消耗 GPIO_ResetBits(LED_GPIO_PORT, LED_GPIO_PIN); // 进入stop模式 PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI); // 唤醒后从这里继续执行 // 关键先把系统时钟恢复过来再做外设重新初始化 SystemInit(); RCC_Configuration(); GPIO_Configuration(); EXTI_Configuration(); NVIC_Configuration(); GPIO_SetBits(LED_GPIO_PORT, LED_GPIO_PIN); printf(Wakeup from STOP Mode!\r\n); }这里PWR_STOPEntry_WFI表示用WFI指令等待中断唤醒。如果你用的是WFE方式唤醒条件会多一些一般测试例程都建议用WFI逻辑更简单。唤醒后第一步是重新执行SystemInit()和时钟配置这点极其关键。我最早测试的时候偷懒没恢复时钟结果唤醒后LED和串口全都不正常串口打的字符全是乱码。原因是主频从72MHz掉到了8MHz串口波特率计算全部错位。还有一个细节是串口外设的重新初始化。stop模式不丢失外设寄存器配置理论上串口配置还在但实际上唤醒后我见过好几次串口状态异常的情况原因和时钟恢复时序有关。保险起见唤醒后把需要用的外设全部重新初始化一遍代码多一点但可靠性高很多。4. 实测数据与结果分析4.1 测量方法和仪器选择测低功耗电流不是随便拿个万用表就能测准的。市面上常见的九块九万用表电流档内阻大、分辨率低测出来到1mA级别就差不多了想看看微安级的差异基本看个零。建议用台式万用表或者钳形表至少四位半分辨率电流档分辨率要到0.1μA。更精确的方法是在电源和板子之间串一个1Ω的精密电阻用示波器测电阻两端电压。这样能看到设备进入stop模式瞬间的电流波形也能捕捉到唤醒瞬间的尖峰电流这个尖峰往往是电池供电设计里最头疼的地方因为峰值电流可能导致电池电压跌落引起复位。4.2 实测数据记录我测了一组数据用同一块板子在室温25℃下电源电压3.3V稳定供电。工作状态实测电流备注正常运行LED亮串口空闲26.8 mA72MHz主频正常运行LED灭串口空闲24.2 mA外设时钟未关闭stop模式LowPower稳压器7.8 μA所有外设时钟关闭stop模式ON稳压器11.6 μA唤醒速度更快串口空闲但未关闭外设时钟670 μA测试失误时钟没关完对比下来同样进stop模式低压差稳压器模式比正常稳压器模式电流低差不多4μA在电池供电的项目里这个差距很重要。另一方面如果进入stop前没把USART和GPIO的外设时钟关掉电流直接上到几百微安说明“关闭不用的外设时钟”这一步不是可选项而是必须项。我还拿示波器测了唤醒时间从按键按下到LED重新点亮大概需要150μs其中大部分时间花在系统时钟恢复等待HSE起振和PLL锁定上。如果对唤醒时间有严格要求可以考虑用HSI做唤醒后的临时时钟先把关键逻辑跑起来再切换回HSEPLL这样能把唤醒后的有效响应时间压到几十微秒级别。5. 常见问题与避坑经验5.1 唤醒后系统时钟丢失串口乱码这个坑我上面提过第一次测试时候就踩了。现象是唤醒后能执行代码但是串口打印乱码、定时器不准确。原因就是stop模式把所有时钟源都关了唤醒后MCU默认只跑HSI 8MHz而没有72MHz。解决办法必须在唤醒后的第一件事就是重新配置时钟不要先操作外设。我习惯把时钟配置、GPIO配置、外设配置打包成一个System_Reinit()函数这样进入stop前和唤醒后调用的是同一套代码逻辑清晰也能减少漏配的情况发生。5.2 EXTI无法唤醒的排查思路如果按键按下之后系统纹丝不动先检查三处。第一GPIO_EXTILineConfig有没有调用这个配置漏了EXTI永远收不到信号。第二中断优先级分组没有配置或者NVIC没有使能对应的中断通道。第三检查按键按下时的电平状态如果按键接法相反触发沿也要改成Rising。还有一个容易忽略的点如果进入stop之前还开着别的外设中断比如串口接收中断、定时器中断这些中断事件在stop状态下同样可以唤醒CPU。我调试的时候遇到过按一下按键系统醒了两次的现象后来排查发现是串口空闲中断也触发了一次唤醒而且唤醒后没做时钟恢复直接跑飞了。5.3 调试器连接对功耗和测试结果的影响这是新手最容易怀疑人生的地方。用ST-Link或者J-Link连着板子测电流数值永远不会低于几毫安。因为调试器通过SWD接口给目标芯片供电而且调试器的电平转换器本身就在漏电。更恶心的是调试接口的连接状态会阻止MCU真正进入低功耗模式你看到的电流根本不能代表实际运行状态。正确做法是程序烧录完成后断开调试器用独立的稳压电源给板子供电再测电流。如果需要看唤醒波形用示波器探头接在LED引脚或者自定义的测试引脚上而不是通过调试器实时查看。我建议在板子上预留一个IO口作为状态指示进入stop前拉低唤醒后拉高用示波器测这个高低电平变化就能准确判断唤醒时机和工作状态。写在最后的一点经验这个例程我自己前后跑了三天最深的体会是低功耗测试这件事软件代码只占一小半功夫剩下的大半都是在跟测量环境和外围电路较劲。最开始测出的电流仿真和实测差了十倍排查下来发现是板子上一个LED没有完全关闭另外一个原因是免调试器和串口转USB工具一直挂在板子上偷电。如果你后续想在产品上真正用好stop模式我建议把这个测试例程继续扩展一是加入RTC定时唤醒实现周期性的低功耗采集上报二是加入备用寄存器保存唤醒次数用来区分是首次上电还是从stop模式唤醒三是把不用的引脚都配置成模拟输入或者固定电平尽量减小漏电流路径。这些都是在实际产品里比“按按键唤醒”更有价值的场景也是把stop模式从例程变成生产力的必经之路。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。