☰
STM32F030 RTC 1秒中断:低功耗唤醒的完整实现与坑点解析
2026/10/14 18:44:39 网站建设 项目流程

用STM32F030R8Tx做RTC 1秒中断,这事儿看着不起眼,实际做起来坑不少。之前有个电池供电的环境监测模块,主控就是F030R8T6,要求MCU大部分时间睡觉,每隔1秒醒来采集一次传感器数据再继续睡。第一反应是用SysTick或者普通定时器,但这两个东西在低功耗模式下根本不走,板子上又没预算加外部RTC芯片,最后只能把主意打在片内RTC上。F0系列这颗RTC远比F103上那个简单计数器强大——完整日历、闹钟、唤醒定时器都集成在里面,只要配置对了,1秒中断可以非常干净地实现,还能顺便唤醒Stop模式。

我把整个调试过程的方案选型、HAL库配置顺序、代码实现和踩过的坑整理出来,项目里要复用的直接抄,想弄清楚原理的也能看明白。

1. 需求拆解与方案选型:为什么是RTC而不是定时器

1.1 秒中断背后到底解决什么问题

"1秒中断"听上去只是个定时需求,但它背后的真实场景往往有几种,决定了你用什么器件:

  • 低功耗周期唤醒:产品99%时间在Stop模式待机,每秒醒一次做几件事然后继续睡。这种场景普通定时器直接出局,因为Stop模式下TIM和SysTick的时钟都停了,只有RTC、外部中断等少数源能唤醒。
  • 系统时间刷新:驱动电子钟、字幕屏或者本地日志,每秒需要把日历时间推送到显示/存储。RTC本身在走日历,但如果没有"秒中断"通知,外部只能不断轮询,浪费CPU。
  • 周期任务调度:比如传感器以1秒为周期采样,用RTC唤醒定时器做节拍,可以省出通用定时器资源做其他事。

我这次是第一种加第三种混合:一秒采样一次,中间休眠。既然要休眠,RTC就是绕不开的选择。

1.2 三种实现方案横评:Alarm、WakeUp Timer、定时器

先看普通定时器方案。TIM在运行模式下完全没问题,1秒中断太容易了,但芯片一旦进入Stop模式,TIM外设时钟被关闭,中断自然没了。即便不睡觉,定时器也不能在掉电后保留节拍。如果你只需要运行模式下的秒节拍,TIM没毛病,但别指望它扛低功耗。

再看RTC闹钟(Alarm)方案。F0的闹钟比较的是完整日历字段,年月日时分秒都参与匹配。要做出严格意义的"每秒中断",逻辑上很别扭:

  • 把分钟、小时、日期字段掩掉,只比较秒值,那得到的匹配周期其实是60秒,不是1秒。
  • 在闹钟中断里不断把闹钟时间改成当前时间加1秒,倒是能做到,但每次都要操作寄存器、处理秒边界,还会有累加误差,代码也啰嗦。

所以Alarm更适合"每天某个时刻""每周一上午9点"这种定点事件,用来做1秒节拍是舍近求远。

最后是唤醒定时器(WakeUp Timer)方案。F0 RTC内部带一个16位递减计数器,它才是做周期中断的正主。配置成1Hz计数时钟,计数器初值设0,每个计数周期就是1秒,递减到0自动产生唤醒事件,而且计数器具备自动重载能力,中断处理完后下一次节拍自动开始,不需要软件手动重新装载。这就是为周期性唤醒设计的硬件,跟1秒中断的诉求完全贴合。

三种方案对比很快能得出结论:用RTC WakeUp Timer。

方案能否1秒周期低功耗可用是否自动重载适合场景
SysTick/普通TIM能否是运行模式节拍
RTC Alarm掩码否能否定点闹钟
RTC Alarm中断重设勉强能能否不推荐
RTC WakeUp Timer能能是周期唤醒/秒中断

1.3 时钟源选型:LSE还是LSI

RTC要工作,先得有它的时钟源。F0里RTC时钟可以从LSE、LSI或者HSE分频得到。这步选错,后面精度和稳定性都无从谈起。

