☰
STM32L431低功耗双唤醒实战:LPUART+RTC配置与避坑指南
2026/10/15 1:23:41 网站建设 项目流程

简介:这份资源面向STM32L4系列低功耗开发的学习者与工程师,聚焦STM32L431在Stop休眠模式下由RTC闹钟与LPUART接收事件双重唤醒的完整实现,适合需要长时间待机、快速响应外部事件的物联网与传感器节点场景。压缩包共1377个文件,约31.23MB,以692个C源文件、306个头文件、142个汇编文件为核心,辅以IAR工程配置、链接脚本、库文件与少量说明文档,构成可直接编译调试的工程结构。资源中已包含经实测可正常休眠与唤醒的代码,涵盖RTC时钟与闹钟配置、LPUART低功耗串口唤醒、RCC电源模式切换以及唤醒后系统时钟与外设恢复等关键环节,读者可据此对照理解Stop模式下的状态保存与中断标志处理,快速搭建自己的低功耗框架。目前已有5097人学习下载,适合作为低功耗串口唤醒方案的参考工程。

1. STM32L431 低功耗休眠:LPUART+RTC 双唤醒方案为什么值得做

一块用纽扣电池供电的采集板,要求待机电流压到微安级,同时还得随时能被上位机通过串口叫醒、每隔固定时间自己醒来上报一次数据——这是很多手持设备、远程终端、低功耗传感器的真实诉求。STM32L431 属于 L4 系列里性价比很高的一颗,Stop 模式下的功耗表现足够撑起这类场景,但真正让工程师头疼的从来不是"能不能进休眠",而是"进去之后怎么被两种完全不同的源唤醒,而且唤醒后外设不丢状态"。LPUART 负责异步串口唤醒,RTC 负责定时唤醒,两者一个走外部事件、一个走内部定时,凑在一起就是典型的双唤醒架构。这篇笔记就把这套方案从时钟配置、进入 Stop 的条件、唤醒源使能到实测排错完整走一遍,适合正在做低功耗串口终端、又不想在唤醒环节反复翻车的从业者。

2. 先搞懂 Stop 模式与两种唤醒源的底层逻辑

2.1 Stop 模式到底停了什么、留了什么

STM32L431 的低功耗模式里,Sleep 只是关掉内核时钟,功耗降得有限;Standby 几乎全断电,但 SRAM 和寄存器内容丢失,唤醒相当于复位重来;Stop 模式是中间那个甜点区——它关掉了所有高速时钟(HSI、HSE、PLL),内核停止运行,但 SRAM 和寄存器内容保留,GPIO 状态保留,RTC 和 LPUART 这类挂在低功耗时钟域的外设还能继续工作。这就是为什么标题里选 Stop 而不是 Standby:你需要唤醒后接着跑原来的逻辑,而不是从头初始化。

Stop 模式下有几个关键点必须记牢。第一,进入 Stop 前必须把系统时钟切到没有依赖被关闭时钟的源上,通常切到 MSI 或 HSI16,否则一进 Stop 系统就卡死。第二,所有用不到的外设时钟要关掉,GPIO 要配置成模拟输入或按需上拉,悬空的引脚会漏电。第三,唤醒后时钟会默认切回 MSI,如果你原来跑在 PLL 上,得在唤醒中断里重新配置时钟树。很多人第一次做低功耗,休眠电流下不去,八成是某个 GPIO 没处理或者某个外设时钟没关。

STM32L431 的 Stop 模式还分 Stop 0 和 Stop 1(部分型号叫 Stop 2),区别在于保留的外设和唤醒延迟。Stop 0 保留的东西多、唤醒快,但功耗略高;Stop 1 关得更彻底、功耗更低,但唤醒源受限。做 LPUART 唤醒时要注意,LPUART 在 Stop 模式下要能收到起始位并触发唤醒,必须让它挂在能在 Stop 下工作的时钟上,通常是 LSE 或 HSI16,具体看参考手册里 LPUART 在低功耗模式下的时钟可用性。

2.2 LPUART 唤醒:为什么普通 USART 在 Stop 下叫不醒

普通 USART 在 Stop 模式下时钟被关,根本收不到数据,自然无法唤醒。LPUART 是专门为低功耗场景设计的,它可以在 Stop 模式下继续监听 RX 线,检测到起始位就把 MCU 从 Stop 里拉出来。这是它和普通 USART 最本质的区别。

