
简介这份资源面向使用STM32F030系列、尤其是STM32F030C8T芯片进行低功耗开发的嵌入式工程师与学习者聚焦RTC闹钟定时5分钟后从待机模式唤醒这一典型场景。内容围绕LSE时钟源初始化、RTC闹钟寄存器配置、PWR唤醒事件设置以及进入待机模式的完整流程展开并涉及唤醒后PA12、PB3引脚电平翻转的GPIO配置与调试验证可帮助读者掌握低功耗设计中时钟、电源与中断协同处理的关键思路。资源包共548个文件以js、png、h、c、d、crf、o等为主涵盖源码、头文件、编译中间文件、网页素材与工程配置另含hex、map、uvprojx等工程产物压缩包约3.83MB目录结构完整便于直接导入Keil对照学习。目前已有4184人学习下载适合需要快速搭建低功耗RTC唤醒实验、排查待机与唤醒异常的中级开发者参考。1. 从一块纽扣电池撑三个月说起STM32F030 的 RTC 唤醒到底怎么配手头有个采集节点主控是 STM32F030供电是一节 CR2032。需求很朴素每 5 分钟醒一次采一组数据然后立刻睡回去。第一次做的时候我按直觉写了个HAL_Delay(300000)结果电流表指针直接顶到毫安级纽扣电池撑不过两天。后来把唤醒源换成 RTC 的 WakeUp Timer让 MCU 在 Stop 模式下待着电流才掉到微安级一颗纽扣电池能撑几个月。这个项目要解决的就是这件事用 STM32F030 的 RTC 周期唤醒功能把「定时 5 分钟」和「低功耗」这两件事同时做对。它适合正在做电池供电节点、无线传感器、便携采集设备的开发者尤其是那些已经会点灯、会串口但一碰低功耗就翻车的朋友。下面我按自己拆项目的顺序把时钟源、RTC 配置、Stop 模式进出、唤醒后的现场恢复以及几个必踩的坑一条条讲清楚。2. 低功耗方案选型为什么是 RTC WakeUp Timer 而不是 SysTick2.1 三种定时方案的功耗账在 STM32F030 上做「定时 5 分钟」能想到的路子无非三条SysTick 轮询、普通定时器中断、RTC WakeUp Timer。前两条在 Run 模式下跑内核时钟不停电流基本在 mA 级别只有 RTC 能在 Stop 模式下继续计数因为它的时钟源来自 LSE 或 LSI独立于系统时钟。方案运行模式典型电流5 分钟定时精度适用场景SysTick 轮询Run数 mA高短时延时、非电池设备TIM 中断Run数 mA高毫秒级周期任务RTC WakeUpStop微安级秒级足够电池供电、分钟级唤醒选 RTC 的核心原因不是精度而是它能在 CPU 停摆时继续走。5 分钟的定时秒级误差完全可以接受换来的是两个数量级的功耗下降。2.2 时钟源怎么选LSE 还是 LSIRTC 的时钟源有两个常见选择外部 32.768kHz 晶振LSE和内部低速 RCLSI。LSE 精度高、温漂小但需要板子上焊晶振还要配负载电容LSI 省事但精度差datasheet 里给的偏差范围能到几十个百分点温度一变更飘。我一般这么定如果板子已经有 32.768kHz 晶振无脑用 LSE如果是成本压到极致的方案且对 5 分钟这个量级的定时精度要求不严比如允许 ±1 分钟用 LSI 也能接受。下面配置按 LSE 走用 LSI 的话把RCC_OscInitStruct.LSEState换成RCC_LSI_ONRTCClockSource换成RCC_RTCCLKSOURCE_LSI即可。/* 使能电源时钟低功耗配置必须先开 PWR */ __HAL_RCC_PWR_CLK_ENABLE(); /* 配置 LSE 作为 RTC 时钟源 */ RCC_OscInitTypeDef RCC_OscInitStruct {0}; RCC_OscInitStruct.OscillatorType RCC_OSCILLATORTYPE_LSE; RCC_OscInitStruct.LSEState RCC_LSE_ON; RCC_OscInitStruct.LSEDriveCapacity RCC_LSEDRIVE_LOW; /* 低驱动能力省电 */ if (HAL_RCC_OscConfig(RCC_OscInitStruct) ! HAL_OK) { Error_Handler(); } /* 选择 RTC 时钟源为 LSE并使能 RTC 时钟 */ RCC_PeriphCLKInitTypeDef PeriphClkInit {0}; PeriphClkInit.PeriphClockSelection RCC_PERIPHCLK_RTC; PeriphClkInit.RTCClockSelection RCC_RTCCLKSOURCE_LSE; if (HAL_RCCEx_PeriphCLKConfig(PeriphClkInit) ! HAL_OK) { Error_Handler(); } __HAL_RCC_RTC_ENABLE();这段代码做了三件事开 PWR 时钟后面进 Stop 模式要用、把 LSE 起振、把 RTC 的时钟源切到 LSE。LSEDriveCapacity设成 LOW 是为了减小驱动电流晶振负载轻的时候能省一点。注意 LSE 起振需要时间HAL_RCC_OscConfig内部有超时等待如果卡在这里先查晶振有没有焊好、负载电容对不对。2.3 唤醒周期怎么算一个公式定参数RTC WakeUp Timer 的周期由WakeUpCounter和WakeUpClock共同决定。公式是唤醒周期 (WakeUpCounter 1) × 时钟分频周期WakeUpClock可选RTC_WAKEUPCLOCK_RTCCLK_DIV16、DIV8、DIV4、DIV2或1Hz。以 LSE 32.768kHz 为例选DIV16时每个计数周期是 16/32768 0.48828ms要凑 5 分钟300 秒WakeUpCounter 300 / 0.00048828 - 1 ≈ 614399。这个值超过 16 位计数器上限 65535所以得换更慢的时钟。选RTC_WAKEUPCLOCK_CK_SPRE_16BITS即 1Hz时每个计数周期正好 1 秒WakeUpCounter 300 - 1 299简单直接。代价是唤醒精度降到秒级但对 5 分钟定时完全够用。我一般就用 1Hz 这档参数好算代码也好读。3. RTC 初始化与 Stop 模式进出一个完整唤醒周期的代码链路3.1 RTC 初始化把 1Hz 唤醒定时器配起来初始化分两步先配 RTC 基本参数异步/同步分频再配 WakeUp Timer。异步分频AsynchPrediv和同步分频SynchPrediv共同把 LSE 分频到 1Hz标准配法是AsynchPrediv 0x7F127、SynchPrediv 0xFF255(1271)×(2551) 32768正好 1Hz。RTC_HandleTypeDef hrtc {0}; hrtc.Instance RTC; hrtc.Init.AsynchPrediv 0x7F; /* 异步分频 1271 */ hrtc.Init.SynchPrediv 0xFF; /* 同步分频 2551合计 32768 分频到 1Hz */ hrtc.Init.HourFormat RTC_HOURFORMAT_24; if (HAL_RTC_Init(hrtc) ! HAL_OK) { Error_Handler(); } /* 配置 WakeUp Timer1Hz 时钟源计数值 299 对应 300 秒 */ if (HAL_RTCEx_SetWakeUpTimer_IT(hrtc, 299, RTC_WAKEUPCLOCK_CK_SPRE_16BITS) ! HAL_OK) { Error_Handler(); }HAL_RTCEx_SetWakeUpTimer_IT这个函数名里的_IT表示使能中断唤醒后会触发RTC_WKUP_IRQHandler。计数值传 299 而不是 300是因为计数器从 0 开始299 对应 300 个周期。这一步配完RTC 就已经在后台数数了跟 CPU 睡不睡没关系。3.2 进 Stop 模式进之前要关什么、留什么进 Stop 模式不是一句HAL_PWR_EnterSTOPMode就完事。进之前要把不用的外设时钟关掉、GPIO 配成模拟输入或保持状态、确认唤醒源已经使能。我一般会写一个Enter_Stop_Mode函数把这些收拢在一起。void Enter_Stop_Mode(void) { /* 关闭 SysTick 中断否则会周期性唤醒 CPU */ HAL_SuspendTick(); /* 清除 WakeUp 标志避免刚进就被旧标志唤醒 */ __HAL_RTC_WAKEUPTIMER_CLEAR_FLAG(hrtc, RTC_FLAG_WUTF); __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU); /* 使能 RTC WakeUp 中断作为唤醒源 */ HAL_NVIC_EnableIRQ(RTC_IRQn); /* 进入 Stop 模式调压器保持低功耗状态 */ HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); /* 唤醒后系统时钟会切回 HSI需要重新配置 */ SystemClock_Config(); HAL_ResumeTick(); }几个关键点HAL_SuspendTick必须调不然 SysTick 每毫秒把 CPU 拽醒一次功耗下不去清WUTF标志是防止上一次的唤醒标志残留导致立即退出PWR_STOPENTRY_WFI是等待中断指令CPU 停在这里直到 RTC 中断到来。唤醒后时钟源默认切回 HSI如果外设依赖原来的时钟比如串口波特率必须重新跑一遍SystemClock_Config。3.3 唤醒中断只做标记不做重活RTC 唤醒中断服务函数里我只做一件事清标志、置一个软件标记。真正的采集、发送逻辑放到主循环里根据标记执行。原因是中断里跑太多代码会拉长唤醒后的高功耗时间而且容易出重入问题。volatile uint8_t g_wakeup_flag 0; void RTC_IRQHandler(void) { /* 判断是不是 WakeUp Timer 触发 */ if (__HAL_RTC_WAKEUPTIMER_GET_FLAG(hrtc, RTC_FLAG_WUTF) ! RESET) { __HAL_RTC_WAKEUPTIMER_CLEAR_FLAG(hrtc, RTC_FLAG_WUTF); g_wakeup_flag 1; } } int main(void) { HAL_Init(); SystemClock_Config(); MX_RTC_Init(); while (1) { if (g_wakeup_flag) { g_wakeup_flag 0; Do_Sensor_Acquire(); /* 采集 */ Do_Data_Send(); /* 发送 */ } Enter_Stop_Mode(); /* 干完活立刻睡 */ } }主循环的结构是「检查标记 → 干活 → 睡」。注意g_wakeup_flag要加volatile否则编译器优化后可能一直读寄存器里的旧值导致永远不进 if。这个坑我在早期项目里踩过现象是程序跑一次就再也不采集了查了半天才发现是优化等级开高了。3.4 唤醒后的现场恢复时钟和外设要重新初始化Stop 模式唤醒后系统时钟默认回到 HSI8MHz 或 16MHz看型号原来配的 PLL 没了。如果采集或发送依赖特定时钟比如 UART 波特率、SPI 速率不重新配时钟就会通信乱码。我的做法是在Enter_Stop_Mode返回后立刻调SystemClock_Config然后再初始化用到的外设。void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct {0}; /* 重新起振 HSI 并配 PLL 到 48MHz */ RCC_OscInitStruct.OscillatorType RCC_OSCILLATORTYPE_HSI; RCC_OscInitStruct.HSIState RCC_HSI_ON; RCC_OscInitStruct.HSICalibrationValue RCC_HSICALIBRATION_DEFAULT; RCC_OscInitStruct.PLL.PLLState RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource RCC_PLLSOURCE_HSI; RCC_OscInitStruct.PLL.PLLMUL RCC_PLL_MUL12; /* 4MHz x 12 48MHz */ HAL_RCC_OscConfig(RCC_OscInitStruct); RCC_ClkInitStruct.ClockType RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1; RCC_ClkInitStruct.SYSCLKSource RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider RCC_HCLK_DIV1; HAL_RCC_ClockConfig(RCC_ClkInitStruct, FLASH_LATENCY_1); }这段是标准的时钟恢复代码PLL 倍频系数按实际晶振和主频需求改。如果唤醒后只做 GPIO 操作、不跑通信其实可以不重配时钟省掉这段开销让唤醒时间更短、平均功耗更低。这是个取舍看你的任务里有没有时序敏感的外设。4. 避坑与排查低功耗调试里最容易翻车的五个点4.1 电流降不下来还是毫安级现象代码里明明进了 Stop 模式电流表读数还是几毫安。原因通常是某个外设时钟没关或者 GPIO 悬空导致漏电。解决进 Stop 前把不用的外设时钟用__HAL_RCC_xxx_CLK_DISABLE()关掉所有未使用的 GPIO 配成模拟输入GPIO_MODE_ANALOG避免输入缓冲器来回翻转。我一般会写个GPIO_Init_LowPower把所有引脚统一处理一遍。4.2 唤醒后串口乱码现象唤醒后第一次发串口数据上位机收到的是乱码。原因Stop 唤醒后时钟切回 HSIUART 的波特率寄存器还是按原来 PLL 时钟算的分频比不对。解决唤醒后先SystemClock_Config再重新MX_USART1_UART_Init。如果嫌麻烦可以在唤醒后延时一小段等时钟稳定但重新初始化最稳妥。4.3 定时不准5 分钟变成 4 分半现象实测唤醒周期比设定值短或长。原因用 LSI 当时钟源时RC 振荡器本身偏差大温度一变更明显。解决对精度有要求就换 LSE 晶振实在要用 LSI得在出厂时做一次校准把实测偏差写进WakeUpCounter做补偿。另外注意WakeUpCounter是 16 位超过 65535 要换更慢的时钟档。4.4 程序跑一次就不再唤醒现象第一次唤醒正常之后再也不醒。原因唤醒标志没清干净或者g_wakeup_flag没加volatile被编译器优化掉。解决中断里确认清了RTC_FLAG_WUTF主循环里的标记变量加volatile。还有一种可能是进 Stop 前没清PWR_FLAG_WU导致 PWR 认为有未处理的唤醒事件。4.5 调试器一连上功耗就正常断开就高现象用调试器挂着测电流正常拔掉调试器电流飙升。原因调试器连接时会影响 MCU 的低功耗行为而且 SWD 引脚在 Stop 模式下如果没配好会漏电。解决测真实功耗要断开调试器用独立供电加电流表SWD 引脚SWDIO、SWCLK在量产固件里配成模拟输入或上拉别悬空。5. 二次开发改造把 5 分钟唤醒做成可配置的低功耗框架5.1 把唤醒周期抽成宏方便改原始代码里 299 这个数字是写死的改周期要翻代码算。我一般抽成宏顺便把计算逻辑也写进去改的时候只动一个数。/* 唤醒周期秒改这里就行 */ #define WAKEUP_PERIOD_SEC 300 /* 1Hz 时钟源下计数值 周期 - 1 */ #define WAKEUP_COUNTER (WAKEUP_PERIOD_SEC - 1) /* 初始化时直接用 */ HAL_RTCEx_SetWakeUpTimer_IT(hrtc, WAKEUP_COUNTER, RTC_WAKEUPCLOCK_CK_SPRE_16BITS);这样改周期只动WAKEUP_PERIOD_SEC编译期自动算好计数值。注意这个宏只在 1Hz 时钟源下成立如果换成DIV16那档得重新推公式。5.2 加一个唤醒计数做长周期任务有些场景需要「每 5 分钟采一次但每 1 小时才发一次」。可以在 RTC 备份寄存器里存一个计数每次唤醒加一到 12 次再触发发送。备份寄存器在 Stop 模式下不掉电只要 VBAT 有电适合存这种状态。/* 用备份寄存器 BKP0R 存唤醒次数 */ uint32_t wake_count HAL_RTCEx_BKUPRead(hrtc, RTC_BKP_DR0); wake_count; if (wake_count 12) { /* 12 x 5min 1 小时 */ wake_count 0; Do_Data_Send(); /* 到点才发送 */ } HAL_RTCEx_BKUPWrite(hrtc, RTC_BKP_DR0, wake_count);HAL_RTCEx_BKUPRead/Write操作的是 RTC 备份寄存器掉电不丢有 VBAT 时。这个技巧在需要「快采慢发」的节点里很实用能进一步降低无线模块的开启频率。5.3 验证方法怎么确认真的省电了改完代码别急着下结论按这个顺序验一遍先用调试器跑通逻辑确认每次唤醒都能正确采集和发送然后断开调试器用万用表或电流表串在供电回路里测 Stop 模式电流应该在微安级最后挂上电池跑一整天看电压下降曲线是否符合预期。我一般会算一笔账假设 Stop 电流 2μA、唤醒时 10mA 持续 50ms、周期 300 秒平均电流约 2μA 10mA×0.05/300 ≈ 3.7μACR2032 容量 220mAh 的话理论能撑 6 万多小时实际打对折也有几年。从那以后我每次做低功耗项目都强制走一遍「断开调试器测电流」这一步因为调试器连着的时候数据全是假的。希望这些能帮到你少走点我当年踩过的弯路。本文还有配套的精品资源点击获取