LSE是外部32.768kHz晶振,接在PC14/PC15上。频率固定,晶体精度高,温度漂移小,用它做日历和秒中断,走一个月误差通常只在几秒到几十秒级别。代价是PCB上要加一颗晶振和两个负载电容,占用空间也增加成本。对时间敏感的产品,LSE是默认答案。

LSI是内部RC振荡器,F0系列典型值40kHz,范围在30kHz到50kHz之间,不用任何外部器件,省成本省面积。但它是RC振荡器,随温度和电压漂移明显。用LSI做1秒中断,当前能配到1秒,环境温度一变频率就偏,累积误差很快。更适合那种"只要周期大致在秒级、不需要精确时间"的场景。

HSE分频这条路一般没人走。HSE本来就可以停振省电,RTC在低功耗下依赖它不合理,除非系统里有特殊考虑。

我这次项目对时间精度有点要求,板子又正好规划了LSE晶振的位置,直接选LSE。如果后面有低成本要求,换成LSI只需要改一部分配置,代码结构不用动。这个兼容性在设计时留好,后面改动会轻松很多。

2. F030的RTC时钟域与CubeMX配置细节

2.1 备份域、写保护与RCC_BDCR的关系

F0的RTC挂在备份域里,APB总线只是访问它的窗口,真正驱动它的是RTC专用时钟域。备份域的寄存器由VBAT引脚供电,主电源掉电时RTC照样走。这也意味着RTC相关寄存器不是想写就能写,要先解开两层关卡。

第一层是备份域写保护。芯片复位后,RTC的寄存器处于写保护状态,需要先把PWR时钟打开,然后调用HAL_PWR_EnableBkUpAccess()把备份域访问使能位置位。这步不做,后面操作RTC时钟源配置会因为寄存器写不进去而失败,或者行为诡异。

第二层是RTC时钟源选择在RCC_BDCR寄存器里,它属于备份域控制寄存器,修改它要在解锁备份域之后。RTCSEL位决定是LSE、LSI还是HSE,LSEON位控制LSE振荡器开关。初始化MspInit里那一串代码,本质上就是先把写保护打开,再配置LSE,再把RTC使能位设置好。

有个容易忽略的点:备份域寄存器在系统复位后并不会被清掉,RTC配置一次就记住了。所以同一个芯片反复下载程序、反复调试时,可能上次的配置还残留在RTC寄存器里,导致CubeMX重新生成的初始化代码走到某一步不正常。这种情况不用慌,把备份域整体复位一下就能恢复,后面排查章节会细讲。

2.2 异步分频、同步分频与1Hz时钟的产生

F0 RTC内部有异步预分频器和同步预分频器,配合外部时钟源生成日历用的1Hz信号。异步预分频值AsynchPrediv和同步预分频值SynchPrediv之间有关系:

RTCCLK = (AsynchPrediv + 1) * (SynchPrediv + 1) * fCKSPRE

fCKSPRE就是日历秒信号,也就是1Hz。LSE频率是32768Hz,最简单的一组参数是:

  • AsynchPrediv = 127
  • SynchPrediv = 255

代入算一下:(127 + 1) * (255 + 1) = 128 * 256 = 32768,刚好把32.768kHz分频到1Hz。这是CubeMX默认推荐的值,也是一般HAL例程里最常见的配法。

如果换成LSI当RTC时钟,比如LSI实测是40000Hz,那这两组分频值就得重算。先把40000拆成可以整除的因子:40000 = 160 * 250,所以可以设AsynchPrediv = 159,SynchPrediv = 249,得到1Hz。但LSI本身频率会漂,即便在某个温度点算准了,环境一变又偏。这也是我在项目里坚持用LSE的原因。

WakeUp Timer的1Hz模式,实际就是直接拿这个同步预分频器输出的一秒信号当计数时钟。所以分频参数不对,WakeUp的"1Hz"也不对,1秒中断自然不准。两者是串在一起的。

2.3 CubeMX里的逐项配置

CubeMX配置F0 RTC比较直观,我按顺序列一下。

第一步,RCC页面把LSE设为Crystal/Ceramic Resonator。如果板子没按晶振,这里选BYPASS Clock Source或者直接用LSI。