LPUART 唤醒的机制是:使能 LPUART 的唤醒中断(WUF,Wakeup From Stop),当 RX 线上出现下降沿(起始位)时,硬件触发唤醒事件,MCU 退出 Stop,然后你可以选择在中断里接收完整数据帧。这里有个容易忽略的点——唤醒事件和接收中断是两回事。唤醒只是把你叫醒,数据能不能正确收到,取决于唤醒后 LPUART 时钟恢复的速度和波特率是否匹配。如果波特率较高而唤醒延迟较长,第一个字节可能丢,常见做法是让上位机在正式发数据前先发一个唤醒字节(比如 0x00 或 0xFF),MCU 醒来后再接收真正的命令。

配置 LPUART 唤醒的核心步骤是:把 LPUART 时钟源设为 LSE 或 HSI16(保证 Stop 下可用),使能 LPUART 的 STOP 模式唤醒功能,配置 NVIC 使能 LPUART 全局中断,然后在进入 Stop 前调用HAL_UARTEx_EnableStopMode()之类的接口(标准库或 HAL 里函数名略有差异)。唤醒后要清除 WUF 标志,否则会反复触发。

2.3 RTC 唤醒:定时器怎么在断电时钟域里活着

RTC 挂在备份域,由 LSE(32.768kHz 晶振)或 LSI 驱动,只要 VBAT 或主电源在,它就一直走。RTC 的唤醒定时器(Wakeup Timer)可以周期性产生唤醒事件,把 MCU 从 Stop 里拉出来。这是低功耗设备做定时上报的标准手段。

RTC 唤醒定时器的配置关键是分频和计数值。假设 LSE 是 32768Hz,你想每 10 秒唤醒一次,就把唤醒定时器时钟分频到 1Hz,然后计数值设为 10。分频系数和计数值的组合决定了唤醒周期,具体公式在参考手册的 RTC 章节有表。配置时要注意先关闭唤醒定时器再改参数,改完再使能,否则参数不生效。另外,RTC 唤醒中断也要在 NVIC 里使能,并且清除对应的中断标志。

RTC 和 LPUART 两个唤醒源可以同时使能,MCU 被任意一个唤醒后,通过查询标志位判断是谁唤醒的,再走对应逻辑。这就是双唤醒架构的基本形态。

3. 从时钟树到唤醒中断:LPUART+RTC 双唤醒的完整配置

3.1 时钟树配置:Stop 模式下哪些时钟必须活着

时钟配置是这套方案的地基。进入 Stop 前,系统时钟要从 PLL 切到 MSI 或 HSI16,因为 PLL 在 Stop 下会被关掉。LPUART 的时钟源要选 LSE 或 HSI16,RTC 的时钟源选 LSE。下面是一段基于 HAL 库的时钟配置示例,展示进入 Stop 前的切换逻辑。

/* 进入 Stop 前把系统时钟切到 MSI,保证 Stop 下时钟可用 */ void SystemClock_ToMSI(void) { RCC_OscInitTypeDef osc = {0}; RCC_ClkInitTypeDef clk = {0}; /* 使能 MSI,频率设为 4MHz,作为 Stop 下的系统时钟 */ osc.OscillatorType = RCC_OSCILLATORTYPE_MSI; osc.MSIState = RCC_MSI_ON; osc.MSIClockRange = RCC_MSIRANGE_6; /* 4MHz */ osc.MSICalibrationValue = RCC_MSICALIBRATION_DEFAULT; osc.PLL.PLLState = RCC_PLL_OFF; /* Stop 下 PLL 必须关 */ HAL_RCC_OscConfig(&osc); /* 切换系统时钟到 MSI */ clk.ClockType = RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; clk.SYSCLKSource = RCC_SYSCLKSOURCE_MSI; clk.AHBCLKDivider = RCC_SYSCLK_DIV1; clk.APB1CLKDivider = RCC_HCLK_DIV1; clk.APB2CLKDivider = RCC_HCLK_DIV1; HAL_RCC_ClockConfig(&clk, FLASH_LATENCY_0); }

这段代码的逻辑是:先把 MSI 打开并设到 4MHz,关掉 PLL,然后把 SYSCLK 切到 MSI。参数RCC_MSIRANGE_6对应 4MHz,你可以根据唤醒后需要的处理速度调整,频率越低功耗越低但唤醒后跑得越慢。FLASH_LATENCY_0是因为 4MHz 下 Flash 不需要等待周期。切完时钟再进 Stop,系统才不会卡死。

