简介:这份资源面向使用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)和内部低速 RC(LSI)。LSE 精度高、温漂小,但需要板子上焊晶振,还要配负载电容;LSI 省事,但精度差,datasheet 里给的偏差范围能到几十个百分点,温度一变更飘。我一般这么定:如果板子已经有 32.768kHz 晶振,无脑用 LSE;如果是成本压到极致的方案,且对 5 分钟这个量级的定时精度要求不严(比如允许 ±1 分钟),用 LSI 也能接受。下面配置按 LSE 走,用 LSI 的话把RCC_OscInitStruct.LSEState换成RCC_LSI_ON,RTCClockSource换成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 = 0x7F(127)、SynchPrediv = 0xFF(255),(127+1)×(255+1) = 32768,正好 1Hz。RTC_HandleTypeDef hrtc = {0}; hrtc.Instance = RTC; hrtc.Init.AsynchPrediv = 0x7F; /* 异步分频 127+1 */ hrtc.Init.SynchPrediv = 0xFF; /* 同步分频 255+1,合计 32768 分频到 1Hz */ hrtc.Init.HourFormat = RTC_HOURFORMAT_24; if (HAL_RTC_Init(&hrtc) != HAL_OK) { Error_Handler(); } /* 配置 WakeUp Timer:1Hz 时钟源,计数值 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 模式唤醒后,系统时钟默认回到 HSI(8MHz 或 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 唤醒后时钟切回 HSI,UART 的波特率寄存器还是按原来 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μA,CR2032 容量 220mAh 的话理论能撑 6 万多小时,实际打对折也有几年。从那以后我每次做低功耗项目,都强制走一遍「断开调试器测电流」这一步,因为调试器连着的时候数据全是假的。希望这些能帮到你,少走点我当年踩过的弯路。本文还有配套的精品资源,点击获取
简介:这份资源面向使用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)和内部低速 RC(LSI)。LSE 精度高、温漂小,但需要板子上焊晶振,还要配负载电容;LSI 省事,但精度差,datasheet 里给的偏差范围能到几十个百分点,温度一变更飘。我一般这么定:如果板子已经有 32.768kHz 晶振,无脑用 LSE;如果是成本压到极致的方案,且对 5 分钟这个量级的定时精度要求不严(比如允许 ±1 分钟),用 LSI 也能接受。下面配置按 LSE 走,用 LSI 的话把RCC_OscInitStruct.LSEState换成RCC_LSI_ON,RTCClockSource换成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 = 0x7F(127)、SynchPrediv = 0xFF(255),(127+1)×(255+1) = 32768,正好 1Hz。RTC_HandleTypeDef hrtc = {0}; hrtc.Instance = RTC; hrtc.Init.AsynchPrediv = 0x7F; /* 异步分频 127+1 */ hrtc.Init.SynchPrediv = 0xFF; /* 同步分频 255+1,合计 32768 分频到 1Hz */ hrtc.Init.HourFormat = RTC_HOURFORMAT_24; if (HAL_RTC_Init(&hrtc) != HAL_OK) { Error_Handler(); } /* 配置 WakeUp Timer:1Hz 时钟源,计数值 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 模式唤醒后,系统时钟默认回到 HSI(8MHz 或 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 唤醒后时钟切回 HSI,UART 的波特率寄存器还是按原来 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μA,CR2032 容量 220mAh 的话理论能撑 6 万多小时,实际打对折也有几年。从那以后我每次做低功耗项目,都强制走一遍「断开调试器测电流」这一步,因为调试器连着的时候数据全是假的。希望这些能帮到你,少走点我当年踩过的弯路。本文还有配套的精品资源,点击获取