第二步,RTC页面把Activate Clock Source勾上,时钟源选LSE。然后激活Calendar,把初始时间日期设好。CubeMX可以在配置界面直接填日期时间,生成的代码会在初始化时写进RTC。

第三步,关键一步,WakeUp Timer配置。在RTC页面的WakeUp Timer区域里,WakeUp Clock选CK_SPRE 1Hz,WakeUp Counter设为0。这样生成代码后,HAL_RTCEx_SetWakeUpTimer_IT里的参数就是精确的1秒周期。

第四步,使能WakeUp Timer Interrupt。NVIC页面里RTC global interrupt必须打开,优先级根据项目设置,我一般给0或者1。

第五步,生成代码。CubeMX会在HAL_RTC_MspInit里自动生成LSE配置、RTC时钟源选择、备份域解锁和NVIC使能代码,你不用手动写。真正需要手动改的地方不多,后面代码章节会讲。

生成完代码后有个小检查点:看HAL_RTC_MspInit里的顺序。不同版本的CubeMX生成顺序略有差异,有的先生成RTC使能再生成时钟配置,有的反过来。只要编译能过、下载后能正常进入主循环,顺序通常就没问题。

3. HAL库核心API与代码级实现

3.1 HAL_RTC_MspInit:底层使能的标准写法

CubeMX生成代码后,MspInit基本不用大动。但不依赖CubeMX、纯手写工程时,这一段得能自己写出来。以下是我整理后的标准写法,适用于F030R8T6配合LSE:

void HAL_RTC_MspInit(RTC_HandleTypeDef* hrtc) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_PeriphCLKInitTypeDef PeriphClkInitStruct = {0}; if (hrtc->Instance == RTC) { __HAL_RCC_PWR_CLK_ENABLE(); HAL_PWR_EnableBkUpAccess(); RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_LSE; RCC_OscInitStruct.LSEState = RCC_LSE_ON; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE; if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { Error_Handler(); } PeriphClkInitStruct.PeriphClockSelection = RCC_PERIPHCLK_RTC; PeriphClkInitStruct.RTCClockSelection = RCC_RTCCLKSOURCE_LSE; if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInitStruct) != HAL_OK) { Error_Handler(); } __HAL_RCC_RTC_ENABLE(); HAL_NVIC_SetPriority(RTC_IRQn, 0, 0); HAL_NVIC_EnableIRQ(RTC_IRQn); } }

注意几个顺序。HAL_PWR_EnableBkUpAccess必须在配置BDCR寄存器之前,否则LSE配置和时钟源选择都会被写保护挡住。__HAL_RCC_RTC_ENABLE要放在时钟源配置之后,确保RTC模块拿到正确的时钟再开始工作。F0的Cortex-M0内核不支持像M3/M4那样的中断优先级分组,HAL_NVIC_SetPriority的第三个参数无效,第二个参数只能在0到3之间取值。

3.2 时间日期初始化:首次上电才需要写

RTC日历寄存器只要初始化过一次,在VBAT供电正常的情况下会一直走。每次MCU复位都去写时间日期反而不合适,会把系统拉回固定起点。更好的办法是先检查RTC初始化标志,只在需要时写入初值。

if ((RTC->ISR & RTC_ISR_INITS) == 0) { RTC_TimeTypeDef sTime = {0}; sTime.Hours = 0; sTime.Minutes = 0; sTime.Seconds = 0; RTC_DateTypeDef sDate = {0}; sDate.WeekDay = RTC_WEEKDAY_MONDAY; sDate.Month = RTC_MONTH_JANUARY; sDate.Date = 1; sDate.Year = 0; if (HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN) != HAL_OK) { Error_Handler(); } if (HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN) != HAL_OK) { Error_Handler(); } }

INITS是RTC的日历初始化状态位,置1表示日历寄存器已经写入过有效值。这个标志在系统复位后不会清零,只有VBAT掉电或者备份域被复位才会回到0。用这个位做判断,既避免了反复写时间,也天然符合"上电设置一次、之后保持走时"的使用逻辑。

如果项目里每次开机都需要重新对时,比如产品有按键校时功能,那就不需要这个判断,直接HAL_RTC_SetTime就好。但强烈建议保留读时间的接口,让系统随时能拿到当前日历。

