简介:APM32F407实现RTC定时器的完整工程,基于Cortex-M4内核的APM32F4系列单片机,适合需要实时时钟与低功耗唤醒功能的嵌入式开发者直接参考。压缩包共97个文件,主体为46个C源文件与46个头文件,覆盖驱动、BSP、CMSIS与标准外设库,另含2个汇编启动文件、MDK工程文件(uvprojx/uvoptx)以及可烧录的hex文件,整体389KB,目录模块清晰,可按 Drivers、CMSIS、User 分层阅读。工程实现了RTC初始化、时钟源配置、日历时间设置、闹钟中断处理和低功耗电源管理等关键环节,代码已编译运行验证,涵盖驱动层到应用层,可直接导入Keil编译烧录,也可移植到其他APM32F4型号。目前已有86人学习,适合正在调试RTC周期闹钟、日期变更或STOP/STANDBY模式下保持计时的开发者;对照源码可快速理解APM32F4系列RTC的完整工作流程,并为物联网设备、智能仪表、监控系统等场景提供可靠的时间基准,缩短项目开发周期。
1. 把APM32F407的RTC独立出来用
APM32F407经常被拿去替代STM32F407做电机控制、采集网关这类角色,但实际项目里用到RTC(Real-Time Clock)时,很多人只是把官方例程的初始化函数复制过来,然后就不管了。直到产品在低功耗模式下频繁唤醒、日历走时漂移、或者备份域寄存器在复位后丢失,才会回来查RTC的时钟源和电源配置。这个项目正是围绕APM32F4系列单片机的RTC定时器展开的,代码基于标准外设库(StdPeriphDriver)编写,已经编译验证过,产物直接输出atk_f407.hex,配套了USMART调试组件,适合做低功耗设备、定时采集、周期唤醒以及RTC日历管理。如果你手里正好是APM32F407或者其他APM32F4系列,跟着这份工程能把RTC从初始化到中断处理一次理清。
2. 先了解RTC外设在APM32F407内部是怎么工作的
2.1 RTC、备份域和电源的关系
APM32F407的RTC并不是和GPIO、UART一样挂在APB1总线上随用随开的普通外设。它和备份寄存器、硬件时钟校准部分一起挂在备份域(Backup Domain)里,电源来自VBAT引脚或主电源VDD。当系统进入STOP或STANDBY模式、甚至主电源断电但VBAT有电时,RTC依然能依靠备份域电源维持走时。这一点决定了它的使用习惯:凡是涉及RTC的配置,都要先打开PWR和BKP外设时钟,并且对备份域做写保护解除,否则寄存器写不进去。很多APM32F407项目在调试RTC时遇到“初始化成功但时间不跑”的问题,都是因为掉电后备份域没有被正确复位,或者代码里没有处理RTC寄存器在复位后的状态。
在校验RTC是否真正跑起来时,不建议只看RTC_GetTime返回的值,因为读到的数据可能来自上次复位前遗留的备份寄存器。更可靠的办法是读取RTC的初始化状态标志,比如RTC_InitStatus,或者在备份寄存器里写入一个约定的魔数,启动时读取魔数判断。这个状态在备份域复位后会被清除,应用启动时可以据此决定是直接读取时间,还是需要重新执行SetTime流程。工程代码里把RTC初始化放在main函数入口附近,也是为了保证在用户代码执行之前,备份域和RTC状态是确定的。
2.2 LSE和LSI,选错了时间精度就保不住
APM32F407的RTC时钟源支持LSE(外部32.768kHz晶振)、LSI(内部约32kHz RC)以及HSE分频。其中LSE是RTC高精度方案的标准选择,因为32.768kHz经过15级二分频后正好是1Hz,日历寄存器每秒加一次,误差主要取决于晶振本身的频率偏差,通常能做到几十ppm以内。LSI虽然不需要外部晶振,能省两个引脚和一颗物料,但它的频率受温度和电压影响明显,实际误差在几百ppm到几千ppm量级,用作RTC日历会一星期差出几十秒甚至几分钟。
| 对比项 | LSE | LSI |
|---|---|---|
| 频率 | 外部32.768kHz晶振 | 内部约32kHz RC |
| 外部器件 | 需要晶振和两颗负载电容 | 不需要 |
| 走时精度 | 高,通常几十ppm | 低,漂移明显 |
| 温度影响 | 小 | 大 |
| 功耗 | 可接受 | 较低 |
| 适用场景 | 日历、定时唤醒、事件记录 | 粗略计时、系统无法启动时的兜底 |
另外很多人误以为RTC的精度只由晶振决定,其实PCB上OSC32_IN和OSC32_OUT引脚之间的走线、负载电容的取值也很关键。APM32F407的数据手册推荐的负载电容常见在6pF到12.5pF之间,具体以实际晶振规格为准。布线时尽量把晶振靠近MCU,两个引脚之间不要铺地铜,周围不要走高速信号。如果产品在高温或高湿环境下工作,优先选择带温度补偿的晶振,单纯靠软件校准很难解决温度漂移问题。
2.3 备份域复位行为决定初始化策略
备份域由独立的复位信号控制,系统复位、看门狗复位都不会影响到RTC,只有备份域复位或者VBAT掉电才会清空RTC日历和备份寄存器。因此在代码里,一般会通过备份寄存器里的标识数据来判断RTC是否已经被初始化过,避免每次上电都强制重新写时间。标识数据不一定要很复杂,写入一个固定魔数即可,但要注意这个魔数在备份域没有供电时会丢失,所以“写标识”这个动作本身也必须在RTC初始化流程里完成。
外部晶振的起振需要时间,LSE从使能到稳定振荡通常需要几百毫秒到一秒以上,具体与晶振型号、负载电容有关。初始化流程里应该等待LSE就绪,但在低功耗产品里不能盲目死等。常见做法是设置一个超时循环,超时后切换到LSI保证系统能起来,类似下面这段逻辑:
uint32_t wait_cnt = 0xFFFFF; RCC_LSEConfig(RCC_LSE_ON); while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET && wait_cnt) { wait_cnt--; } if (wait_cnt == 0) { /* LSE未起振,切LSI作为RTC时钟源,保证功能可用 */ RCC_LSICmd(ENABLE); RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI); RCC_RTCCLKCmd(ENABLE); }这里的wait_cnt是经验值,具体要看系统时钟频率。若使用SystemCoreClock等于168MHz的配置,0xFFFFF大约能撑几毫秒到十几毫秒,远不够LSE起振。真正的产品里建议用SysTick延时500ms到1000ms作为超时,或者把等待循环次数放到100万以上并用示波器确认实际等待时间。LSE起振失败的常见原因是晶振虚焊、负载电容不匹配或者OSC32_IN和OSC32_OUT引脚上残留助焊剂,这些硬件问题在产线批量测试时最容易暴露。
3. 在标准外设库工程里把RTC跑起来
3.1 工程结构与外设库文件的关系
工程包里的Drivers目录下分成BSP和APM32F4xx_StdPeriphDriver两层,外设驱动的主要代码在StdPeriphDriver里,每个外设一个源文件和头文件,RTC相关的文件是apm32f4xx_rtc.c和apm32f4xx_rtc.h。CMSIS设备层里放着芯片寄存器定义和系统时钟初始化逻辑,其中system_apm32f4xx.c负责把系统时钟配置到工作频率。RTC模块启动时需要用到PWR、BKP、RCC这几个外设,它们的时钟使能和寄存器操作都在这个框架里完成。
| 目录 | 作用 | 与RTC的关系 |
|---|---|---|
| Drivers/BSP | 开发板外设板级配置 | 放置LED、串口等初始化 |
| Drivers/APM32F4xx_StdPeriphDriver | 标准外设库源码 | RTC、PWR、BKP、RCC驱动 |
| CMSIS/Device | 芯片寄存器与中断向量 | 提供外设地址映射 |
| User/main.c | 主函数入口 | 调用RTC初始化与测试逻辑 |
| Projects/MDK-ARM | Keil工程文件 | 编译入口 |
| Middlewares/USMART | 串口调试组件 | 运行时读取RTC |
| Output/atk_f407.hex | 编译产物 | 可直接烧录 |
对RTC来说,StdPeriphDriver里的函数封装了寄存器操作,正常应用不需要直接读写RTC->ISR这类寄存器,但调试时借助寄存器窗口观察这些位,能更准确判断当前RTC状态。USMART组件放在Middlewares目录下,后面会单独讲怎么用它做RTC运行时的调参与验证,这对排查“时间走了但没走到预期值”之类的问题帮助很大。
3.2 初始化序列与关键参数
RTC初始化看起来只是调用一个RTC_Config,但真正的执行顺序是有讲究的。首先使能PWR和BKP的时钟,然后解除备份域写保护,接着判断备份寄存器中的标识,再配置LSE、选择RTC时钟源、等待同步并设置分频系数。下面这份是工程里RTC配置部分的典型写法,配合main函数中的步骤可以完整复现。
void RTC_Config(void) { /* 使能电源接口和备份域时钟 */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); /* 解除备份域写保护,允许访问RTC和备份寄存器 */ PWR_BackupAccessCmd(ENABLE); /* 备份寄存器里的魔数不是0xA5A5,说明备份域刚上电或RESET过 */ if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5) { BKP_WriteBackupRegister(BKP_DR1, 0xA5A5); /* 使能LSE时钟并等待就绪 */ RCC_LSEConfig(RCC_LSE_ON); while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET) { } /* 选择LSE作为RTC时钟源,并使能RTC时钟 */ RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); RCC_RTCCLKCmd(ENABLE); /* 等待RTC寄存器同步完成,再开始配置日历 */ RTC_WaitForSynchro(); RTC_InitTypeDef RTC_InitStructure; RTC_InitStructure.RTC_HourFormat = RTC_HourFormat_24; RTC_InitStructure.RTC_AsynchPrediv = 0x7F; RTC_InitStructure.RTC_SynchPrediv = 0xFF; RTC_Init(&RTC_InitStructure); } else { /* 已经初始化过,重新使能时钟并等待同步即可 */ RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); RCC_RTCCLKCmd(ENABLE); RTC_WaitForSynchro(); } }这段代码先使能PWR和BKP的APB1时钟,之后才能访问备份域。PWR_BackupAccessCmd的作用是清除备份域访问保护,否则后续对RTC寄存器的写操作会被硬件拒绝。LSE就绪等待的while循环里最好加超时,用DWT或SysTick计时,避免晶振不起振时程序卡死。异步预分频0x7F和同步预分频0xFF组合后,把32.768kHz时钟分频到1Hz,这是RTC日历计数的基础。
在主函数中的调用顺序一般像下面这样处理RTC初始化和时间设置:
int main(void) { uint32_t need_set_time; /* 系统时钟初始化,这里会把工作频率配置到168MHz */ SystemInit(); /* 串口初始化用于调试 */ USART_Config(); /* 在初始化RTC之前,先记住是否需要重新设置时间 */ need_set_time = (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5) ? 1 : 0; /* RTC时钟配置 */ RTC_Config(); if (need_set_time) { /* 第一次上电,设置一个初始时间 */ RTC_SetTime(2025, 5, 18, 10, 30, 0); } while (1) { /* 主循环中读取时间或等待RTC中断 */ RTC_ReadTime(); Delay(1000); } }注意need_set_time这个判断必须在RTC_Config之前做,因为RTC_Config内部会写入魔数0xA5A5。如果先调RTC_Config再判断,第二次软复位时依然会走RTC_SetTime,把当前时间覆盖成初始值。实际产品里还会把“是否需要重新设置时间”这件事做成一个独立标志,甚至允许用户通过串口命令手工校准时间,这样既不会在复位时丢时间,也方便产线批量设置。
3.3 时间读取与BCD转换
APM32F407的RTC日历寄存器内部按BCD码存储时间,读取后不能直接当十进制使用。RTC_GetTime返回的结构体里通常把秒、分、时等字段以BCD方式放好,需要做一次转换。工程代码里一般会在读取后对所有字段执行高低四位拆分,像秒钟0x59表示十位是5、个位是9,实际十进制是59。
建议使用下面这段代码来把时间转换为十进制后打印或显示:
void RTC_ReadTime(void) { RTC_TimeStruct RTC_TimeStructure; RTC_GetTime(&RTC_TimeStructure); uint8_t second = RTC_TimeStructure.RTC_Second; uint8_t minute = RTC_TimeStructure.RTC_Minute; uint8_t hour = RTC_TimeStructure.RTC_Hour; /* BCD转十进制 */ uint8_t dec_sec = (second >> 4) * 10 + (second & 0x0F); uint8_t dec_min = (minute >> 4) * 10 + (minute & 0x0F); uint8_t dec_hr = (hour >> 4) * 10 + (hour & 0x0F); printf("RTC Time: %02d:%02d:%02d\n", dec_hr, dec_min, dec_sec); }RTC_GetTime读取到的BCD字节,例如0x45,高4位对应十位,低4位对应个位,转换表达式(value >> 4)* 10 + (value & 0x0F)就是标准做法。为什么需要这一步,是因为APM32F407的BCD存储方式本身就是为了兼容老式数字电路设计,不是为CPU直接运算设计的。如果项目里需要做时间比较或计算时间差,建议全部转成十进制后再处理,而不是直接把BCD字节相加,否则会出现0x29加1变成0x30这样的错误。
4. 闹钟中断、唤醒定时器与低功耗的配合
4.1 配置闹钟中断实现定时触发
RTC定时器场景里,闹钟是最常用的功能:设置一个目标时间,当RTC计数到这个时间时触发中断,CPU在中断服务程序里执行任务,比如上报数据、翻转电平或者唤醒系统。APM32F407的RTC在中断方面支持闹钟匹配(Alarm A)和唤醒定时器中断,和日历寄存器配合可以实现秒级到天级的定时触发,对“定时采集一次传感器”这类需求足够使用。
以常用的闹钟配置为例:
void RTC_AlarmExample(void) { RTC_AlarmStruct RTC_AlarmStructure; /* 使能闹钟中断 */ RTC_ITConfig(RTC_IT_ALRA, ENABLE); /* 配置闹钟匹配值 */ RTC_AlarmStructure.RTC_AlarmTime.RTC_Hour = 10; RTC_AlarmStructure.RTC_AlarmTime.RTC_Minute = 30; RTC_AlarmStructure.RTC_AlarmTime.RTC_Second = 0; RTC_AlarmStructure.RTC_AlarmTime.RTC_H12 = RTC_H12_AM; RTC_AlarmStructure.RTC_AlarmDateWeekSel = RTC_AlarmDateWeekSel_Date; RTC_AlarmStructure.RTC_AlarmDateWeek = 18; RTC_AlarmStructure.RTC_AlarmMask = RTC_AlarmMask_None; RTC_AlarmConfig(RTC_Alarm_A, &RTC_AlarmStructure); RTC_AlarmCmd(RTC_Alarm_A, ENABLE); }RTC_AlarmMask用于选择不参与匹配的字段,设置为RTC_AlarmMask_None表示时分秒和日期全部参与匹配,会在2025年5月18日10:30:00触发。如果要实现每天固定时刻触发而不关注日期,可以设置RTC_AlarmMask为RTC_AlarmMask_DateWeek,让闹钟仅匹配时分秒。APM32F407的日期和星期匹配字段在设计上比较灵活,业务上“每周一早上8点”“每月1日0点”都可以用不同的Mask组合来实现。
需要注意的是闹钟配置完成后要调用NVIC配置使能RTC中断通道,同时在中断处理函数的开头读取RTC状态寄存器清除闹钟标志,否则中断标志一直挂起,系统会频繁进入中断。判断中断是否发生,不能只调试时用断点看,最好在ISR里加一个gpio翻转或者串口打印,确认捕获到的实际触发时间和预设值是否一致。
4.2 周期唤醒定时器与STOP模式联动
除了闹钟,APM32F407的RTC还带一个唤醒定时器,它和闹钟的区别在于它是一个递减计数器,从设定初值减到0后触发唤醒事件。这个特性非常适合低功耗设备周期唤醒的场景:在进入STOP模式之前先把唤醒定时器设为需要的周期,然后执行WFI或WFE睡眠指令,系统在唤醒事件到来时自动恢复运行。工程中把这部分和RTC主逻辑整合,可以做到深度睡眠下平均电流非常低,同时又能按周期醒来处理任务。
STOP模式和STANDBY模式对RTC的影响对比:
| 模式 | CPU状态 | RTC状态 | 唤醒源 | RAM保持 |
|---|---|---|---|---|
| STOP | 停止 | 继续运行 | 闹钟、唤醒定时器、外部中断 | 保持 |
| STANDBY | 断电 | 继续运行 | 唤醒引脚、RTC闹钟 | 丢失 |
当使用STANDBY模式时,唤醒后CPU会重新执行启动代码,main函数里的初始化代码会被再次执行,这时前面提到的need_set_time判断就尤其关键。唤醒定时器的配置如下:
/* 使能PWR时钟并进入STOP模式前配置唤醒定时器 */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); PWR_BackupAccessCmd(ENABLE); /* 唤醒周期由RTC时钟分频决定,这里配置为1Hz计数5次 */ RTC_WakeUpConfig(RTC_WakeUpClock_CK_SPRE_1Hz, 5); RTC_WakeUpCmd(ENABLE); /* 清除唤醒标志并进入STOP模式 */ RTC_ClearFlag(RTC_FLAG_WUTF); PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI);RTC_WakeUpConfig的第一个参数是唤醒定时器的计数时钟源,选用1Hz时第二个参数5代表5秒后触发唤醒。在STOP模式下,RTC依然能保持走时和唤醒计数,这是因为RTC处于备份域电源域,不受主电源关闭影响。进入STOP前必须确保RTC唤醒中断已经在NVIC中使能,并且退出STOP后要重新配置系统时钟,因为STOP模式下HSE或PLL会停下来,唤醒后如果不重新配置时钟,代码实际运行频率会回落,所有跟延时和波特率相关的模块都会异常。
5. 用USMART在串口命令行里验证RTC状态
5.1 注册RTC函数到USMART
这个工程里最容易被忽略的是Middlewares目录下的USMART组件。它本质上是一个串口调试通道,把C函数名做成命令表,运行期通过串口输入函数名和参数即可直接调用函数,返回结果通过串口打印出来。对比反复烧录验证,USMART对RTC调参数、查时间、测试闹钟场景更高效。工程默认把main、LED和USART相关函数注册进去了,如果想在命令行中操作RTC,需要把RTC_SetTime、RTC_GetTime注册到命令表。
以RTC时间设置为例,先写好带参数的可调用函数,注册即可。工程usmart.c中维护一个命令表:
struct _m_usmart_nametab usmart_nametab[] = { #if USMART_USE_WRFUNS {"RTC_SetTime", (void(*)(void))RTC_SetTime, "void RTC_SetTime(u8 year,u8 month,u8 date,u8 hour,u8 min,u8 sec)"}, {"RTC_GetTime", (void(*)(void))RTC_GetTime, "void RTC_GetTime(void)"}, #endif };注册的格式是函数名、函数指针、参数说明字符串。USMART解析串口输入后,会按命令表逐项匹配,匹配成功就调用对应函数指针,并把返回值格式化输出。默认情况下RTC_SetTime的返回值类型是void,但不影响调用,参数类型在说明字符串里标明,方便电脑端的串口工具自动补全。接入usmart_dev.init()之后,串口发送RTC_GetTime,即可直接返回时间。
5.2 实际调试命令与验证技巧
完成注册并重新编译烧录后,在串口调试助手的发送区输入RTC_GetTime,回车后能看到类似RTC Time: 10:30:00的输出。用RTC_SetTime(25,5,18,12,0,0)命令把时间设置为2025年5月18日12点,比把代码重新烧录一遍高效得多。这里提到的年份参数在标准外设库RTC驱动中通常使用两位年份表示法,实际打印时记得加上2000偏移。
USMART对RTC调试的价值还在于可以连续设置多次闹钟,验证中断行为。可以先把RTC_AlarmExample注册进命令表,设置一个1分钟后的闹钟,再用串口输入命令触发并确认中断是否正常。如果中断没有发生,就在串口里输入RTC_GetTime确认时间是否正确,输入list命令查看当前注册了哪些函数,再结合RTC_Config确认初始化是否完成。RTC这类外设的调试逻辑就是先确认“时间在走”,再确认“时间到点”,USMART两条命令就能分别覆盖,比在代码里加断点省时省力。
最后提一个USMART用于RTC调试时的常见注意点:串口命令是文本传输,编译器和串口工具之间的换行符差异可能导致最后一条命令不被识别。多数串口助手在发送选项里要选择“发送新行”,同时检查命令是否存在中文输入法打出的全角空格。在批量生产环境下,可以单独保留这条串口调试通道,通过出厂测试脚本用简单的自定义帧设置产线时间,比每台设备单独烧录不同固件更直接。
本文还有配套的精品资源,点击获取