地奇星RTC外设深度解析:从日历/二进制双模式到闹钟中断的实战应用
最近在做一个需要精确计时和定时唤醒的项目,用到了地奇星微控制器内置的RTC模块。说实话,刚开始看手册时,被它那两种计数模式和一堆中断类型搞得有点懵。但实际用下来发现,这个RTC功能真的很强大,尤其是它的日历模式和闹钟中断,用好了能让你的项目省心不少。今天我就把自己折腾明白的这些东西,结合实际的代码,给大家掰开揉碎了讲一讲,目标是让你看完就能在自己的板子上跑起来。
1. 认识地奇星的RTC:不止是“掉电时钟”
咱们先别急着写代码,得先搞清楚这个RTC到底是个啥,能干啥。很多朋友以为RTC就是个“掉电还能走的钟”,这没错,但地奇星的RTC可不止这么简单。
1.1 RTC的核心特性:两种模式,各有所长
地奇星的RTC本质上是一个在系统主电源(VDD)掉电后,依靠备用电池(通过VBAT引脚供电)仍能继续运行的定时器。这里有个关键点:当VDD有效时,RTC由VDD供电;当VDD掉电但VBAT有电(比如接了纽扣电池)时,只有日历计数功能还能工作。如果VDD和VBAT都掉电了,那计时数据就丢了。
它最独特的地方在于提供了两种计数模式,你可以根据项目需要来选择:
- 日历计数模式:这就是我们最熟悉的“万年历”。它能自动计算年、月、日、星期、时、分、秒,支持从2000年到2099年共100年的日历,并且能自动处理闰年。时间用BCD码表示,方便显示。比如你要做一个带时钟的智能家居面板,用这个模式最合适。
- 二进制计数模式:这个模式不关心具体的年月日,它就是一个纯粹的32位二进制秒计数器,精度可以达到1/128秒。它记录的是一个连续的秒数串行值。这个模式特别适合用于非公历(比如农历、伊斯兰历)的计时系统,或者你只需要一个高精度的、长时间运行的秒表/计时器。
为了方便你选择,我把两种模式的核心区别总结了一下:
| 特性 | 日历计数模式 | 二进制计数模式 |
|---|---|---|
| 计数内容 | 年、月、日、星期、时、分、秒 | 32位二进制秒数(可精确到1/128秒) |
| 显示格式 | BCD码 | 二进制串行值 |
| 闰年处理 | 自动调整 | 无(需要软件处理) |
| 典型应用 | 电子钟、日程提醒、时间戳记录 | 自定义历法计时、长时间间隔测量、高精度定时 |
1.2 RTC的“心脏”:时钟源选择
RTC要工作,总得有个“心跳”吧?这个心跳就是时钟源。地奇星RTC支持两种时钟源:
- 子时钟振荡器 (Sub-Clock, XCIN/XCOUT):需要外接一个32.768kHz的晶体。这是最常用、最精准的方式,功耗也低,适合电池供电场景。
- 内部低速振荡器 (LOCO):芯片自带的低速时钟,频率大约是32.768kHz(但会有偏差)。好处是不需要外接晶体,节省成本和PCB空间;缺点是精度和稳定性不如外部晶体。
无论选择哪种时钟源,RTC内部都会通过一个预分频器,最终产生一个128Hz的基准时钟来驱动计数器。这个128Hz的时钟是后续所有时间单位(秒、分等)和中断的基础。
1.3 强大的中断与功能
光会计时还不够,RTC还能在特定时刻“通知”CPU,这就是中断。地奇星RTC的中断功能相当丰富:
- 闹钟中断 (RTC_ALM):可以设定一个未来的时间点,到了就触发中断。
- 日历模式:可以精细到匹配年、月、日、星期、时、分、秒中的任意几项。比如设定“每周一上午8点”响铃。
- 二进制模式:可以设定32位计数器的任意一个值作为触发点。
- 周期中断 (RTC_PRD):像节拍器一样,按固定周期触发中断。周期从2秒到1/256秒(约3.9毫秒)可选,非常灵活。可以用来做“秒闪”LED,或者高精度的定时采样。
- 进位中断 (RTC_CUP):当64Hz计数器向秒计数器进位时触发。这个在需要精确同步的场合有用。
- 时间捕获功能:通过专用的
RTCICn输入引脚,可以在外部事件(如按键)发生的瞬间,“抓拍”下当前的精确时间并产生中断。这在记录事件发生时刻时非常有用。 - 唤醒功能:闹钟中断或周期中断可以把芯片从深度睡眠(软件待机/深度软件待机模式)中唤醒,这是低功耗设计的关键。
2. 动手搭建环境:硬件连接与FSP配置
理论说完了,咱们开始动手。首先确保你的地奇星开发板硬件连接正确。
2.1 硬件引脚连接
RTC相关的引脚不多,但很重要:
| 引脚名称 | I/O 类型 | 描述与连接 |
|---|---|---|
| XCIN | Input | 连接32.768kHz晶体的一个脚。这个引脚和XCOUT配对,专用于RTC时钟输入,没有其他复用功能。 |
| XCOUT | Output | 连接32.768kHz晶体的另一个脚。 |
| RTCOUT | Output | 可以输出1Hz或64Hz的时钟波形。注意:在深度软件待机模式下这个引脚不工作。 |
| RTCIC0, RTCIC1 | Input | 时间捕获功能的事件输入引脚,共有2个独立通道。如果你不用捕获功能,可以不管。 |
注意:如果你想使用高精度的外部晶体时钟,务必在开发板上找到并焊接好32.768kHz的晶体,并正确连接到XCIN和XCOUT。如果为了快速验证,可以先使用内部的LOCO时钟。
2.2 使用FSP配置工具进行图形化配置
地奇星推荐使用FSP(Flexible Software Package)配置工具来初始化外设,这比直接撸寄存器代码方便多了。咱们一步步来。
第一步:配置引脚
- 打开你的工程,进入
FSP Configuration视图。 - 找到
Pins->Peripherals->Timers:RTC->RTC。 - 在这里,你可以看到RTC相关的引脚(XCIN, XCOUT等)。通常,如果你使用了硬件板载的晶体,FSP会根据板级支持包(BSP)自动配置好。检查一下确保引脚分配正确即可。
第二步:添加并配置RTC堆栈
- 在
FSP Configuration视图的Stacks选项卡下,点击New Stack。 - 选择
Timers->Realtime Clock (r_rtc)。这会创建一个g_rtc0模块。 - 点击这个新生成的
g_rtc0模块,打开它的属性(Property)进行详细配置。
下面这张图是配置的核心,我结合关键参数给你解释:
(此处应有一张FSP配置属性图,图中高亮显示以下配置项)
- Clock Source: 选择时钟源。
Sub-Clock(外部晶体)或LOCO(内部低速时钟)。初次实验可选LOCO。 - Frequency Comparision Value (LOCO): 仅当选择LOCO时有效。这个值(RFRL寄存器)用于控制预分频器。当LOCO频率为32.768 kHz时,这个值应设置为
0x00FF。计算一下:32768 Hz / 256 = 128 Hz,正好是RTC所需的基础频率。 - Automatic Adjustment Mode/Period/Type/Value: 这是一组用于时钟误差校正的高级功能。如果你的外部晶体有微小偏差,可以用它来补偿(比如每秒加/减几个脉冲)。刚开始可以设为
NONE或默认值。 - Callback:这里填你的中断回调函数名,比如
rtc_callback。当闹钟、周期中断触发时,就会跳转到这个函数。 - Alarm/Period/Carry Interrupt Priority: 分别设置闹钟中断、周期中断、进位中断的优先级。根据你系统中其他中断的重要性来设定。
配置完成后,记得按Ctrl+S保存,然后点击Generate Project Content按钮生成代码。这样,FSP就会在后台帮你生成初始化RTC所需的所有底层代码了。
3. 实战编程(一):实现日历时钟与串口打印
环境配好了,我们来写代码实现一个最基本的日历时钟,并通过串口每秒打印一次时间。假设你已经有一个能正常打印的串口工程(原文提到是在UART工程基础上开发)。
3.1 代码文件结构
我们通常会新建或使用以下几个文件来组织代码,让结构更清晰:
hal_entry.c: 主入口文件,调用主函数。app.c / app.h: 应用层主逻辑。bsp_rtc.c / bsp_rtc.h: RTC的底层驱动和业务逻辑封装。
3.2 核心代码解析
咱们先看bsp_rtc.h头文件,它定义了接口和常量:
#ifndef BSP_RTC_H_ #define BSP_RTC_H_ #include "hal_data.h" #include "stdio.h" /* 定义年份修正常量。 * 注意:地奇星RTC的年份是从1900年开始计的,所以软件年份 = 实际年份 - 1900。 * 设置时间和获取时间时都需要用这个常量进行转换。 */ #define YEAR_FIXED 1900 void RTC_Init(void); // RTC初始化函数 bool Rtc_GetTime(rtc_time_t *Nne_time); // 获取当前时间(通过中断标志) void FormatRtcTime(const rtc_time_t *time, char *buffer, size_t size); // 格式化时间为字符串 #endif接下来是重头戏bsp_rtc.c,我逐段讲解:
#include "bsp_rtc.h" // 星期几的中文转换表,方便显示 const char* Convet_tsr[7] = {"一","二","三","四","五","六","天"}; // 全局中断标志位,用于主循环查询 bool Rtc_Flag_Interr = false; // 用于在中断中暂存获取到的时间 static rtc_time_t Get_Current_Time; /* 初始化时间设置。 * 这里我们设置一个初始时间:2025年7月15日,星期二,23:59:40。 * 注意:tm_year = 2025 - YEAR_FIXED = 125, tm_mon 从1开始(1代表一月)。 */ static rtc_time_t Set_Init_Time = { .tm_sec = 40, // 秒 .tm_min = 59, // 分 .tm_hour = 23, // 小时 .tm_wday = 2, // 星期(1=周日,2=周一,依此类推。这里2是周一,但我们的显示函数会处理) .tm_mday = 15, // 日 .tm_mon = 7, // 月(1-12) .tm_year = 2025 - YEAR_FIXED // 年份,减去1900 }; /* RTC初始化函数 */ void RTC_Init(void) { fsp_err_t err = FSP_SUCCESS; /* 1. 打开RTC外设,初始化硬件 */ err = R_RTC_Open(g_rtc0.p_ctrl, g_rtc0.p_cfg); if (FSP_SUCCESS != err) { printf("错误:RTC初始化失败\n"); return; // 初始化失败,直接返回 } /* 2. 设置RTC的初始日历时间 */ err = R_RTC_CalendarTimeSet(g_rtc0.p_ctrl, &Set_Init_Time); if (FSP_SUCCESS != err) { printf("错误:RTC计时设置失败\n"); } /* 3. 使能周期中断,并设置为每秒触发一次。 * 这样,每过一秒,rtc_callback函数就会被调用一次。 */ err = R_RTC_PeriodicIrqRateSet(g_rtc0.p_ctrl, RTC_PERIODIC_IRQ_SELECT_1_SECOND); if (FSP_SUCCESS != err) { printf("错误:RTC周期中断设置失败\n"); } // 注意:R_RTC_Open之后,RTC可能默认是停止状态,如果需要立即开始计时,可能还需要调用R_RTC_ClockStart }提示:
R_RTC_Open函数非常重要,它根据你在FSP中的配置(g_rtc0.p_cfg)来初始化RTC硬件。g_rtc0是FSP自动生成的一个全局实例,包含了控制块和配置结构。
初始化完成后,RTC就开始走时了。每秒的周期中断会触发回调函数:
/* RTC中断回调函数 * 这个函数在RTC中断发生时被自动调用。 */ void rtc_callback(rtc_callback_args_t *p_args) { // 判断中断事件类型 if (p_args->event == RTC_EVENT_PERIODIC_IRQ) // 周期中断事件 { // 在中断服务程序中,获取当前最新的日历时间 fsp_err_t err = R_RTC_CalendarTimeGet(g_rtc0.p_ctrl, &Get_Current_Time); if (FSP_SUCCESS != err) { // 获取时间失败,这里简单打印错误。实际产品中可能需要更健壮的处理。 printf("错误:获取时间失败\n"); } // 设置一个标志位,通知主循环时间已更新 Rtc_Flag_Interr = true; } // 后续闹钟实验会在这里添加 RTC_EVENT_ALARM_IRQ 的处理 }中断函数里不宜做耗时操作(比如串口打印),所以我们只获取时间并设置标志位。主循环中检测到这个标志位后,再处理显示:
/* 提供给主循环调用的“获取时间”函数。 * 它检查中断标志,如果时间已更新,就将时间复制出来,并清除标志。 */ bool Rtc_GetTime(rtc_time_t *Nne_time) { if (Rtc_Flag_Interr) { // 复制中断中保存的时间 *Nne_time = Get_Current_Time; // 清除标志,准备接收下一次更新 Rtc_Flag_Interr = false; return true; // 成功获取到新时间 } return false; // 时间尚未更新 } /* 将 rtc_time_t 结构体的时间格式化成易读的字符串 */ void FormatRtcTime(const rtc_time_t *time, char *buffer, size_t size) { // 调整星期显示:RTC的tm_wday是1=周日,我们转换为0=周一...6=周日 int wday = time->tm_wday - 1; if (wday < 0 || wday > 6) wday = 0; // 安全处理,防止越界 if (time && buffer && size > 0) { snprintf(buffer, size, "%.4d-%.2d-%.2d %.2d:%.2d:%.2d 星期:%s", time->tm_year + YEAR_FIXED, // 年份加1900恢复为实际年份 time->tm_mon, time->tm_mday, time->tm_hour, time->tm_min, time->tm_sec, Convet_tsr[wday] // 使用中文星期 ); } }最后,在app.c的主循环中,我们这样使用:
void Run(void) { char time_str[33] = {0}; // 缓冲区存放时间字符串 UART0_Init(); // 初始化串口(假设已有此函数) RTC_Init(); // 初始化RTC printf("欢迎使用地奇星开发板\r\n"); printf("RTC日历时钟实验开始:\r\n"); while(1) { // 不断检查是否有新的时间数据 if (Rtc_GetTime(&Get_Current_Time)) // 这里Get_Current_Time是全局变量 { // 获取成功,格式化并打印 FormatRtcTime(&Get_Current_Time, time_str, sizeof(time_str)); printf("当前RTC时间: %s\r\n", time_str); } // 这里可以添加其他任务或延时 } }编译下载后,打开串口助手,你应该能看到每秒打印一次的时间信息,并且时间在递增。这就完成了最基本的RTC日历功能。
4. 实战编程(二):配置与使用闹钟中断
日历走起来了,接下来玩点更实用的——闹钟。比如让你的设备在每天特定时间执行某个任务。
4.1 在FSP中启用闹钟中断
回到FSP Configuration,找到之前配置的g_rtc0堆栈属性。你需要确保:
Callback已经设置好(例如rtc_callback)。Alarm Interrupt Priority设置了一个合适的优先级(比如比周期中断高或低,根据需求定)。 保存并重新生成工程代码。
4.2 闹钟设置代码解析
闹钟功能需要新增一个闹钟时间结构体rtc_alarm_time_t并进行设置。我们在bsp_rtc.c中增加如下代码:
// 新增一个闹钟触发标志位 bool Rtc_Alarm_Flag = false; /* 定义闹钟时间。 * 我们设定闹钟在 2025年7月16日,星期三,00:00:00 触发。 * 注意:time 结构体里的字段和设置初始时间一样。 * 关键是下面的 *_match 字段,它们决定比较哪些部分。 */ rtc_alarm_time_t Set_Alarm__Time = { .time = { .tm_sec = 00, .tm_min = 00, .tm_hour = 00, .tm_wday = 3, // 星期三 (1=周日, 3=周二?这里需要确认,根据上文2是周一,则3是周二) .tm_mday = 16, .tm_mon = 7, .tm_year = 2025 - YEAR_FIXED }, // 下面的匹配字段:0 = 不比较,1 = 比较 .year_match = 0, // 不比较年份 .mon_match = 0, // 不比较月份 .mday_match = 0, // 不比较日期 .hour_match = 0, // 不比较小时 .dayofweek_match = 0, // 不比较星期 .min_match = 0, // 不比较分钟 .sec_match = 1, // **只比较秒**!当秒数达到00时触发。 };重要:
*_match字段是闹钟配置的精髓。上面这个配置的意思是:忽略年、月、日、时、分、星期,只关心秒数。当RTC时间的秒数变为00时,闹钟就触发。这实际上就变成了一个“每分钟触发一次”的闹钟(在00秒时)。如果你想设定一个精确的日期时间点,就需要把对应的匹配字段都设为1。
然后,修改RTC_Init函数,在初始化后设置这个闹钟:
void RTC_Init(void) { fsp_err_t err = FSP_SUCCESS; err = R_RTC_Open(g_rtc0.p_ctrl, g_rtc0.p_cfg); if (FSP_SUCCESS != err) { /* 错误处理 */ } err = R_RTC_CalendarTimeSet(g_rtc0.p_ctrl, &Set_Init_Time); if (FSP_SUCCESS != err) { /* 错误处理 */ } /* 新增:配置RTC闹钟 */ err = R_RTC_CalendarAlarmSet(g_rtc0.p_ctrl, &Set_Alarm__Time); if (FSP_SUCCESS != err) { printf("错误:RTC闹钟设置失败\n"); } err = R_RTC_PeriodicIrqRateSet(g_rtc0.p_ctrl, RTC_PERIODIC_IRQ_SELECT_1_SECOND); if (FSP_SUCCESS != err) { /* 错误处理 */ } }4.3 在中断回调中处理闹钟事件
现在需要修改中断回调函数rtc_callback,让它也能响应闹钟中断:
void rtc_callback(rtc_callback_args_t *p_args) { switch(p_args->event) // 根据事件类型进行分支处理 { case RTC_EVENT_ALARM_IRQ: // 闹钟中断事件 Rtc_Alarm_Flag = true; // 设置闹钟标志位 // 注意:闹钟中断发生后,通常需要重新设置下一次闹钟时间,否则只会触发一次。 // 如果需要周期性闹钟(如每天),需要在这里重新调用 R_RTC_CalendarAlarmSet。 break; case RTC_EVENT_PERIODIC_IRQ: // 周期中断事件(每秒) fsp_err_t err = R_RTC_CalendarTimeGet(g_rtc0.p_ctrl, &Get_Current_Time); if (FSP_SUCCESS != err) { printf("错误:获取时间失败\n"); } Rtc_Flag_Interr = true; break; default: break; // 处理其他可能的事件类型 } }同样,我们提供一个函数供主循环查询闹钟状态:
bool RTC_Alarm_GetTime(rtc_time_t *Nne_time) { if(Rtc_Alarm_Flag) { *Nne_time = Get_Current_Time; // 获取触发闹钟时的瞬间时间 Rtc_Alarm_Flag = false; return true; } return false; }最后,在app.c的主循环中,我们同时检查时间更新和闹钟触发:
void Run(void) { char time_str[33]={0}; UART0_Init(); RTC_Init(); printf("欢迎使用地奇星开发板\r\n"); printf("RTC闹钟实验开始:\r\n"); printf("闹钟设定为每分钟的00秒触发。\r\n"); while(1) { // 1. 检查并打印常规时间更新 if (Rtc_GetTime(&Get_Current_Time)) { FormatRtcTime(&Get_Current_Time, time_str, sizeof(time_str)); printf("当前RTC时间: %s\r\n", time_str); } // 2. 检查闹钟是否触发 if (RTC_Alarm_GetTime(&Get_Current_Time)) { FormatRtcTime(&Get_Current_Time, time_str, sizeof(time_str)); printf("[ALARM] 闹钟已触发,触发时间:%s\r\n", time_str); // 这里可以添加你的闹钟任务,比如点亮LED、发送信号等 } } }运行程序,你会看到每秒打印的时间,同时每到整分钟(秒数为00)时,会多打印一行闹钟触发的信息。
5. 避坑指南与经验分享
折腾完这两个实验,我总结几个容易踩坑的地方:
- 年份基准问题:地奇星RTC的
tm_year是从1900年开始计的。设置时间(CalendarTimeSet)和读取时间(CalendarTimeGet)时,都要记得加上或减去YEAR_FIXED(1900)。这是最容易出错的地方之一。 - 闹钟匹配字段:
rtc_alarm_time_t结构体里的*_match字段一定要理解清楚。0是忽略,1是比较。如果你设定了闹钟时间但没触发,第一件事就是检查这些匹配位是否设置正确。一个常见的需求是“每天固定时间响铃”,这时需要设置hour_match,min_match,sec_match为1,而年、月、日、星期的匹配设为0。 - 中断回调函数名:在FSP配置中填写的
Callback函数名,必须和你在代码中实现的函数名完全一致,包括大小写。否则链接时会报错,或者中断无法触发。 - LOCO时钟精度:如果项目对时间精度要求高,务必使用外部32.768kHz晶体。LOCO的频率误差较大(通常±1%以上),长时间运行累积误差会非常明显。使用外部晶体时,别忘了在FSP中把
Clock Source改为Sub-Clock。 - 闹钟单次触发:默认情况下,闹钟触发一次后就不会再触发了。如果你需要周期性的闹钟(比如每小时),需要在闹钟中断回调函数(
RTC_EVENT_ALARM_IRQ处理分支)里,重新计算并设置下一次的闹钟时间。 - VBAT供电:如果产品需要掉电保持计时,一定要在VBAT引脚连接备用电池(如CR2032纽扣电池),并确保在主电源断开时,VBAT的供电电路是通的。同时注意,在VBAT单独供电时,只有日历计数功能可用,其他一些功能(如RTCOUT输出)可能不可用。
地奇星的RTC模块功能全面,从简单的时钟到复杂的定时任务都能胜任。关键是理解好两种工作模式的适用场景,以及灵活运用闹钟和周期中断。希望这篇教程能帮你顺利上手,在实际项目中用好这个重要的外设。