3.3 WakeUp Timer配置:1秒中断的真正开关

日历初始化完,重头戏来了,配置唤醒定时器:

RTC_WakeUpTypeDef sWakeUp = {0}; sWakeUp.WakeUpClock = RTC_WAKEUPCLOCK_CK_SPRE_1HZ; sWakeUp.WakeUpCounter = 0; if (HAL_RTCEx_SetWakeUpTimer_IT(&hrtc, &sWakeUp) != HAL_OK) { Error_Handler(); }

先说参数怎么理解。WakeUpClock选RTC_WAKEUPCLOCK_CK_SPRE_1HZ,意思是唤醒计数器直接以日历秒信号作为计数时钟。WakeUpCounter设为0,对应周期公式:

WakeUp周期 = (WakeUpCounter + 1) * 计数时钟周期

代入就是 (0 + 1) * 1秒 = 1秒。如果你需要2秒中断,这里就填1,5秒就填4。这个公式要记牢,很多人把计数器值和秒数搞混,填成0以为没中断,其实0正好代表1个周期。

如果WakeUpClock改选RTC_WAKEUPCLOCK_RTCCLK_DIV系列,计数时钟就是RTC时钟分频后的信号,周期公式变成 (WakeUpCounter + 1) * (2^k / RTCCLK)。以LSE为例,RTCCLK=32768,选/16分频即2048Hz,想要1秒就得填2047。这种方式能提供更细的时间分辨率,但计算复杂一些,常规1秒中断直接用1Hz模式最省心。

HAL_RTCEx_SetWakeUpTimer_IT内部会自动处理两件事:清除可能残留的唤醒标志,使能唤醒中断。它还会把计数器设为自动重载,中断触发后计数器自动恢复初始值继续递减,不需要你在中断服务程序里重新调用任何设置函数。这一点我在实际调试时专门验证过,连续跑几个小时,秒中断的间隔没有出现一次跳变。

3.4 中断服务函数与回调:中断链的最后两环

中断来了之后,硬件自动跳到RTC_IRQHandler。标准HAL程序在stm32f0xx_it.c里只写一行:

void RTC_IRQHandler(void) { HAL_RTCEx_WakeUpTimerIRQHandler(&hrtc); }

HAL_RTCEx_WakeUpTimerIRQHandler内部检查唤醒事件标志,确认后清除标志并且调用用户回调函数HAL_RTCEx_WakeUpTimerEventCallback。这里需要记住:HAL库设计的是"中断服务函数里做标志处理,用户业务写在回调函数里"。你如果直接在RTC_IRQHandler里写业务代码,绕过了HAL的回调机制,也能工作,但和HAL的设计风格不一致,移植性也差。

回调函数长这样:

volatile uint32_t g_sec_counter = 0; void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc) { g_sec_counter++; HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); }

实际项目里,回调只应该做最轻量的事情。我见过有人直接在回调里调HAL_UART_Transmit往串口打印,一次打印耗时几十毫秒,导致后续中断被阻塞,逻辑紊乱。正确做法是回调里置一个标志或者累加一个计数器,真正的数据处理放到主循环。比如:

volatile uint8_t g_sec_flag = 0; void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc) { g_sec_flag = 1; }

主循环检测到这个标志后清零并处理业务。秒中断只是"时间到了"的通知,不是让你在中断里把活干完。

4. 完整代码与演示效果

4.1 初始化流程与主循环

整合前面的内容,一个可直接验证的最小工程结构是这样:

RTC_HandleTypeDef hrtc; volatile uint32_t g_sec_counter = 0; int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_RTC_Init(); while (1) { if (g_sec_flag == 1) { g_sec_flag = 0; g_sec_counter++; HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } } }

SystemClock_Config用CubeMX默认生成的48MHz主频配置即可。MX_GPIO_Init里把LED引脚配置为输出,我这里用的是板载LED对应的普通GPIO,具体引脚号按你自己的板子改。

MX_RTC_Init整合了RTC句柄初始化、日历初始化、唤醒配置三部分:

void MX_RTC_Init(void) { hrtc.Instance = RTC; hrtc.Init.HourFormat = RTC_HOURFORMAT_24HOUR; hrtc.Init.AsynchPrediv = 127; hrtc.Init.SynchPrediv = 255; hrtc.Init.OutPut = RTC_OUTPUT_DISABLE; hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH; hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN; if (HAL_RTC_Init(&hrtc) != HAL_OK) { Error_Handler(); } if ((RTC->ISR & RTC_ISR_INITS) == 0) { RTC_TimeTypeDef sTime = {0}; sTime.Hours = 0; sTime.Minutes = 0; sTime.Seconds = 0; if (HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN) != HAL_OK) { Error_Handler(); } RTC_DateTypeDef sDate = {0}; sDate.WeekDay = RTC_WEEKDAY_MONDAY; sDate.Month = RTC_MONTH_JANUARY; sDate.Date = 1; sDate.Year = 0; if (HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN) != HAL_OK) { Error_Handler(); } } RTC_WakeUpTypeDef sWakeUp = {0}; sWakeUp.WakeUpClock = RTC_WAKEUPCLOCK_CK_SPRE_1HZ; sWakeUp.WakeUpCounter = 0; if (HAL_RTCEx_SetWakeUpTimer_IT(&hrtc, &sWakeUp) != HAL_OK) { Error_Handler(); } }

这段代码拿到F030R8T6上编译下载,LED应该以1秒周期稳定翻转。再把示波器接到LED引脚上看波形,相邻上升沿间隔应该正好是1秒。

4.2 读取当前时间:别踩同步标志的坑

秒中断跑起来以后,读取RTC时间就顺理成章了。HAL库提供了现成接口:

RTC_TimeTypeDef sTime = {0}; HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN); printf("%02d:%02d:%02d\n", sTime.Hours, sTime.Minutes, sTime.Seconds);

但F0的RTC在APB时钟和RTC时钟不同步时,直接读时间寄存器的值可能不可靠。稳妥的做法是先检查同步标志RSF,或者读之前加一点等待:

uint32_t timeout = 0xFFFF; while (((RTC->ISR & RTC_ISR_RSF) == 0) && (timeout-- > 0)) { ; } RTC_TimeTypeDef sTime = {0}; HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);

实际测试中,RTC正常运行后RSF基本是置位状态,等待循环会瞬间通过。但在刚切换到RTC时钟或者复位后立刻访问时,这个等待能避免读到全0或者旧数据的尴尬。加上这个判断不会影响性能,建议保留。

另外注意F0系列RTC并没有F4那种"读完时间必须读日期才解锁影子寄存器"的设计,HAL_RTC_GetTime可以独立使用。如果你从F4平台移植代码,别把读日期的逻辑强行搬过来,F0不需要。

4.3 部署到低功耗流程:唤醒后再复位定时器

1秒中断配合低功耗才是完整形态。代码逻辑是进入Stop模式前,保证RTC唤醒中断处于使能状态,然后执行WFI让内核睡眠:

HAL_RTCEx_SetWakeUpTimer_IT(&hrtc, &sWakeUp); HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); SystemClock_Config();

注意F0从Stop模式唤醒后,系统时钟默认回到HSI,需要重新调用SystemClock_Config把时钟配置回目标频率。RTC本身不会因为Stop模式而停止,因为它有自己的时钟域和电源域,这正是RTC秒中断和普通定时器最本质的区别。

WakeUp唤醒事件在F0内部会连接到EXTI Line 20,HAL_RTCEx_SetWakeUpTimer_IT内部已经处理了相应配置,所以直接HAL_PWR_EnterSTOPMode就能被RTC秒中断唤醒。这是我在实际落板时验证过的,不是纸面结论。

如果项目只要秒中断、不需要低功耗,那WakeUpTimer照常配置即可,不用进Stop模式,每秒进入一次中断回调,表现形式和普通定时器中断没有差别。

5. 常见问题与排查技巧实录

5.1 程序卡死在HAL_RTC_Init里面怎么办