LPUART 的时钟源配置单独说。在 CubeMX 里要把 LPUART1 的时钟源选成 LSE 或 HSI16,代码里对应的是RCC_PERIPHCLK_LPUART1的配置。如果选 LSE,波特率不能太高,因为 32768Hz 分频后能支持的波特率有限,常见做法是 9600 或更低。选 HSI16 可以支持更高波特率,但 HSI16 在 Stop 下的行为要确认,部分型号 Stop 下 HSI16 会关,那就只能用 LSE。

3.2 LPUART 唤醒配置:使能 WUF 与中断优先级

LPUART 唤醒的配置分几步:初始化 LPUART、使能 Stop 模式唤醒、配置 NVIC。下面这段代码展示关键部分。

/* LPUART 初始化,时钟源选 LSE,波特率 9600 */ void LPUART_Init(void) { huart1.Instance = LPUART1; huart1.Init.BaudRate = 9600; huart1.Init.WordLength = UART_WORDLENGTH_8B; huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_NONE; huart1.Init.Mode = UART_MODE_TX_RX; huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart1.Init.OverSampling = UART_OVERSAMPLING_16; HAL_UART_Init(&huart1); /* 使能 Stop 模式下的唤醒功能 */ HAL_UARTEx_EnableStopMode(&huart1); /* 使能 LPUART 全局中断,优先级设为较高 */ HAL_NVIC_SetPriority(LPUART1_IRQn, 1, 0); HAL_NVIC_EnableIRQ(LPUART1_IRQn); }

HAL_UARTEx_EnableStopMode这个函数的作用是置位 LPUART 的唤醒使能位,让它在 Stop 下监听 RX。NVIC 优先级设高一点,是因为唤醒后要尽快处理,避免丢数据。波特率 9600 是 LSE 时钟下的稳妥选择,如果你用 HSI16 可以往上提,但要实测唤醒延迟。

唤醒中断服务函数里要做两件事:清 WUF 标志、判断是不是真的收到了数据。下面是一个简化的中断处理。

void LPUART1_IRQHandler(void) { /* 检查唤醒标志 */ if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_WUF)) { __HAL_UART_CLEAR_FLAG(&huart1, UART_FLAG_WUF); /* 标记被 LPUART 唤醒,主循环里处理接收 */ wakeup_source = WAKEUP_BY_LPUART; } HAL_UART_IRQHandler(&huart1); }

注意 WUF 标志必须清,否则会反复进中断。wakeup_source是一个全局变量,主循环根据它判断唤醒来源。

3.3 RTC 唤醒定时器配置:分频与计数值怎么算

RTC 唤醒定时器的配置相对独立。下面这段代码设置每 10 秒唤醒一次。

/* RTC 唤醒定时器配置,LSE 32768Hz,10 秒唤醒 */ void RTC_WakeupConfig(void) { RTC_HandleTypeDef hrtc = {0}; hrtc.Instance = RTC; hrtc.Init.HourFormat = RTC_HOURFORMAT_24; hrtc.Init.AsynchPrediv = 0x7F; /* 异步分频 128 */ hrtc.Init.SynchPrediv = 0xFF; /* 同步分频 256 */ hrtc.Init.OutPut = RTC_OUTPUT_DISABLE; HAL_RTC_Init(&hrtc); /* 关闭唤醒定时器再配置 */ HAL_RTCEx_DeactivateWakeUpTimer(&hrtc); /* 时钟分频到 1Hz,计数值 10 表示 10 秒 */ /* WakeUpClock 选 RTC_WAKEUPCLOCK_CK_SPRE_16BITS 即 1Hz */ HAL_RTCEx_SetWakeUpTimer_IT(&hrtc, 10, RTC_WAKEUPCLOCK_CK_SPRE_16BITS); /* 使能 RTC 唤醒中断 */ HAL_NVIC_SetPriority(RTC_WKUP_IRQn, 2, 0); HAL_NVIC_EnableIRQ(RTC_WKUP_IRQn); }

AsynchPrediv和SynchPrediv的组合把 32768Hz 分频到 1Hz,这是 RTC 走时的基础。唤醒定时器的时钟选RTC_WAKEUPCLOCK_CK_SPRE_16BITS,这个时钟就是 1Hz,计数值 10 就是 10 秒。如果你要更长的周期,可以选 1Hz 时钟配更大的计数值,或者选其他分频。改参数前一定要先Deactivate,否则新参数不生效,这是血泪经验。

RTC 唤醒中断里同样要清标志,并标记唤醒源。

void RTC_WKUP_IRQHandler(void) { HAL_RTCEx_WakeUpTimerIRQHandler(&hrtc); wakeup_source = WAKEUP_BY_RTC; }

