STM32 低功耗设计全攻略:PWR 模块原理 + 睡眠 / 停止 / 待机模式实战(串口 + 红外 + RTC 应用全解析)
文章目录
- PWR
- PWR(电源控制模块)核心功能
- 电源框图
- 上电复位和掉电复位
- 可编程电压监测器
- 低功耗模式
- 模式选择
- 睡眠模式
- 停止模式
- 待机模式
- 修改主频
- 一、准备工作
- 二、修改主频的核心步骤:宏定义配置
- 三、程序流程:时钟配置函数解析
- 四、注意事项
- 总结流程图
- 睡眠模式+串口的接收与发送数据
- 一、需求背景:为什么需要低功耗模式?
- 1. 项目功能描述
- 2. 低功耗模式的目标
- 二、低功耗模式对比与选择
- 1. 睡眠模式(Sleep Mode)
- 2. 停机模式(Stop Mode)/ 停止模式
- 3. 待机模式(Standby Mode)
- 最终选择:睡眠模式
- 三、睡眠模式代码实现步骤
- 1. 初始代码:未优化的耗电场景
- 2. 加入睡眠模式:使用 `WFI` 指令
- 3. 睡眠模式的配置细节
- 四、程序执行流程解析
- 1. 初始化阶段
- 2. 主循环流程(无中断时)
- 3. 中断唤醒流程(收到串口数据时)
- 4. 关键现象验证
- 停止模式+红外对射传感器计次
- 一、停止模式引入背景与原理
- 1. 耗电问题分析
- 2. 停止模式工作原理
- 二、PWR 外设库函数详解
- 1. 相关函数列表与功能
- 2. 停止模式核心函数 `PWR_EnterStopMode()`
- 三、停止模式代码实现步骤
- 1. 添加运行状态指示(非必需,便于调试)
- 2. 开启 PWR 外设时钟
- 3. 调用函数进入停止模式
- 四、测试现象、问题分析与解决
- 1. 首次测试现象
- 2. 问题原因分析
- 3. 解决方案
- 五、程序执行流程总结
- 待机模式+实时时钟
- 待机模式(Standby Mode)实现
- 1. 时钟配置
- 2. 进入待机模式的函数调用
- 3. 外部模块省电处理
- 唤醒功能测试
- 1. WAKEUP 引脚唤醒
- 2. 闹钟唤醒
PWR
PWR(电源控制模块)核心功能
- 电源管理
- 负责 STM32 内部 模拟供电(VDDA)、数字供电(VDD,含 1.8V 降压区域)、后备供电(VBAT) 的统筹管理,确保各电路模块(CPU、SRAM、外设、RTC 等)在不同工作模式下的供电稳定。
- 通过电压调节器(如 1.8V 区域的低功耗模式配置)动态调整供电电压,降低空闲时的静态功耗(如停机 / 待机模式下关闭 1.8V 区域电源,实现微安级功耗)。
- 可编程电压监测器(PVD)
- 功能:实时监控 VDD 电源电压(阈值范围 2.2V~2.9V 可调,通过
PWR_PVDCR
寄存器配置),当电压 低于 / 高于阈值 时触发中断(映射到外部中断线),用于执行紧急任务(如保存数据、关机预警)。 - 原理:采用迟滞比较器(40mV 迟滞)避免电压波动误触发,确保在电池低电量或电源不稳定时可靠响应(POR/PDR 阈值分别为 1.92V/1.88V,与 PVD 形成电源异常分层处理)。
- 功能:实时监控 VDD 电源电压(阈值范围 2.2V~2.9V 可调,通过
- 低功耗模式实现
- 睡眠模式(Sleep)
- 进入:调用
WFI()
/WFE()
,仅关闭 CPU 时钟,外设(如串口、定时器)保持运行。 - 唤醒:任意中断(如串口数据接收、定时器溢出),唤醒后从暂停处继续执行(后面会实现演示程序通过串口数据唤醒,实现 “空闲睡眠 - 工作唤醒” 的节能逻辑)。
- 功耗:毫安级(约 30mA,比正常运行省 40% 以上),适合需快速响应的轻量级低功耗场景。
- 进入:调用
- 停机模式(Stop)
- 进入:设置
SLEEPDEEP=1
+PDDS=0
+WFI()
/WFE()
,关闭 1.8V 区域时钟、HSI/HSE 高速时钟,仅保留 LSI/LSE 低速时钟。 - 唤醒:外部中断(如对射红外传感器触发、PVD 中断、RTC 闹钟映射到外部中断),唤醒后需重新初始化主频(默认 HSI 8MHz,需恢复 HSE/PLL 到 72MHz,程序要通过此步骤恢复正常运行)。
- 功耗:微安级(14-24μA,接近 “深度睡眠”),适合中长时间空闲且需保留 SRAM 数据的场景(如物联网节点周期性休眠 - 唤醒)。
- 进入:设置
- 待机模式(Standby):
- 进入:设置
SLEEPDEEP=1
+PDDS=1
+WFI()
/WFE()
,关闭 1.8V 区域电源、所有时钟,仅 VBAT 供电维持 RTC / 备份寄存器。 - 唤醒:WAKEUP 引脚上升沿、RTC 闹钟、复位信号(会议中演示程序通过 RTC 每 10 秒唤醒,执行任务后再次待机)。
- 功耗:纳安级(2-3μA,极致省电),数据仅备份寄存器保留(SRAM 清零,需冷启动初始化),适合超长待机设备(如遥控器、低功耗传感器节点)。
- 进入:设置
- 睡眠模式(Sleep)
电源框图
一、电源分区功能与低功耗模式关联
1. VDDA 供电区域(模拟电路)
- 模块:ADC、温度传感器、PLL 等,独立供电确保模拟信号纯净(如 ADC 精度依赖 VDDA 稳定性)。
- 低功耗模式:始终供电(睡眠 / 停机 / 待机模式均不关闭),因模拟电路功耗占比低,优先保证信号质量。
2. VDD 供电区域(数字电路,含 1.8V 降压)
- I/O 电路:直接由 VDD(3.3V)供电,支持高电压 IO(如 5V 容忍),低功耗模式下:
- 睡眠模式:仅关 CPU 时钟,I/O 外设(串口)仍工作(可唤醒,如串口数据唤醒)。
- 停机模式:关 1.8V 区域时钟(CPU / 内存 / 数字外设时钟停,电源未断,数据保留),仅 LSI/LSE 运行(外设中断可唤醒)。
- 待机模式:关 1.8V 区域电源(数据丢失,仅 VBAT 供电),I/O 高阻态。
- 电压调节器
- 睡眠 / 停机模式:降压 1.8V 持续供电(停机模式低功耗,
LPDS=1
)。 - 待机模式:断电(
PDDS=1
),1.8V 区域无供电(功耗降至纳安级)。
- 睡眠 / 停机模式:降压 1.8V 持续供电(停机模式低功耗,
- 低电压检测器(PVD):监测 VDD,阈值 2.2-2.9V(用于电源异常中断,如电池低电量预警)。
3. 后备供电区域(VBAT)
- 模块:RTC、LSE(32K 晶振)、备份寄存器,由 VBAT(3V 电池)独立供电。
- 低功耗模式
- 睡眠 / 停机模式:VBAT 与 VDD 同时供电(RTC 持续运行,如会议中停机模式 RTC 未关闭)。
- 待机模式:唯一供电源(VDD 断电),维持 RTC 计时(会议中 RTC 闹钟每 10 秒唤醒,依赖 LSE)。
二、电源架构对低功耗设计的支撑
- 分区断电的能效梯度:
- 睡眠模式:仅 CPU 时钟关(1.8V 区域供电,功耗毫安级)→ 快速响应中断(如串口数据)。
- 停机模式:1.8V 区域时钟关(电源未断,数据保留,功耗微安级)→ 中长待机(如物联网节点周期性休眠)。
- 待机模式:1.8V 区域断电(仅 VBAT,数据丢失,功耗纳安级)→ 超长待机(如遥控器,数年待机)。
- 时钟与供电的联动控制:
- 高速时钟(HSI/HSE):停机 / 待机模式下关闭(VDD 供电区域时钟源切断),唤醒后需重新初始化(停机模式唤醒后恢复 72MHz 主频)。
- 低速时钟(LSI/LSE)
- LSI(8kHz):停机模式为 IWDG 供电(框图隐含)。
- LSE(32.768kHz):VBAT 供电,RTC 依赖其计时(待机模式 RTC 闹钟基于 LSE)。
- 复位与 PVD 的电源保护:
- POR/PDR:上电 / 掉电复位(VDDA 供电,阈值 1.92V/1.88V,迟滞 40mV,避免波动误触发)。
- PVD 中断:VDD 异常时触发(如电池即将耗尽),执行紧急任务(如保存数据)。
上电复位和掉电复位
- POR:电源上电时,通过电压监测、迟滞和延时,确保系统从已知状态启动,适用于初始化。
- PDR:电源欠压时,快速复位保护数据,依赖 VBAT 维持关键信息,适用于低功耗设备的电源故障处理。
- 二者通过 迟滞 增强抗干扰,通过 复位持续时间 确保稳定,与 PVD(电压监测)、VBAT 协同,构成 STM32 电源管理的核心机制(如物联网设备的电源异常防护与低功耗运行)。
可编程电压监测器
PVD 核心功能与原理
- 电源监测
- 实时监控
VDD
/VDDA
电压,阈值范围 2.2V~2.9V(8 级可调),通过迟滞比较器(100mV 迟滞,波形图所示)避免电压波动误触发,确保信号稳定。 - 当电压 低于 / 高于阈值 时,PVD 输出信号翻转,可触发中断(映射到外部中断线,如 EXTI16),用于执行紧急任务(如数据保存、低电量预警)。
- 实时监控
- 工作流程
- 电压上升:超过阈值时,PVD 输出高电平(或低电平,依配置),触发中断(如电源恢复,初始化系统)。
- 电压下降:低于阈值时,PVD 输出低电平(或高电平),触发中断(如电池低电量,保存数据后进入低功耗模式)。
- 迟滞设计(100mV)确保仅持续越界时响应,过滤电源纹波(如会议中强调 “避免输出抖动”,提升抗干扰能力)。
低功耗模式
模式选择
睡眠模式
停止模式
待机模式
修改主频
一、准备工作
- 解除文件只读属性
- system_stm32f10x.c 文件为只读(图标带钥匙符号),需手动解除:
- 右键文件所在文件夹,选择 “属性”,取消 “只读” 勾选。
- 确认后,文件可编辑(钥匙图标消失)。
- system_stm32f10x.c 文件为只读(图标带钥匙符号),需手动解除:
二、修改主频的核心步骤:宏定义配置
- 定位宏定义位置
- 打开
system_stm32f10x.c
文件,找到预编译宏定义区域(文件头部)。 - 核心配置项为
#define SYSCLK_FREQ_XXX
,其中XXX
对应不同主频(如 72MHz、36MHz 等)。
- 打开
- 根据设备型号选择配置分支
- 超值系列(如 VL 型号):仅支持 8MHz 和 24MHz 主频。
- 非超值系列(如 F103C8T6):支持 8MHz、24MHz、36MHz、48MHz、56MHz、72MHz 等。
- 通过预编译指令(
#ifdef
)判断设备类型,选择对应配置分支(非超值系列查看#else
分支)。
- 修改主频宏定义
- 示例:从 72MHz 改为 36MHz
- 注释原默认宏
#define SYSCLK_FREQ_72MHz
。 - 取消注释
#define SYSCLK_FREQ_36MHz
(或直接修改数值)。
- 注释原默认宏
- 其他主频:按需选择对应宏定义(如
SYSCLK_FREQ_48MHz
),原理类似。
- 示例:从 72MHz 改为 36MHz
三、程序流程:时钟配置函数解析
-
系统初始化函数
system_init()
- 作用:复位后自动调用(在启动文件中触发),配置时钟树。
- 流程
- 开启内部时钟
HSI
(默认 8MHz)。 - 恢复默认配置(重置外设时钟、禁用不必要功能)。
- 调用
set_system_clock()
函数,根据宏定义选择主频配置。
- 开启内部时钟
-
主频配置函数
set_system_clock()
- 作用:根据宏定义选择具体的时钟配置函数(如
set_system_clock_to_72()
、set_system_clock_to_36()
)。 - 核心逻辑(以 72MHz 为例)
- 使能外部晶振
HSE
(8MHz)。 - 配置锁相环(PLL):选择
HSE
作为输入,设置倍频系数为 9(8MHz × 9 = 72MHz
)。 - 等待
HSE
和PLL
就绪后,将PLL
输出设为系统时钟(SystemClock
)。
- 使能外部晶振
- 其他主频:仅倍频系数不同(如 36MHz 为
HSE ÷ 2 × 9 = 36MHz
)。 - #if 这样的形式叫预编译,就是如果定义 。。。 就执行。。。,否则 就执行 。。 下面如果使用_VL 超值系列,可以选择两种配置,否则可以选择#else的这些配置。
- 作用:根据宏定义选择具体的时钟配置函数(如
四、注意事项
- 外设时钟依赖主频
- 修改主频后,外设时钟(如 AHB、APB1/2)会按分频系数自动调整(默认分频:AHB=72MHz,APB2=72MHz,APB1=36MHz),需确保外设配置与新主频匹配。
- 延时函数自适应问题
- 若延时函数(如
delay_ms
)硬编码依赖 72MHz 主频,需根据system_core_clock
变量动态计算延时参数,避免计时错误。
- 若延时函数(如
- 谨慎修改主频
- 非必要情况下保持默认主频(72MHz),随意修改可能导致程序兼容性问题或外设异常。
总结流程图
复制工程 → 解除文件只读 → 修改宏定义(SYSCLK_FREQ_XXX)→ 编译下载 → 验证主频显示与代码运行速度↓配置流程:system_init() → set_system_clock() → 选择 PLL 倍频系数 → 系统时钟更新
首先调用 system_init() 使用修改寄存器的写法,作用就是恢复缺省配置,恢复完之后会调用 SetSysClock(); 函数该函数会根据所选宏的不同,调用不用的函数,这些函数对时钟进行配置。
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"int main(void)
{OLED_Init();OLED_ShowString(1, 1, "Sysclock");OLED_ShowNum(2, 1, SystemCoreClock, 8);while(1){OLED_ShowString(3, 1, "Running");Delay_ms(500);OLED_ShowString(3, 1, " ");Delay_ms(500);}
}
睡眠模式+串口的接收与发送数据
一、需求背景:为什么需要低功耗模式?
1. 项目功能描述
- 使用 SDK 32 开发下位机,功能为:
- 接收电脑通过串口发送的指令,并执行相应操作;
- 电脑指令发送时间不确定(可能随时发送,也可能长时间无操作)。
- 核心矛盾
- 为了随时响应指令,主循环需持续检查标志位或等待中断,但无指令时,CPU 空转导致功耗浪费。
2. 低功耗模式的目标
- 空闲时降低功耗:无指令时让 CPU 进入低功耗状态,仅在中断(如串口接收数据)到来时唤醒,恢复工作。
二、低功耗模式对比与选择
STM32 支持多种低功耗模式,会议重点分析了以下三种:
1. 睡眠模式(Sleep Mode)
- 特性
- CPU 停止运行,但外设时钟(如 USART)保持开启,硬件电路可正常接收数据;
- 中断唤醒:当 USART 接收到数据时,产生中断唤醒 CPU,恢复主循环执行。
- 适用场景:需要外设持续监听信号,仅 CPU 休眠的场景(如本项目的串口通信)。
2. 停机模式(Stop Mode)/ 停止模式
- 特性
- 关闭 1.8V 区域所有时钟,CPU 和外设均停止运行;
- 无法通过 USART 中断唤醒:因外设已断电,无法接收数据或产生中断。
- 结论:不适用于本项目(需持续监听串口指令)。
3. 待机模式(Standby Mode)
- 特性:功耗更低,但唤醒时间更长,且需重新初始化外设。
- 结论:同样不满足 “快速响应串口指令” 的需求。
最终选择:睡眠模式
原因:仅 CPU 休眠,外设保持工作,可通过串口中断快速唤醒,平衡功耗与响应速度。
三、睡眠模式代码实现步骤
1. 初始代码:未优化的耗电场景
-
功能:通过 OLED 显示 “running” 字样,主循环中循环显示和清除,模拟无指令时的空转。
-
代码片段
while (1) {OLED_ShowString(2, 1, "running"); // 显示“running”delay_ms(100); // 延迟 100 毫秒OLED_ShowString(2, 1, " "); // 清除显示 }
-
现象:无指令时,“running” 持续闪烁,主循环不断运行,CPU 持续耗电。
2. 加入睡眠模式:使用 WFI
指令
-
核心指令
__WFI();
(Wait For Interrupt,等待中断唤醒)- 作用:使 CPU 进入睡眠模式,直到中断发生。
-
代码优化
while (1) {// 原有逻辑:检查标志位、处理业务OLED_ShowString(2, 1, "running");delay_ms(100);OLED_ShowString(2, 1, " ");// 新增:主循环末尾加入睡眠指令__WFI(); // CPU 进入睡眠,等待中断唤醒 }
WFI vs WFE
WFI
:通过中断唤醒,适用于带中断的程序(如串口通信),配置简单;WFE
:通过事件唤醒,需额外配置事件寄存器,复杂度较高
3. 睡眠模式的配置细节
-
默认配置
- 睡眠模式涉及两个寄存器位:sleepdeep和sleep on exit,库函数未提供便捷配置接口,暂使用默认值0即
- 进入默认睡眠模式(非深度睡眠);
- 执行
WFI
后立即睡眠,无需等待中断结束。
- 睡眠模式涉及两个寄存器位:sleepdeep和sleep on exit,库函数未提供便捷配置接口,暂使用默认值0即
-
-
注意:直接操作寄存器需谨慎,需对照芯片手册确认位定义。
-
四、程序执行流程解析
1. 初始化阶段
- 配置串口(USART)参数,使能接收中断;
- 初始化 OLED 显示屏。
2. 主循环流程(无中断时)
- 显示 “running” 并延迟,模拟业务逻辑;
- 执行
__WFI();
,CPU 进入睡眠状态,主循环暂停; - 此时状态:CPU 休眠,USART 外设持续监听串口数据。
3. 中断唤醒流程(收到串口数据时)
- USART 接收到数据,触发中断;
- CPU 从睡眠中唤醒,暂停当前代码,跳转至 USART 中断处理函数;
- 中断函数中:读取数据、设置标志位(如
rx_flag = 1
)、清除中断标志; - 中断处理完成后,返回主循环,继续执行
WFI
之后的代码(即再次进入循环开头); - 主循环检测到
rx_flag
为真,执行数据回传和显示逻辑; - 逻辑执行完毕后,再次执行
__WFI();
,CPU 重新进入睡眠,等待下一次中断。
4. 关键现象验证
- 无数据时:“running” 不再闪烁,表明主循环停止,CPU 休眠;
- 有数据时:每发送一次数据,“running” 闪烁一次(主循环执行一次),数据正常回传,证明中断唤醒机制有效。
//main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
int Data;
int main(void)
{OLED_Init();Serial_Init();while(1){//如果接收到接收数据寄存器中的非空标志位,就可以读取数据if(Serial_GetFlag() == 1){Data = Serial_GetData();Serial_SendBtye(Data);OLED_ShowHexNum(1, 1, Data, 2);//不用手动清除标志位,读取数据会自动清除}OLED_ShowString(2, 1, "Running");Delay_ms(100);OLED_ShowString(2, 1, " ");Delay_ms(100);__WFI();}
}
//Serial.c 串口的主要逻辑
#include "stm32f10x.h" // Device header
#include <stdio.h>
#include <stdarg.h>int Serial_Data;
int Serial_flag;
void Serial_Init(void)
{//1.开启时钟 //开启GPIO时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);//初始化gpioinit参数中的结构体GPIO_InitTypeDef GPIO_InitStructure;//将gpio口设置为推挽输出,因为非工作模式默认为高电平,所以配置为上拉GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;//初始化发送引脚 9号引脚GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_InitStructure);//接收数据初始化 10号引脚 GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;//初始化发送引脚 9号引脚GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;USART_InitTypeDef USART_InitStruct;//要配置的波特率USART_InitStruct.USART_BaudRate = 9600;//有没有启动硬件数据流控USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//模式输出模式USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;//奇偶校验位USART_InitStruct.USART_Parity = USART_Parity_No;//停止位USART_InitStruct.USART_StopBits = USART_StopBits_1;USART_InitStruct.USART_WordLength = USART_WordLength_8b;USART_Init(USART1, &USART_InitStruct);//如果想要使用中断,就开启中断,然后配置NVICUSART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//先设置中断分组NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);NVIC_InitTypeDef NVIC_InitStruct;NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;NVIC_Init(&NVIC_InitStruct);//开始USART1的总开关USART_Cmd(USART1, ENABLE);
}//封装发送数据的函数
void Serial_SendBtye(uint8_t byte)
{USART_SendData(USART1, byte);//等待数据发送完成,读取标志位while( USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);//标志位置一之后不需要手动清零,进行写操作会自动清零}
void Serial_SendArray(uint8_t *Array, uint8_t length)
{uint8_t i;for(i = 0; i < length; i++){Serial_SendBtye(Array[i]);}
}void Serial_SendString(char *string)
{for(uint8_t i = 0; string[i] != '\0'; i++){Serial_SendBtye(string[i]);}
}
uint32_t Pow(uint8_t x, uint8_t y)
{uint32_t result = 1;while(y--){result *= x;}return result;
}
void Serial_SendNum(uint32_t Number, uint8_t length)
{for(uint8_t i = 0; i < length; i++){Serial_SendBtye(Number / Pow(10, length - i - 1) % 10 + '0');}
}
/*** 函 数:使用printf需要重定向的底层函数* 参 数:保持原始格式即可,无需变动* 返 回 值:保持原始格式即可,无需变动*/
int fputc(int ch, FILE *f)
{Serial_SendBtye(ch); //将printf的底层重定向到自己的发送字节函数return ch;
}
/*** 函 数:自己封装的prinf函数* 参 数:format 格式化字符串* 参 数:... 可变的参数列表* 返 回 值:无*/
void Serial_Printf(char *format, ...)
{char String[100]; //定义字符数组va_list arg; //定义可变参数列表数据类型的变量argva_start(arg, format); //从format开始,接收参数列表到arg变量vsprintf(String, format, arg); //使用vsprintf打印格式化字符串和参数列表到字符数组中va_end(arg); //结束变量argSerial_SendString(String); //串口发送字符数组(字符串)
}
int Serial_GetFlag(void)
{//实现读取标志位后自动清除的功能if(Serial_flag == 1){Serial_flag = 0;return 1;}return 0;
}
int Serial_GetData(void)
{return Serial_Data;
}void USART1_IRQHandler(void)
{if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET){Serial_Data = USART_ReceiveData(USART1);Serial_flag = 1;//不用手动清除标志位,读取数据会自动清除}
}
停止模式+红外对射传感器计次
一、停止模式引入背景与原理
1. 耗电问题分析
- 当前代码在无外部中断信号时,持续执行
counter Sensor
的get
操作及外循环刷新,导致不必要的功耗。 - 优化目标:空闲时进入低功耗模式,仅在外部中断触发时唤醒系统。
2. 停止模式工作原理
- 低功耗特性
- 关闭 1.8V 区域时钟,CPU 和外设停止工作,但外部中断(EXTI)无需时钟即可工作。
- 内核停止运行,但寄存器和 SRAM 内容保留。
- 关键依据
- 初始化时未开启 EXTI 时钟,证明 EXTI 在时钟关闭时仍可响应中断(硬件特性)。
二、PWR 外设库函数详解
1. 相关函数列表与功能
函数名 | 功能描述 |
---|---|
PWR_DeInit() | 恢复 PWR 外设默认配置 |
PWR_BackupAccessCmd(ENABLE) | 使能后备区域访问(用于待机模式等场景) |
PWR_PVDCmd(ENABLE) | 使能电源电压检测(PVD)功能 |
PWR_PVDLevelConfig() | 配置 PVD 阈值电压 |
PWR_WakeUpPinCmd(ENABLE) | 使能 PA0 引脚作为唤醒引脚(配合待机模式) |
PWR_EnterStopMode() | 进入停止模式(核心函数) |
PWR_EnterStandbyMode() | 进入待机模式 |
PWR_GetFlagStatus() | 获取 PWR 状态标志位(如唤醒标志) |
PWR_ClearFlag() | 清除 PWR 状态标志位 |
2. 停止模式核心函数 PWR_EnterStopMode()
- 参数说明
- 第一个参数:指定电压调节器状态
PWR_Regulator_ON
:调节器开启(功耗略高,恢复速度快)PWR_Regulator_LowPower
:调节器低功耗模式(更省电,但唤醒后时钟需重新配置)
- 第二个参数:选择进入模式的指令
PWR_StopMode_WFI
:通过WFI
(等待中断)指令进入停止模式PWR_StopMode_WFE
:通过WFE
(等待事件)指令进入停止模式
- 第一个参数:指定电压调节器状态
三、停止模式代码实现步骤
1. 添加运行状态指示(非必需,便于调试)
-
在主循环中添加代码:
OVERIDE_ShowString(2, 1, "running"); // 在指定位置显示"running" delay(100); // 延时100ms OVERIDE_ClearString(); // 清除显示
-
作用:通过 “running” 闪烁判断主循环是否运行,辅助观察低功耗效果。
2. 开启 PWR 外设时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); // 开启PWR时钟
- 注意:若未开启时钟,对 PWR 寄存器的读写操作无效(寄存器值全为 0)。
3. 调用函数进入停止模式
-
在主循环末尾添加:
PWR_EnterStopMode(PWR_Regulator_ON, PWR_StopMode_WFI); // 选择调节器开启,WFI指令
-
函数内部流程
- 读取
PWR_CR
寄存器值到临时变量。 - 清除
PDDS
位(选择停止模式,非待机模式)。 - 根据参数设置
LPDS
位(0:调节器开启;1:调节器低功耗)。 - 写入新配置到
PWR_CR
寄存器。 - 设置内核
sleepdeep
位(使能深度睡眠,进入停止模式)。 - 执行
WFI
指令,系统进入停止模式,程序暂停。 - 唤醒后:自动清除
sleepdeep
位,程序从暂停处继续执行。
- 读取
四、测试现象、问题分析与解决
1. 首次测试现象
- 正常表现
- 无中断时,“running” 停止闪烁,主循环暂停(进入停止模式)。
- 遮挡红外传感器(触发中断)时,“running” 闪烁一次(系统唤醒并执行一次主循环)。
- 异常问题
- 复位后首次 “running” 闪烁快(正常速度),但中断唤醒后闪烁变慢。
2. 问题原因分析
- 时钟切换机制
- 复位后默认使用HSE(高速外部时钟,8MHz 晶振 ×9 倍频 = 72MHz 主频)。
- 停止模式退出后,系统默认切换为HSI(高速内部时钟,8MHz),导致程序运行变慢。
3. 解决方案
-
修复方法:在退出停止模式后,重新配置系统时钟为 HSE×9 倍频(72MHz)。
- 代码:在主循环中调用
SystemInit()
函数(该函数初始化 HSE 时钟配置)。
PWR_EnterStopMode(...); // 进入停止模式 SystemInit(); // 退出后重新配置72MHz主频
- 代码:在主循环中调用
-
验证结果:重新编译下载后,中断唤醒后 “running” 闪烁恢复正常速度。
五、程序执行流程总结
- 复位与初始化
- 执行系统初始化(
SystemInit()
),配置 HSE 为系统时钟(72MHz)。 - 初始化外设(如红外传感器对应的 EXTI 中断)。
- 执行系统初始化(
- 进入主循环
- 显示 “running”→ 延时→ 清除显示→ 准备进入停止模式。
- 进入停止模式
- 调用
PWR_EnterStopMode()
,执行WFI
指令,系统暂停,等待中断。
- 调用
- 中断唤醒
- 外部中断(如传感器遮挡)触发,系统唤醒。
- 执行中断处理函数,随后从
WFI
指令处继续执行主循环。
- 时钟恢复
- 调用
SystemInit()
,重新配置 HSE 时钟,确保主频为 72MHz。
- 调用
- 循环往复:重复主循环流程,空闲时再次进入停止模式。
//main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "CountSensor.h"int main(void)
{OLED_Init();OLED_ShowString(1, 1, "count:");CountSensor_Init();//开启PWR时钟RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);while(1){OLED_ShowNum(1, 7, Count_Get(), 5);OLED_ShowString(2, 1, "Running");Delay_ms(100);OLED_ShowString(2, 1, " ");Delay_ms(100);PWR_EnterSTOPMode(PWR_Regulator_ON, PWR_STOPEntry_WFE);//停止模式结束会默认时钟为LSI时钟 8 MHZ 所以结束停止模式要重新设置时钟频率SystemInit();}
}
//红外计数传感器
#include "stm32f10x.h" // Device header
#include "CountSensor.h"uint16_t count;
void CountSensor_Init(void)
{//配置时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);//初始化io口GPIO_InitTypeDef GPIO_InitStruct;GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;GPIO_InitStruct.GPIO_Pin = GPIO_Pin_14;GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOB, &GPIO_InitStruct);//AFIO选择GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);//配置EXTIEXTI_InitTypeDef EXTI_InitStruct;EXTI_InitStruct.EXTI_Line = EXTI_Line14;EXTI_InitStruct.EXTI_LineCmd = ENABLE;EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Rising;EXTI_Init(& EXTI_InitStruct);//先配置中断分组NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//配置NVICNVIC_InitTypeDef NVIC_InitStruct;NVIC_InitStruct.NVIC_IRQChannel = EXTI15_10_IRQn;NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;NVIC_Init(& NVIC_InitStruct);}
uint16_t Count_Get(void)
{return count;
} //中断函数的名字是固定的,可以参考启动文件中的函数名
void EXTI15_10_IRQHandler(void)
{//在中断函数中一般要查看是不是我们想要的中断进来的if( EXTI_GetITStatus(EXTI_Line14) == SET){count++;//使用完成后将中断标志位手动清零EXTI_ClearITPendingBit(EXTI_Line14);}
}
待机模式+实时时钟
待机模式(Standby Mode)实现
1. 时钟配置
- 必要性:使用 PWR 外设需开启其时钟(
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE)
)。 - 代码独立性:即使 RTC 初始化中已开启 PWR 时钟,仍需在待机模式代码中单独开启,避免代码耦合(若后续删除 RTC 代码,可能导致待机模式失效)。
2. 进入待机模式的函数调用
- 函数选择:调用
PWR_EnterSTANDBYMode()
,内部执行步骤:
① 清除 WAKEUP 标志位;
② 设置 PWR 控制寄存器(PWR_CR)的 SBF 位,进入待机模式;
③ 置位SLEEPDEEP
位,调用WFI
指令进入深度睡眠。 - 关键点:待机模式不区分
WFI
/WFE
,唤醒条件为指定 4 个信号(如闹钟、WAKEUP 引脚等)。
3. 外部模块省电处理
- 原则:待机模式下 STM32 本身仅耗微安级电流,但外部模块(如显示屏、电机)可能耗电数十毫安,需彻底关闭。
- 硬件方案
- 使用带使能端的稳压器,或在外部模块供电回路中添加开关电路。
- 软件模拟:进入待机前执行
OID_Clear()
清屏,并在 4 行 9 列显示 “standby”(停留 1 秒),模拟模块关闭动作。
唤醒功能测试
1. WAKEUP 引脚唤醒
- 代码配置:调用
PWR_WakeUpPinCmd(ENABLE)
使能唤醒引脚,无需 GPIO 初始化(手册规定:使能后自动配置为输入下拉)。 - 测试方法
- 引脚默认下拉(悬空为低电平),接高电平时触发唤醒。
- 可接入外部传感器信号,实现自动化唤醒。
2. 闹钟唤醒
- 逻辑:待机模式下,RTC 闹钟触发后,STM32 自动退出待机,从程序起始位置重新执行(因闹钟设定代码在
while
循环前,唤醒后闹钟值会重新初始化)。
//main.c
//闹钟唤醒#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyRTC.h"int main(void)
{OLED_Init();RTC_Init();//开启PWR时钟RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);OLED_ShowString(1, 1, "CNT:");OLED_ShowString(2, 1, "ALR:");OLED_ShowString(3, 1, "FLAG_ALR:");uint32_t ALR = RTC_GetCounter() + 10;RTC_SetAlarm(ALR);OLED_ShowNum(2, 5, ALR, 10);while(1){OLED_ShowNum(1, 5, RTC_GetCounter(), 10); //显示32位的秒计数器OLED_ShowNum(3, 10, RTC_GetFlagStatus(RTC_FLAG_ALR), 1);OLED_ShowString(4, 1, "Running");Delay_ms(100);OLED_ShowString(4, 1, " ");Delay_ms(100);OLED_Clear();PWR_EnterSTANDBYMode();}
}// wakeup唤醒
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyRTC.h"int main(void)
{OLED_Init();RTC_Init();//开启PWR时钟RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);//开启wakeup使能PWR_WakeUpPinCmd(ENABLE);OLED_ShowString(1, 1, "CNT:");OLED_ShowString(2, 1, "ALR:");OLED_ShowString(3, 1, "FLAG_ALR:");uint32_t ALR = RTC_GetCounter() + 10;RTC_SetAlarm(ALR);OLED_ShowNum(2, 5, ALR, 10);while(1){OLED_ShowNum(1, 5, RTC_GetCounter(), 10); //显示32位的秒计数器OLED_ShowNum(3, 10, RTC_GetFlagStatus(RTC_FLAG_ALR), 1);OLED_ShowString(4, 1, "Running");Delay_ms(100);OLED_ShowString(4, 1, " ");Delay_ms(100);OLED_Clear();PWR_EnterSTANDBYMode();}
}