一类经典问题是复位后初始化卡死。HAL_RTC_Init内部会检查日历是否处于初始化状态,并尝试配置预分频器,如果LSE没有输出时钟,内部等待会超时,最终进入Error_Handler。我当时第一次做这个项目,下载后直接卡死,排查了一圈发现是LSE晶振没焊接好。

处理思路是逐层排查:

  • 用示波器测PC14/PC15有没有32.768kHz波形,没有波形检查晶振引脚焊接、负载电容参数。负载电容一般在6pF到12.5pF之间,具体看晶振规格书。
  • 在MspInit里把LSE配置断点打开,单步执行看LSEON后有没有很快进入LSE RDY状态。F0的RCC_BDCR里有LSERDY位,HAL_RCC_OscConfig内部会等待它置位,如果一直不置位就是起振失败。
  • 排除法:临时把时钟源改成LSI跑一遍,能过初始化说明RTC本身没问题,问题锁定在LSE电路上。

还有一类情况是同一个芯片之前被其他程序配置过RTC,备份域留下了不符合预期的时间或者模式。这种时候可以先备份需要保留的数据(如果有),然后强制复位备份域:

__HAL_RCC_BACKUPRESET_FORCE(); __HAL_RCC_BACKUPRESET_RELEASE();

执行完后所有RTC配置都恢复到出厂状态,再走初始化流程。这段代码写完最好立刻从正式程序里移除,否则每次上电都清RTC,日历永远记不住时间。

5.2 WakeUp中断不进入:先查清标志位

中断没有的现象在调试时很常见。代码编译下载后LED纹丝不动,说明要么没进RTC_IRQHandler,要么进了但没有触发回调。这一步一步查,别上来就怀疑库写错了。

先查NVIC设置。RTC_IRQn的优先级和使能在MspInit里配置后,确认没有被其他代码覆盖。F0的Cortex-M0内核只有0到3四个优先级,你设成0或者1都没问题。

再查唤醒标志。程序运行一小段时间后,在调试器里看RTC->ISR寄存器的WUTF位。正常情况下WakeUp模式配置正确时,每秒这个位会定时翻转,如果一直是0说明计数器没有在跑,如果一直是1说明中断没有被响应清除。

再查EXTI配置。F0中RTC唤醒事件连接到EXTI Line 20,如果EXTI没有使能,即使RTC内部产生了唤醒事件,也无法送达NVIC。HAL_RTCEx_SetWakeUpTimer_IT按理应该处理好,但GitHub上有不少开发者反馈个别HAL版本有遗漏,所以手动补一遍也正常:

EXTI->IMR |= RTC_EXTI_LINE_WAKEUPTIMER_EVENT; EXTI->RTSR |= RTC_EXTI_LINE_WAKEUPTIMER_EVENT; EXTI->EMR |= RTC_EXTI_LINE_WAKEUPTIMER_EVENT;

这三个寄存器分别是中断屏蔽、上升沿触发选择、事件屏蔽。RTC_EXTI_LINE_WAKEUPTIMER_EVENT在F0的HAL库里定义成了常量。补上这段后,中断应该就能正常送达。

最后查回调函数。HAL_RTCEx_WakeUpTimerIRQHandler找到后,在HAL_RTCEx_WakeUpTimerEventCallback里打断点,看能不能停住。如果回调能进但LED不闪,那就是GPIO初始化的问题,和RTC无关了。

5.3 用LSI时1秒中断不准怎么办

如果项目因为成本选了LSI,那1秒中断的精度问题迟早会冒出来。LSI典型值40kHz,但实际在30kHz到50kHz之间波动。配置成1Hz模式后,秒信号由LSI分频而来,LSI偏多少,秒就偏多少。

我的建议是先把LSI实际频率测准再配分频系数。可以配置LSI连接到一个TIM输入捕获或者MCO输出,实测当前温度下的精确频率,然后把AsynchPrediv和SynchPrediv按实际频率重算。但这只解决"当前温度下的校准",温度一变又跑了。

F0系列RTC没有类似F4/L4那种精细校准寄存器,这一点设计选型时要注意。如果产品对时间精度有硬性要求,老老实实用LSE,别在LSI上硬撑。LSI适合的场景是"周期大概一秒就行"的低功耗外围唤醒,不适合计时。

