ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

APM32F103 RTC定时器驱动:低功耗唤醒与备份域实践

APM32F103 RTC定时器驱动:低功耗唤醒与备份域实践 简介APM32F103 RTC定时器标准库驱动已打包成zip资源面向基于灵动微APM32F1系列单片机进行开发、需要快速实现日期时间管理与周期性任务的嵌入式工程师。压缩包共235个文件、大小5.29MB其中包含47个头文件、39个C源码文件及40个编译中间文件另有链接脚本ld、工程配置uvprojx/uvoptx与调试辅助文件便于直接导入标准库工程后编译运行。驱动覆盖RTC初始化、时钟源选择、时间日期设置、启动控制、中断配置、写保护开关以及备份寄存器读写等关键操作并给出可直接运行的示例工程完整演示了从初始化到中断触发的实现路径能够帮助开发者绕过配置陷阱、快速集成RTC功能。目前已有100人学习适合正在学习APM32F103外设驱动或需要快速集成RTC定时器的工程师参考。1. APM32F103 的 RTC 定时器驱动为什么不是用 SysTick 做定时在一个用 APM32F103 做低功耗表计的项目里最常见的定时需求不是让 LED 闪几下而是「设备睡下去过 5 分钟自己醒来采一次数据再睡回去」。用 SysTick 或者 TIM 定时器当然能延时但主控进 STOP 模式后内核时钟停了这些定时器全部失效只有 RTC 挂在 VBAT 供电的备份域上主电源断了也能继续走时并且能用闹钟中断把芯片从 STOP 里唤醒。也就是说标题里的「RTC 定时器」不是拿 RTC 去计时显示日期而是把它当成一个永不掉电的长周期定时器来用。这篇讲的是基于 APM32 标准外设库把 RTC 封装成驱动层覆盖初始化、校时、秒中断、闹钟中断、低功耗唤醒和掉电保持这几个直接能落地的点。适读人群是正在从 STM32F103 迁移到 APM32F103、或者第一次接触 APM32F1 系列标准库的嵌入式工程师。2. APM32F103 RTC 外设结构备份域写保护、时钟源与标准库驱动选型2.1 RTC 不是普通定时器备份域、VBAT 与写保护APM32F103 与 STM32F103 的 RTC 外设在寄存器层面高度一致整个 RTC 单元由三部分组成一个 32 位向上计数器、一个可编程预分频器、以及一组备份寄存器 BKP_DR。这套东西不挂在 APB2 主时钟域里而是放在独立的备份域中由 VBAT 引脚单独供电。主电源 VDD 掉电后只要 VBAT 上有电池或超级电容RTC 计数器就继续走BKP_DR 里的数据也不丢。「独立供电」的代价是「写保护」。备份域里的寄存器默认是不可写的必须先打开时钟再解锁否则写操作会被硬件忽略。这个坑我见过不止一次有人把RTC_SetPrescaler()放在初始化最前面结果 RTC 完全不动查了半天才发现 APB1 上 PWR 和 BKP 的时钟没使能。标准写法是固定三步RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); PWR_BackupAccessCmd(ENABLE); /* 置位 PWR_CR 的 DBP解锁备份域写保护 */第一行使能电源接口和备份域的外设时钟第二行才是真正打开写保护。只要这两句没执行后面所有RTC_xxx和BKP_xxx写函数都是空操作。这也是 RTC 驱动和普通 GPIO/TIM 驱动最大的风格差异普通外设初始化是「开时钟→配寄存器→用」RTC 则是「开时钟→解锁→初始化→每次写后都要等上一次写完成」。另一个容易忽略的点是复位与备份域的关系。芯片的 NRST 复位、看门狗复位都不会复位备份域所以 RTC 走时在软件复位后仍然保持但如果是上电复位且 VBAT 没接、VDD 直接掉电备份域会丢。驱动里必须通过 BKP_DR 里的标志位来判断「这次上电是冷启动还是热启动」而不是一上来就把时间重置掉。2.2 时钟源选型LSE 32768Hz、LSI 40kHz 与 HSE 分频的取舍RTC 的定时精度完全取决于输入时钟。APM32F103 的 RTC 时钟无外乎三个来源外部低速晶振 LSE32.768kHz、内部低速 RC 振荡器 LSI标称 40kHz、以及 HSE 经 128 分频后的时钟。三者对应关系可以归纳成下面的表。时钟源标称频率典型分频值优点主要问题LSE32768 Hz32767精度高可做到 1Hz 秒中断需要外部晶振与负载电容起振慢LSI40 kHz见下方说明无需外部器件起振快温漂大且受 F1 兼容设计预分频位宽限制HSE/12862.5 kHz8MHz 时62499可复用主晶振随主频切换变化低功耗时不可用RTC 的输出周期公式是T (PRL 1) / f_inPRL 是 15 位预分频值。用 LSE 时写RTC_SetPrescaler(32767)得到32768 / 32768 1Hz这就是秒中断的基础。LSI 的坑要单独说。F1 兼容内核的 RTC 预分频寄存器在很多参考手册里写的是 15 位最大只能写 32767。LSI 标称 40kHz想分频到 1Hz 需要写 39999这个值在 15 位硬件上落不进去实际秒中断会变成约 1.22Hz。所以我的建议是做秒中断、日历这种需要秒对齐的场景老老实实上 LSELSI 只适合那种「大概几分几秒唤醒一次不需要绝对对齐」的长周期闹钟或者你愿意在软件里按实测频率做补偿的场合。2.3 标准库驱动与寄存器版、HAL 版的边界标题强调「标准库驱动」这里的标准库指的是 APM32F10x 标准外设库。它和 STM32F1 的 StdPeriph 风格几乎一致函数命名、参数结构体都能直接对应。存量 STM32F103 项目往 APM32F103 迁移时RTC 部分基本就是改个头文件的事这也是为什么 APM32F1 系列很多人宁愿继续用标准库而不是切 HAL。寄存器版驱动不是不能写RTC 寄存器就那么几个CRL、CRH、PRLH/PRLL、CNTH/CNTL、ALRH/ALRL。问题在于 RTC 寄存器访问有硬件状态机写之前要等 RTOFF、读之前要等 RSF这些等待逻辑在标准库里已经封好了自己写寄存器要复刻一遍还容易漏。HAL 版的HAL_RTC_GetTime()接口更重而且 APM32F103 的 HAL 生态不如标准库成熟。我一般会把 RTC 封装成rtc_drv.c / rtc_drv.h对外只暴露rtc_drv_init()、rtc_drv_set_time()、rtc_drv_get_time()、rtc_drv_set_alarm()这几个函数内部再用标准库接口。这样上层业务代码不用知道备份域、RTOFF、RSF 这些细节。3. 标准库驱动初始化与校时备份标志、预分频与读取 RTC 时间3.1 初始化流程先开备份域写保护再判断是否首次配置用标准库驱动写初始化函数核心不是「每次都把寄存器设一遍」而是「根据备份标志决定要不要配置」。下面的代码是 APM32F103 上标准的 RTC 初始化套路可抄进工程直接用。/* rtc_drv.c —— APM32F103 标准库 RTC 驱动初始化 */ void rtc_drv_init(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); PWR_BackupAccessCmd(ENABLE); /* 解锁备份域写保护 */ RCC_LSEConfig(RCC_LSE_ON); /* 启动外部低速晶振 */ while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) RESET) {} /* 等待起振生产环境要加超时 */ if (BKP_ReadBackupRegister(BKP_DR1) ! 0xA5A5) { /* 冷启动备份域无有效配置做完整初始化 */ BKP_DeInit(); /* 复位备份域寄存器 */ RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); /* RTC 时钟源选 LSE */ RCC_RTCCLKCmd(ENABLE); /* 使能 RTC 时钟 */ RTC_WaitForSynchro(); /* 等待寄存器同步完成 */ RTC_WaitForLastTask(); /* 等待上一次写完成 */ RTC_SetPrescaler(32767); /* 32768 / (327671) 1Hz */ RTC_WaitForLastTask(); RTC_SetCounter(0); /* 秒计数从 0 开始 */ RTC_WaitForLastTask(); BKP_WriteBackupRegister(BKP_DR1, 0xA5A5); /* 写初始化完成标志 */ } else { /* 热启动备份域已配置只需重新使能 RTC 时钟 */ RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); RCC_RTCCLKCmd(ENABLE); RTC_WaitForSynchro(); } }逻辑说明PWR_BackupAccessCmd(ENABLE)是整段代码的前提漏掉它后面所有 BKP/RTC 写操作都会静默失败。BKP_DR1被当作配置标志位写入0xA5A5表示「这份备份域是驱动自己初始化过的」热启动时读到这个值就跳过全量配置避免复位后时间被清零。RTC_WaitForSynchro()等的是 RTC 时钟域与 APB1 总线域的同步RTC_WaitForLastTask()等的是上一次写 RTC 寄存器操作的完成标准库的RTC_SetCounter()这类函数内部不会自动等待必须手动调用。参数说明里有一个容易被忽略的点RTC_SetPrescaler(32767)的分频参数不是分频系数本身而是「分频系数减一」。手册上写的是把输入时钟除以PRL 1所以 32768Hz 晶振配 32767 才能得到 1Hz写成 32768 会得到略小于 1Hz 的频率。此外while等待 LSE 就绪的写法在 LSE 不起振的板上会死等量产代码里建议加一个超时计数器超时后切到 LSI 并上报时钟异常。3.2 设置当前时间与读取 RTC 时间RTC_SetCounter 与 RTC_GetCounterAPM32F103 的 RTC 没有「年/月/日」寄存器它只有一个 32 位秒计数器。所谓校时就是把当前 Unix 时间戳或者从某个基准点开始的秒数写进计数器。读取时间也一样读出来就是秒计数。设置和读取的代码非常简单但等待逻辑不能省。/* 设置当前时间seconds 为从 1970-01-01 00:00:00 起的秒计数 */ void rtc_drv_set_time(uint32_t seconds) { RTC_WaitForLastTask(); /* 确保前一次写操作已完成 */ RTC_SetCounter(seconds); /* 写入 32 位秒计数 */ RTC_WaitForLastTask(); /* 等待本次写完成 */ } /* 读取当前时间返回同样的秒计数 */ uint32_t rtc_drv_get_time(void) { RTC_WaitForSynchro(); /* 等待 RTC 时钟域同步到 APB 域 */ return RTC_GetCounter(); /* 读 32 位计数器 */ }这里要强调RTC_GetCounter()前面的RTC_WaitForSynchro()。RTC 计数器在低速时钟域下运行APB1 总线去读它时如果恰好赶上计数器进位可能读到高低 16 位不一致的数据。RTC_WaitForSynchro()等待的是硬件把 RTC 域的数据同步到总线域标准库已经把这个等待封装好了跳过它你会在某些时刻读到跳变的时间值。拿到秒计数之后要显示成时分秒就纯属软件运算了。常见做法是用div连续做三次除法uint8_t hour (seconds / 3600) % 24; uint8_t minute (seconds / 60) % 60; uint8_t second seconds % 60;如果要做日历需要自己实现公历转秒和秒转公历的算法或者串口校时时直接传 Unix 时间戳由上位机换算。我一般倾向在驱动层保持秒计数把时间转换放到应用层这样驱动接口更干净也方便测试时直接比较数值。3.3 校时的三种方式与边界参数实际项目中校时常见三种路径按可靠性排分别是串口/UART 命令校时、外接高精度时钟源对时、按键手动校时。串口校时最直接收到一个十进制或十六进制的秒计数后调用rtc_drv_set_time()即可但要注意写入动作对边界值的容忍度。RTC_SetCounter()可以接受任何 32 位无符号值0 和 0xFFFFFFFF 都能写进去驱动不需要在校验上做太多。真正的边界在应用层如果你的设备是低频采集场景RTC 秒计数到 2038 年之后的溢出问题暂时不用管但如果上位机下发的时间戳是带时区的驱动层保持 UTC 秒计数、应用层做时区偏移会比在驱动里直接改时间更省事。另外rtc_drv_set_time()的写入不是瞬间完成的。RTC_WaitForLastTask()在 LSE 32768Hz 下通常等几个微秒到十几个微秒但如果 RTC 时钟源配置异常、频率很低这个等待可能卡住。生产环境里建议给等待加一个毫秒级超时超时后返回错误码而不是让设备死等。4. RTC 闹钟定时器与秒中断周期任务、低功耗唤醒与参数配置4.1 秒中断最小周期任务的两种触发方式把 RTC 当定时器用最基础的是秒中断。APM32F103 的 RTC 有两个中断源秒中断RTC_IT_SEC和闹钟中断RTC_IT_ALR。秒中断每秒触发一次适合做 LED 心跳、采集节拍这类低频周期任务闹钟中断在计数器等于闹钟比较值时触发一次适合做「到点干什么」的单次定时。开启秒中断的标准库写法如下void rtc_drv_enable_second_irq(void) { RTC_WaitForLastTask(); RTC_ITConfig(RTC_IT_SEC, ENABLE); /* 打开秒中断 */ RTC_WaitForLastTask(); NVIC_InitTypeDef nvic; nvic.NVIC_IRQChannel RTC_IRQn; nvic.NVIC_IRQChannelPreemptionPriority 2; nvic.NVIC_IRQChannelSubPriority 0; nvic.NVIC_IRQChannelCmd ENABLE; NVIC_Init(nvic); } void RTC_IRQHandler(void) { if (RTC_GetITStatus(RTC_IT_SEC) ! RESET) { RTC_ClearITPendingBit(RTC_IT_SEC); /* 先清标志再做业务 */ /* 每秒执行的任务翻转测试引脚、累加计数器等 */ } }这段代码有两个关键参数。第一个是RTC_IT_SEC必须在 NVIC 配置之前使能否则中断挂起位来了但 CPU 不响应。第二个是抢占优先级PreemptionPriority 2这个值不是固定的要配合NVIC_PriorityGroupConfig()的分组方式来看。如果工程里把优先级分组设为NVIC_PriorityGroup_2那抢占优先级可以取 0~3RTC 中断一般不需要最高优先级让它排在系统节拍和关键通信之下就行。秒中断里最容易犯的错是把耗时操作直接塞进去。RTC 中断频率是 1Hz单次执行时间看起来宽裕但如果你在中断里做串口打印或者读外部 Flash一旦被打断的任务正好也在操作共享资源就会出问题。我的习惯是中断函数里只置标志位真正的业务逻辑放到主循环的rtc_drv_second_flag轮询里去跑。4.2 闹钟中断把 RTC 当长周期定时器用闹钟中断比秒中断更贴合「RTC 定时器」这个标题。它的原理是拿计数器值和预设的闹钟值做比较相等时触发RTC_IT_ALR中断。因为计数器是 32 位的闹钟定时范围可以从秒级一直到 136 年这是普通定时器完全做不到的。/* 设置闹钟alarm_seconds 为期望触发时的秒计数 */ void rtc_drv_set_alarm(uint32_t alarm_seconds) { RTC_WaitForLastTask(); RTC_SetAlarm(alarm_seconds); /* 写入闹钟比较值 */ RTC_WaitForLastTask(); RTC_ITConfig(RTC_IT_ALR, ENABLE); /* 使能闹钟中断 */ RTC_WaitForLastTask(); }使用闹钟时有一个必须注意的边界闹钟值必须大于当前计数器值。如果写入的alarm_seconds小于或等于RTC_GetCounter()的当前值F1 兼容设计中这个条件是「已过去的时间」闹钟不会等下一轮而是立即触发或者干脆不触发。所以应用层设置闹钟前最好先读一次当前时间按当前时间 期望延时来算闹钟值。闹钟中断的服务函数和秒中断共用同一个RTC_IRQHandler区别在于判断的位不同void RTC_IRQHandler(void) { if (RTC_GetITStatus(RTC_IT_ALR) ! RESET) { RTC_ClearITPendingBit(RTC_IT_ALR); /* 到点后的处理唤醒业务线程、启动采集、输出脉冲等 */ } if (RTC_GetITStatus(RTC_IT_SEC) ! RESET) { RTC_ClearITPendingBit(RTC_IT_SEC); /* 秒级任务 */ } }如果芯片的 RTC 闹钟在内部还映射到了 EXTI 线上比如 F1 兼容设计中常见的 EXTI17那中断服务程序里可能还需要清一次EXTI的挂起位具体看 APM32F103 头文件里EXTI_Line17是否有定义。没有定义就不需要处理驱动代码里可以留一个防御性的条件编译。4.3 低功耗配合STOP 模式下的闹钟唤醒参数RTC 定时器最典型的应用场景是低功耗。设备进入 STOP 模式后主频、外设时钟全部停止但 RTC 仍在 LSE 或 LSI 下运行。闹钟中断可以作为唤醒源把芯片拉起来整个过程代码如下/* 设置闹钟 5 分钟后唤醒然后进 STOP */ rtc_drv_set_alarm(rtc_drv_get_time() 300); PWR_EnterSTOPMode(PWR_REGULATOR_ON, PWR_STOPENTRY_WFI); /* 从 STOP 唤醒后先恢复系统时钟再清闹钟标志 */ SystemInit(); while (RCC_GetFlagStatus(RCC_FLAG_HSIRDY) RESET) {}这里有三个参数需要根据实际需求调整。第一个是PWR_REGULATOR_ON它表示 STOP 模式下调压器保持开启唤醒速度快但功耗相对高追求更低功耗可以换成PWR_REGULATOR_LOWPOWER代价是唤醒时间变长。第二个是PWR_STOPENTRY_WFI它让 CPU 在遇到 WFI 指令时进 STOP在主循环里调用没问题如果在中断里调用要特别小心。第三个是唤醒后的时钟恢复STOP 退出后系统时钟默认回到 HSI不复位外设时钟可能导致外设工作异常。下面是几组实际的预分频和闹钟周期参数对照覆盖标题里「RTC 定时器」最常见的使用场景。使用场景时钟源预分频值中断选择实际周期备注心跳/秒计数LSE 32768Hz32767RTC_IT_SEC1s最准推荐短定时闹钟LSE 32768Hz32767RTC_IT_ALR秒级到小时级闹钟值 当前 延时低功耗长唤醒LSE 32768Hz32767RTC_IT_ALR分钟级到天级STOP 下可唤醒无晶振低成本LSI 40kHz按实测校准RTC_IT_ALR约 1.22s 基准需要软件补偿温漂低功耗场景还有一个细节进 STOP 之前闹钟中断里如果用到了唤醒后的 GPIO 翻转最好把 GPIO 配成推挽输出而非开漏避免唤醒后电平状态不确定。另外闹钟触发后中断标志不会自动清除如果唤醒后不清RTC_IT_ALR挂起位下次进 STOP 会被同一个标志立刻唤醒设备表现为「睡不下去」。5. RTC 定时器驱动排错与验证LSI 温漂、复位掉电与时间毛刺5.1 复位后时间回到初始值备份标志与 VBAT 的排查如果每次上电 RTC 时间都回到初始化值驱动写的标志没生效。排查顺序是先确认PWR_BackupAccessCmd(ENABLE)有没有在BKP_WriteBackupRegister()之前执行再检查 VBAT 引脚是否真正接到了电池或电容注意很多开发板把 VBAT 直接接 VDD整板断电备份域照样丢最后查BKP_DR1地址是否和库头文件里定义一致有的芯片 BKP 寄存器数量不同用错索引会写出到别的寄存器。5.2 每天快慢几分钟LSI 温漂与预分频校准用 LSI 做 RTC 时钟源时温漂导致的累计误差可以用一个简单的软件校准法来修正。先让设备连续运行一小时同时用秒表或上位机记录参考时间算出 RTC 计数偏差然后按 ppm 补偿。比如实测 LSI 为 42.5kHz而代码里按 40kHz 写的那就把分频值从 39999 改成42500 - 1前提是分频寄存器位宽放得下放不下就走软件补偿每 N 秒额外跳过一个计数。校准时要有耐心至少观察 24 小时再下结论因为环境温度变化会改变 LSI 输出频率短时间校准在昼夜温差大的场景里不具备代表性。5.3 验证驱动是否正常的三个步骤验证 RTC 定时器驱动有没有写对可以用逻辑分析仪和串口做三件事。第一是秒中断测试初始化后打开秒中断中断服务里翻转一个 GPIO用逻辑分析仪抓波形理论上应该看到稳定的 1Hz 方波周期偏差在 LSE 下应该小于几十 ppm第二是闹钟唤醒测试设置 10 秒闹钟进 STOP用示波器测唤醒后的 GPIO 动作时间对比是否在当前时间 10s 唤醒时间附近第三是掉电保持测试初始化后写时间然后 VDD 断电、VBAT 保持重新上电后读时间两次读数应基本一致偏差不超过 RTC 在断电期间走时的累计误差。最后提醒一处rtc_drv_get_time()读出来的秒计数受RTC_WaitForSynchro()影响调试时如果发现串口打印的时间偶尔往回跳先别怀疑 RTC 芯片多半是读取时没等同步标志。用两次rtc_drv_get_time()连续读并对差值做合理性判断差值落在 0 或 1 就是正常的如果出现大范围跳变再回头检查同步等待和全局中断开关的状态。本文还有配套的精品资源点击获取
返回列表