ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32F103 Stop模式低功耗实战:从唤醒配置到功耗调优

STM32F103 Stop模式低功耗实战:从唤醒配置到功耗调优 简介面向嵌入式开发者的一份STM32F103停止模式Stop低功耗测试例程对应金龙107开发板重点解决低功耗状态下的时钟配置、外设关闭、唤醒源设置与系统恢复问题。压缩包内含143个文件以31个头文件和30个C源文件为工程主体另含Keil工程配置、启动汇编、Hex固件、链接映射、编译中间文件及说明文档整体大小仅2.44MB便于快速下载和工程对照。例程完整演示了初始化时钟与外设、配置PWR电源管理寄存器、进入停止模式、关闭非必要外设以及通过EXTI外部中断唤醒后重新初始化时钟与GPIO的整个流程。内置循环进出停止模式的测试逻辑可帮助观察唤醒事件、验证电流功耗表现并配有调试信息便于跟踪运行状态。这份例程已有1254人学习适合电池供电或功耗敏感类项目开发也可作为理解Cortex-M3内核低功耗机制的入门参考。 搞嵌入式这几年手里出过好几版带电池的产品跟低功耗打交道是家常便饭。最近又帮一个同事排查他那个手环项目发现他把 STM32F103 的 Stop 模式代码写得乱七八糟唤醒之后串口直接乱码查了半天是系统时钟没重配。这让我想起自己刚开始折腾 Stop 模式时踩过的一堆坑干脆把这几年积累的 Stop 模式测试例程和调试心得整理出来给正在做低功耗项目的朋友一个可以直接抄作业的参考。这篇东西的核心是基于 STM32F103 标准外设库V3.5写的一个 Stop 模式测试例程覆盖了从进入休眠、唤醒源配置、唤醒后时钟恢复到功耗实测的完整链路。不管你是刚接触低功耗的新手还是已经被唤醒乱码、功耗降不下去折磨过几回的老手这篇文章都能给你一些实在的参考。1. 为什么产品级开发绕不开 Stop 模式——三种低功耗模式该怎么选STM32F103 参考手册里写了三种低功耗模式Sleep、Stop 和 Standby。很多初学者一开始容易犯迷糊觉得低功耗就是“让芯片睡觉”直接往最省电的 Standby 里钻。实际上这里面的门道多得很选错了模式后面的开发难度完全是两个量级。1.1 Stop 模式的省电原理Stop 模式的核心机制是内核时钟停止也就是 CPU 不再取指执行同时内部的所有时钟包括 HSE 和 HSI都被关闭但芯片内部的 1.8V 供电区域就是 SRAM、寄存器、部分外设所在的那个域依然保持供电。这意味着 SRAM 里的数据不会丢寄存器状态也会保留你进入休眠之前设置的各项配置都还在。这个机制带来的直接好处是唤醒速度极快典型恢复时间在微秒级别而且是“续跑”而不是“重启”。我用逻辑分析仪实测过从外部中断触发到 CPU 恢复到正常跑代码大约几个微秒就能完成。对比一下 Standby 模式那个模式相当于把整个芯片都断电了唤醒之后必须从 main 函数重新初始化就跟按了复位键一样。1.2 三种模式的差异对比我画了一张表把三种模式的关键参数列出来方便你选型的时候直接对照对比项Sleep 模式Stop 模式Standby 模式内核时钟停止停止停止HSE/HSI 时钟保持运行关闭关闭SRAM 数据保持保持丢失寄存器状态保持保持丢失唤醒后行为从断点续跑从断点续跑复位重启典型唤醒时间微秒级微秒级毫秒级典型电流毫安级十几到几十微安微安级甚至更低注意一下F103 的 Stop 模式电流典型值其实不低数据手册上写的是典型十几微安具体跟 LDO 配置有关跟你用过的 F0、L0 系列完全不是一个量级。但它的优势在于唤醒速度和代码兼容性在很多场景下性价比很高。1.3 什么时候必须用 Stop 而不是 Standby这是我被问得最多的问题。答案是看你的唤醒需求如果你有 RTC 闹钟定时唤醒、外部中断实时唤醒这类需求同时唤醒后希望系统快速恢复到工作状态那就必须用 Stop。举个实际例子我做一个表头设备平时显示了当前测量值用户按一下按键要立刻唤醒并采集数据这个过程要求毫秒级响应。如果用 Standby唤醒后要重新初始化时钟、外设、显示驱动一整套流程走完人眼都能看出卡顿。Stop 模式就不存在这个问题唤醒后一个中断服务函数就能把活干完。反过来如果你的产品是个传感器节点每五分钟上报一次数据上报完直接睡到下一次中途没有任何外部交互需求那 Standby 明显更合适毕竟静态功耗更低。所以我的建议是要快就 Stop要极致省电就 Standby别一上来就盲目追求最低功耗而选了不适合项目的模式。2. 进入 Stop 模式的完整代码从时钟配置到 WFI 指令看芯片手册只知道原理落地到代码才是关键。这里我直接给出基于标准库 V3.5 的完整实现分步骤拆开讲清楚每一行代码背后在干啥。2.1 进入前的关键准备工作进入 Stop 模式之前有几件容易被忽略但必须做的事第一把不需要的外设时钟关掉。别小看这一步很多人进了 Stop 模式功耗还是居高不下查来查去发现是某个外设没关。我现在习惯的做法是写一个ENTER_STOP_MODE_PREPARE()宏把 ADC、定时器、DMA 这些外设逐个xxx_Cmd(ENABLE 换成 DISABLE)并把对应的时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, DISABLE)关掉。哪些外设要关、哪些可以留取决于你的产品设计。比如我这版例程里留着 EXTI 的时钟它是唤醒源而把用不到的串口、ADC 全部关掉。第二配置好唤醒源对应的 EXTI 通道和 NVIC。注意不是所有引脚都能唤醒 Stop 模式F103 上只有 PA0EXTI0到 PG15EXTI15这些支持 EXTI 的引脚才可以连接在 EXTI 线上的引脚都可以作为唤醒源。我们常用的方式是配置一个 GPIO 为输入模式开启 EXTI 中断使能 NVIC然后挂一个中断服务函数。第三确认 SysTick 定时器不会打扰休眠。我们的测试例程里通常有Delay_ms()之类的软件延时底层依赖 SysTick 中断。如果在 Stop 模式下 SysTick 还在跑它会持续产生中断把 CPU 从休眠中拉出来导致功耗根本降不下去。所以我习惯进入 Stop 模式前手动把SysTick-CTRL清零也就是关闭 SysTick。2.2 进入 Stop 模式的代码实现下面是核心函数我放在bsp_lowpower.c里/** * brief 进入Stop模式 * param 无 * retval 无 */ void LOWPOWER_EnterStopMode(void) { /* 1. 关闭不需要的外设时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_USART1, DISABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2 | RCC_APB1Periph_TIM2, DISABLE); /* 2. 关闭SysTick防止中断频繁唤醒 */ SysTick-CTRL ~SysTick_CTRL_ENABLE_Msk; /* 3. 确保PWR时钟已开启配置电源控制寄存器 */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI); /* 4. 从Stop模式唤醒后会继续执行这里 */ /* 此时HSE和HSI已经关闭需要重新配置系统时钟 */ SystemInit(); /* SystemInit()之后还需要重新使能外设时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_USART1, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2 | RCC_APB1Periph_TIM2, ENABLE); }这段代码里PWR_EnterSTOPMode有两个参数值得展开说PWR_Regulator_LowPower把内部稳压器切换到低功耗模式。这个选项在 F103 上意味着供电电流被限流但 SRAM 还能保持数据。如果你的代码在休眠期间还要保持某些特殊外设工作那可以考虑PWR_Regulator_ON不过功耗会高一些我自己实测大概高几个微安。PWR_STOPEntry_WFI使用 WFIWait For Interrupt指令进入休眠。有朋友问过为什么不选PWR_STOPEntry_WFE这个后面单独讲。2.3 关于 WFI 和 WFE 的选择WFI 和 WFE 是 Cortex-M3 内核的两条休眠指令标准库里封装成了PWR_STOPEntry_WFI和PWR_STOPEntry_WFE两个选项。它们的行为差异很微妙我在实际项目里踩过几次才摸清楚WFI只要有中断被挂起或产生芯片就唤醒。适合“用中断唤醒”的设计这也是绝大多数场景的默认选择。WFE唤醒条件是“事件”Event而事件触发通常由外部信号或SEV指令产生也可以通过__WFE()配合中断挂起标志产生。WFE 的好处是不会被 SysTick 这类频繁中断反复唤醒但副作用是唤醒控制不够直观对新手不友好。我的个人习惯是默认用 WFI配合中断唤醒逻辑清晰、排查方便。WFE 更适合那种“让 CPU 轮询某个事件而不用关中断”的场景但真正在 F103 低功耗项目里用得并不多。所以不要被名字唬住实际工程里 WFI 够用了。3. 唤醒源配置与实测EXTI 和 RTC 各自的使用场景进得去还得出得来唤醒源的设计直接决定你的产品交互逻辑。我常用的唤醒源有两大类外部引脚触发EXTI和 RTC 闹钟事件。下面给出各自的应用场景、接线方式和标准库配置代码。3.1 用 EXTI 外部中断唤醒按键唤醒的实现这是最直观、也最常用的一种方式。比如低功耗表头、遥控器、门锁面板用户按一下按键系统立刻从 Stop 模式醒来开始工作。硬件上我一般把按键接到 PA0/WKUP 引脚这是 F103 上专用的唤醒引脚电平触发即可。注意在休眠期间这个引脚的上下拉要处理好防止悬空电平抖动造成误唤醒。我的习惯是在按键两端加上拉电阻到 VCC按下接地这样常态是高电平只有按下瞬间产生下降沿触发 EXTI。标准库配置代码如下/** * brief 配置PA0为外部中断输入下降沿触发 * param 无 * retval 无 */ void LOWPOWER_EXTI_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; EXTI_InitTypeDef EXTI_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; /* 使能GPIOA时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; /* 上拉输入 */ GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); /* 选择PA0作为EXTI0的输入线 */ 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); 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); }中断服务函数里做两件关键事情清中断标志位然后设置一个全局状态标志主循环检测到这个标志后继续执行唤醒后的业务逻辑。volatile uint8_t g_wakeup_flag 0; void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) ! RESET) { EXTI_ClearITPendingBit(EXTI_Line0); g_wakeup_flag 1; } }注意中断服务函数尽量保持轻量不要把恢复时钟、重新初始化外设这类耗时操作放在里面。我的做法是中断里只置标志位主循环里轮询标志去执行后续工作。这样既避免了中断嵌套的问题也让时序可控。3.2 用 RTC 闹钟唤醒定时唤醒的实现如果你做的是“定时采集”、“定时上报”类产品用 RTC 闹钟唤醒是最优雅的方案。比如环境监测节点每分钟唤醒一次采集温湿度然后继续休眠这比外部引脚触发更符合业务逻辑。RTC 作为唤醒源的好处是它自己在 Stop 模式下依然工作RTC 时钟走的是 LSI 或 LSE它们没有被 Stop 模式关闭所以能精确地在预定时间点产生闹钟事件。配置 RTC 的代码相对繁琐建议直接用标准库里的 RTC 例程改核心步骤是/** * brief 配置RTC闹钟设置唤醒时间 * param None * retval None */ void LOWPOWER_RTC_AlarmConfig(void) { /* 开启PWR时钟允许访问备份域 */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); PWR_BackupAccessCmd(ENABLE); /* 配置LSE或LSI作为RTC时钟源一般用LSE 32768Hz比较准 */ RCC_LSEConfig(RCC_LSE_ON); while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) RESET); RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); RCC_RTCCLKCmd(ENABLE); RTC_WaitForSynchro(); RTC_WaitForLastTask(); /* 使能RTC闹钟中断 */ RTC_ITConfig(RTC_IT_ALR, ENABLE); RTC_WaitForLastTask(); NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel RTC_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); }RTC 闹钟中断服务函数里同样只做清标志和置标志的工作void RTC_IRQHandler(void) { if (RTC_GetITStatus(RTC_IT_ALR) ! RESET) { RTC_ClearITPendingBit(RTC_IT_ALR); g_wakeup_flag 1; } }说句题外话在用 RTC 做唤醒源之前我一直以为 LSE 晶振起振很容易结果在低温环境下遇到过几块板子 LSE 起振失败导致 RTC 不走、闹钟不响。后来查了一些应用笔记才发现 LSE 起振对 PCB 布局很敏感晶振下面不能铺地负载电容的容值也不能随意配。现在我的板子都是按参考设计来做基本没再出过问题。3.3 唤醒后的第一件事恢复系统时钟这是整篇文章最核心的坑没有之一。Stop 模式会关闭 HSE 和 HSI所以唤醒之后芯片内部默认的时钟源是 HSI频率降到 8MHz默认 SYSCLK 也是从 HSI 来的但 PLL 已关闭。如果你唤醒后不重新配置时钟串口波特率会直接错乱定时器的时间基准也会全偏。正确的做法是在PWR_EnterSTOPMode返回后立即调用SystemInit()它会重新初始化时钟树把 SYSCLK 配回 PLL 输出。这一步我在前面的代码里已经加上了。实际上SystemInit()这个函数在系统启动时会被调用作用是通过启动文件里的上电初始化流程设置好时钟。唤醒后手动调用它等于强制把时钟再走一遍初始化流程。注意这个操作会覆盖你在SystemInit()之后设置过的任何时钟相关配置比如某些外设的时钟分频所以保险起见唤醒后最好把你项目初始化函数里跟时钟相关的部分也重新执行一遍。我见过不少人在这个环节翻车时钟没恢复串口发出来的数据全是乱码还以为是串口配置被冲掉了。排查半天最后才发现只是系统时钟还是 8MHz串口外设却还按 72MHz 的配置在跑波特率自然对不上。4. 实测功耗数据与测量方法光是理论省电没用代码写完功耗到底降没降得上实测数据说话。这一节我记录了自己在测试板上测到的一组数值以及测量时常用的几套工具和方法。4.1 三种模式下的实测电流对比我用的是一块自制的 STM32F103RCT6 最小系统板外部接了 8MHz HSE 晶振、LSE 32768Hz 晶振、一个电源指示灯和一个用户按键。测量点是 VDD 供电入口串联的电流采样电阻两端用万用表记电流再用示波器抓唤醒瞬间的电流波形。工作模式实测电流VDD3.3V备注正常运行72MHz跑主循环约 16.5mA外设基本全开Sleep 模式约 5.2mACPU 停止HSE 仍在运行Stop 模式LDO 低功耗约 21µA所有时钟关仅保留唤醒源Stop 模式调试器断开后约 18µA比接线时更接近数据手册值注意第一行和第三行差了三个数量级这就是 Stop 模式对电池供电产品的意义所在。实测里有个规律接上 ST-Link 调试器后Stop 模式电流会明显偏高几个微安到几十微安这是因为调试接口本身有漏电路径。所以做功耗标定的时候一定要拔掉调试器再测。4.2 功耗测量工具与方法测低功耗电流常规的万用表电流档位其实不够用。Stop 模式下电流在微安级别便宜的万用表分辨率不够而且电流档本身的内阻还会影响测量结果。我常用的方案有两种第一种是高精度万用表直接串到供电回路里。万用表单块表能测到微安级但需要注意量程自动切换时的反应速度有些表换挡期间会断开回路导致芯片复位。第二种是电流探头加示波器适合看动态功耗变化。把探头夹在 VDD 通路上的采样电阻两端示波器设置为高灵敏度可以看到芯片从运行到休眠、再从休眠到唤醒的完整电流曲线。这个方法能精准抓到唤醒瞬间的电流尖峰和恢复时间对排查问题帮助很大。如果手头只想要个大概数据还可以用一节 100mAh 的纽扣电池做放电实验记录电压从 3.3V 掉到 3.0V 用了多长时间反推平均功耗。这个方法精度低胜在简单直观纯看个量级够用了。4.3 影响功耗的物理因素GPIO 漏电代码配置没问题功耗还是偏高那十有八九是 GPIO 状态的问题。这是我调低功耗排障时排查优先级最高的一项。具体来说进入 Stop 模式之前所有不用的 GPIO 如果还处于浮空输入状态芯片休眠后这些引脚的电平不确定可能会通过内部保护二极管或寄生路径漏电。更麻烦的是浮空输入还有可能让引脚电平在高低之间抖动导致 CMOS 输入级反复翻转产生额外的动态功耗。我的处理习惯是把所有不用的 GPIO 统一配置为模拟输入GPIO_Mode_AIN或者推挽输出低电平。优先用模拟输入因为它直接断开施密特触发器完全杜绝了电平翻转引起的功耗。这个方法几乎零成本效果立竿见影。我有一块板子只做这一步Stop 模式电流就降了将近 10µA。另外外部上拉/下拉电阻也会影响功耗它们通过引脚形成直流回路休眠时依然在耗电。选电阻值时要算一下静态功耗比如 10k 上拉到 3.3V理论上就有 330µA 的灌电流路径当然通常不会全部灌进引脚但在设计时要做个预算。5. 我在调试 Stop 模式时踩过的坑最后这一节聊聊调试中那些让人抓狂的问题。这些坑可能代码和原理上都没有直接体现但实际项目里经常遇到我把排查思路写出来方便你少走弯路。5.1 调试器连着的板子永远进不了 Stop用 ST-Link 或 J-Link 连着开发板调试低功耗代码时芯片几乎不可能真正睡下去。原因很简单调试器通过 SWD 接口持续访问内核每次访问都会打断休眠过程而且调试接口本身的时钟域是独立供电的停不掉。我在刚开始调这个例程时就是开着调试器看变量结果功耗永远是几十毫安下不去。后来排查到是调试器的问题按下调试器的Reset按钮断开调试连接再重新上电跑程序Stop 模式的电流才回归正常。所以调试低功耗代码一个实用技巧是用RTT或者串口打印的方式把关键信息发出来而不是依赖在线调试。需要看变量时把变量写到串口输出或者存到 Flash运行完再读回来。系统真正睡下去之后在线仿真本来就帮不上忙了。5.2 中断标志位不清除导致唤醒后立即再次进入休眠这是一个典型的逻辑坑。用 EXTI 按键唤醒时如果你的中断服务函数里没清除挂起位或者清除了但主循环还没处理完就被再次触发芯片会陷入“唤醒—立刻又睡”的死循环。我的建议是中断服务函数里清标志位是底线操作但主循环里也要有防止“热唤醒”的逻辑。具体做法设置唤醒标志后延时一段时间比如 20ms等待按键抖动过去再去判断按键是否真的被按下。否则一次抖动产生的多个下降沿会让芯片在几次唤醒之间反复横跳功耗没降下来系统行为也变得不可控。5.3 FLASH 读取等待周期和 RTC 跑飞的细节最后说一个不太常见但一旦碰上就很烦的问题系统时钟配置为 72MHz 时FLASH 读取等待周期应该设置为 2 个等待周期。如果你在SystemInit()里配置了 PLL 和 FLASH 等待周期但唤醒后手动恢复时钟时漏了这一步芯片正常运行会偶发死机或程序跑飞。因为 FLASH 没等到足够的时钟周期指令读取可能出错。我吃过这个亏之后现在的习惯是唤醒后不是直接调用SystemInit()了事而是把SystemInit()里跟 FLASH 相关的那几行也单独检查一遍。具体来说就是这几行FLASH-ACR ~FLASH_ACR_LATENCY; FLASH-ACR | FLASH_ACR_LATENCY_2; /* 72MHz下需要2个等待周期 */另外一个跟 RTC 相关的细节算是一个提醒RTC 在 Stop 模式下继续走时但如果你调整了备份域的预分频或者闹钟值要做一次RTC_WaitForLastTask()确保上次操作完成再写下一次否则 RTC 会进入忙状态闹钟不触发。这个不是每次都会踩但一旦出现排查难度很高因为现象是“偶尔不唤醒”没有固定规律。用 F103 做低功耗开发Stop 模式测试例程这事说难不算难但细节非常多。从模式选型、时钟配置、唤醒源设计到 GPIO 漏电处理每个环节都有值得留意的点。希望这篇文章能帮你把链路理清楚代码直接抄走坑提前避开早点把功耗调到你想要的水平。本文还有配套的精品资源点击获取
返回列表