5.4 仿真调试中的几个隐蔽现象

F0的RTC是异步时钟域模块,调试器造成的现象容易让人误判。最常见的是单步调试时发现RTC时间不走了。其实RTC一直在走,只是内核停下来后,调试器对RTC寄存器的刷新时机不同,看着像停止。实际上,在调试界面刷新寄存器窗口,秒值会跳变。

另外一个现象是断点停在中断回调里,超时后程序跑飞或者死循环。原因是在断点停留期间,RTC中断标志可能又多次置位,恢复执行后中断服务程序把多个事件连续处理,如果回调里刚好有阻塞操作,就会表现异常。这不是RTC的问题,是我在调试时人为放大了回调耗时的影响。建议回调保持短小,调试时不要长时间停在中断里。

还有个老生常谈:拔掉调试器单独运行和连接调试器运行时行为不同。业内不少工程师在F0上遇到过,连接调试器时LSE起振正常,拔掉后第一次上电程序卡死在RTC初始化。这种多半是晶体负载电容匹配不佳导致起振裕量不足,调试器连接时引脚负载变化帮助了起振。加大一点负载电容,或者换起振能力更强的晶振方案能改善。

5.5 注意事项速查表

把常踩的坑浓缩成一个表,放在工程注释里也方便:

现象根因处理方式
HAL_RTC_Init卡死LSE未起振检查晶振/负载电容,或临时切LSI
RTC时间一复位就丢VBAT没接检查VBAT引脚供电
反复上电日期回到初值每次写时间日期用INITS标志判断首次写入
WakeUp中断不触发EXTI Line 20未开手动配置IMR/RTSR/EMR
秒中断偶尔跳变回调耗时超过1秒回调只置标志,主循环处理
单步调试看不出秒翻转异步时钟域刷新全速运行观察LED/寄存器
拔掉调试器后卡初始化LSE起振裕量不足调整负载电容

6. 一点扩展经验

6.1 用备份寄存器做掉电记忆

RTC的备份寄存器在VBAT保持期间不丢数据,这是很多设计都用上但新手容易忽略的。秒中断计数、设备状态、校准参数都可以存在备份寄存器里,主电源断了再恢复,数据还在。我在环境监测模块里就把采集累计次数存在备份寄存器0里,每次秒中断累加一次,备份寄存器读写接口是HAL_RTCEx_BKUPWrite和HAL_RTCEx_BKUPRead。

需要注意的是备份寄存器数量在F030上是有限的,我记得是5个32位寄存器。存放数据前先规划好,别把用不上的数据也往里塞。

6.2 秒中断+校准的思路

RTC走时总会有误差,LSE晶振本身的频偏一般在20ppm以内,对应一个月误差大约一两分钟。如果产品要求更高精度,可以在秒中断里做软件校准。方法是在固定时间段内统计参考时钟源的脉冲数,算出实际RTC秒信号偏差,然后定期调整日历时间。

F0没有硬件的亚秒校准单元,但利用WakeUp Timer可以实现微调。比如使用RTCCLK分频模式代替1Hz模式,把计数初值在248和250之间按统计结果切换,等效于平均频率接近精确值。这个话题展开又是一篇文章,我这里只提醒一点:软件校准方案要从设计一开始就预留接口,后面补会比较痛苦。

6.3 别在中断回调里做重活

这条说再多也不为过。中断回调相当于在硬件中断上下文里执行用户代码,时间过长会阻塞其他中断,甚至因为RTC唤醒事件频繁到来导致系统看起来像死机。我的习惯是秒中断只做三件事:置任务标志、清看门狗、累加计数器。采样、通信、存储全部放主循环。

在项目里加看门狗也要小心,看门狗喂狗放在秒中断虽然能保证系统不死,但掩盖了主循环卡死的问题。更好的是主循环里喂狗,秒中断只放标志,这样看门狗能真实反映整个系统的工作状态。

这套RTC秒中断的方案我在F030R8T6上验证过,也在同系列的F030C6上移植过,逻辑完全一致,只是引脚和外设映射有差异。做低功耗产品、做精确定时任务,这条路可以放心走。

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

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

立即咨询