免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

STM32低功耗串口唤醒实战:睡眠与停止模式详解及代码实现

STM32低功耗串口唤醒实战:睡眠与停止模式详解及代码实现 简介本资源是一份面向STM32嵌入式开发者的低功耗模式实践工程聚焦于停止模式Stop Mode与睡眠模式Sleep Mode的完整实现与唤醒机制设计特别强化了串口USART作为唤醒源的配置与验证适用于电池供电、物联网终端等对功耗敏感的应用场景适合具备C语言基础和STM32外设开发经验的中级工程师学习与项目复用。压缩包共172个文件含65个头文件.h定义寄存器与接口、61个C源文件.c实现系统时钟、PWR电源控制、EXTI中断、USART唤醒及LED指示逻辑另有汇编启动文件.s、Keil工程配置.uvprojx/.uvoptx、批处理脚本.bat及说明文档.docx/.txt整体体积仅725KB结构清晰、模块解耦度高。已有354人下载学习提供可直接编译运行的完整Keil MDK工程包含多版本GUI配置备份与hex烧录文件便于快速验证不同唤醒路径下的功耗表现与响应时序。 最近在做一个电池供电的小设备MCU 平时得把功耗压到微安级但上位机随时可能通过串口发一帧数据把它叫醒。我手里正好有现成的 STM32 板子于是把睡眠模式、停止模式以及串口唤醒整个方案调通了。这篇文章把实现思路、代码细节和踩过的坑一起梳理出来适合正在做低功耗设备、或者想搞明白“串口怎么把 MCU 从低功耗状态拉起来”的开发者。不管是刚接触低功耗的小白还是已经在项目里碰过壁的老手下面这套流程应该都能直接拿来参考。1. 先搞明白睡眠模式和停止模式到底差在哪很多人在选低功耗模式的时候容易犯迷糊觉得“反正都是睡觉选个最省电的不就完了”。实际上睡眠模式和停止模式的功耗、唤醒方式、外设状态差别非常大选错了一个可能整个方案都立不住。1.1 三种低功耗模式的行为对照STM32 里常用的低功耗模式有三个层级睡眠模式Sleep、停止模式Stop、待机模式Standby。这次项目主要用到前两个但必须先看清三者差异。模式内核时钟外设时钟典型电流F103 3.3V唤醒方式唤醒后执行睡眠模式关闭CPU停保持几 mA 级别主要看外设任意中断/事件直接从 WFI/WFE 下一条指令继续执行停止模式关闭CPU停关闭部分可选保持十几 uA 左右EXTI、RTC、部分串口等先走中断再继续执行需重新配置时钟待机模式关闭关闭1 uA 左右RTC、WKUP引脚、复位相当于复位程序重新从 main 开始从表格能看出睡眠模式其实很“浅”CPU 停了但外设时钟还在跑所以唤醒非常快中断一来直接继续跑不用重新初始化任何东西。停止模式就“睡”得深一些所有时钟基本停摆只有少数唤醒源还在工作唤醒后系统时钟处于初始状态必须重新配置。1.2 停止模式唤醒后第一件事恢复时钟这是停止模式最容易翻车的地方。进入停止模式后HSE、PLL 全部关闭系统时钟会被切换回 HSI。唤醒后如果你不做任何处理代码会以 HSI 的频率继续跑而你的串口波特率、定时器参数全都是以原来的主频计算好的自然全乱了。我当时的做法是唤醒后立即调用SystemClock_Config()重新配置 PLL 和总线时钟再初始化串口等外设。这一步不能省也不能放在中断回调里做太久最好只在中断里置一个标志位回到主循环后再统一恢复。注意HAL 库的HAL_PWR_EnterSTOPMode在进入停止前会自动把时钟切到 HSI但唤醒后不会自动恢复你原来的时钟配置这个责任在应用层。2. 串口唤醒方案的选型思路串口唤醒这件事看起来简单但里面有两条完全不同的技术路线。选型选错了轻则功能实现不了重则外设没法用。我在实际项目里把两条路都试过先说结论追求通用性、用 STM32F1/F4 等主流系列用RX 引脚的外部中断EXTI唤醒可靠且代码可控。用 STM32L4 或者带 LPUART 的低功耗系列可以用UART 外设本身的起始位唤醒数据还能保留但接口和配置复杂一些。2.1 方案 ARX 引脚外部中断唤醒F1/F4 通用串口空闲时RX 线是高电平。对方发送数据时第一帧的第一个字节一定会有起始位也就是拉低电平产生一个下降沿。这个下降沿正好可以作为外部中断的触发源。具体做法把 RX 引脚同时配置为GPIO_MODE_IT_FALLING下降沿外部中断进入停止模式前使能这个中断。对方发数据时下降沿把 MCU 从停止模式唤醒然后在 EXTI 中断回调里置一个标志位主循环里恢复时钟、重新初始化串口再处理后续数据。这里要解释一个很多人困惑的点引脚配置成串口的复用模式后还能同时配置成外部中断吗答案是能。STM32 的 GPIO 在复用功能模式下输入通路依然是有效的EXTI 检测的就是引脚上的电平变化和引脚是否用于 UART 接收不冲突。实际测试中这个方案在 F103 和 F407 上都稳定工作。2.2 方案 BUART 外设本身唤醒L4 / LPUARTSTM32L4 及更新系列的 UART 支持一种特殊机制进入停止模式前配置好唤醒源UART 外设会在停止模式下继续检测 RX 线上的起始位检测到后直接唤醒 MCU并且这个起始位信号可以被硬件保留唤醒后读取寄存器还能拿到第一个字节。这种方式的好处是不用另配 EXTI而且不会丢第一个数据的起始位。但代价是必须使用支持该特性的系列且 HAL 库接口比较绕需要额外调用HAL_UARTEx_EnableStopMode和HAL_UARTEx_StopModeWakeUpSourceConfig这类函数。2.3 两种方案怎么选对比项EXTI 唤醒方案AUART 起始位唤醒方案B适用系列所有带 GPIO EXTI 的 STM32需要 UART/LPUART 支持 Stop Mode Wakeup常见于 L4、U5 等第一字节数据大概率丢失需要重传机制可保留起始位数据配置复杂度低思路直观高接口多且系列差异大代码可移植性强换系列只改引脚弱换系列可能要重写实测可靠性很稳但要处理丢字节很稳但配置要精细我的建议是如果你手头就是 F1/F4直接采用方案 A如果是新项目选型且后续对功耗要求更苛刻可以考虑上 L4 系列用方案 B。下面的核心代码部分我会重点讲方案 A 的完整流程因为它在工程中最常用也最容易复现。3. 代码实现睡眠模式 串口唤醒睡眠模式是三个模式里最“轻”的适合那些需要频繁唤醒、且对唤醒延迟敏感的场景。比如设备每 10ms 醒一次处理定时任务平时用睡眠模式挂着就行。3.1 用 CubeMX 搭一个最小工程我这里以 STM32F103C8T6 为例使用 HAL 库。CubeMX 里的配置很简单打开 USART1异步模式波特率 115200使能 USART1 全局中断时钟配置为外部晶振 8MHzPLL 倍频到 72MHz不用的 GPIO 全部配置为模拟输入避免漏电。生成代码后main 函数里先初始化串口然后进入主循环。3.2 睡眠模式的核心代码与执行流程睡眠模式的唤醒依赖中断。串口每收到一个字节就会触发HAL_UART_RxCpltCallback在这个回调里处理数据即可。进入睡眠前需要调用HAL_SuspendTick()因为 SysTick 在睡眠模式下也会停掉如果唤醒后继续用 HAL_Delay会直接卡死。主循环里的进入睡眠代码如下uint8_t rx_data 0; int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); // 启动串口中断接收接收一个字节进 rx_data HAL_UART_Receive_IT(huart1, rx_data, 1); while (1) { // 关键睡眠前挂起 SysTick防止唤醒后 HAL_Delay 卡死 HAL_SuspendTick(); // 进入睡眠模式等待任意中断唤醒 HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI); // 唤醒后恢复 SysTick HAL_ResumeTick(); // 中断里已经收到数据这里就可以做业务处理了 // 注意处理逻辑要放在进入睡眠之后确保收到数据后能立刻响应 } }串口中断回调void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { // 收到数据重新开启下一次接收 HAL_UART_Receive_IT(huart1, rx_data, 1); // 在这里置位标志位主循环里轮询处理 g_data_received 1; } }这个流程跑起来后用 USB 转串口CH340一般要先装驱动连接到板子打开串口调试助手发一个字节观察电流表或者看程序里某个 GPIO 翻转状态就能验证唤醒是否成功。3.3 睡眠模式实测体会睡眠模式唤醒可以说毫无压力中断响应是纳秒到微秒级别的几乎感觉不到延迟。实测中我拿串口调试助手向板子连续发数据每个字节都能被完整接收处理速度完全跟得上。不过这里要提醒一点睡眠模式下外设时钟没关所以功耗并不低。我实测 F103 板载 LED 灯拆掉后整体电流在 3~5mA 左右和正常运行的十几 mA 比是降了一些但离“微安级”还差得远。如果需要做纽扣电池供电的设备睡眠模式只能作为中间态最终还是要进停止模式。4. 代码实现停止模式 串口唤醒核心章节这一部分是整个项目的重点。停止模式才真正把功耗压到了微安级同时还能被串口唤醒。整个流程比睡眠模式复杂但只要把几个关键点理清楚代码量其实不大。4.1 引脚复用与 EXTI 配置前面说过RX 引脚既要作为 UART 的接收脚又要作为外部中断的唤醒源。CubeMX 配置 GPIO 时不需要对同一个引脚做两套配置只需在普通初始化里把它定义为GPIO_MODE_IT_FALLING并开启上拉然后在代码里把串口初始化放在前面串口的 AF 配置会自动生效。实际上在 HAL 库中HAL_UART_Init内部会设置GPIO_InitStruct.Mode GPIO_MODE_AF_PP这时串口功能正常而 EXTI 的使能在HAL_GPIO_Init时通过GPIO_MODE_IT_FALLING配置两者同时存在。也就是说你在 CubeMX 的 GPIO 页面把这个引脚配置为外部中断模式后串口初始化代码在跑的时候仍然会把它配置为复用推挽EXTI 的中断通道照样能触发。实测下来两者共存没有问题。关键在于进入停止模式前要确保这个引脚的 EXTI 中断被使能并且在 NVIC 里有对应的中断号。void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; /* 使能 GPIOA 时钟 */ __HAL_RCC_GPIOA_CLK_ENABLE(); /* PA10 是 USART1_RX同时作为 EXTI 唤醒源下降沿触发 */ GPIO_InitStruct.Pin GPIO_PIN_10; GPIO_InitStruct.Mode GPIO_MODE_IT_FALLING; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); /* NVIC 里使能 EXTI15_10 中断PA10 对应这一组 */ HAL_NVIC_SetPriority(EXTI15_10_IRQn, 2, 0); HAL_NVIC_EnableIRQ(EXTI15_10_IRQn); }4.2 进入停止模式的标准流程进入停止模式前要做几件收尾工作关闭串口中断接收防止唤醒瞬间收到中断导致异常打开 RX 引脚的 EXTI 中断关闭不必要的外设时钟大部分外设时钟在停止模式下会自动关闭但保险起见可以手动关掉调用HAL_PWR_EnterSTOPMode进入停止。进入停止的代码void Enter_StopMode(void) { // 停止前关闭串口中断接收进入停止后 UART 不会工作 HAL_UART_AbortReceive(huart1); // 确保 EXTI 唤醒中断是开着的 HAL_NVIC_EnableIRQ(EXTI15_10_IRQn); // 进入停止模式主稳压器保持开启WFI 等待中断唤醒 HAL_PWR_EnterSTOPMode(PWR_MAINREGULATOR_ON, PWR_STOPENTRY_WFI); // 唤醒后第一件事重新配置系统时钟 SystemClock_Config(); // 重新初始化串口 MX_USART1_UART_Init(); // 重新启动串口接收 HAL_UART_Receive_IT(huart1, rx_data, 1); }有人可能会问为什么进入停止模式前要把串口接收关掉因为停止模式下时钟停了UART 外设已经不再工作如果还留着中断接收一旦唤醒时 UART 产生中断处理逻辑会乱。实际上停止模式唤醒后时钟刚开始恢复UART 状态是不确定的最好一切重新初始化。4.3 唤醒中断的处理EXTI 中断回调里只做一件事置一个唤醒标志位。volatile uint8_t g_wakeup_flag 0; volatile uint8_t g_data_received 0; void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin GPIO_PIN_10) { g_wakeup_flag 1; // 只置标志不做耗时操作 } }主循环里的逻辑就变成一个有限状态机while (1) { if (g_wakeup_flag) { g_wakeup_flag 0; // 唤醒后立刻重新配置时钟和串口 Enter_StopMode(); // 实际上是唤醒后恢复函数名有点误导但逻辑没问题 } if (g_data_received) { g_data_received 0; // 处理上位机发来的数据 } // 没有任务时进入停止模式等待下一次唤醒 Enter_StopMode(); }这里有个更优雅的设计把唤醒恢复的逻辑放在Enter_StopMode函数内部主循环每次调用它要么进入停止要么唤醒后恢复并立刻回到主循环。上面代码里我其实写的是一个恢复函数和状态判断拆开的结构实际项目里建议用一个函数封装好调用一次就完成“进入停止—等待唤醒—恢复外设”的完整周期。4.4 L4 系列 UART 起始位唤醒的参考实现如果你的板子是 STM32L4、U575 这一类可以试试串口外设原生唤醒。核心代码大致如下// 进入停止模式前 HAL_UART_AbortReceive(huart1); // 允许 UART 在停止模式下工作 HAL_UARTEx_EnableStopMode(huart1); // 配置唤醒源为起始位 HAL_UARTEx_StopModeWakeUpSourceConfig(huart1, UART_WAKE_SOURCE_START_BIT); // 使能唤醒中断 __HAL_UART_ENABLE_IT(huart1, UART_IT_WUF); // 进入停止 HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);唤醒后在HAL_UARTEx_WakeupCallback里处理然后重新初始化并关闭停止模式下的唤醒功能。这个方案的好处是数据不丢坏处是 HAL 库接口在不同系列之间差异有点大移植时需要重新查手册。5. 实测数据与可靠性设计代码调通了不代表方案就可靠。我在实际测试中发现停止模式下的功耗、唤醒时间、以及唤醒后的第一字节处理都是需要专门设计的。5.1 功耗实测与优化我手头这块 F103 板子进入停止模式后实测电流大概在 15uA 左右。但当时第一次测电流高达 2mA排查了半天发现三个漏电大户漏电原因典型电流解决办法GPIO 悬空输入每个引脚几 uA 到几十 uA不用的引脚全部配置为模拟输入或者带上拉/下拉板载 LED 没有拆1~2mA跳线断开或把 LED 的 GPIO 配置为高阻供电部分 LDO 静态电流依芯片而定换低静态电流的 LDO或直接电池供电调试器 ST-Link 还连着1~3mA实测时必须拔掉调试器只保留串口线的 TX/RX/GND这些坑不踩一遍真的想不到。测试功耗时串口模块的 TX 引脚也会漏电建议外部用三极管或光耦隔离或者干脆调试时把串口模块的供电单独开关。5.2 唤醒时间与第一字节丢失停止模式唤醒时间通常需要几十微秒我实测 F103 从收到下降沿到系统时钟恢复大概 50us 左右。这个时间看起来很短但对串口来说却是致命的对方以 115200 波特率发一个字节大约需要 87usMCU 还没醒过来起始位就过去了第一个字节必丢。解决思路有两个第一上位机发数据前先发一个或多个“唤醒字节”。比如统一约定上位机先发0x00 0x00 0xAA 0xAA前面两个字节专门用来唤醒后面才开始真正的指令。这样 MCU 醒来后至少能保证从第三个字节开始是完整数据不会因为起始位丢失而解析失败。第二如果协议不允许加唤醒字节那就需要在 MCU 唤醒后主动要求对方重传。比如 MCU 醒来后先发一个“已就绪”的应答信号上位机收到后再发正式指令可靠性更高但会增加一次交互的延迟。我在项目里用的是第一种方案简单粗暴效果好。实际测试中上位机连发 4 个字节MCU 在第二个字节前后醒来从第三个字节开始就能正常接收数据完整性基本能达到 100%。5.3 可靠性设计空闲电平与唤醒沿串口空闲时 RX 线为高电平起始位是下降沿这个知识点是整个方案能够成立的基础。但也意味着如果外部干扰导致 RX 线上出现抖动即便没有真实数据发送也可能触发 EXTI 唤醒。我在实际部署时加了两个保护措施RX 引脚配置为上拉输入确保空闲时稳定为高在唤醒标志置位后主循环里先做一次“接收确认”如果在超时时间内没有检测到完整帧数据就认为是误唤醒重新进入停止模式。另外如果使用 RS485 总线半双工模式下收发切换会引入一段总线空闲时间需要注意不能让收发切换时的电平抖动触发误唤醒必要时在 RS485 芯片的 DE 引脚上做延迟控制或者把唤醒源改为“地址匹配唤醒”——这个就涉及到更深一层了这里不展开。6. 常见问题与排查实录调试低功耗的过程说白了就是不断踩坑和填坑的过程。我把这次项目里遇到的高频问题整理成一个速查表方便大家直接对号入座。现象可能原因解决办法程序下载后调试器连不上进入停止模式后 SWD 失效按住复位键在 IDE 里设置 Connect Under Reset再点连接唤醒后程序跑飞或者卡在 HAL_Delay唤醒后没有再配置系统时钟唤醒后第一件事就调用 SystemClock_Config串口收到乱码唤醒后波特率不对或时钟还没恢复就初始化串口确保 SystemClock_Config 先执行再初始化 UART设备反复唤醒一直在处理数据EXTI 中断标志没有清除或者 RX 线有抖动在回调里清除中断标志并做帧超时确认功耗比手册值大很多有 GPIO 悬空、外设没关、LED 没拆按 5.1 节逐个排查睡眠模式没问题停止模式不行停止模式下时钟和外设全停唤醒后需要重新初始化把外设初始化统一放到唤醒后执行EXTI 收不到唤醒RX 引脚被配置成模拟输入没有数字边沿检测把引脚模式改为 MODE_IT_FALLING并设置为上拉6.1 调试器连不上这是最慌的坑我第一次让程序跑进停止模式后发现 ST-Link 再也连不上了芯片就好像“死”了一样。实际上芯片没死只是停止模式下调试接口不响应。遇到这种情况最稳妥的操作是按住板子上的复位键不放点击下载按钮等软件开始连接后再松开复位。如果手头有支持 Connect Under Reset 的调试器比如 ST-Link 在 Keil/CubeIDE 里都有这个选项勾选后就能恢复。6.2 唤醒后 HAL_Delay 卡死这个坑在睡眠模式里也提到过。停止模式唤醒后SysTick 并没有自动恢复所以直接调用 HAL_Delay 会死等。解决方法是进入低功耗前调用HAL_SuspendTick()唤醒后调用HAL_ResumeTick()。如果用的不是 HAL 而是标准外设库就手动重新初始化 SysTick。6.3 串口唤醒后收到乱码乱码十有八九是时钟配置的问题。我在前期调试时唤醒后先初始化串口、后配置时钟结果串口按 115200 跑出来的数据全是乱码。把顺序改成“先 SystemClock_Config再 MX_USART1_UART_Init”后问题立刻解决。记住停止模式唤醒后系统运行在 HSI 上波特率完全不是你以为的那个数值。6.4 反复唤醒导致系统无法休眠如果 EXTI 中断标志位没有及时清除唤醒后会立刻再次触发中断导致 MCU 一直在“醒来—处理—再醒来”的循环里打转。HAL 库的HAL_GPIO_EXTI_IRQHandler会帮你清标志但如果你在回调里没有正确处理或者 RX 线上电平本来就是低的比如对方一直占用总线下降沿不会触发反而会一直保持低电平。这种情况要检查串口线是否接反、对方是否有持续发送数据。我在实际项目中还遇到过一个问题上位机用串口调试助手发送数据时助手的“发送新行”功能会默认追加\r\n导致 MCU 唤醒后收到的第一个“完整数据”其实是回车符。这个不算 bug但很坑建议在协议里做好帧头校验不要把触发唤醒的字节当有效数据解析。我的几点实操心得这套方案做完之后我自己最深的感触是低功耗设计不是把“睡觉”这个动作写对就完事了真正的工程量在唤醒后的状态恢复、数据可靠性处理和杂散漏电排查上。如果你现在也在做类似项目我建议严格按照这个顺序来推进先用睡眠模式把串口通信整个链路调通再切换到停止模式验证 EXTI 唤醒是否可靠最后再花时间去压功耗。不要一上来就直接进停止模式否则出了问题很难定位是串口配置的问题、时钟恢复的问题还是功耗设置的问题。另外项目里最好准备一个低功耗电流表或者支持微安级测量的万用表。调试功耗时把调试器、串口模块、LED 这些外围统统断开再做测量否则你测出来的数字会让你对“低功耗”产生深深的误解。串口唤醒到这一步已经能在实际项目里稳定工作了。如果后面想把功耗再往下压可以考虑外置的 RTC 定时唤醒和串口事件唤醒结合使用或者换用支持更低功耗模式的系列芯片那又是一套新的优化思路了。本文还有配套的精品资源点击获取
返回列表