目录
[1 前言](#1 前言)
[2 PWR 简介及电源系统](#2 PWR 简介及电源系统)
[2.1 PWR 概述](#2.1 PWR 概述)
[2.2 PWR 模块剖析](#2.2 PWR 模块剖析)
[2.2.1 模拟电源域(VDDA/VSSA)](#2.2.1 模拟电源域(VDDA/VSSA))
[2.2.2 数字核心电源域(VDD 与 1.8V )](#2.2.2 数字核心电源域(VDD 与 1.8V ))
[2.2.3 后备电源域(VBAT)](#2.2.3 后备电源域(VBAT))
[2.3 电压调节器的工作机制](#2.3 电压调节器的工作机制)
[2.4 电源管理器](#2.4 电源管理器)
[2.4.1 上电复位(POR)和掉电复位(PDR)](#2.4.1 上电复位(POR)和掉电复位(PDR))
[2.4.2 可编程电压监测器(PVD)](#2.4.2 可编程电压监测器(PVD))
[3 三种低功耗模式详解](#3 三种低功耗模式详解)
[3.1 低功耗模式概述](#3.1 低功耗模式概述)
[3.2 睡眠模式 (Sleep Mode)](#3.2 睡眠模式 (Sleep Mode))
[3.3 停止模式 (Stop Mode)](#3.3 停止模式 (Stop Mode))
[3.4 待机模式 (Standby Mode)](#3.4 待机模式 (Standby Mode))
[4. 本章节实验](#4. 本章节实验)
[4.1 实验一:修改系统主频(SYSCLK)](#4.1 实验一:修改系统主频(SYSCLK))
[4.1.1 实验目标](#4.1.1 实验目标)
[4.1.2 实验原理](#4.1.2 实验原理)
[4.1.3 硬件设计](#4.1.3 硬件设计)
[4.1.4 软件设计](#4.1.4 软件设计)
[4.1.5 实验现象](#4.1.5 实验现象)
[4.2 实验二:睡眠模式与串口中断唤醒](#4.2 实验二:睡眠模式与串口中断唤醒)
[4.2.1 实验目标](#4.2.1 实验目标)
[4.2.2 实验原理](#4.2.2 实验原理)
[4.2.3 硬件设计](#4.2.3 硬件设计)
[4.2.4 软件设计](#4.2.4 软件设计)
[4.2.5 实验现象](#4.2.5 实验现象)
[4.3 实验三:停止模式与对射式红外传感器计次](#4.3 实验三:停止模式与对射式红外传感器计次)
[4.3.1 实验目标](#4.3.1 实验目标)
[4.3.2 实验原理](#4.3.2 实验原理)
[4.3.3 硬件设计](#4.3.3 硬件设计)
[4.3.4 软件设计](#4.3.4 软件设计)
[4.3.5 实验现象](#4.3.5 实验现象)
[4.4 实验四:待机模式与实时时钟唤醒](#4.4 实验四:待机模式与实时时钟唤醒)
[4.4.1 实验目标](#4.4.1 实验目标)
[4.4.2 实验原理](#4.4.2 实验原理)
[4.4.3 硬件设计](#4.4.3 硬件设计)
[4.4.4 软件设计](#4.4.4 软件设计)
[4.4.5 实验现象](#4.4.5 实验现象)
1 前言
在嵌入式系统设计中,功耗控制是衡量设备可靠性与续航能力的重要指标。对于无线传感节点、便携式仪表、安防设备等长期运行的嵌入式终端而言,系统大部分时间通常处于空闲等待状态。若微控制器(MCU)始终保持全速运行,不仅会造成大量无效功耗,还可能带来发热增加、续航缩短等问题。因此,如何在"保持系统可响应"的同时尽可能关闭不必要的硬件电路,成为低功耗设计的核心。
STM32F10x系列内部集成了专用的电源控制模块PWR(Power Control),用于统一管理芯片不同供电区域与低功耗状态。借助PWR模块,STM32提供了三种标准低功耗模式:
- 睡眠模式(Sleep)
- 停止模式(Stop)
- 待机模式(Standby)
三者在CPU工作状态、时钟保持情况、数据保存能力以及唤醒速度等方面各不相同,可适用于不同类型的低功耗应用场景。
本篇博客将结合STM32F10x的底层电源架构,系统分析PWR模块、电源域划分以及三种低功耗模式的工作原理。随后再通过实验演示其具体配置与工程应用,包括:
- 调整系统主频实现基础动态功耗控制
- 睡眠模式结合USART串口实现中断唤醒
- 停止模式结合EXTI外部中断实现低功耗唤醒
- 待机模式结合RTC实现定时唤醒
2 PWR 简介及电源系统
2.1 PWR 概述
PWR (Power Control,电源控制)模块是STM32内部负责低功耗管理与供电调度的核心外设,其本质作用是在系统运行状态、时钟活动以及内部供电之间建立统一的硬件控制机制。PWR与RCC(复位与时钟控制)以及Cortex-M3内核中的SCB(System Control Block,系统控制块)协同工作,共同决定系统进入何种功耗状态以及如何被重新唤醒。
在软件编程层面,PWR模块主要承担以下两类核心功能:
- 控制内部电压调节器(Voltage Regulator)的工作模式,从而实现Sleep、Stop、Standby等不同等级的低功耗状态切换
- 提供可编程电压监测器(PVD,Programmable Voltage Detector),用于监测VDD电压是否低于设定阈值,以便系统提前进行掉电保护或数据保存
需要特别说明的是,STM32的低功耗机制并不仅仅是"CPU暂停运行"这么简单,而是涉及多个供电域、电压调节器、时钟源以及备份区域之间的硬件级协同控制。因此,在理解低功耗模式之前,必须先明确STM32内部的电源架构划分方式。
2.2 PWR 模块剖析
STM32将芯片内部不同功能模块划分到多个独立的供电区域(Power Domain)中,以便实现更细粒度的功耗控制。从整体结构上看,STM32F10x的电源系统主要由以下三个部分构成:
- 模拟电源域(VDDA / VSSA)
- 数字核心电源域(VDD / 1.8V)
- 后备电源域(VBAT)
如下图所示:
图 - STM32 电源系统框图
2.2.1 模拟电源域(VDDA/VSSA)
模拟电源域以VDDA为正极、VSSA为负极,专门为对电源噪声敏感的模拟电路供电。该区域主要包括:
- ADC模数转换器
- 内部温度传感器
- PLL锁相环中的模拟部分
- 上电/掉电复位相关模拟电路

根据STM32F10xxx参考手册要求,VDDA 和 VSSA必须分别连接到 VDD 和 VSS。也就是说,即使工程中未使用 ADC,也不能悬空 VDDA / VSSA,否则芯片可能无法正常工作。
对于ADC参考电压,STM32不同封装形式的处理方式有所区别:
- 在100引脚与144引脚封装中,VREF+与VREF−被独立引出,可接入外部高精度基准源,以提高ADC精度;
- 在64引脚及以下封装(如STM32F103C8T6)中,没有独立的VREF引脚,其内部已自动将VREF+连接至VDDA,VREF−连接至VSSA;
因此,小封装器件无法外接独立ADC参考源。
2.2.2 **数字核心电源域(VDD 与 1.8V )**
数字电源域是STM32的主体供电网络,其结构可以进一步划分为两层:
- 外部输入的 VDD 供电区域;
- 内部稳压器生成的 1.8V 核心供电区域;

其中,外部输入电压VDD(典型工作值为3.3V)直接为微控制器的GPIO电路、待机唤醒逻辑以及独立看门狗(IWDG)供电,同时还作为内部电压调节器(Voltage Regulator)的输入。
内部电压调节器会将VDD降压至约1.8V,形成独立的核心供电域,用于驱动:
- Cortex-M3 CPU内核
- SRAM
- Flash数字接口
- 内部数字外设
- 总线矩阵与逻辑控制电路
这种"外部高压输入+内部低压核心"的设计,是现代MCU常见的低功耗架构,将核心逻辑从3.3V降低到1.8V,可显著降低CPU与数字逻辑的功耗。
后续在分析Stop与Standby模式时,必须明确区分:
- VDD域是否仍保持供电
- 1.8V核心域是否仍被稳压器维持
因为这决定了SRAM、寄存器以及CPU上下文是否能够继续保存。
2.2.3 后备电源域(VBAT)
后备电源域(Backup Domain)用于在主电源断电后,继续维持RTC与关键数据的运行与保存,是STM32实现"掉电保持"的核心硬件结构。该供电区域由VBAT引脚供电,主要包含以下模块:
- RTC 实时时钟
- LSE(32.768kHz低速外部晶振)
- BKP 后备/备份寄存器
- RCC_BDCR 备份域控制寄存器

如图所示,STM32 内部集成了自动电源切换电路,用于在 VDD 与 VBAT 之间自动选择后备域供电来源。
- 当 VDD 正常存在时,内部模拟开关自动连接至 VDD,此时后备域由VDD供电,外部VBAT电池不消耗能量;
- 当系统检测到 VDD 掉电或触发 PDR 掉电复位时,模拟开关会自动切换至VBAT,由外部电池(如纽扣电池)维持RTC和BKP寄存器的工作。这便是STM32在系统完全断电后仍能保持时间的根本原因。若未使用外部后备电池,应将VBAT直接外接至VDD并添加一个100nF滤波电容。
**注意:**PC13、PC14、PC15 这三个引脚也属于后备域供电范围。由于该区域的内部模拟开关仅能提供有限电流,这些引脚的驱动能力较弱,其GPIO输出速度须限制在 2MHz 以下,负载电容不超过 30pF,不适合驱动大电流负载,一般仅用于按键输入、LSE晶振连接或小负载状态指示。
2.3 电压调节器的工作机制
电压调节器(Voltage Regulator) 用于将外部 VDD 电源转换为芯片内部使用的 1.8V 核心电压,为 CPU 内核、SRAM 以及片上数字逻辑提供稳定供电,其在 STM32 内部供电结构中的位置如下图所示:

不同低功耗模式对应不同的调压器工作状态。STM32 通过切换调压器的工作模式,在系统功耗、数据保持能力与唤醒时间之间实现平衡。主要包含以下三种工作状态。
-
**正常运行模式(Normal Mode):**用于运行模式(Run)与睡眠模式(Sleep)。在该状态下,调压器提供完整的输出驱动能力,可满足 CPU 与片上外设正常运行所需的供电电流。此时内部 1.8V 核心供电域保持完全工作,因此系统能够以最高性能运行。
由于调压器始终处于全功能工作状态,因此该模式具有最短的唤醒时间,但静态功耗也最高。
-
低功耗模式(Low-Power Mode) **:**用于停止模式(Stop)。在该状态下,调压器进入低功耗稳压状态,降低输出驱动能力与静态电流消耗,从而进一步降低系统功耗。虽然系统主时钟停止、CPU 与大部分数字外设暂停工作,但 SRAM 与关键电路仍保持供电,因此 SRAM 中的数据不会丢失,程序运行现场也能够在唤醒后继续恢复。
由于调压器需要在唤醒过程中重新恢复到正常输出能力,因此系统从 Stop 模式退出时会增加一定的稳压器启动延迟。
-
断电模式 **(Power Down Mode):**用于待机模式(Standby)。进入该状态后,电压调节器被关闭,内部 1.8V 核心供电域断电,因此 CPU、SRAM 以及大部分数字外设停止供电。
此时芯片功耗降至最低,典型电流约为 3μA,但 SRAM 与绝大多数普通寄存器中的数据都会丢失,仅后备供电域中的 RTC、BKP 备份寄存器等电路仍可继续保持工作。
2.4 电源管理器
为了确保系统在电源波动、上电或掉电瞬间的物理稳定性,PWR 模块内建了两级核心电压监控电路,共同构成了从硬件底层保护到软件应用层预防的系统级监控链路。
2.4.1 上电复位(POR)和掉电复位(PDR)
该模块是系统最底层的强制性硬件保护电路,无需任何软件配置即可工作。它持续监测VDD与VDDA的电压电平,确保MCU仅在电压足以维持逻辑电路正常工作的范围内运行。
-
上电阶段 :当VDD从 0 V 上升时,在电压尚未达到上电复位阈值
之前,系统会一直保持复位状态。即使 VDD 越过
,复位信号不会立即撤销,而是由硬件强制延时一段滞后时间
后才予以释放。此设计旨在等待外部晶体振荡器起振并确保内部数字电路彻底稳定,防止内核在电源上升沿的建立期内执行无效指令。
-
掉电阶段 :当系统电源
跌落至掉电复位阈值
之下时,监控电路会立即拉低复位信号。此时系统被强制终止一切总线活动与程序执行,避免因驱动电压不足导致外设寄存器状态不可控或闪存数据被意外覆写。

波形解析:
-
迟滞特性 :根据波形图所示,
与
之间存在约 40 mV 的迟滞电压区间。迟滞比较器的引入能够有效抑制电源在阈值临界点附近,由微小高频纹波或负载阶跃产生的频繁电平翻转,从而避免微控制器陷入反复复位的振荡状态。
-
滞后时间(
) :在
超过
后,图中 Reset 信号并不会立即跳变为高电平,而是延时了一段
。这是为了给晶振起振和内部数字电路稳定预留充足的时间。
2.4.2 可编程电压监测器(PVD)
与 POR/PDR 的强制硬件复位机制不同,可编程电压监测器(Programmable Voltage Detector, PVD)是一种软件可配置的预防性低电压报警机制,它允许开发者在系统彻底崩溃前(即VDD跌落至之前),提前获得预警并执行紧急任务。
-
配置方式 :系统硬件提供 8 个离散的 PVD 监测阈值(典型绝对电压范围处于 2.2 V 至 2.9 V 之间,步进分辨率约为 0.1 V)。开发者需向电源控制寄存器 PWR_CR 中的 PLS[2:0] 位域写入特定配置字,以选定符合当前硬件环境的目标门限。如下图所示位7:5。

-
触发机制 :通过使能 PWR_CR 寄存器中的 PVDE 位即可激活 PVD 电源电压监测器。当
跌落至预设 PVD 阈值(降压触发)或从阈值以下攀升恢复(升压触发)时,PVD 电路会生成硬件跳变信号。

波形解析:
-
PVD 迟滞:参照 PVD 的门限波形图,PVD 同样具备比较器迟滞特性,其标称迟滞量约为 100 mV。降压触发报警门限物理上略低于升压恢复检测门限,该不对称设定确保了输出中断信号在临界电压点附近的逻辑稳定性。
-
中断响应:PVD 的硬件输出信号物理连接至内部嵌套向量中断控制器(NVIC)的 EXTI 线 16。当触发条件满足时,系统会生成高优先级的外部中断请求。如下图所示:

架构说明:在 STM32 的中断路由总线中,EXTI 线 16 至 19 专用于映射内部独立外设的触发事件。具体分配为:EXTI 线 16 映射至 PVD 报警,EXTI 线 17 映射至 RTC 闹钟事件,EXTI 线 18 映射至 USB 唤醒事件,EXTI 线 19 映射至以太网唤醒事件。
由于 PVD 的报警触发阈值绝对电平显著高于 PDR 的硬件复位电平(通常高出数百毫伏),该压差为系统处理异常断电提供了临界响应时间窗口。在 EXTI16 的中断服务例程(ISR)中,必须配置高效的非阻塞代码以执行系统级保护动作,典型工程处理包括:
-
数据固化:将内存中的核心运算变量快速转移至由电池独立供电的 BKP(备份寄存器)网络,或急速写入外部非易失性存储器(如 EEPROM 或 SPI Flash)。
-
安全停机保护:向电机驱动硬件接口直接输出制动信号,或强行将功率开关管栅极置于关断电平,防止逆变器桥臂因栅极驱动逻辑电平跌落而发生毁灭性的直通短路故障。
-
状态记录:在 BKP 寄存器中写入特定的"异常掉电"标志字。待系统电源恢复并重新启动后,主程序可通过读取该标志字完成硬件故障的软件溯源。
通过 POR/PDR 底层硬件防护与 PVD 软件预警机制的系统级协同,微控制器能够实现供电建立阶段的稳健启动,并具备应对意外电网跌落的容错机制。
3 三种低功耗模式详解
3.1 低功耗模式概述
STM32F10x系列微控制器提供了三种递进式低功耗模式:睡眠模式(Sleep)、停止模式(Stop)与待机模式(Standby)。它们本质上对应着芯片对"时钟网络"和"供电网络"两类硬件资源的不同级别干预。
从底层硬件角度来看,降低功耗主要依赖以下两种物理手段:
-
**时钟门控:**关闭时钟网络后,依赖时钟驱动的同步逻辑电路将停止翻转,动态功耗显著下降,但由于供电仍然存在,因此SRAM、CPU寄存器以及外设寄存器中的数据依旧能够保持。
-
**电源域断电:**关闭供电区域时,不仅动态功耗消失,对应逻辑单元的静态漏电流也会被进一步消除,因此系统功耗能够降低至微安级,但代价是该供电域中的SRAM、寄存器以及运行上下文将全部丢失。
STM32的三种低功耗模式,正是基于这两类硬件干预手段逐级演化而来的:
- Sleep:仅关闭 CPU 内核时钟;
- Stop:关闭整个系统主时钟树,但维持 1.8V 核心电源域供电;
- Standby:彻底切断 1.8V 核心电源域供电。
随着低功耗等级的不断加深,系统功耗指标逐级优化,但硬件的数据保持能力随之减弱,唤醒链路的触发条件也愈发严格。下图展示了 STM32 低功耗模式的底层状态机判定逻辑:
图 - STM32 低功耗模式进入逻辑
该状态机的核心控制位包括:
- SLEEPDEEP:决定进入浅睡眠还是深度睡眠
- PDDS:决定深度睡眠属于Stop还是Standby
- LPDS:决定Stop模式下电压调节器是否进入低功耗状态
- SLEEPONEXIT:决定Sleep模式是否在退出中断后自动重新进入休眠
整个低功耗模式的触发序列,本质上是 Cortex-M3 内核在执行 WFI(Wait For Interrupt,等待中断) 或**WFE(Wait For Event,等待事件)**指令触发后,由系统硬件状态机根据上述控制位完成模式分支的自动切换。
| 模式 | 说明 | 进入方式 | 唤醒源 | 对 1.8V 区域 时钟的影响 | 对 VDD 区域 时钟的影响 | 电压调节器 |
|---|---|---|---|---|---|---|
| 睡眠模式 (SLEEP-NOW 或 SLEEP-ON-EXIT) | 内核停止,所有 外设包括 M3 核 心的外设,如 NVIC、系统时 钟(SysTick)等仍 在运行 | WFI | 任一中断 | CPU 时钟关,对其他时钟和 ADC 时钟无影响 | 无 | 开 |
| 睡眠模式 (SLEEP-NOW 或 SLEEP-ON-EXIT) | 内核停止,所有 外设包括 M3 核 心的外设,如 NVIC、系统时 钟(SysTick)等仍 在运行 | WFE | 唤醒事件 | CPU 时钟关,对其他时钟和 ADC 时钟无影响 | 无 | 开 |
| 停机模式 | 所有的时钟都已停止 | PDDS 和 LPDS 位 +SLEEPDEEP 位 + WFI 或 WFE | 任一外部中断(在外部中断寄存器中设置) | 关闭所有 1.8V 区域的时钟 | HSI 和 HSE 的振荡器关闭 | 开启或处于低功耗模式(依据电源控制寄存器 PWR_CR 的设定) |
| 待机模式 | 1.8V 电源关闭 | PDDS 位 + SLEEPDEEP 位 + WFI 或 WFE | WKUP 引脚的上升沿、RTC 闹钟事件、NRST 引脚上的外部复位、IWDG 复位 | 关闭所有 1.8V 区域的时钟 | HSI 和 HSE 的振荡器关闭 | 关 |
| [表1 - 低功耗模式对照表(基于 3.3V 典型供电环境)] |
3.2 睡眠模式 (Sleep Mode)
睡眠模式是功耗最低的浅睡眠模式。它通过时钟门控关闭CPU时钟,使 Cortex-M3 内核暂停指令读取动作。在此期间,系统的整体主时钟树和 1.8V 核心电源域维持运行。这种机制确保了所有外设继续工作,同时静态随机存取存储器(SRAM)、寄存器数据以及通用输入输出(GPIO)的电平状态均得以完整保留。睡眠模式的核心优势在于唤醒延迟极低,CPU 能在检测到唤醒条件的瞬间恢复代码执行,非常适合需要快速响应异步通信或进行周期性采样的应用场景。
系统进入睡眠模式的动作由内核指令 __WFI(等待中断)或 __WFE(等待事件)触发,具体的进入时机受系统控制寄存器(SCB_SCR)中的 SLEEPONEXIT 位控制:
- 当 SLEEPONEXIT 配置为 0 时(SLEEP-NOW 模式),在将内核寄存器的 SLEEPDEEP 清零后,调用指令会使系统立即休眠。
- 当 SLEEPONEXIT 配置为 1 时(SLEEP-ON-EXIT 模式),系统在执行休眠指令后,需等待当前所有处于挂起状态的中断服务例程(ISR)执行完毕,且硬件完成寄存器上下文恢复后,方可由硬件状态机驱动进入休眠模式。。
系统控制寄存器 (SCB_SCR)
睡眠模式的唤醒方式取决于进入休眠时使用的指令。若使用 WFI 指令进入睡眠,系统可被任意已使能的嵌套向量中断控制器(NVIC)中断唤醒。若使用 WFE 指令进入睡眠,系统需由事件(如配置为事件模式的外部中断控制器 EXTI 线、挂起事件等)唤醒。
由于事件唤醒路径在硬件上无需执行完整的中断响应流程,因此其响应效率相对更高。系统退出睡眠模式后,若是中断唤醒,会先进入对应的中断服务程序,退出中断后再接着执行 WFI 指令之后的代码;若是事件唤醒,系统不经历复位过程,直接继续执行 WFE 指令之后的程序。
其各种特性见下表:
| 特性 | 说明 |
|---|---|
| 进入条件 | 内核寄存器 SLEEPDEEP = 0,然后调用 WFI 或 WFE 指令即可进入睡眠模式; 若 SLEEPONEXIT = 0,调用 WFI/WFE 立即进入睡眠模式; 若 SLEEPONEXIT = 1,退出优先级最低的中断服务程序后进入睡眠模式。 |
| 唤醒方式 | 如果是使用 WFI 指令睡眠的,则可使用任意中断唤醒; 如果是使用 WFE 指令睡眠的,则由事件唤醒。 |
| 睡眠状态 | 内核时钟关闭,CPU 暂停,而外设正常运行,系统保留睡眠前的内核寄存器、内存的数据。软件表现为不再执行新代码。 |
| 唤醒延迟 | 无延迟。 |
| 唤醒后执行路径 | 若由中断唤醒,先进入中断,退出中断服务程序后,接着执行 WFI 指令后的程序;若由事件唤醒,直接接着执行 WFE 后的程序。 |
| [表1 - 睡眠模式的各种特性] |
3.3 停止模式 (Stop Mode)
停止模式是一种基于 Cortex-M3 内核 Deep Sleep深度睡眠机制的低功耗模式。在维持 1.8V 核心电源域供电的前提下,系统会关闭整个时钟树,包括HSI、HSE和PLL等主时钟源。此时 CPU 停止运行,所有依赖系统时钟的同步外设(如 USART 、定时器等)均停止工作。
而停止模式与待机模式的本质区别在于其核心电源域不断电,因此 SRAM 里的数据、寄存器内容以及 GPIO 引脚状态都能被完整保留。为了实现功耗的最小化,进入此模式前,应主动关闭ADC等模拟外设以进一步降低功耗。
进入停止模式,首先需要将系统控制寄存器中的 SLEEPDEEP 位置 1,以触发 Cortex-M3 内核的深度睡眠硬件时序。随后需清除所有 EXTI 挂起标志位。随后需对电源控制寄存器(PWR_CR)进行置位与清零操作:将掉电深度睡眠位(PDDS)硬件清零,以此在状态机分支中锁定停止模式的触发路径,同时配置低功耗深度睡眠控制位(LPDS)来决定内部电压调节器在休眠期间是保持正常模式还是切换至低功耗模式。最后执行__WFI或__WFE指令完成进入操作。
在停止模式下,系统只能被 EXTI 相关的异步事件唤醒,例如 GPIO 外部中断、RTC闹钟等。
- 若使用 WFI 指令睡眠,可使用任意 EXTI 线的中断进行唤醒;
- 若使用 WFE 指令睡眠,则需使用配置为事件模式的 EXTI 线事件进行唤醒。
唤醒后,硬件会自动开启内部 HSI 振荡器,并将其强制切换为系统当前的主时钟源,此时系统进入休眠前基于 HSE 和 PLL 的时钟配置随之失效。因此,唤醒后必须在软件代码中显式调用系统时钟初始化函数,重新配置时钟树,否则依赖原系统时钟频率的外设将出现工作异常。
其各种特性见下表:
| 特性 | 说明 |
|---|---|
| 进入条件 | 内核寄存器的 SLEEPDEEP = 1,PWR_CR 寄存器中的 PDDS = 0,然后调用 WFI 或 WFE 指令即可进入停止模式; PWR_CR 的 LPDS 位决定调压器处于正常或低功耗模式。 LPDS = 0 时,调压器工作在正常模式; LPDS = 1 时,工作在低功耗模式; |
| 唤醒方式 | 如果是使用 WFI 指令睡眠的,可使用任意 EXTI 线的中断唤醒; 如果是使用 WFE 指令睡眠的,可使用任意配置为事件模式的 EXTI 线事件唤醒。 |
| 睡眠状态 | CPU 及片上外设停止运行。系统保留停止前的内核寄存器和内存数据。 |
| 唤醒延迟 | 基础延迟为 HSI 振荡器的启动时间,若调压器工作在低功耗模式,还需要加上调压器从低功耗切换至正常模式下的时间。 |
| 唤醒后执行路径 | 若由中断唤醒,先执行中断程序,接着执行 WFI 指令后的程序; 若由事件唤醒,直接接着执行 WFE 后的程序。 唤醒后,STM32 会使用 HSI 作为系统时钟。 |
| [表2 - 停止模式的各种特性] |
3.4 待机模式 (Standby Mode)
待机模式是功耗最低的工作状态。其核心机制是关闭内部电压调节器,彻底切断 1.8V 核心供电域的供电。这一操作会导致位于该电源域内的 SRAM、CPU 及所有外设寄存器的数据全部丢失。
与停止模式采用的"关断时钟"策略不同,待机模式采用了"关断核心供电"的策略。总功耗由此降至微安级别,但系统同时丧失了数据保持能力。在此模式下,只有位于后备电源域的 RTC、备份寄存器以及少量的唤醒逻辑电路,在有独立电源(VDD 或后备电池 VBAT)供电时仍可维持工作。绝大部分 GPIO 引脚会自动进入高阻态,仅有外部复位引脚(NRST)、特定唤醒引脚(WKUP)等保留其功能。
进入待机模式的配置流程同样需要先将 SLEEPDEEP 位置 1。随后在电源控制寄存器(PWR_CR)中,将 PDDS 位置 1 以明确选择待机模式,并清除电源控制/状态寄存器(PWR_CSR)中的唤醒状态标志位 WUF。最后,执行执行__WFI或__WFE指令进入待机状态。
待机模式的唤醒源被严格限制为四种方式:
- WKUP 引脚检测到的上升沿信号;
- RTC 相关事件(包括闹钟、唤醒、引脚入侵或时间戳事件);
- NRST 引脚触发的外部复位;
- 独立看门狗(IWDG)触发的复位;
在硬件逻辑上,待机模式的唤醒等同于执行了一次完整的系统复位过程。唤醒后,CPU 将从微控制器的复位向量表起点开始重新执行程序,而不会继续执行进入休眠指令之后的代码。为在软件层面区分系统是经历了正常的上电复位还是从待机模式唤醒,程序可通过检查 PWR_CSR 寄存器中的待机标志位(SBF),或读取处于后备域的备份寄存器特征值来进行逻辑判定。
其各种特性见下表:
| 特性 | 说明 |
|---|---|
| 进入条件 | 内核寄存器的 SLEEPDEEP = 1,PWR_CR 寄存器中的 PDDS = 1,PWR_CR 寄存器中的唤醒状态位 WUF = 0,然后调用 WFI 或 WFE 指令即可进入待机模式; |
| 唤醒方式 | 通过 WKUP 引脚上升沿、RTC 事件、NRST 引脚外部复位或 IWDG 复位唤醒。 |
| 睡眠状态 | CPU 及片上外设停止运行。1.8V核心电源断电,内核寄存器及内存数据丢失。除复位引脚、RTC_AF1 及 WKUP 引脚外,其余 I/O 口进入高阻态。 |
| 唤醒延迟 | 等同于芯片完整的复位启动时间。 |
| 唤醒后执行路径 | 相当于芯片复位,在程序表现为从头开始执行代码。 |
| [表3 - 待机模式的各种特性] |
4. 本章节实验
4.1 实验一:修改系统主频(SYSCLK)
4.1.1 实验目标
-
掌握固件库配置逻辑:理解STM32F10x标准外设库中,系统时钟是如何通过system_stm32f10x.c文件中的宏定义进行配置的。
-
直观感知主频影响:通过修改宏定义,改变系统主频(SYSCLK),并观察同一段延时程序执行效果的变化,直观感受时钟频率对程序执行速度的决定性影响。
-
验证时钟配置生效:通过读取系统全局变量SystemCoreClock的值,从软件层面确认硬件主频是否已按预期完成切换。
4.1.2 实验原理
STM32的时钟树配置在标准外设库工程中,被高度封装在 system_stm32f10x.c 文件的SystemInit() 函数里。程序的执行流程如下:
-
启动调用:单片机上电后,在跳转至 main 函数之前,启动文件会首先调用 SystemInit()。这意味着在用户代码执行前,系统主频已经按预设配置运行完毕。
-
宏定义驱动:system_stm32f10x.c 文件的开头部分,通过一组互斥的宏定义(如SYSCLK_FREQ_72MHz, SYSCLK_FREQ_36MHz等)和条件编译(#if defined)逻辑,来决定SystemInit()内部会调用哪个具体的时钟配置函数(如 SetSysClockTo72MHz() 或SetSysClockTo36MHz())。

- 本实验原理:默认配置下,系统主频为 72MHz。本实验通过修改文件中的宏定义,激活SetSysClockTo36MHz() 函数,将系统主频切换至 36MHz。然后,我们在 main 函数的主循环中,使用 Delay_ms(500) 函数控制 OLED 屏幕上"Running"字符串以固定时间间隔闪烁。当主频改变后,由于Delay_ms函数的延时基准(SystemCoreClock)发生变化,肉眼将能直接观察到 "Running" 字符串的闪烁快慢发生显著变化。

4.1.3 硬件设计

4.1.4 软件设计
本实验通过修改底层的配置文件 system_stm32f10x.c ,实现系统主频从默认 72 MHz 降频至 36 MHz 的操作。
第一步:修改系统时钟配置文件
-
在工程中找到并打开 system_stm32f10x.c 文件。
-
定位到文件起始处的宏定义区域(约第110行附近)。
-
注释掉默认的72MHz主频定义,取消注释36MHz主频定义,操作如下:
cpp
#if defined (STM32F10X_LD_VL) || (defined STM32F10X_MD_VL) || (defined STM32F10X_HD_VL)
/* #define SYSCLK_FREQ_HSE HSE_VALUE */
#define SYSCLK_FREQ_24MHz 24000000
#else
/* #define SYSCLK_FREQ_HSE HSE_VALUE */
/* #define SYSCLK_FREQ_24MHz 24000000 */
#define SYSCLK_FREQ_36MHz 36000000 // <-- 取消此行的注释,选择36MHz主频
/* #define SYSCLK_FREQ_48MHz 48000000 */
/* #define SYSCLK_FREQ_56MHz 56000000 */
// #define SYSCLK_FREQ_72MHz 72000000 // <-- 注释掉此行,放弃72MHz主频
#endif
第二步:底层硬件时序的自动执行
完成上述宏定义修改后,当芯片复位启动,SystemInit() 函数会调用 SetSysClockTo36(),该函数内部将自动完成以下硬件时序:
-
开启HSE:置位 RCC->CR 寄存器中的 HSEON 位,启动 8 MHz 的外部高速晶振,并通过软件循环检测 HSERDY 标志位,直至时钟源完全稳定。
-
配置Flash等待周期:由于嵌入式闪存(Flash)的物理读取速度受限,当内核主频变化时,必须匹配适当的等待周期。当系统主频满足 24 MHz < SYSCLK ≤ 48 MHz 关系时,系统将 FLASH->ACR 寄存器的 LATENCY 字段配置为 0x01(即 1 个等待状态,1 WS),防止 CPU 取指速度超过 Flash 的物理响应上限。
-
分配总线时钟:配置 AHB 预分频器为不分频(HCLK = SYSCLK = 36 MHz);配置 APB2 预分频器为不分频(PCLK2 = 36 MHz);配置 APB1 预分频器为 2 分频(PCLK1 = 18 MHz),以确保低速总线 APB1 的物理频率不超过其 36 MHz 的硬件上限。
-
锁定PLL:选择 HSE 进行时钟源输入。根据底层源码逻辑,在 36 MHz 分支下,系统会配置配置寄存器将 HSE 进行 2 分频(即 8 MHz / 2 = 4 MHz),再使能锁相环配置倍频因子为 9(4 MHz × 9 = 36 MHz)。使能 PLLON 并等待 PLLRDY 标志位置位后,最终修改 RCC_CFGR 中的 SW 位,将系统时钟源切换为 PLL 输出,作为系统主频(SYSCLK=36MHz)。

第三步:应用层主程序编写
在 main.c 中调用 OLED 驱动接口,实时提取并验证时钟配置参数,同时利用Running视觉闪烁感知程序运行速率。
cpp
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
int main(void)
{
/* SystemInit 已在启动阶段根据宏定义完成时钟切换 */
OLED_Init();
// 在第一行显示当前系统主频变量的值,用于验证配置生效
OLED_ShowString(1, 1, "SYSCLK:");
OLED_ShowNum(1, 8, SystemCoreClock, 8); // 预期显示:03600000
while (1)
{
// 通过"Running"字符串的闪烁,直观感受主频变化对延时的直接影响
OLED_ShowString(2, 1, "Running");
Delay_ms(500); // 在主频改变后,该延时函数的真实耗时已发生变化
OLED_ShowString(2, 1, " ");
Delay_ms(500);
}
}
4.1.5 实验现象
在主程序 main() 中,编写一个基于空循环的软件延时函数 Delay(),并在主循环中持续翻转 LED 的电平状态。
-
高频状态验证:主频设定为 72 MHz 时,观察 LED 闪烁频率,记录其作为基准对照组。
-
降频状态验证:将主频降至 32 MHz,重新编译并烧录。在完全不修改 Delay() 循环计数值的条件下,可以观察到 LED 的闪烁周期发生显著延长(频率下降至原本的约 44%)。
该现象直观证明了 CPU 的指令取指速度与内核工作时钟频率呈严格的正相关。系统时钟频率越低,单条指令的物理执行周期越长,相应的动态功耗也越低。在随后的睡眠模式及停止模式实验中,对时钟树的管理将是降低系统功耗的最核心手段。
4.2 实验二:睡眠模式与串口中断唤醒
4.2.1 实验目标
- 掌握睡眠模式的进入方法:学会调用内核指令 __WFI() 使 Cortex-M3 内核进入睡眠状态,暂停CPU取指。
- 验证外设时钟独立性:理解在睡眠模式下,CPU时钟被关闭,但USART等外设总线时钟依然保持运行,外设可继续工作。
- 理解中断唤醒链路:深刻体会 USART 接收非空(RXNE)中断如何将系统从睡眠模式中唤醒,以及唤醒后程序的执行路径(先处理中断,再回到主循环)。
4.2.2 实验原理
之前发布的博客,链接如下:【江科大STM32学习笔记-09】USART串口协议 - 9.1 STM32 USART串口外设_stm32江科大-CSDN博客 在其中的《9-2 串口发送+接收》实验环节,我们实现了基于中断的串口数据收发:主程序通过不断轮询软件标志位 Serial_RxFlag 来获知新数据是否到达。这种架构下,即使没有数据到来,CPU 依然全速运行,不停查询,产生大量无意义的动态功耗。
而在电池供电的物联网设备中,上位机指令往往是随机下发的,中间可能存在长时间的静默期。针对这种"由外部异步事件驱动、无事件时无运算任务"的场景,睡眠模式是最佳选择。其核心机制是:
-
时钟门控:执行 __WFI() 后,硬件仅切断 Cortex-M3 内核的时钟(HCLK),CPU暂停运行,程序计数器(一个专用寄存器,其核心功能是指示当前指令的地址或下一条将要执行指令的地址)停止递增。但 1.8V 核心电源域仍正常供电,APB1/APB2 外设总线时钟继续保持。
-
外设独立工作:由于 USART 的时钟未被关闭,其波特率发生器和接收移位寄存器在 CPU 睡眠期间依然可以独立完成串行数据采样和字节接收。
-
中断唤醒:当完整的一个字节接收完毕后,USART 硬件置位 RXNE 标志,向 NVIC 发出中断请求。NVIC 收到请求后,会立即恢复 CPU 时钟,强制 CPU 退出睡眠,跳转到对应的中断服务函数 USART1_IRQHandler。中断返回后,程序将从 __WFI() 的下一条指令继续执行。
本实验在原有串口接收工程的基础上,于主循环中插入 __WFI() 指令,并通过串口打印信息来直观展示"进入睡眠 → 中断唤醒 → 继续执行"的完整过程。
4.2.3 硬件设计

4.2.4 软件设计
本实验的软件架构在《9-2 串口发送+接收》实现的 Serial 驱动模块基础上,对 main.c 主程序进行了面向低功耗的改造。Serial 驱动部分保持不变,其关键配置可简要概括为:
- USART1 时钟及 GPIO (PA9~TX, PA10~RX) 配置;
- 波特率 9600,8N1 帧格式,全双工模式;
- 使能 RXNE 接收中断,NVIC 中配置 USART1 中断通道;
- 中断服务函数中读取接收字节并置位软件标志 Serial_RxFlag;
- 提供 Serial_GetRxFlag()、Serial_GetRxData() 等接口函数供主程序调用;
主程序在此基础上引入了睡眠模式,其逻辑流程分为三个阶段:中断响应处理 、OLED 运行指示 和 睡眠进入与唤醒。
-
中断响应处理:主循环开始后,首先调用 Serial_GetRxFlag() 判断是否有新数据到达。若标志为 1,则读取数据字节到 RxData,并通过 Serial_SendByte(RxData) 回传给上位机,同时在 OLED 第一行显示该数据。
-
OLED 运行指示:无论是否有数据更新,程序都会在 OLED 第二行延时 100ms显示一次 "Running" 字符串。这一"闪烁"效果直观表明了主循环仍处于活跃状态,是实验者可以直接观察到的运行节律。
-
睡眠进入与唤醒:OLED 闪烁一次 "Running" 后,程序执行__WFI() 指令。此时 CPU 内核时钟被硬件门控切断,系统进入睡眠模式,程序计数器停止,主循环暂停。但 USART 外设时钟依然运行,可独立完成串行数据的接收。
当上位机通过串口下发任意字节时,USART 完成接收后触发 RXNE 中断,NVIC 立即恢复 CPU 时钟,强制唤醒内核并跳转至中断服务函数。中断服务函数内会读取数据寄存器并置位 Serial_RxFlag,然后返回。唤醒后程序从 __WFI() 后的下一条指令(即 while 循环开头)继续执行,第一个动作便是检查 Serial_RxFlag,从而无缝处理新接收到的数据。
主程序核心代码 (main.c)
cpp
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
uint8_t RxData; //定义用于接收串口数据的变量
int main(void)
{
OLED_Init(); //OLED初始化
OLED_ShowString(1, 1, "RxData:"); //显示静态字符串
Serial_Init(); //串口初始化
while (1)
{
/* 检查并处理接收数据 */
if (Serial_GetRxFlag() == 1)
{
RxData = Serial_GetRxData(); //获取串口接收的数据
Serial_SendByte(RxData); //回传接收数据,用于验证全双工链路
OLED_ShowHexNum(1, 8, RxData, 2); //在OLED上显示接收的十六进制数据
}
/* OLED显示"Running"闪烁,指示主循环正在运行 */
OLED_ShowString(2, 1, "Running");
Delay_ms(100);
OLED_ShowString(2, 1, " ");
Delay_ms(100);
/* 执行WFI指令,CPU进入睡眠模式,等待中断唤醒 */
__WFI();
}
}
中断服务函数 (位于 stm32f10x_it.c)
cpp
void USART1_IRQHandler(void)
{
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
Serial_RxData = USART_ReceiveData(USART1); // 读取数据,自动清除RXNE标志
Serial_RxFlag = 1; // 置位软件标志,通知主程序
USART_ClearITPendingBit(USART1, USART_IT_RXNE); // 冗余清除,确保可靠
}
}
串口驱动(Serial.c)
cpp
#include "stm32f10x.h" // Device header
#include <stdio.h>
#include <stdarg.h>
uint8_t Serial_RxData; //定义串口接收的数据变量
uint8_t Serial_RxFlag; //定义串口接收的标志位变量
/**
* 函 数:串口初始化
* 参 数:无
* 返 回 值:无
*/
void Serial_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //开启USART1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA9引脚初始化为复用推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA10引脚初始化为上拉输入
/*USART初始化*/
USART_InitTypeDef USART_InitStructure; //定义结构体变量
USART_InitStructure.USART_BaudRate = 9600; //波特率
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控制,不需要
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; //模式,发送模式和接收模式均选择
USART_InitStructure.USART_Parity = USART_Parity_No; //奇偶校验,不需要
USART_InitStructure.USART_StopBits = USART_StopBits_1; //停止位,选择1位
USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字长,选择8位
USART_Init(USART1, &USART_InitStructure); //将结构体变量交给USART_Init,配置USART1
/*中断输出配置*/
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //开启串口接收数据的中断
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; //选择配置NVIC的USART1线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定NVIC线路的抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
/*USART使能*/
USART_Cmd(USART1, ENABLE); //使能USART1,串口开始运行
}
/**
* 函 数:串口发送一个字节
* 参 数:Byte 要发送的一个字节
* 返 回 值:无
*/
void Serial_SendByte(uint8_t Byte)
{
USART_SendData(USART1, Byte); //将字节数据写入数据寄存器,写入后USART自动生成时序波形
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); //等待发送完成
/*下次写入数据寄存器会自动清除发送完成标志位,故此循环后,无需清除标志位*/
}
/**
* 函 数:串口发送一个数组
* 参 数:Array 要发送数组的首地址
* 参 数:Length 要发送数组的长度
* 返 回 值:无
*/
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
uint16_t i;
for (i = 0; i < Length; i ++) //遍历数组
{
Serial_SendByte(Array[i]); //依次调用Serial_SendByte发送每个字节数据
}
}
/**
* 函 数:串口发送一个字符串
* 参 数:String 要发送字符串的首地址
* 返 回 值:无
*/
void Serial_SendString(char *String)
{
uint8_t i;
for (i = 0; String[i] != '\0'; i ++)//遍历字符数组(字符串),遇到字符串结束标志位后停止
{
Serial_SendByte(String[i]); //依次调用Serial_SendByte发送每个字节数据
}
}
/**
* 函 数:次方函数(内部使用)
* 返 回 值:返回值等于X的Y次方
*/
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1; //设置结果初值为1
while (Y --) //执行Y次
{
Result *= X; //将X累乘到结果
}
return Result;
}
/**
* 函 数:串口发送数字
* 参 数:Number 要发送的数字,范围:0~4294967295
* 参 数:Length 要发送数字的长度,范围:0~10
* 返 回 值:无
*/
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i ++) //根据数字长度遍历数字的每一位
{
Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0'); //依次调用Serial_SendByte发送每位数字
}
}
/**
* 函 数:使用printf需要重定向的底层函数
* 参 数:保持原始格式即可,无需变动
* 返 回 值:保持原始格式即可,无需变动
*/
int fputc(int ch, FILE *f)
{
Serial_SendByte(ch); //将printf的底层重定向到自己的发送字节函数
return ch;
}
/**
* 函 数:自己封装的prinf函数
* 参 数:format 格式化字符串
* 参 数:... 可变的参数列表
* 返 回 值:无
*/
void Serial_Printf(char *format, ...)
{
char String[100]; //定义字符数组
va_list arg; //定义可变参数列表数据类型的变量arg
va_start(arg, format); //从format开始,接收参数列表到arg变量
vsprintf(String, format, arg); //使用vsprintf打印格式化字符串和参数列表到字符数组中
va_end(arg); //结束变量arg
Serial_SendString(String); //串口发送字符数组(字符串)
}
/**
* 函 数:获取串口接收标志位
* 参 数:无
* 返 回 值:串口接收标志位,范围:0~1,接收到数据后,标志位置1,读取后标志位自动清零
*/
uint8_t Serial_GetRxFlag(void)
{
if (Serial_RxFlag == 1) //如果标志位为1
{
Serial_RxFlag = 0;
return 1; //则返回1,并自动清零标志位
}
return 0; //如果标志位为0,则返回0
}
/**
* 函 数:获取串口接收的数据
* 参 数:无
* 返 回 值:接收的数据,范围:0~255
*/
uint8_t Serial_GetRxData(void)
{
return Serial_RxData; //返回接收的数据变量
}
/**
* 函 数:USART1中断函数
* 参 数:无
* 返 回 值:无
* 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
* 函数名为预留的指定名称,可以从启动文件复制
* 请确保函数名正确,不能有任何差异,否则中断函数将不能进入
*/
void USART1_IRQHandler(void)
{
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) //判断是否是USART1的接收事件触发的中断
{
Serial_RxData = USART_ReceiveData(USART1); //读取数据寄存器,存放在接收的数据变量
Serial_RxFlag = 1; //置接收标志位变量为1
USART_ClearITPendingBit(USART1, USART_IT_RXNE); //清除USART1的RXNE标志位
//读取数据寄存器会自动清除此标志位
//如果已经读取了数据寄存器,也可以不执行此代码
}
}
调试注意事项
在内核时钟被切断期间,依赖内核总线同步的 SWD 或 JTAG 调试接口将丧失物理同步能力,导致 IDE 下载报错。标准的工程规避操作为:按住硬件复位键(NRST)不放 -> 点击 IDE 的下载按钮 -> 在调试器尝试建立连接的瞬间释放复位键。
4.2.5 实验现象
程序烧录后,开发板上电运行,现象按时间顺序如下:
1. 上电初始化,短暂闪烁后进入睡眠
- 系统复位启动,OLED 屏幕第一行立即显示 RxData:,提示进入待接收状态。
- 第二行先显示 Running 约 100ms,随即被清空(显示空白)约 100ms。这一亮一灭构成一次完整的闪烁,表明主循环已完整运行了一遍。
- 紧接着,系统执行 __WFI() 指令,CPU 进入睡眠模式。此时 OLED 屏幕第一行仍保持 RxData: 字样,第二行保持空白,整个屏幕画面完全静止,不再有任何刷新变化,直观反映出 CPU 已停止工作,系统进入低功耗待机状态。
2. 串口数据唤醒,处理并再次闪烁后睡眠
-
在 PC 端串口助手发送一个十六进制字节(例如 0xA5)。
-
USART 外设接收完毕后,立即通过 RXNE 中断唤醒 CPU。唤醒后系统经历以下动作:
-
主循环检测到 Serial_RxFlag == 1,读取接收数据 0xA5。
-
通过 Serial_SendByte 向上位机回传接收到的数据 A5,验证全双工链路。
-
OLED 第一行的 RxData: 右侧随即更新显示为 A5,确认新数据已被成功接收和处理。
-
紧接着,第二行闪烁一次 Running 之后,再次清空。
-
-
上述步骤完成后,系统再次执行 __WFI() 进入睡眠,OLED 画面恢复静止(第一行显示 RxData:A5,第二行空白),等待下一次数据唤醒。
3. 循环往复
- 之后每次通过串口发送新数据,OLED 上的接收值都会更新,第二行都会出现一次 Running 闪烁,然后系统立即重新进入睡眠。若无数据发送,屏幕始终保持静止状态,系统一直处于低功耗睡眠中。
本实验清晰地展示了睡眠模式"一次唤醒、一次处理、一次闪烁、立即休眠"的间歇工作模型。__WFI 指令使 CPU 在空闲时处于睡眠,仅在外部事件(串口接收)到来时才被唤醒并短暂工作,完成任务后再次进入睡眠。这种机制在维持串口实时监听的条件下,最大限度削减了 CPU 空转功耗。
4.3 实验三:停止模式与对射式红外传感器计次
4.3.1 实验目标
-
掌握停止模式的进入与唤醒机制:学会调用 PWR_EnterSTOPMode() 使系统进入深度睡眠,并理解 EXTI 外部中断作为唤醒源的配置方法。
-
验证时钟停振与数据保持特性:确认在停止模式下所有时钟源(HSE、HSI、PLL)均被关闭,但 SRAM 及外设寄存器数据依然保留,唤醒后计数变量不会丢失。
-
理解唤醒后时钟回退现象:深刻认识到从停止模式唤醒后,系统主频会自动回退为 HSI(8MHz),必须显式调用 SystemInit() 恢复时钟树,否则外设工作将异常。
-
明确外部中断的异步特性:掌握 EXTI 边沿检测电路在无系统时钟的情况下仍能独立工作的原理。
4.3.2 实验原理
在博客:【江科大STM32学习笔记-05】EXTI外部中断-CSDN博客的实验《5-1 对射式红外传感器计次》中,我们实现了基于 EXTI 外部中断的传感器计数功能:系统全速运行,CPU 持续刷新 OLED 显示。但在电池供电的实际应用中,传感器触发频率可能极低(如每小时只触发数次),让 CPU 始终全速运转将产生极大的无效功耗。
针对此类"由稀疏外部事件驱动"的场景,停止模式是兼顾极低功耗与数据现场保持的理想选择。其核心工作机制如下:
-
时钟树全面停振:进入停止模式后,系统会关闭 HSE、HSI 和 PLL 等所有时钟源,CPU、定时器、串口等所有依赖时钟的同步外设均停止工作。
-
核心域维持供电,数据不丢失 :与待机模式不同,停止模式不切断内部电压调节器,1.8V 核心域继续保有微弱供电,因此 SRAM 中的所有变量和外设寄存器的内容都能完整保留。这正是停止模式能够保存计数值、唤醒后从断点继续运行的根本原因。
-
EXTI 异步唤醒 :虽然时钟已停振,但 EXTI 模块的边沿检测电路是纯硬件异步逻辑,其工作不依赖任何系统时钟。只要 VDD 区域仍有供电,EXTI 就能持续监测引脚电平跳变,并在满足触发条件时向 NVIC 发出唤醒请求,强制恢复内部时钟振荡器,使系统退出停止模式。
-
唤醒后时钟回退与重构:系统被唤醒后,硬件会自动开启 HSI 振荡器(8MHz)并将其作为系统主时钟,而非恢复进入停止模式前的高频配置。因此,唤醒后必须在软件中立即调用 SystemInit() 重新配置 HSE 和 PLL,将系统主频恢复至额定值(如 72MHz),否则所有依赖时钟频率的外设(如 Delay_ms、OLED 刷新)都会出现时序错乱。
本实验在原有红外计次工程的基础上,于主循环中加入 PWR_EnterSTOPMode(),使系统在完成一次显示刷新后立即进入停止模式,等待传感器遮挡信号唤醒,从而构造出"触发一次、唤醒一次、计数一次、显示一次、再次睡眠"的低功耗工作模式。
特别说明:外部中断不需要时钟
EXTI 的边沿检测电路是由纯组合逻辑和异步触发器构成的,它不依赖 APB 总线时钟(PCLK)。在停止模式下,尽管 APB2 时钟已停止,EXIT 模块的寄存器不能被访问,但其已配置好的触发逻辑仍然处于激活状态,能持续监测 GPIO 引脚的电平变化。因此,外部中断能够作为停止模式的可靠唤醒源。
4.3.3 硬件设计

4.3.4 软件设计
本实验的软件在原有 CountSensor 驱动(负责传感器初始化、EXTI 配置、中断服务函数)的基础上,对 main.c 进行了面向停止模式的改造。
(1)传感器驱动层(CountSensor.c)
该部分沿用《5-1》实验的配置,核心逻辑概括如下:
- 使能 GPIOB 与 AFIO 时钟
- 将 PB14 配置为上拉输入
- 通过 GPIO_EXTILineConfig 将 PB14 映射到 EXTI14
- 配置 EXTI14 为下降沿触发中断模式
- 在 NVIC 中使能 EXTI15_10 中断通道
- 中断服务函数 EXTI15_10_IRQHandler 中判断 EXTI14 触发,令全局变量 CountSensor_Count 自增,并清除挂起标志
这些底层配置无需修改,可直接支撑停止模式的唤醒链路。
2. 主程序逻辑(main.c)
主程序引入了 PWR 电源控制模块和停止模式进入函数,其运行流程如下:
-
初始化:完成 OLED 和计数传感器的初始化。关键一步:调用 RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE) 使能 PWR 外设时钟,这是进入停止模式的前置条件。
-
数据显示:在 OLED 第一行显示 Count: 及当前计数值。
-
运行指示:在 OLED 第二行短暂显示 "Running" 并保持 100ms,然后清空并延时 100ms,构成一次可见的闪烁,表示主循环正在工作。
-
进入停止模式:调用 PWR_EnterSTOPMode(PWR_Regulator_ON, PWR_STOPEntry_WFI)。该函数底层会置位内核 SLEEPDEEP 位,执行 __WFI 指令,最终使系统时钟停振,CPU 停止运行,进入深度低功耗状态。参数 PWR_Regulator_ON 使电压调节器保持在正常模式,以简化唤醒后时序(可换为低功耗模式以进一步省电)。
-
唤醒与恢复:当传感器被遮挡并移开时,PB14 产生下降沿,EXTI14 触发中断。NVIC 唤醒 CPU,先执行 EXTI15_10_IRQHandler 使计数值加 1。中断返回后,程序从 PWR_EnterSTOPMode() 之后的第一条语句 SystemInit() 开始继续执行。SystemInit() 会重新启动 HSE 并锁定 PLL,将系统主频恢复到 72MHz,否则系统仍处于唤醒后的默认 HSI(8MHz),后续的 Delay_ms 和 OLED 刷新都会严重变慢。
-
循环:时钟恢复后,while 循环回到开头,OLED 更新计数值并再次闪烁 Running,然后再次进入停止模式,如此往复。
- 主程序逻辑 (main.c)
cpp
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#include "CountSensor.h"
int main(void)
{
/* 模块初始化 */
OLED_Init();
CountSensor_Init(); // 计数传感器(含EXTI/NVIC)初始化
/* 必须使能PWR时钟,否则无法进入停止模式 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
/* 显示静态字符串 */
OLED_ShowString(1, 1, "Count:");
while (1)
{
/* 显示最新计数值 */
OLED_ShowNum(1, 7, CountSensor_Get(), 5);
/* 运行指示:显示Running约100ms后清除 */
OLED_ShowString(2, 1, "Running");
Delay_ms(100);
OLED_ShowString(2, 1, " ");
Delay_ms(100);
/* 进入停止模式,等待EXTI中断唤醒
* PWR_Regulator_ON: 强制电压调节器进入微功耗运行状态以最小化静态漏电流
* PWR_STOPEntry_WFI: 明确指定内核的唤醒机制为等待硬件中断 (WFI)
*/
PWR_EnterSTOPMode(PWR_Regulator_ON, PWR_STOPEntry_WFI);
/* --------------------------------------------------------------------
* 物理唤醒点:EXTI 中断 ISR 执行完毕后,内核硬件自动退出 Deep Sleep 状态并恢复上下文,程序指针(PC)回到此处继续运行。
* 警告:此时 SYSCLK 已被硬件回退为内部 HSI (8 MHz),必须立即重构时钟树。
* -------------------------------------------------------------------- */
SystemInit();
}
}
- 传感器驱动逻辑 (CountSensor.c)
cpp
#include "stm32f10x.h" // Device header
uint16_t CountSensor_Count; //全局变量,用于计数
/**
* 函 数:计数传感器初始化
* 参 数:无
* 返 回 值:无
*/
void CountSensor_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); //开启AFIO的时钟,外部中断必须开启AFIO的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); //将PB14引脚初始化为上拉输入
/*AFIO选择中断引脚*/
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);//将外部中断的14号线映射到GPIOB,即选择PB14为外部中断引脚
/*EXTI初始化*/
EXTI_InitTypeDef EXTI_InitStructure; //定义结构体变量
EXTI_InitStructure.EXTI_Line = EXTI_Line14; //选择配置外部中断的14号线
EXTI_InitStructure.EXTI_LineCmd = ENABLE; //指定外部中断线使能
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //指定外部中断线为中断模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; //指定外部中断线为下降沿触发
EXTI_Init(&EXTI_InitStructure); //将结构体变量交给EXTI_Init,配置EXTI外设
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn; //选择配置NVIC的EXTI15_10线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定NVIC线路的抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
}
/**
* 函 数:获取计数传感器的计数值
* 参 数:无
* 返 回 值:计数值,范围:0~65535
*/
uint16_t CountSensor_Get(void)
{
return CountSensor_Count;
}
- 中断服务函数(CountSensor.c 中)
cpp
/**
* 函 数:EXTI15_10外部中断函数
* 参 数:无
* 返 回 值:无
* 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
* 函数名为预留的指定名称,可以从启动文件复制
* 请确保函数名正确,不能有任何差异,否则中断函数将不能进入
*/
void EXTI15_10_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line14) == SET) //判断是否是外部中断14号线触发的中断
{
/*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_14) == 0)
{
CountSensor_Count ++; //计数值自增一次
}
EXTI_ClearITPendingBit(EXTI_Line14); //清除外部中断14号线的中断标志位
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}
}
4.3.5 实验现象
-
上电显示与首次睡眠:系统上电后,OLED 第一行显示 Count:0,第二行短暂显示 Running 后清空。随后系统进入停止模式,此时所有时钟停振,OLED 画面静止,MCU 功耗骤降至微安级。
-
遮挡唤醒与计数更新:用挡光片遮挡并移开红外传感器光路,触发 PB14 下降沿。唤醒后,CPU 先执行中断服务函数将 CountSensor_Count 加 1,然后返回主循环,重新执行 SystemInit() 恢复 72MHz 主频。OLED 随即刷新,第一行计数值更新(如变为 Count:1),第二行再次出现一次 Running 闪烁。闪烁结束后,系统再次进入停止模式。
-
重复触发:此后每遮挡一次,计数值递增一次,Running 闪烁一次,然后系统立即重回静默的低功耗睡眠状态。若长时间无遮挡,OLED 屏幕始终定格在最后一次显示的计数值,系统一直保持极低功耗。
验证:SystemInit() 的必要性
若在主循环中注释掉 SystemInit(); 并重新烧录,唤醒后 OLED 刷新和 Delay_ms 的节奏会显著变慢(约 9 倍),串口等外设也无法正常工作。这确凿地证明了停止模式唤醒后系统时钟已回退至 8MHz HSI,必须软件重构时钟树才能恢复预期性能。
本实验成功展示了停止模式在外部中断驱动下的间歇工作模型。通过 EXTI 的异步监测能力,系统在绝大多数时间处于深度睡眠,仅在真实事件发生时被唤醒并完成任务,随后立即再次休眠,极大压缩了无效功耗,同时完整保留了计数状态,是电池供电传感器节点的典型低功耗设计方案。
4.4 实验四:待机模式与实时时钟唤醒
4.4.1 实验目标
-
掌握待机模式的进入与唤醒机制:学会调用 PWR_EnterSTANDBYMode() 使系统进入最低功耗状态,并理解 RTC 闹钟事件和 WKUP 引脚上升沿作为唤醒源的配置方法。
-
验证待机模式的核心特性:深刻理解待机模式切断 1.8V 核心域供电后,SRAM 及所有外设寄存器数据全部丢失的物理本质。
-
理解唤醒即复位的执行逻辑:认识到待机模式唤醒后,程序并非从进入点继续运行,而是触发完整的系统复位,从头执行 main 函数。
-
体会备份域独立供电的优势:通过 RTC 在待机期间继续走时且 CNT 值不丢失的现象,验证后备电源域(VBAT)独立于核心域的供电机制。
4.4.2 实验原理
在博客:【STM32学习笔记-12】Unix 时间戳、BKP 备份寄存器与 RTC 实时时钟-CSDN博客的实验《12-2 实时时钟》实验中,我们实现了 RTC 的初始化和时间显示。本实验在此基础上引入待机模式,以实现周期性自动唤醒的数据采集或状态上报等低功耗应用场景。
待机模式是 STM32 三种低功耗模式中功耗最低的一种,其核心机制如下:
-
彻底切断核心供电 :进入待机模式后,内部电压调节器被关闭,1.8V 核心电源域彻底断电。这意味着CPU、SRAM、以及绝大多数外设寄存器的数据全部丢失,程序运行的任何中间状态(局部变量、全局变量、外设配置)都无法保留。
-
仅后备域维持工作:位于后备域(Backup Domain)的 RTC 实时时钟、BKP 备份寄存器和少数唤醒逻辑电路,在 VBAT 引脚有独立供电的条件下仍可继续运行。这正是 RTC 能在待机期间持续走时的根本原因。
-
唤醒等同于复位 :待机模式的唤醒源被严格限制为以下四种:WKUP 引脚上升沿、RTC 闹钟事件、NRST 引脚外部复位、IWDG 独立看门狗复位。一旦被唤醒,系统执行的是完整的上电复位序列,包括重新启动 HSI、调用 SystemInit() 初始化时钟树,然后从复位向量表跳转到 main 函数重新开始执行。因此,PWR_EnterSTANDBYMode() 之后的任何代码都永远不会被执行。
-
自动时钟恢复:由于唤醒即复位,启动文件会在进入 main 之前自动调用 SystemInit() ,无需像停止模式那样在唤醒后手动重构时钟树。
RTC外设结构框图
本实验设定 RTC 闹钟为当前时间后 10 秒,同时使能 WKUP 引脚(PA0)作为备选唤醒源,参考上图所示。系统进入待机模式后,OLED 熄屏模拟关闭所有外部负载,每隔 10 秒由 RTC 闹钟自动唤醒,或由 WKUP 引脚上升沿手动唤醒。唤醒后程序重头执行,OLED 重新显示当前计数器和闹钟值,闪烁一次 Running 和 STANDBY 后再次进入待机。
4.4.3 硬件设计
本实验硬件连接在《12-2 实时时钟》基础上,增加 WKUP 引脚的预备接线:
-
VBAT 备用电源 --- 通过 ST-Link 的 3.3V 连接至开发板 VBAT 引脚(若无纽扣电池),确保待机期间 RTC 持续走时。
-
WKUP 引脚(PA0) --- 已由 PWR_WakeUpPinCmd(ENABLE) 在内部强制配置为下拉输入模式,无需额外对 PA0 进行 GPIO 初始化。实验时可用导线将 PA0 短暂触碰 3.3V,模拟上升沿唤醒。

4.4.4 软件设计
本实验的软件在 MyRTC 驱动模块(负责 RTC 初始化、时间读写、特征码检测)基础上,对 main.c 进行了面向待机模式的改造。
1. RTC 驱动层(MyRTC.c)
该部分沿用《12-2》实验的完整实现,核心功能概括如下:
-
使能 PWR 和 BKP 的 APB1 时钟,解除后备域写保护
-
通过 BKP_DR1 特征码(0xA5A5)判断 RTC 是否已完成初始化
-
首次上电时:配置 LSE 为 RTC 时钟源,设置 32767 预分频产生 1Hz 秒脉冲,写入初始时间,最后将特征码存入 BKP_DR1
-
非首次上电时:直接等待寄存器同步,保留原有走时
-
提供 MyRTC_ReadTime() 和 MyRTC_SetTime() 实现 UTC 时间戳与北京时间的双向转换
2. 主程序逻辑(main.c)
主程序引入了待机模式相关配置,其完整运行流程如下:
**(1)模块初始化:**调用 OLED_Init() 和 MyRTC_Init() 完成显示和时钟初始化。MyRTC_Init() 内部已开启 PWR 时钟并解除后备域写保护,但为确保代码独立性和可读性,main 中再次调用 RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE) 显式开启 PWR 时钟。
**(2)显示静态标签:**在 OLED 前三行分别显示 CNT :、ALR :、ALRF:,为后续动态数值刷新预留位置。
**(3)使能 WKUP 引脚:**调用 PWR_WakeUpPinCmd(ENABLE) 使能 PA0 引脚的 WKUP 唤醒功能。此函数内部会自动将 PA0 强制配置为下拉输入模式(引脚悬空就是低电平),因此无需再手动对 PA0 进行 GPIO 初始化。当 PA0 出现上升沿(如用导线触碰 3.3V)时,硬件将触发待机唤醒。
**(4)设定 RTC 闹钟:**读取当前 RTC 计数器值 RTC_GetCounter(),加上 10 秒偏移量作为闹钟目标时间,通过 RTC_SetAlarm(Alarm) 写入闹钟寄存器 RTC_ALR,并将闹钟值显示在 OLED 第二行。当 RTC_CNT 与 RTC_ALR 匹配时,硬件产生闹钟信号,触发待机唤醒。
**(5)进入主循环:**while 循环中依次执行以下操作:
-
动态数据显示:刷新 OLED 第一行的 RTC_CNT 当前计数值,第三行的闹钟标志位 ALRF(唤醒后首次显示时为 1,表明闹钟已触发)。
-
运行指示闪烁:在第四行左侧显示 Running 约 100ms 后清空,表明当前处于活跃状态。
-
待机指示闪烁:在第四行右侧显示 STANDBY 约 1000ms 后清空,提示即将进入待机模式,给观察者预留反应时间。
-
清屏并进入待机:调用 OLED_Clear() 清除 OLED 所有显示内容,模拟关闭所有外部耗电负载。随后调用 PWR_EnterSTANDBYMode() 进入待机模式,1.8V 核心域断电,系统进入最低功耗状态。
**(6)唤醒后行为:**当 RTC 闹钟触发(10 秒后)或 PA0 检测到上升沿时,系统执行上电复位序列,程序从 main 函数开头重新执行。这意味着:
-
SystemInit() 在启动阶段自动被调用,无需手动恢复时钟。
-
所有外设(OLED、RTC 等)重新初始化。
-
while 循环再次执行,显示最新数据、闪烁指示、清屏、进入待机......形成周期性循环。
-
PWR_EnterSTANDBYMode() 之后的代码永远没有机会执行。
主程序逻辑 (main.c):
cpp
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#include "MyRTC.h"
int main(void)
{
/* 模块初始化 */
OLED_Init();
MyRTC_Init(); // RTC初始化(内部已开启PWR/BKP时钟、解除后备域写保护)
/* 显式开启PWR时钟,保持代码独立性 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
/* 显示静态字符串 */
OLED_ShowString(1, 1, "CNT :");
OLED_ShowString(2, 1, "ALR :");
OLED_ShowString(3, 1, "ALRF:");
/* 使能WKUP引脚(PA0),内部自动配置为下拉输入,上升沿唤醒待机模式 */
PWR_WakeUpPinCmd(ENABLE);
/* 设定RTC闹钟为当前时间后10秒 */
uint32_t Alarm = RTC_GetCounter() + 10;
RTC_SetAlarm(Alarm);
OLED_ShowNum(2, 6, Alarm, 10); // 显示闹钟目标值
while (1)
{
/* 刷新当前计数值和闹钟标志位 */
OLED_ShowNum(1, 6, RTC_GetCounter(), 10);
OLED_ShowNum(3, 6, RTC_GetFlagStatus(RTC_FLAG_ALR), 1);
/* 闪烁Running,指示当前主循环正在运行 */
OLED_ShowString(4, 1, "Running");
Delay_ms(100);
OLED_ShowString(4, 1, " ");
Delay_ms(100);
/* 闪烁STANDBY,提示即将进入待机模式 */
OLED_ShowString(4, 9, "STANDBY");
Delay_ms(1000);
OLED_ShowString(4, 9, " ");
Delay_ms(100);
/* 清屏,模拟关闭所有外部耗电设备 */
OLED_Clear();
/* 进入待机模式,等待WKUP上升沿或RTC闹钟唤醒 */
PWR_EnterSTANDBYMode();
/*
* 注意:待机模式唤醒后程序从头开始执行,
* 以下代码永远不会被执行到。
*/
}
}
RTC 驱动层(MyRTC.c)
cpp
#include "stm32f10x.h" // Device header
#include <time.h>
uint16_t MyRTC_Time[] = {2023, 1, 1, 23, 59, 55}; //定义全局的时间数组,数组内容分别为年、月、日、时、分、秒
void MyRTC_SetTime(void); //函数声明
/**
* 函 数:RTC初始化
* 参 数:无
* 返 回 值:无
*/
void MyRTC_Init(void)
{
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); //开启PWR的时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE); //开启BKP的时钟
/*备份寄存器访问使能*/
PWR_BackupAccessCmd(ENABLE); //使用PWR开启对备份寄存器的访问
if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5) //通过写入备份寄存器的标志位,判断RTC是否是第一次配置
//if成立则执行第一次的RTC配置
{
RCC_LSEConfig(RCC_LSE_ON); //开启LSE时钟
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET); //等待LSE准备就绪
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //选择RTCCLK来源为LSE
RCC_RTCCLKCmd(ENABLE); //RTCCLK使能
RTC_WaitForSynchro(); //等待同步
RTC_WaitForLastTask(); //等待上一次操作完成
RTC_SetPrescaler(32768 - 1); //设置RTC预分频器,预分频后的计数频率为1Hz
RTC_WaitForLastTask(); //等待上一次操作完成
MyRTC_SetTime(); //设置时间,调用此函数,全局数组里时间值刷新到RTC硬件电路
BKP_WriteBackupRegister(BKP_DR1, 0xA5A5); //在备份寄存器写入自己规定的标志位,用于判断RTC是不是第一次执行配置
}
else //RTC不是第一次配置
{
RTC_WaitForSynchro(); //等待同步
RTC_WaitForLastTask(); //等待上一次操作完成
}
}
//如果LSE无法起振导致程序卡死在初始化函数中
//可将初始化函数替换为下述代码,使用LSI当作RTCCLK
//LSI无法由备用电源供电,故主电源掉电时,RTC走时会暂停
/*
void MyRTC_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
PWR_BackupAccessCmd(ENABLE);
if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5)
{
RCC_LSICmd(ENABLE);
while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) != SET);
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);
RCC_RTCCLKCmd(ENABLE);
RTC_WaitForSynchro();
RTC_WaitForLastTask();
RTC_SetPrescaler(40000 - 1);
RTC_WaitForLastTask();
MyRTC_SetTime();
BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);
}
else
{
RCC_LSICmd(ENABLE); //即使不是第一次配置,也需要再次开启LSI时钟
while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) != SET);
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);
RCC_RTCCLKCmd(ENABLE);
RTC_WaitForSynchro();
RTC_WaitForLastTask();
}
}*/
/**
* 函 数:RTC设置时间
* 参 数:无
* 返 回 值:无
* 说 明:调用此函数后,全局数组里时间值将刷新到RTC硬件电路
*/
void MyRTC_SetTime(void)
{
time_t time_cnt; //定义秒计数器数据类型
struct tm time_date; //定义日期时间数据类型
time_date.tm_year = MyRTC_Time[0] - 1900; //将数组的时间赋值给日期时间结构体
time_date.tm_mon = MyRTC_Time[1] - 1;
time_date.tm_mday = MyRTC_Time[2];
time_date.tm_hour = MyRTC_Time[3];
time_date.tm_min = MyRTC_Time[4];
time_date.tm_sec = MyRTC_Time[5];
time_cnt = mktime(&time_date) - 8 * 60 * 60; //调用mktime函数,将日期时间转换为秒计数器格式
//- 8 * 60 * 60为东八区的时区调整
RTC_SetCounter(time_cnt); //将秒计数器写入到RTC的CNT中
RTC_WaitForLastTask(); //等待上一次操作完成
}
/**
* 函 数:RTC读取时间
* 参 数:无
* 返 回 值:无
* 说 明:调用此函数后,RTC硬件电路里时间值将刷新到全局数组
*/
void MyRTC_ReadTime(void)
{
time_t time_cnt; //定义秒计数器数据类型
struct tm time_date; //定义日期时间数据类型
time_cnt = RTC_GetCounter() + 8 * 60 * 60; //读取RTC的CNT,获取当前的秒计数器
//+ 8 * 60 * 60为东八区的时区调整
time_date = *localtime(&time_cnt); //使用localtime函数,将秒计数器转换为日期时间格式
MyRTC_Time[0] = time_date.tm_year + 1900; //将日期时间结构体赋值给数组的时间
MyRTC_Time[1] = time_date.tm_mon + 1;
MyRTC_Time[2] = time_date.tm_mday;
MyRTC_Time[3] = time_date.tm_hour;
MyRTC_Time[4] = time_date.tm_min;
MyRTC_Time[5] = time_date.tm_sec;
}
4.4.5 实验现象
-
首次上电显示与进入待机:系统上电后,OLED 前三行分别显示 CNT : 当前计数值、ALR : 闹钟目标值、ALRF: 闹钟标志位状态。第四行先后闪烁 Running(短暂)和 STANDBY(约 1 秒),随后 OLED 屏幕被清空,系统进入待机模式,整机功耗骤降至约 3μA。
-
RTC 闹钟定时唤醒:约 10 秒后,RTC 内部计数器 CNT 累加至与 ALR 设定值相等,硬件闹钟事件触发,系统被唤醒并执行上电复位。OLED 重新点亮,第一行显示的 CNT 值已比进入待机前增加了约 10,完美验证了 RTC 在待机断电期间依靠 VBAT 持续走时。第三行 ALRF 短暂显示 1(表明闹钟已触发),随后 Running 和 STANDBY 各闪烁一次,再次清屏进入待机,循环往复。
-
WKUP 引脚手动唤醒:在系统处于待机模式(OLED 熄屏)的任意时刻,用导线将 PA0 引脚短暂触碰 3.3V,产生一个上升沿信号。系统被立即唤醒,复位重启,OLED 点亮并显示最新数据,闪烁指示后再次进入待机。这证明了 WKUP 引脚唤醒机制的有效性。
-
while 循环的单次执行特性:从现象上看,每次唤醒后 OLED 都会经历"显示数据 → 闪烁 Running → 闪烁 STANDBY → 清屏"的完整过程,然后系统再次进入待机。这直观地印证了唤醒即复位的逻辑:PWR_EnterSTANDBYMode() 之后的代码永远不会被执行,while 循环实际每次唤醒只执行一遍即再次进入待机。
本实验成功展示了待机模式在 RTC 闹钟驱动下的周期性唤醒模型。系统在两次唤醒之间处于极低功耗的待机状态,仅在闹钟触发时短暂工作并更新显示,随后立即再次休眠。这种"周期性唤醒---短暂工作---深度睡眠"的架构,是电池供电物联网节点(如环境监测器、定时上报终端)的标准低功耗设计方案。