1. 从一次掉电丢时间的Bug说起
前阵子帮朋友调一块基于RT-Thread的工控板子,功能都跑通了,唯独有个问题让他很头疼:设备断电再上电,系统时间就回到了一个莫名其妙的默认值,日志时间戳全乱套,定时任务也跟着错乱。他一开始以为是RTC电池没装好,换了三颗CR2032还是老样子。后来我让他把RT-Thread Studio里的RTC配置截图发过来一看,果然,问题出在软件配置和驱动对接上,跟硬件电池一点关系都没有。
这个场景其实特别典型。很多刚接触RT-Thread Studio的朋友,在IDE里点几下图形化配置,把RTC勾上,以为就万事大吉了,结果一跑起来发现时间不走、掉电不保存、闹钟不触发,各种幺蛾子。RT-Thread Studio作为一款基于Eclipse的集成开发环境,把RT-Thread内核、驱动框架、组件包都做了图形化封装,确实降低了上手门槛,但RTC这块涉及硬件时钟源选型、驱动框架对接、时间同步策略、掉电保持机制等多个层面,光靠点鼠标是点不明白的。
这篇笔记就是把我自己在几块不同MCU平台上折腾RTC的经验整理出来,从RTC设备框架的注册流程,到alarm闹钟的实战用法,再到掉电时间保持的完整方案,一步步拆开讲。不管你是刚拿到RT-Thread Studio的新手,还是已经用过一段时间但RTC这块一直没吃透的老哥,应该都能从里面找到能直接抄作业的东西。文章里涉及的具体芯片型号我会做模糊处理,但配置思路和代码结构是通用的,你照着套到自己的平台上就行。
2. RTC在RT-Thread里的整体设计思路
2.1 为什么RT-Thread要单独抽象一层RTC设备框架
裸机时代用RTC,大家都是直接怼寄存器,写个RTC_SetCounter()、RTC_GetCounter()就完事了。但到了RTOS环境,尤其是RT-Thread这种强调组件化和可移植性的系统,直接操作寄存器就有问题了:换个MCU,上层应用代码全得重写。
RT-Thread的做法是引入RTC设备驱动框架,在drivers/rtc/目录下定义了一套统一的设备接口。上层应用通过rt_device_find("rtc")找到设备,然后用rt_device_control()发命令来设置时间、读时间、设闹钟。底层驱动只需要实现rt_rtc_ops结构体里的几个回调函数,把具体寄存器的操作包进去就行。
这个设计的好处很直接:应用层代码跟硬件解耦。你今天用A芯片,明天换B芯片,只要B芯片的RTC驱动也注册到了框架里,上层读时间的代码一行都不用改。RT-Thread Studio在图形化配置里勾选RTC之后,它会自动帮你把对应BSP的RTC驱动文件加入编译,并且调用rt_hw_rtc_register()完成注册。
注意:有些BSP的RTC驱动默认是关闭的,你在RT-Thread Studio的
RT-Thread Settings里勾了RTC,但对应的rtconfig.h里BSP_USING_RTC宏可能没打开,需要手动确认一下。这个坑我踩过不止一次。
2.2 RTC设备框架的核心数据结构
要玩转RTC,得先搞清楚框架里几个关键结构体。第一个是rt_rtc_ops,它定义了底层驱动需要实现的回调:
struct rt_rtc_ops { rt_err_t (*init)(void); rt_err_t (*get_secs)(time_t *sec); rt_err_t (*set_secs)(time_t *sec); rt_err_t (*get_alarm)(struct rt_rtc_wkalarm *alarm); rt_err_t (*set_alarm)(struct rt_rtc_wkalarm *alarm); rt_err_t (*get_timeval)(struct timeval *tv); rt_err_t (*set_timeval)(struct timeval *tv); };不是所有平台都实现了全部回调,比如有些低成本MCU的RTC没有闹钟功能,那set_alarm就可以是RT_NULL。但get_secs和set_secs这两个是必须的,否则框架没法工作。
第二个是rt_rtc_wkalarm,闹钟配置结构体:
struct rt_rtc_wkalarm { rt_bool_t enable; rt_int32_t tm_sec; rt_int32_t tm_min; rt_int32_t tm_hour; rt_int32_t tm_mday; rt_int32_t tm_mon; rt_int32_t tm_year; };这里有个细节要注意:不同芯片的闹钟精度不一样。有的只支持到"每天几点几分",有的支持到"某年某月某日几点几分"。你在设置闹钟之前,最好先看看芯片手册里RTC闹钟寄存器的位宽。如果芯片只支持日级闹钟,你设了年月日它也会忽略,别到时候闹钟不响还以为是代码写错了。
2.3 RT-Thread Studio里的RTC配置入口
在RT-Thread Studio里配置RTC,主要走两条路。第一条是在RT-Thread Settings的图形界面里,找到硬件→片上外设→RTC,勾选启用。这一步会自动修改rtconfig.h,把BSP_USING_RTC和RT_USING_RTC这两个宏打开。
第二条路是直接改board.h或者rtconfig.h。有些BSP的RTC配置项比较多,比如需要选择时钟源(LSE还是LSI)、是否需要校准、闹钟中断优先级等,这些在图形界面里可能没有全部暴露出来,就得手动改宏定义。
我个人的习惯是:先用图形界面勾选,然后打开rtconfig.h检查一遍,看看有没有遗漏的配置项。特别是RT_USING_ALARM这个宏,如果你要用闹钟功能,光勾RTC是不够的,还得把ALARM组件也打开。
3. RTC驱动注册与初始化的完整流程
3.1 从rt_hw_rtc_register说起
RTC驱动的注册入口是rt_hw_rtc_register(),这个函数在drivers/rtc/rtc.c里实现。它的核心逻辑是:分配一个rt_rtc_device结构体,把底层传进来的ops挂上去,然后调用rt_device_register()把设备注册到系统里,设备名叫"rtc"。
rt_err_t rt_hw_rtc_register(rt_rtc_device_t rtc, const char *name, rt_uint32_t flag, void *data) { RT_ASSERT(rtc != RT_NULL); rtc->ops = &rtc_ops; // 底层驱动实现的ops return rt_device_register(&(rtc->parent), name, flag); }底层BSP的驱动代码里,通常会有一个rt_hw_rtc_init()函数,在里面填充rt_rtc_ops结构体,然后调用rt_hw_rtc_register()。这个初始化函数一般放在board.c的rt_hw_board_init()里被调用,或者通过INIT_DEVICE_EXPORT宏自动初始化。
实操心得:如果你发现
rt_device_find("rtc")返回RT_NULL,先检查rt_hw_rtc_init()有没有被调用。可以在里面加一句rt_kprintf打印,确认初始化流程走到了。很多时候是BSP的初始化顺序问题,RTC初始化被放在了某个条件编译分支里,而那个宏没打开。
3.2 时钟源选择:LSE、LSI还是外部时钟
RTC的时钟源选择是个绕不开的话题。常见的有三种:
| 时钟源 | 精度 | 功耗 | 成本 | 适用场景 |
|---|---|---|---|---|
| LSE(外部低速晶振) | 高,典型±20ppm | 低 | 需要外接32.768kHz晶振 | 对时间精度要求高的场景 |
| LSI(内部低速RC) | 低,典型±5000ppm | 低 | 无需外部器件 | 对精度要求不高的场合 |
| 外部时钟输入 | 取决于源 | 中 | 需要外部时钟源 | 特殊同步需求 |
LSE的精度比LSI高两个数量级,但需要板子上焊一颗32.768kHz的晶振,还要配两个负载电容。很多低成本板子为了省这几个器件,就用LSI,结果时间一天能差好几分钟。
在RT-Thread Studio里,时钟源的选择通常不在图形界面里,而是在BSP的board.h或者drv_rtc.c里通过宏定义来配。比如:
#define RTC_CLOCK_SOURCE_LSE // 或者 RTC_CLOCK_SOURCE_LSI如果你对时间精度有要求,强烈建议用LSE。多花几毛钱的晶振成本,省掉后面无数的时间校准麻烦。我见过一个项目用LSI,设备运行一周后时间差了将近半小时,最后不得不加了个定期对时机制来兜底。
3.3 初始化过程中的常见坑
第一个坑是备份域访问权限。很多MCU的RTC寄存器位于备份域,默认是写保护的,需要先打开电源接口时钟和备份域访问使能,才能配置RTC。这个在BSP驱动里一般已经处理了,但如果你自己改驱动,忘了这一步,就会出现"配置写不进去,读出来全是默认值"的情况。
第二个坑是首次上电的初始化标志。RTC的备份域里通常有一个标志位,用来判断是不是第一次配置RTC。如果这个标志位没清,每次上电都会重新初始化RTC,时间就被重置了。正确的做法是:检查标志位,如果已经配置过,就跳过初始化,直接读时间。
if (RTC_ReadBackupRegister(RTC_BKP_DR0) != 0x5A5A) { // 首次上电,配置RTC RTC_Init(); RTC_WriteBackupRegister(RTC_BKP_DR0, 0x5A5A); } else { // 已经配置过,等待同步后直接使用 RTC_WaitForSynchro(); }第三个坑是同步等待。RTC的寄存器在APB时钟域和RTC时钟域之间同步,写完配置后需要等待同步完成才能读。如果不等,读出来的可能是旧值。这个在驱动里一般有RTC_WaitForSynchro()之类的函数来处理,但如果你自己写裸机代码,很容易忽略。
4. RTC时间读写与闹钟实战
4.1 设置时间和读取时间的标准姿势
在应用层,设置RTC时间的标准流程是这样的:
#include <rtthread.h> #include <rtdevice.h> #include <sys/time.h> rt_device_t rtc_dev; time_t timestamp; rtc_dev = rt_device_find("rtc"); if (rtc_dev == RT_NULL) { rt_kprintf("find rtc device failed!\n"); return; } rt_device_open(rtc_dev, RT_DEVICE_OFLAG_RDWR); // 设置时间:2025年1月1日 00:00:00 struct tm tm_set = {0}; tm_set.tm_year = 2025 - 1900; // 注意年份要减1900 tm_set.tm_mon = 0; // 月份从0开始 tm_set.tm_mday = 1; tm_set.tm_hour = 0; tm_set.tm_min = 0; tm_set.tm_sec = 0; timestamp = mktime(&tm_set); rt_device_control(rtc_dev, RT_DEVICE_CTRL_RTC_SET_TIME, ×tamp); // 读取时间 rt_device_control(rtc_dev, RT_DEVICE_CTRL_RTC_GET_TIME, ×tamp); struct tm *tm_now = localtime(×tamp); rt_kprintf("now: %04d-%02d-%02d %02d:%02d:%02d\n", tm_now->tm_year + 1900, tm_now->tm_mon + 1, tm_now->tm_mday, tm_now->tm_hour, tm_now->tm_min, tm_now->tm_sec);这里有几个容易出错的点。第一,年份要减1900,月份从0开始,这是C标准库struct tm的约定,不习惯的话很容易写错。第二,mktime()返回的是time_t类型,在32位系统上通常是秒数,从1970年1月1日开始算。第三,RT_DEVICE_CTRL_RTC_SET_TIME和RT_DEVICE_CTRL_RTC_GET_TIME这两个命令码,在rtdevice.h里有定义,别自己瞎编。
注意:有些BSP的RTC驱动只实现了
get_secs和set_secs,没有实现get_timeval和set_timeval。这种情况下,RT_DEVICE_CTRL_RTC_GET_TIME命令会走get_secs分支,返回的是秒数,精度到秒。如果你需要毫秒级精度,得确认驱动是否支持timeval。
4.2 闹钟功能的正确打开方式
闹钟功能在工控场景里用得很多,比如定时唤醒、定时采集、定时上报。RT-Thread的RTC框架里,闹钟是通过RT_DEVICE_CTRL_RTC_SET_ALARM命令来设置的。
struct rt_rtc_wkalarm wkalarm; wkalarm.enable = RT_TRUE; wkalarm.tm_year = 2025 - 1900; wkalarm.tm_mon = 0; wkalarm.tm_mday = 1; wkalarm.tm_hour = 8; wkalarm.tm_min = 30; wkalarm.tm_sec = 0; rt_device_control(rtc_dev, RT_DEVICE_CTRL_RTC_SET_ALARM, &wkalarm);设置完闹钟之后,还需要注册一个回调函数来处理闹钟中断:
static void rtc_alarm_callback(rt_device_t dev, rt_size_t size) { rt_kprintf("RTC alarm triggered!\n"); // 在这里做你需要的事情,比如唤醒某个线程 } rt_device_set_rx_indicate(rtc_dev, rtc_alarm_callback);这里有个关键点:闹钟中断的处理函数是在中断上下文里执行的,所以里面不能做耗时操作,不能调用可能导致睡眠的API。正确的做法是发一个信号量或者事件给某个线程,让线程去处理具体逻辑。
static struct rt_semaphore alarm_sem; static void rtc_alarm_callback(rt_device_t dev, rt_size_t size) { rt_sem_release(&alarm_sem); } // 在某个线程里 while (1) { rt_sem_take(&alarm_sem, RT_WAITING_FOREVER); rt_kprintf("do alarm task here\n"); // 执行具体的闹钟任务 }4.3 闹钟不触发的问题排查
闹钟不触发是最常见的问题之一。排查思路可以按这个顺序来:
第一步,确认闹钟中断使能了没有。很多BSP的RTC驱动在初始化的时候只开了秒中断,没开闹钟中断。你需要检查drv_rtc.c里有没有调用RTC_ITConfig(RTC_IT_ALRA, ENABLE)之类的代码。
第二步,确认NVIC中断优先级配置了没有。RTC闹钟中断是一个独立的中断向量,需要在NVIC里使能。有些BSP在rt_hw_rtc_init()里配了,有些没配,需要自己加。
第三步,确认闹钟时间设置正确。特别是年份和月份,如果设成了过去的时间,闹钟可能立即触发或者永远不触发,取决于芯片的行为。
第四步,确认回调函数注册了没有。rt_device_set_rx_indicate()这个调用不能少,否则中断来了框架也不知道该通知谁。
我遇到过最隐蔽的一个问题是:闹钟中断标志没有清除。有些芯片的RTC闹钟中断标志需要手动清除,如果不清,中断会一直触发,或者第二次就不触发了。在中断处理函数里加一句清标志的代码就能解决。
5. 掉电时间保持与系统时间同步
5.1 掉电保持的硬件前提
RTC掉电保持的前提是VBAT引脚有电。大多数MCU的RTC模块有独立的电源域,主电源掉电后,VBAT引脚上的纽扣电池继续给RTC供电,保证时间和备份寄存器不丢失。
这里有几个硬件设计上的注意事项:
- VBAT引脚的供电范围要符合芯片手册要求,一般是1.8V到3.6V。CR2032标称3V,但新电池可能到3.3V,旧电池可能降到2.7V,都在范围内。
- VBAT引脚要加去耦电容,典型值100nF,靠近引脚放置。
- 如果不用电池,VBAT引脚不能悬空,要接到VDD或者GND,具体看芯片手册。悬空可能导致RTC工作异常。
- 纽扣电池座的正负极别搞反了,我见过有人焊反了,结果电池发热,还好没炸。
5.2 软件层面的时间同步策略
硬件保证了RTC在掉电时继续走,但软件层面还需要一套同步策略,把RTC时间和系统时间对齐。
RT-Thread里有一个set_date()和set_time()的API,可以设置系统时间。但系统时间在掉电后是不保存的,所以每次上电都需要从RTC读时间,然后设置到系统里。
static void sync_time_from_rtc(void) { rt_device_t rtc_dev = rt_device_find("rtc"); time_t timestamp; if (rtc_dev == RT_NULL) return; rt_device_open(rtc_dev, RT_DEVICE_OFLAG_RDWR); rt_device_control(rtc_dev, RT_DEVICE_CTRL_RTC_GET_TIME, ×tamp); struct tm *tm_now = localtime(×tamp); set_date(tm_now->tm_year + 1900, tm_now->tm_mon + 1, tm_now->tm_mday); set_time(tm_now->tm_hour, tm_now->tm_min, tm_now->tm_sec); rt_device_close(rtc_dev); }这个函数可以放在系统启动的后期,比如在某个初始化线程里调用。注意不要在RTC驱动还没注册的时候就调用,否则rt_device_find会返回RT_NULL。
5.3 时间校准的实用技巧
即使使用了LSE晶振,时间久了也会有累积误差。LSE的典型精度是±20ppm,换算下来一天大概差1.7秒,一个月差50秒左右。如果项目对时间精度要求高,就需要定期校准。
校准的方式有几种:
第一种,手动校准。提供一个接口,让用户通过串口或者网络下发正确的时间,然后调用set_date()和set_time()更新系统时间和RTC时间。
第二种,自动校准。如果设备有网络功能,可以定期从时间服务器获取标准时间,然后同步到RTC。RT-Thread有NTP客户端组件,可以配合使用。
第三种,温度补偿。有些高端MCU的RTC支持温度补偿,根据芯片温度调整RTC的计数频率,减小温漂影响。这个需要芯片支持,软件上配置一下就行。
实操心得:我在一个户外设备项目里用过LSI,夏天和冬天的时间误差能差出一倍。后来换成LSE,并且加了每周一次的网络对时,时间精度就稳定在秒级了。如果实在不能用LSE,至少也要加个定期对时机制,别让时间自由漂移。
6. 常见问题速查与避坑指南
6.1 RTC使用问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
rt_device_find("rtc")返回NULL | 驱动未注册 | 检查rt_hw_rtc_init()是否被调用 | 确认BSP_USING_RTC宏已打开,初始化函数已加入编译 |
| 时间不走 | 时钟源未起振 | 用示波器测32.768kHz晶振引脚 | 检查晶振和负载电容,确认LSE使能 |
| 掉电后时间丢失 | VBAT无供电 | 测VBAT引脚电压 | 检查电池座焊接,更换电池 |
| 闹钟不触发 | 中断未使能 | 检查NVIC和RTC中断配置 | 使能闹钟中断,注册回调函数 |
| 时间设置后立即变回默认值 | 备份域写保护 | 检查是否使能了备份域访问 | 打开PWR和BKP时钟,解除写保护 |
| 时间误差大 | 用了LSI | 检查时钟源配置 | 改用LSE,或加定期对时 |
| 读时间返回0 | 未等待同步 | 检查是否有RTC_WaitForSynchro() | 在读写前等待同步完成 |
6.2 几个容易忽略的细节
第一个细节:rt_device_open的flag。RTC设备一般用RT_DEVICE_OFLAG_RDWR打开就行,但有些BSP的驱动对flag有要求,比如必须带RT_DEVICE_FLAG_STANDALONE。打开失败的话,先看看驱动里open函数的实现。
第二个细节:多线程访问RTC的互斥。如果多个线程同时读写RTC,可能会有竞态问题。虽然RTC操作本身不算频繁,但保险起见,可以用一个互斥量保护一下。
第三个细节:time_t的位宽。在32位系统上,time_t是32位有符号数,最大到2038年。如果你的设备要跑过2038年,得考虑用64位的time_t,或者自己处理溢出。这个坑现在还早,但做长生命周期产品的话,最好提前规划。
第四个细节:RTC中断优先级。RTC闹钟中断的优先级不要设得太高,否则可能影响其他关键中断的响应。一般设成中等优先级就行。
6.3 调试RTC的实用命令
RT-Thread的msh命令行里有一些现成的命令可以用来调试RTC:
# 查看RTC设备是否注册 list_device # 查看系统时间 date # 设置系统时间 date 2025 01 01 00 00 00list_device会列出所有注册的设备,如果RTC注册成功,你会看到一行rtc设备。date命令可以查看和设置系统时间,但它操作的是系统时间,不是RTC时间。要验证RTC时间,还是得用rt_device_control读出来看。
我一般会在调试阶段加一个自定义的msh命令,专门用来读RTC时间:
static int rtc_dump(int argc, char **argv) { rt_device_t dev = rt_device_find("rtc"); time_t timestamp; struct tm *tm_now; if (dev == RT_NULL) { rt_kprintf("rtc not found\n"); return -1; } rt_device_open(dev, RT_DEVICE_OFLAG_RDWR); rt_device_control(dev, RT_DEVICE_CTRL_RTC_GET_TIME, ×tamp); tm_now = localtime(×tamp); rt_kprintf("RTC: %04d-%02d-%02d %02d:%02d:%02d\n", tm_now->tm_year + 1900, tm_now->tm_mon + 1, tm_now->tm_mday, tm_now->tm_hour, tm_now->tm_min, tm_now->tm_sec); rt_device_close(dev); return 0; } MSH_CMD_EXPORT(rtc_dump, dump rtc time);这个命令在调试的时候特别方便,不用每次都改代码重新烧录,直接在串口终端里敲rtc_dump就能看到RTC当前时间。
7. 不同MCU平台的RTC适配经验
7.1 主流MCU的RTC差异对比
不同厂商的MCU,RTC模块的设计差异挺大的。下面这张表是我在实际项目中总结的几个常见平台的对比:
| 特性 | 平台A | 平台B | 平台C |
|---|---|---|---|
| 时钟源 | LSE/LSI/HSE分频 | LSE/LSI | LSE/LSI |
| 闹钟精度 | 日级 | 秒级 | 秒级 |
| 备份寄存器 | 10个16位 | 5个32位 | 20个32位 |
| 温度补偿 | 不支持 | 支持 | 支持 |
| 唤醒中断 | 支持 | 支持 | 支持 |
| 校准方式 | 粗调+微调 | 数字校准 | 模拟校准 |
平台A的闹钟只支持到"每天几点几分",做不了"某月某日几点几分"的闹钟。如果你的应用需要特定日期的闹钟,就得在软件层面做处理,比如每天设一次闹钟,然后在回调里判断是不是目标日期。
平台B和平台C的闹钟精度到秒,用起来就灵活多了。但平台B的备份寄存器是32位的,写的时候要注意对齐访问,有些编译器对非对齐访问会报错。
7.2 从裸机驱动移植到RT-Thread框架
如果你手头有一个裸机的RTC驱动,想移植到RT-Thread的RTC框架里,步骤大概是这样的:
第一步,把裸机驱动里的初始化函数改造成rt_rtc_ops的init回调。注意这个回调的返回值是rt_err_t,成功返回RT_EOK,失败返回-RT_ERROR。
第二步,实现get_secs和set_secs。这两个是必须的。get_secs把RTC寄存器里的年月日时分秒转换成time_t返回,set_secs反过来,把time_t拆成年月日时分秒写到寄存器里。
第三步,如果芯片支持闹钟,实现get_alarm和set_alarm。注意rt_rtc_wkalarm结构体里的年份是从1900开始的偏移,月份从0开始,跟struct tm的约定一致。
第四步,填充rt_rtc_ops结构体,调用rt_hw_rtc_register()注册设备。
第五步,把初始化函数挂到自动初始化宏里,或者手动在rt_hw_board_init()里调用。
移植过程中最容易出问题的是时间格式的转换。裸机驱动可能直接用BCD码存时间,而RT-Thread框架用的是time_t秒数。BCD码和二进制之间的转换、闰年判断、月份天数计算,这些都要仔细处理。
7.3 低功耗场景下的RTC配置
在电池供电的低功耗设备里,RTC的配置策略跟常供电设备不太一样。主要考虑两点:功耗和唤醒能力。
功耗方面,LSE的功耗通常在1微安以下,LSI的功耗略高但也在微安级别。如果设备大部分时间在休眠,RTC的功耗就是整机功耗的大头。这时候要确保RTC的时钟源选的是低功耗模式,并且关闭不必要的RTC功能,比如关闭秒中断,只保留闹钟中断。
唤醒能力方面,很多低功耗MCU支持RTC闹钟从深度休眠中唤醒。配置的时候要注意:唤醒后的时钟恢复时间。有些MCU从深度休眠唤醒后,系统时钟需要一段时间才能稳定,如果RTC中断处理函数里直接操作外设,可能会因为时钟没稳定而失败。稳妥的做法是在中断里只发信号,让系统完全唤醒后再处理具体任务。
实操心得:我在一个低功耗采集器项目里,用RTC闹钟每10分钟唤醒一次采集数据。一开始把采集逻辑直接放在中断回调里,结果偶尔出现数据异常。后来改成中断只发信号量,主线程收到信号量后再采集,问题就消失了。中断上下文里能做的事情很有限,这个原则在RTC闹钟处理上同样适用。
8. 写在最后的一些个人体会
RTC这个东西,说简单也简单,说复杂也复杂。简单在于它的功能很单一,就是计时和闹钟;复杂在于它涉及硬件、驱动、框架、应用多个层面,任何一个环节出问题都会导致最终表现异常。
我自己的经验是,调试RTC问题一定要分层排查。先确认硬件层面VBAT有电、晶振起振,再确认驱动层面设备注册成功、中断使能,最后确认应用层面时间读写正确、闹钟回调注册。一层一层往下查,比一上来就盯着应用代码看要高效得多。
另外,RT-Thread Studio虽然提供了图形化配置,但RTC这块的配置项比较分散,有些在图形界面里,有些在rtconfig.h里,有些在BSP的驱动文件里。建议在配置完之后,把相关的宏定义和驱动代码都过一遍,确认没有遗漏。特别是RT_USING_RTC、RT_USING_ALARM、BSP_USING_RTC这几个宏,一个都不能少。
最后分享一个小技巧:如果你不确定RTC的时钟源有没有起振,可以在初始化之后读一下RTC的秒寄存器,等一秒再读一次,看值有没有变化。如果两次读出来一样,说明RTC根本没在走,大概率是时钟源的问题。这个方法比用示波器测晶振要方便得多,尤其是在没有示波器的情况下。