3.4 进入 Stop 与唤醒后的处理流程

两个唤醒源都配好后,进入 Stop 的流程是:切时钟到 MSI、关不用的外设时钟、配置 GPIO 为低功耗状态、调用HAL_PWR_EnterSTOPMode。下面是一个完整的进入函数。

/* 进入 Stop 模式 */ void Enter_StopMode(void) { /* 切系统时钟到 MSI */ SystemClock_ToMSI(); /* 关闭不用的外设时钟,这里以关闭 ADC、SPI 为例 */ __HAL_RCC_ADC_CLK_DISABLE(); __HAL_RCC_SPI1_CLK_DISABLE(); /* 配置未使用 GPIO 为模拟输入,避免漏电 */ GPIO_LowPowerConfig(); /* 清除唤醒标志 */ __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU); /* 进入 Stop 模式,唤醒后继续执行 */ HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); /* 唤醒后重新配置系统时钟 */ SystemClock_Config(); }

PWR_LOWPOWERREGULATOR_ON表示用低功耗稳压器,功耗更低。PWR_STOPENTRY_WFI是等待中断指令进入 Stop。唤醒后第一件事是重新配置系统时钟,因为 Stop 退出后时钟默认是 MSI,你要切回 PLL 才能跑正常业务。GPIO_LowPowerConfig需要你自己实现,把没用的引脚设成GPIO_MODE_ANALOG,用到的引脚按需上拉或下拉。

唤醒后的主循环逻辑大致是:判断wakeup_source,如果是 LPUART 就接收命令并处理,如果是 RTC 就采集数据并上报,处理完清标志再进 Stop。这个循环就是整个低功耗终端的骨架。

4. 实测休眠正常但唤醒异常?这些坑我替你踩过了

4.1 休眠电流下不去,先查 GPIO 再查外设时钟

现象:进入 Stop 后测电流,发现是几百微安甚至毫安级,远高于数据手册标称的几微安。原因通常是某个 GPIO 悬空或者配置成了推挽输出,导致漏电流;也可能是某个外设时钟没关,比如 ADC、SPI 还在耗电。解决方法是逐个排查:把所有未使用的 GPIO 设成模拟输入,用到的引脚确认上下拉状态;在进入 Stop 前用__HAL_RCC_xxx_CLK_DISABLE()关掉所有不用的外设时钟。我一般会写一个GPIO_LowPowerConfig把所有引脚先设成模拟输入,再单独配置需要保持状态的引脚,这样最不容易漏。

4.2 LPUART 叫不醒,八成是时钟源选错了

现象:RTC 能正常唤醒,但串口发数据没反应,MCU 一直不醒。原因通常是 LPUART 的时钟源选了 PCLK 或 HSI16,而这两个时钟在 Stop 下被关了,LPUART 根本没在工作。解决方法是把 LPUART 时钟源改成 LSE,或者在确认 HSI16 在 Stop 下可用的型号上选 HSI16。改完时钟源后要重新初始化 LPUART,并且确认HAL_UARTEx_EnableStopMode被调用。另外,唤醒字节的波特率要和配置一致,上位机先发一个 0x00 再发正式数据,能显著提高唤醒成功率。

4.3 唤醒后第一个字节丢失,是唤醒延迟在作怪

现象:MCU 能被唤醒,但收到的第一个字节是乱码或者干脆丢了。原因是从 Stop 唤醒到 LPUART 时钟恢复、准备接收,中间有一段延迟,如果上位机紧接着就发数据,第一个字节可能在 MCU 还没准备好时就过去了。解决方法是让上位机在发正式命令前先发一个唤醒字节,MCU 醒来后丢弃这个字节再接收真正的数据;或者降低波特率,给唤醒留出足够时间。9600 波特率下这个问题不明显,115200 就很容易翻车。

4.4 RTC 唤醒周期不对,检查分频和计数值是否匹配

现象:设了 10 秒唤醒,实际却是 5 秒或 20 秒。原因是唤醒定时器的时钟分频和计数值没算对,或者改参数前没先关闭唤醒定时器。解决方法是先HAL_RTCEx_DeactivateWakeUpTimer,再按公式重新算分频和计数值。1Hz 时钟配计数值 N 就是 N 秒,这是最直观的组合。如果用了其他分频,一定要对照参考手册的表格确认实际时钟频率。改完再SetWakeUpTimer_IT使能。

4.5 两个唤醒源互相干扰,标志位要清干净

