
简介基于STM32的RTC定时唤醒完整工程例程面向需要实现低功耗休眠和自动唤醒的嵌入式开发者。示例展示了在秒计数到达设定值后调用Sys_Enter_Standby进入待机模式RTC闹钟中断在指定时刻自动复位唤醒MCU并从头运行主程序资源特别指出了standby唤醒后原执行点之后代码不可达这一关键特性帮助读者规避常见设计误区。工程采用标准外设库编写包含完整源码与Keil工程可在此基础上扩展电池供电、间歇采集等低功耗应用。资源包为rar压缩格式共204个文件大小约10.04MB。文件涵盖h/c源文件、o/d/crf等编译中间产物、hex/axf可执行镜像、uvprojx/uvoptx工程配置、map/sct链接脚本及调试信息目录组织清晰既可直接阅读源码理解RTC配置过程也可利用hex文件快速验证唤醒效果。目前已有6966人学习下载适合正在学习STM32低功耗模式、RTC中断唤醒或准备做便携式设备的开发者作为参考。 有段时间做电池供电的采集节点需求听上去特别简单系统每隔30秒醒来一次做点事情然后继续睡如此循环。第一个版本图省事直接让MCU一直跑用定时器到点干活结果功耗压不下来两节电池撑不了多久。后来换成了STM32的RTC定时唤醒主程序跑起来之后读取当前时间设置一个30秒后的RTC唤醒然后让芯片进入低功耗模式30秒一到RTC把系统拉起来程序从头开始重新执行主程序。代码不复杂整机平均功耗却降了一个量级以上。这篇文章就把这套方案的完整思路写清楚为什么选待机模式、RTC唤醒定时器是怎么算时间的、完整代码怎么组织以及我在实测中踩过的几个坑。适合正准备做低功耗定时唤醒或者刚把RTC跑通但不确定怎么接低功耗的开发者参考。1. 为什么最终选了待机模式而不是停机模式1.1 三种低功耗模式醒来之后的差别很大STM32的低功耗模式常被一句话带过实际用起来差异很明显。常见的有三种睡眠模式Sleep、停机模式Stop、待机模式Standby。Sleep模式CPU停了外设时钟还在跑功耗降低有限。唤醒后从休眠点继续执行对降低平均功耗帮助不大。Stop模式高频时钟全部停掉SRAM和寄存器内容保留唤醒后从中断处继续运行。功耗大约在几十微安级别。Standby模式内部电路几乎全部断电SRAM和寄存器内容丢失唤醒后芯片走一遍完整的复位流程程序从main开头重新执行。功耗最低通常只有几微安。三种模式的差异不只在功耗数字上最关键的区别是唤醒之后程序从哪里继续跑。Stop模式醒来后代码停在进入休眠时的那条指令后面Standby模式醒来后等于重新上电所有变量重新初始化。1.2 从头运行主程序是选型的关键需求里写的是从头开始运行主程序这个要求基本就把模式锁死了直接用Standby模式最省事因为待机唤醒天然就是从头跑。如果用Stop模式唤醒后要继续执行休眠点之后的代码要实现从头运行你得在唤醒回调里做软复位或者设置标志位后在main里调用NVIC_SystemReset()逻辑上绕了一圈。所以我的建议是不要求保留RAM数据时直接上Standby模式硬件复位本身比软件处理可靠代码也少。如果需要保留RAM里的一些数据那就只能Stop模式加软复位或者把关键数据放到RTC备份寄存器里。1.3 唤醒源为什么用RTC而不是普通定时器有人会问用普通定时器不行吗进入Stop或Standby后高频时钟已经停了普通定时器自然不工作。外部中断比如按键、GPIO唤醒虽然可以但无法做到定时唤醒得靠外部给信号。RTC不一样它挂在芯片的备份域里由独立的LSE晶振驱动。待机模式下备份域继续供电RTC照常走时所以它可以像闹钟一样在指定时间把系统唤醒。这也是RTC在低功耗项目中不可替代的原因。2. RTC唤醒定时器把30秒翻译成寄存器值2.1 WakeUpTimer和Alarm闹钟的区别RTC实现定时唤醒有两种方式一种是 Alarm闹钟指定一个绝对时间比如明天上午10点或今天14:30:30到点触发另一种是 WakeUpTimer唤醒定时器它像一个倒计时器设定一个相对时间比如从现在开始30秒后。这个需求写的是在当前时间设置30S后RTC唤醒用WakeUpTimer是最直接的它不需要读当前时间来跟Alarm比较只关心再过多少时间。Alarm更适合每天固定时刻唤醒的场景。2.2 30秒需要计多少个脉冲WakeUpTimer本质是一个16位递减计数器最大只能计65535。它有几个可选时钟源以最常见的LSE 32.768kHz晶振为例时钟源分频后频率30秒所需计数值最大可定时时长评价RTCCLK/162048Hz61440约32秒非常极限不推荐RTCCLK/84096Hz122880约16秒不够用RTCCLK/48192Hz245760约8秒不够用RTCCLK/216384Hz491520约4秒不够用CK_SPRE_1HZ1Hz30约18小时最推荐看到没有使用RTCCLK/16这个档位时30秒需要计数61440已经逼近65535的上限一旦你想改成32秒都做不到。所以这种相对短时长的定时最稳妥的方式是用1Hz的秒脉冲时钟源30秒就计30个数扩展空间大逻辑也清楚。2.3 为什么1Hz时钟源必须开启RTC日历CK_SPRE_1HZ不是凭空产生的它由RTC的异步预分频器AsynchPrediv和同步预分频器SynchPrediv逐级分频得到。CubeMX默认配置通常是128分频乘以256分频正好把32.768kHz变成1Hz。所以使用CK_SPRE_1HZ这个时钟源前提是RTC日历功能已经激活预分频值正确。如果你为了省事不激活日历只想要一个简简单单的RTC唤醒来跑系统调度器那这条路就走不通了只能用RTCCLK分频档位并且自己算好16位计数器是否塞得下。实际项目里我建议宁可多开一个日历功能也别去抠那几秒钟的计数器余量。3. 完整代码设置唤醒、进入休眠、复位重跑3.1 CubeMX里的配置步骤这里以STM32F407为例使用HAL库和CubeMX生成工程。RCC选项中HSE、LSE都打开LSE选择Crystal/Ceramic Resonator。RTC选项中勾选Activate Clock Source勾选Activate Calendar日期时间随意设一个初始值。WakeUp配置勾选Activate WakeUp TimerWakeUp Clock选择RTC_WAKEUPCLOCK_CK_SPRE_1HZWakeUp Counter填写30。电源管理可以不用单独配置待机模式在代码里调用接口即可。NVIC设置里RTC WakeUp interrupt最好保持使能后面解释原因。CubeMX会在生成代码时完成RTC时钟源选择、预分频器配置和唤醒定时器的初始化调用这部分比自己对着寄存器手册写省心很多。3.2 main函数的核心流程生成工程之后把main函数改成下面这种结构就能实现启动处理业务、设置30秒后唤醒、进入待机、唤醒后从头再来的完整循环#include main.h RTC_HandleTypeDef hrtc; void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_RTC_Init(void); int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_RTC_Init(); // 判断本次启动是正常上电还是待机唤醒复位 if (__HAL_RCC_GET_FLAG(RCC_FLAG_SBF) ! RESET) { __HAL_RCC_CLEAR_RESET_FLAGS(); // 从待机唤醒后会走到这里 } // 业务动作LED闪一下代表一个唤醒周期开始 HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET); HAL_Delay(100); HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); // 重新设置30秒后RTC唤醒 if (HAL_RTCEx_SetWakeUpTimer_IT(hrtc, 30, RTC_WAKEUPCLOCK_CK_SPRE_1HZ) ! HAL_OK) { Error_Handler(); } // 进入待机模式唤醒后系统复位回到main开头 HAL_PWR_EnterSTANDBYMode(); while (1) { } }这里的顺序有讲究。CubeMX生成的MX_RTC_Init已经调用过一次HAL_RTCEx_SetWakeUpTimer_ITmain里再调一次是把它刷新成最新值灵活应对动态修改唤醒间隔的场景。HAL_RTCEx_SetWakeUpTimer_IT这个函数参数分别是RTC句柄、计数值和时钟源。计数值30配合1Hz时钟源意思就是30秒。需要注意第二个参数超过65535会返回参数错误所以如果预期唤醒间隔很长要么换更大分频的时钟源要么改用RTC Alarm方式。3.3 中断服务函数不要省CubeMX生成的工程会自动在stm32f4xx_it.c里包含RTC_WKUP_IRQHandlervoid RTC_WKUP_IRQHandler(void) { HAL_RTCEx_WakeUpTimerIRQHandler(hrtc); }待机模式下RTC唤醒事件把芯片拉起来后走的是复位流程这段中断函数实际上不会执行。但请不要因为它不执行就删掉原因有两个一是如果你切换到Stop模式唤醒后会进入这个中断没有正确挂接会出问题二是HAL库的RTC初始化流程里会检查并操作中断相关标志保留它能让两种模式切换时少踩雷。3.4 如果非要保留RAM用Stop模式加软复位有的场景需要在睡眠期间保留一些变量比如统计唤醒次数、临时缓存采集数据。这时候用Standby模式就不行了因为RAM内容会全部丢失。替代方案是Stop模式唤醒后手动触发软复位同样达到从头运行主程序的效果volatile uint8_t wakeup_flag 0; void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc) { wakeup_flag 1; } // 主流程中 HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); if (wakeup_flag) { NVIC_SystemReset(); }要注意NVIC_SystemReset()之后RAM照样会被清掉所以Stop模式加软复位并不能帮你保留变量。真正想跨休眠周期保存数据请使用RTC备份寄存器BKP Register或外部低功耗存储。Stop模式相对Standby的唯一优势是可以利用唤醒中断回调做一些快速处理或者不需要程序从头跑的情况下继续执行。4. 实测中容易踩的四个坑从时间不准到唤醒失败4.1 LSE没起振30秒变成大概30秒RTC要精确计时LSE 32.768kHz晶振必须正常工作。实测中常见问题是板子布局不良、晶振负载电容不匹配导致LSE起振失败或起振极慢。如果LSE没起振CubeMX在初始化时会卡在等待LSE就绪的超时逻辑里整个程序跑不起来。有些非法开发板干脆没焊晶振只能把RTC时钟源改成LSI。但LSI是内部RC振荡器精度远不如晶振标称可能有百分之几的误差实测30秒偏差几百毫秒甚至更多都出现过。所以如果你的应用对唤醒时刻有严格要求必须保证LSE可靠起振并且用示波器确认晶振引脚波形。4.2 设置完唤醒定时器立刻进休眠偶尔不醒有些代码在调用HAL_RTCEx_SetWakeUpTimer_IT之后不做任何检查直接调用HAL_PWR_EnterSTANDBYMode()偶尔会遇到休眠后不唤醒的现象。原因多半是RTC寄存器写入尚未完成或者上一次的唤醒标志没有清掉。稳妥的做法是检查HAL_RTCEx_SetWakeUpTimer_IT的返回值确保它返回HAL_OK再进休眠。如果发现返回值异常优先确认备份域配置和RTC时钟源是否正确。另外进入待机前主动清一次EXTI唤醒线标志也能避免一些残留标志带来的干扰。4.3 待机复位和上电复位傻傻分不清Standby唤醒后芯片会复位但复位原因是待机唤醒复位不是上电复位。两者在RCC复位标志里是能区分开的if (__HAL_RCC_GET_FLAG(RCC_FLAG_SBF) ! RESET) { // 待机唤醒复位 __HAL_RCC_CLEAR_RESET_FLAGS(); } else { // 普通上电复位 }这个判断在项目里很实用。比如你想统计设备一共被RTC唤醒了多少次、每次唤醒后是否要恢复上次未完成的业务流程都可以靠这个标志位作为基础。需要强调的一点是如果使用调试器下载程序某些型号的复位标志可能会残留判断来源时心里有数就行不要过度依赖断点下的标志状态。4.4 唤醒后程序不跑St-Link/J-Link连不上待机模式下芯片大部分电路断电调试器自然无法连接。如果程序写错了导致芯片下载后一直休眠不唤醒你可能会遇到连不上目标的局面。解决方法是按住板子上的复位键点下载在擦除完成前再松开复位。或者把Boot引脚拉高让芯片从系统存储器启动绕过App直接连接擦除后再恢复Boot引脚。这个思路对任何低功耗设备都适用省得被误判成芯片烧掉。最后再分享一点体会这套RTC定时唤醒方案跑通之后我在自己的演示板上量过功耗待机模式下整机电流大概在3到5微安加上外围传感器电源管理整体效果相当可观。实际项目里我还把唤醒间隔做成了可配置参数存放在Flash里每次唤醒后读取一次30秒只是个起点同一套逻辑可以适配十秒、几分钟甚至小时的周期。做低功耗唤醒设计建议先把三个问题想清楚唤醒后要不要从头跑、休眠期间要不要保留RAM数据、定时精度要求是多少。这三个答案确定了模式选型和代码结构基本就定下来了。我还吃过一次亏最开始默认用Stop加软复位逻辑绕了半天后来发现直接用Standby更简洁可靠。所以能用硬件复位解决的别用软件硬凑。本文还有配套的精品资源点击获取