现象:同时使能 LPUART 和 RTC 唤醒后,MCU 频繁醒来,或者唤醒源判断错误。原因是某个中断标志没清,导致中断反复触发;或者两个中断优先级配置不当,一个中断里又触发了另一个。解决方法是在每个中断服务函数里都清对应的标志位,LPUART 清 WUF,RTC 清 WUTF。唤醒源判断用独立的全局变量,在主循环里处理,中断里只做标记。优先级上 RTC 可以比 LPUART 低一点,因为定时唤醒不要求实时性。

5. 把双唤醒方案做稳的几个进阶技巧

5.1 用唤醒标志位做状态机,避免逻辑混乱

双唤醒源最大的麻烦是唤醒后不知道是谁叫的,如果逻辑写成一锅粥,很容易出现"RTC 唤醒却去处理串口命令"这种低级错误。我的习惯是在全局维护一个wakeup_source枚举,中断里只赋值,主循环里用 switch 分派。更进一步,可以把"唤醒后要做什么"抽象成状态机:RTC 唤醒进采集上报状态,LPUART 唤醒进命令处理状态,处理完统一回到休眠状态。这样代码清晰,也方便后续加第三个唤醒源。

5.2 唤醒后时钟恢复要快,别让外设等太久

从 Stop 唤醒后,系统时钟默认是 MSI,如果你原来跑在 80MHz PLL 上,得重新配置 PLL 并切换。这个过程如果太慢,LPUART 接收、SPI 通信都会受影响。优化方法是把时钟恢复代码精简,只配必要的部分;或者唤醒后先不切 PLL,用 MSI 先把紧急的事处理完(比如把收到的数据存进缓冲区),再切 PLL 做重活。下面是一个快速恢复时钟的示例。

/* 唤醒后快速恢复 PLL 时钟 */ void SystemClock_Restore(void) { RCC_OscInitTypeDef osc = {0}; RCC_ClkInitTypeDef clk = {0}; /* 重新使能 PLL */ osc.OscillatorType = RCC_OSCILLATORTYPE_HSE | RCC_OSCILLATORTYPE_MSI; osc.HSEState = RCC_HSE_ON; osc.MSIState = RCC_MSI_ON; osc.PLL.PLLState = RCC_PLL_ON; osc.PLL.PLLSource = RCC_PLLSOURCE_HSE; osc.PLL.PLLM = 1; osc.PLL.PLLN = 20; osc.PLL.PLLR = 2; HAL_RCC_OscConfig(&osc); /* 切换 SYSCLK 到 PLL */ clk.ClockType = RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; clk.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; clk.AHBCLKDivider = RCC_SYSCLK_DIV1; clk.APB1CLKDivider = RCC_HCLK_DIV1; clk.APB2CLKDivider = RCC_HCLK_DIV1; HAL_RCC_ClockConfig(&clk, FLASH_LATENCY_4); }

参数PLLN、PLLR根据你需要的系统频率算,这里只是示例。FLASH_LATENCY_4对应高频下的 Flash 等待周期,具体值查数据手册。这段代码放在唤醒后尽早调用,但如果你用 MSI 先处理数据,可以延后调用。

5.3 用电流探头和串口日志交叉验证

低功耗调试最怕"以为进了 Stop 其实没有"。我的做法是:硬件上用电流探头看电流波形,进 Stop 瞬间电流应该掉到微安级;软件上在进入 Stop 前和唤醒后各翻转一个 GPIO,用示波器看这个引脚的电平变化,确认休眠时长和唤醒时刻。串口日志在休眠时打不出来,但唤醒后可以把关键事件(唤醒源、时间戳)存进 RTC 备份寄存器或 SRAM,醒来后统一打印。这样软硬结合,基本不会误判。

5.4 备份寄存器记录唤醒次数,方便长期观测

RTC 备份寄存器在 Stop 和 Standby 下都不丢,适合记录唤醒次数、最后一次唤醒源这类信息。每次唤醒后往备份寄存器写一个计数,定期上报时读出来,就能知道设备在野外跑了多久、被哪种源唤醒得多。这个技巧在长期部署的设备上特别有用,相当于给低功耗逻辑装了个黑匣子。配置备份寄存器要先使能__HAL_RCC_PWR_CLK_ENABLE()并解除备份域写保护,写完后可以不清除,下次唤醒继续累加。

这套 LPUART+RTC 双唤醒方案我在几个低功耗采集项目里都用过,休眠电流能压到几微安,唤醒成功率也稳定。最容易翻车的永远是时钟源和 GPIO 这两块,把这两处抠干净,剩下的就是按流程配置。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询