5.8 RTC 设备
5.8 RTC 设备
📚 本节导读
学习时长:约 30 分钟
难度级别:⭐⭐⭐☆☆
前置知识:C 语言基础、Unix 时间戳概念、5.1 设备驱动框架
本节讲解 OneOS 的 RTC(Real-Time Clock)实时时钟设备。RTC 是 MCU 内置的、通常由独立电池供电的实时时钟外设,断电后仍能走时。OneOS 的 RTC 设备驱动把 RTC 封装成标准字符设备,并在此基础上提供 rtc_get / rtc_set / set_date / set_time / set_rtc_alarm 等上层 API。
本节会带你:
- 理解 OneOS RTC 使用 Unix 时间戳(
time_t) 作为统一时间接口的设计; - 看懂
struct tm(时间结构体)每个字段的语义、tm_year/tm_mon的偏移坑,以及localtime/mktime的双向换算; - 看懂 HAL 层设备控制块
struct stm32_rtc(内嵌os_device_t)的字段含义; - 逐个拆解
rtc_get/rtc_set/set_date/set_time/set_rtc_alarm的完整源码(含参数校验与localtime/mktime换算); - 掌握 RTC 时间设置、日期设置、闹钟设置的完整流程;
- 了解 NTP 自动校时机制;
- 理解更换 MCU 时如何适配 RTC(HAL 层
drv_rtc.c的rtc_ops契约)的移植方法。
说明:本节所有源码均取自项目
drivers/rtc/rtc.h、drivers/rtc/rtc.c、drivers/device.h、drivers/hal/st/drivers/drv_rtc.c,与项目源码逐字一致(含原始英文注释)。
🎯 学习目标
- 理解 OneOS RTC 以
time_t(Unix 时间戳)为核心的时间模型 - 掌握
struct tm的字段语义与tm_year/tm_mon偏移换算 - 掌握
time_t⇄struct tm的localtime/mktime双向转换 - 掌握 RTC 设备的 IOCTL 命令(GET/SET TIME、GET/SET ALARM)
- 看懂 HAL 层设备控制块
struct stm32_rtc的字段与内嵌os_device_t机制 - 逐个拆解
rtc_get/rtc_set/set_date/set_time/set_rtc_alarm的完整源码 - 掌握日期/时间/闹钟设置与参数校验
- 了解 NTP 自动校时机制
- 理解 RTC 的 HAL 层
rtc_ops接口契约,掌握更换 MCU 时的适配流程
一、概述
1.1 RTC 解决什么问题
一句话核心:RTC 设备解决"需要掉电后仍能维持的实时时钟、并方便地读写年月日时分秒和闹钟"的场景。它让你"设一次时间,之后随时读取,断电不丢"。
为什么用 Unix 时间戳:
- 真实硬件 RTC 芯片的时间存储方式各不相同(有的用寄存器存年月日,有的用计数器)。
- OneOS 统一用
time_t(Unix 时间戳,1970-01-01 起的秒数) 作为应用层时间接口,底层 HAL 负责把它转换成硬件能理解的形式。这样应用代码与具体芯片解耦。 - 上层再通过
localtime/mktime在时间戳与"年月日时分秒"(struct tm)之间换算。
1.2 RTC 设备的分层与 IOCTL
RTC 设备通过标准设备框架 + IOCTL 命令访问:
来源:drivers/rtc/rtc.h
#define OS_DEVICE_CTRL_RTC_GET_TIME IOC_RTC(0) /* Get time. */
#define OS_DEVICE_CTRL_RTC_SET_TIME IOC_RTC(1) /* Set time. */
#define OS_DEVICE_CTRL_RTC_GET_ALARM IOC_RTC(2) /* Get alarm. */
#define OS_DEVICE_CTRL_RTC_SET_ALARM IOC_RTC(3) /* Set alarm. */关键理解 —— 命令语义:
| 命令宏 | 值 | 作用 |
|---|---|---|
OS_DEVICE_CTRL_RTC_GET_TIME | IOC_RTC(0) | 读取时间(time_t) |
OS_DEVICE_CTRL_RTC_SET_TIME | IOC_RTC(1) | 设置时间(time_t) |
OS_DEVICE_CTRL_RTC_GET_ALARM | IOC_RTC(2) | 读取闹钟时间 |
OS_DEVICE_CTRL_RTC_SET_ALARM | IOC_RTC(3) | 设置闹钟时间 |
所有 RTC 操作最终都通过
os_device_control(device, 命令, &arg)下发到 HAL 层的os_device_ops.control实现(如 ST 的os_rtc_control,内部switch(cmd)分发到stm32_rtc_get_timestamp/stm32_rtc_set_time_stamp/stm32_rtc_set_alarm_stamp,见 4 章移植章节)。
数值演算 —— IOC_RTC 命令值是怎么算出来的:
IOC_RTC(cmd) 的定义(drivers/device.h)是一个位运算:
#define IOC_RTC(cmd) ((OS_DEVICE_TYPE_RTC << 8) | (cmd))其中 OS_DEVICE_TYPE_RTC 是设备类型枚举 enum os_device_type 中的第 6 个枚举值(OS_DEVICE_TYPE_CHAR = 0 起数),数值为 5。所以:
| 计算项 | 代入计算 | 结果(十进制) |
|---|---|---|
OS_DEVICE_TYPE_RTC | 枚举第 6 项 | 5 |
IOC_RTC(0)(GET_TIME) | (5 << 8) | 0 | 0x500 = 1280 |
IOC_RTC(1)(SET_TIME) | (5 << 8) | 1 | 0x501 = 1281 |
IOC_RTC(2)(GET_ALARM) | (5 << 8) | 2 | 0x502 = 1282 |
IOC_RTC(3)(SET_ALARM) | (5 << 8) | 3 | 0x503 = 1283 |
为什么用 (type << 8) | cmd 这种编码:把"设备类型"放进高 8 位、把"命令序号"放进低 8 位。这样每个设备类型的命令值都独占地落在自己的区间(RTC 的命令都在 0x500~0x5FF),os_device_control 把命令透传给各设备的 control 回调后,每个设备都能安全地用 switch(cmd) 区分自己的命令,而不会与其它设备类型的命令值冲突。
1.3 RTC 使用场景
1.3.1 一句话核心
RTC 设备适用于需要读取/设置实时时钟时间(年月日时分秒),以及定时闹钟唤醒的场景,尤其适合需要掉电保持时间(如日志时间戳、定时上报)的设备。它让你"设一次时间,之后随时读取,断电不丢"。
1.3.2 与其它"时间相关机制"的对比(为什么用它而不是别的)
OneOS 里不止一种"时间",选型前必须分清楚:
| 维度 | RTC 设备(本节) | timekeeping(系统时间) | HRTIMER(5.7) | clocksource(5.7) |
|---|---|---|---|---|
| 本质 | 独立电池供电的硬件日历时钟 | 软件维护的"系统当前时间"(gettimeofday) | 纳秒级定时触发 | 单调递增的纳秒计数器 |
| 断电保持 | ✅ 硬件 RTC 电池供电,掉电仍走时 | ❌ 掉电丢失,靠 RTC 恢复 | ❌ 掉电丢失 | ❌ 掉电丢失 |
| 时间类型 | 日历时间(可读年月日) | 日历时间(Unix 时间戳) | 相对定时(到点回调) | 单调相对计数 |
| 可被人为修改 | ✅ 可用 rtc_set 校准 | ✅ 可 settimeofday | ❌ | ❌(保证单调) |
| 精度 | 秒级 | 秒级 | 纳秒级 | 纳秒级 |
| 闹钟 | ✅ set_rtc_alarm | ❌ | 可用回调模拟 | ❌ |
结论:要"日历时间 + 掉电保持"用 RTC;要"系统当前时刻"(配合 NTP)用 timekeeping + RTC 校时;要"纳秒级定时触发"用 HRTIMER;要"单调计时"用 clocksource。
1.3.3 典型应用场景
场景一:设置并读取当前时间
> date 2026-08-19 10:30:00 (或调用 set_date + set_time)
> date (或调用 rtc_get 读取)
2026-08-19 10:30:05为什么用 RTC:rtc_get/rtc_set 用统一的 time_t,应用只需一行调用,HAL 处理硬件细节。
场景二:定时闹钟唤醒 / 定期上报
time_t wake = rtc_get() + 3600; /* 1 小时后 */
set_rtc_alarm(wake);为什么用 RTC:set_rtc_alarm 设置闹钟,配合低功耗(5.15 LPMGR)可实现定时唤醒。
1.3.4 选型边界表(何时不该用它)
| 场景 | 该用什么 | 为什么 |
|---|---|---|
| 只需要短时高精度计时 | HRTIMER(5.7)/ 内核定时器 | RTC 是日历时间,不是高精度计时器 |
| 只需单调计时(不关心日历) | clocksource(os_clocksource_gettime) | RTC 时间可被人为修改,不适合单调计时 |
二、核心 API 详解
说明:以下源码均取自
drivers/rtc/rtc.c,与项目源码逐字一致(含原始英文注释)。
2.1 关键结构体讲解
要读懂 RTC 的 API,先要理解 RTC 时间在代码里是用哪三种数据结构表达的,以及它们之间怎么换算。RTC 的核心是 time_t(时间戳) 与 struct tm(时间结构体) 的互相转换,外加 HAL 层的 设备控制块。
2.1.1 time_t —— Unix 时间戳(时间的"唯一真身")
定义(libc/newlib/libc/include/time.h 或平台对应头文件,本质是 typedef long time_t):
typedef long time_t;关键理解:
- RTC 内部所有时间都以 Unix 时间戳(从 1970-01-01 00:00:00 UTC 起的秒数)存储与传递。
rtc_get返回time_t,rtc_set接收time_t,闹钟也是time_t。应用层与 RTC 打交道,本质上就是在传秒数。- 它是一个整数,不是"年月日"——要展示给人看,必须转成
struct tm(见下)。
2.1.2 struct tm —— 时间结构体(把秒数拆成"年月日时分秒")
struct tm 是 C 标准库结构体,把 time_t(秒)拆成人能读的各个字段。set_date/set_time 就是靠它"只改其中几个字段"。
struct tm
{
int tm_sec; /* 秒 [0-59] */
int tm_min; /* 分 [0-59] */
int tm_hour; /* 时 [0-23] */
int tm_mday; /* 日 [1-31] */
int tm_mon; /* 月 [0-11],注意:0 表示 1 月 */
int tm_year; /* 年 [1900 年起偏移],实际年份 = tm_year + 1900 */
int tm_wday; /* 星期 [0-6],0 表示周日(星期几) */
int tm_yday; /* 年内第几天 [0-365],0 表示 1 月 1 日 */
int tm_isdst; /* 夏令时标志 */
};关键理解 —— 两个"坑"字段的偏移:
| 字段 | 存储方式 | 实际值 | 示例 |
|---|---|---|---|
tm_year | 相对于 1900 的偏移 | 实际年份 = tm_year + 1900 | 要表示 2026 年,tm_year = 2026 - 1900 = 126 |
tm_mon | 0 起始的下标 | 实际月份 = tm_mon + 1 | 要表示 8 月,tm_mon = 8 - 1 = 7 |
tm_mday | 1 起始 | 直接就是几号 | 8 月 19 日 → tm_mday = 19 |
tm_hour/min/sec | 直接值 | 直接就是几时几分几秒 | tm_hour = 14 |
这就是为什么项目源码里
set_date设置时写tm_new.tm_year = year - 1900;、tm_new.tm_mon = month - 1;——因为要从"人看的实际年月"反向换算成"结构体存储的偏移"。
为什么会出现偏移:这是 C 标准库的历史约定。tm_year 相对 1900、tm_mon 从 0 开始,是为了兼容早期的 C 语言实现。写 RTC 代码时最容易踩的坑就是忘记 ±1900、±1 的双向换算。
2.1.3 时间戳 ⇄ 结构体:localtime / mktime
time_t 和 struct tm 靠两个函数互相转换(这是 C 标准库函数,set_date/set_time 内部大量使用):
localtime(秒) mktime(结构体)
time_t ──────────────▶ struct tm
◀────────────── struct tm| 函数 | 方向 | 作用 |
|---|---|---|
localtime(&now) | time_t → struct tm | 把时间戳拆成"年月日时分秒"(返回指向静态变量) |
mktime(&tm_new) | struct tm → time_t | 把"年月日时分秒"合回时间戳 |
数值演算(假设当前时间戳 now = 1784613300,对应 2026 年某时刻):
| 步骤 | 代码 | 结果 |
|---|---|---|
| 时间戳 → 结构体 | localtime(&now) | tm.tm_year = 126、tm.tm_mon = 7、tm.tm_mday = 19、tm.tm_hour = 14… |
| 改日期字段 | tm.tm_mday = 20; | 只改日,时分秒保留 |
| 结构体 → 时间戳 | mktime(&tm) | 新的时间戳(对应 2026-08-20 14:...) |
注意:
localtime返回的是指向静态存储区的指针,下一次调用会覆盖同一块内存。项目源码用memcpy(&tm_new, p_tm, sizeof(struct tm))把它拷到自己栈上的tm_new再改字段,正是为了避免这个"静态共享"问题。
2.1.4 HAL 层设备控制块 struct stm32_rtc(内嵌 os_device_t)
框架层 rtc.c 通过 os_device_control(device, ...) 下发命令,device 实际指向的,是 HAL 层定义并注册的设备控制块。以 ST 为例(drivers/hal/st/drivers/drv_rtc.c,与项目逐字一致):
struct stm32_rtc
{
os_device_t rtc;
RTC_HandleTypeDef *hrtc;
};字段详解:
| 字段 | 类型 | 含义 |
|---|---|---|
rtc | os_device_t(内嵌在开头) | 标准设备基类,让这个结构体"是一个设备",可被 os_device_register 注册为 "rtc" |
hrtc | RTC_HandleTypeDef * | STM32 HAL 的 RTC 句柄,封装了具体 RTC 外设的寄存器基址与配置 |
关键理解 —— 为什么 os_device_t 内嵌在最开头:这与 5.2 GPIO 的 os_device_pin_t 同理——rtc(os_device_t)是第一个成员,所以"设备指针 os_device_t *dev"和"控制块指针 struct stm32_rtc *"数值相等。os_rtc_control 里 (struct stm32_rtc *)dev 只是把 dev 用更大的视角去解读同一块内存,从而访问 dev 后面的 hrtc 字段。
为什么 hrtc 用指针而非内嵌:RTC_HandleTypeDef 是 STM32 HAL 库定义的大型结构体(每个厂商、每款芯片不同),用指针 *hrtc 解耦——控制块不关心具体 HAL 结构体长什么样,只存一个句柄,hrtc 的内容由 BSP 的 stm32_rtc_probe 在注册时从板级配置拿。
各厂商 RTC 控制块的"形状"一致:虽然内部句柄类型不同(ST 是 RTC_HandleTypeDef *,GD 是 rtc_* 句柄,华大是 rtc_descriptor_t *),但都是"内嵌 os_device_t + 厂商 RTC 句柄指针"的模式,这是 RTC 设备抽象成标准字符设备的统一约定(详见第四章移植)。
2.2 rtc_get —— 获取当前时间
函数签名:
time_t rtc_get(void);无参数。返回当前 Unix 时间戳(time_t)。
完整源码(drivers/rtc/rtc.c,与项目一致):
time_t rtc_get(void)
{
time_t time_now = 0;
os_device_t *device = os_device_open_s(OS_RTC_DEV_NAME);
if (device == OS_NULL)
{
os_kprintf("find no rtc device\r\n");
return 0;
}
os_device_control(device, OS_DEVICE_CTRL_RTC_GET_TIME, &time_now);
os_device_close(device);
return time_now;
}分步详解:
第 1 步:打开 RTC 设备(条件 device == OS_NULL)
time_t time_now = 0;
os_device_t *device = os_device_open_s(OS_RTC_DEV_NAME);
if (device == OS_NULL)
{
os_kprintf("find no rtc device\r\n");
return 0;
}详细解释:os_device_open_s(OS_RTC_DEV_NAME) 打开名为 OS_RTC_DEV_NAME(通常为 "rtc")的 RTC 设备。打开失败(未注册)打印提示并返回 0。
第 2 步:发 GET_TIME 命令并关闭(无条件执行)
os_device_control(device, OS_DEVICE_CTRL_RTC_GET_TIME, &time_now);
os_device_close(device);
return time_now;
}详细解释:用 OS_DEVICE_CTRL_RTC_GET_TIME 命令让 HAL 层把当前时间写进 time_now。关闭设备后返回时间戳。
rtc_get() 条件走向总结:
rtc_get() 条件走向总结:
├── device = os_device_open_s("rtc")
├── device == OS_NULL(设备未注册)→ 打印 "find no rtc device" + 返回 0
└── device != OS_NULL(打开成功)
├── os_device_control(GET_TIME, &time_now) ← HAL 把时间写进 time_now
├── os_device_close(device)
└── 返回 time_now2.3 rtc_set —— 设置当前时间
函数签名:
os_err_t rtc_set(time_t time);| 参数 | 说明 |
|---|---|
time | 要设置的 Unix 时间戳 |
| 返回 | OS_SUCCESS 成功,OS_FAILURE 失败 |
完整源码(drivers/rtc/rtc.c,与项目一致):
os_err_t rtc_set(time_t time)
{
os_err_t ret = OS_FAILURE;
os_device_t *device = os_device_open_s(OS_RTC_DEV_NAME);
if (device == OS_NULL)
{
os_kprintf("find no rtc device\r\n");
return ret;
}
ret = os_device_control(device, OS_DEVICE_CTRL_RTC_SET_TIME, &time);
os_device_close(device);
#ifdef OS_USING_TIMEKEEPING
os_timekeeping_init();
#endif
return ret;
}分步详解:
第 1 步:打开设备并发 SET_TIME 命令(条件 打开失败)
os_err_t ret = OS_FAILURE;
os_device_t *device = os_device_open_s(OS_RTC_DEV_NAME);
if (device == OS_NULL)
{
os_kprintf("find no rtc device\r\n");
return ret;
}
ret = os_device_control(device, OS_DEVICE_CTRL_RTC_SET_TIME, &time);
os_device_close(device);详细解释:打开设备,用 OS_DEVICE_CTRL_RTC_SET_TIME 命令下发时间。返回 ret 记录 HAL 设置结果,关闭设备。
第 2 步:重新初始化 timekeeping(条件 OS_USING_TIMEKEEPING)
#ifdef OS_USING_TIMEKEEPING
os_timekeeping_init();
#endif
return ret;
}详细解释:若启用了 timekeeping(系统时间保持模块,见 drivers/timer/timekeeping),调用 os_timekeeping_init() 让它以刚设置的 RTC 时间为基准重新初始化,保证系统时间与 RTC 同步。
rtc_set() 条件走向总结:
rtc_set() 条件走向总结:
├── device = os_device_open_s("rtc")
├── device == OS_NULL(设备未注册)→ 打印 "find no rtc device" + 返回 ret(OS_FAILURE)
└── device != OS_NULL(打开成功)
├── ret = os_device_control(SET_TIME, &time) ← HAL 设置时间,ret 记录结果
├── os_device_close(device)
├── #ifdef OS_USING_TIMEKEEPING(编译配置)
│ └── 已定义 → os_timekeeping_init()(让系统时间与 RTC 同步)
└── 返回 ret2.4 set_date —— 设置日期
函数签名:
os_err_t set_date(uint32_t year, uint32_t month, uint32_t day);| 参数 | 说明 |
|---|---|
year | 年(范围 2000~2099) |
month | 月(1~12) |
day | 日(1~31) |
| 返回 | OS_SUCCESS 成功,OS_FAILURE 失败 |
完整源码(drivers/rtc/rtc.c,与项目一致):
os_err_t set_date(uint32_t year, uint32_t month, uint32_t day)
{
time_t now;
struct tm *p_tm;
struct tm tm_new;
os_err_t ret = OS_FAILURE;
int32_t year_range[2];
year_range[0] = 2000;
year_range[1] = 2099;
/* parameter check */
if (year > year_range[1] || year < year_range[0])
{
os_kprintf("year is out of range [%d-%d]\r\n", year_range[0], year_range[1]);
ret = OS_FAILURE;
return ret;
}
if (month == 0 || month > 12)
{
os_kprintf("month is out of range [1-12]\r\n");
ret = OS_FAILURE;
return ret;
}
if (day == 0 || day > 31)
{
os_kprintf("day is out of range [1-31]\r\n");
ret = OS_FAILURE;
return ret;
}
/* get current time */
now = rtc_get();
/* lock scheduler. */
/* os_schedule_lock(); */
/* converts calendar time time into local time. */
p_tm = localtime(&now);
/* copy the statically located variable */
memcpy(&tm_new, p_tm, sizeof(struct tm));
/* unlock scheduler. */
/* os_schedule_unlock(); */
/* update date. */
tm_new.tm_year = year - 1900;
tm_new.tm_mon = month - 1; /* tm_mon: 0~11 */
tm_new.tm_mday = day;
/* converts the local time in time to calendar time. */
now = mktime(&tm_new);
/* update to RTC device. */
ret = rtc_set(now);
return ret;
}分步详解:
第 1 步:参数校验(条件 年/月/日越界)
year_range[0] = 2000;
year_range[1] = 2099;
/* parameter check */
if (year > year_range[1] || year < year_range[0])
{
os_kprintf("year is out of range [%d-%d]\r\n", year_range[0], year_range[1]);
ret = OS_FAILURE;
return ret;
}
if (month == 0 || month > 12)
{
os_kprintf("month is out of range [1-12]\r\n");
ret = OS_FAILURE;
return ret;
}
if (day == 0 || day > 31)
{
os_kprintf("day is out of range [1-31]\r\n");
ret = OS_FAILURE;
return ret;
}详细解释:参数校验:年必须在 [2000, 2099],月 [1,12],日 [1,31],任一越界打印提示并返回 OS_FAILURE。这是"先校验再设置"的典型做法,避免把非法值写进 RTC。
第 2 步:取当前时间并只改日期字段(无条件执行)
/* get current time */
now = rtc_get();
/* lock scheduler. */
/* os_schedule_lock(); */
/* converts calendar time time into local time. */
p_tm = localtime(&now);
/* copy the statically located variable */
memcpy(&tm_new, p_tm, sizeof(struct tm));
/* unlock scheduler. */
/* os_schedule_unlock(); */
/* update date. */
tm_new.tm_year = year - 1900;
tm_new.tm_mon = month - 1; /* tm_mon: 0~11 */
tm_new.tm_mday = day;
/* converts the local time in time to calendar time. */
now = mktime(&tm_new);
/* update to RTC device. */
ret = rtc_set(now);
return ret;
}详细解释:关键理解:set_date 只改"日期"不动"时间"。所以先 rtc_get() 拿当前时间戳,localtime 转成 struct tm,拷到 tm_new,只更新 tm_year(注意要 -1900)、tm_mon(-1,因为 tm_mon 是 0~11)、tm_mday 三个日期字段,保留时分秒。再用 mktime 把 struct tm 转回时间戳,最后 rtc_set 写回 RTC。
为什么 set_date/set_time 都先 rtc_get() 再改字段:这两个函数的目标是"只改日期"或"只改时间",而不是"整个重设"。若直接用用户给的日期去 mktime(时分秒为 0),就会把当前时间清零。所以必须先读当前时间、转成 struct tm、只覆盖目标字段、其余原样保留,再转回时间戳。这保证了"改日期不动时间、改时间不动日期"。
set_date() 条件走向总结:
set_date(year, month, day) 条件走向总结:
├── year > 2099 或 year < 2000(年越界)→ 打印 + 返回 OS_FAILURE
├── month == 0 或 month > 12(月越界)→ 打印 + 返回 OS_FAILURE
├── day == 0 或 day > 31(日越界)→ 打印 + 返回 OS_FAILURE
├── now = rtc_get()(读当前时间)
├── tm_new = localtime(now) 的拷贝
├── 只更新 tm_year(-1900)/tm_mon(-1)/tm_mday,保留时分秒
├── now = mktime(&tm_new)(转回时间戳)
└── return rtc_set(now)2.5 set_time —— 设置时间
函数签名:
os_err_t set_time(uint32_t hour, uint32_t minute, uint32_t second);| 参数 | 说明 |
|---|---|
hour | 时(0~23) |
minute | 分(0~59) |
second | 秒(0~59) |
| 返回 | OS_SUCCESS 成功,OS_FAILURE 失败 |
完整源码(drivers/rtc/rtc.c,与项目一致):
os_err_t set_time(uint32_t hour, uint32_t minute, uint32_t second)
{
time_t now;
struct tm *p_tm;
struct tm tm_new;
os_err_t ret = OS_FAILURE;
if (hour > 23)
{
os_kprintf("hour is out of range [0-23]\r\n");
return ret;
}
if (minute > 59)
{
os_kprintf("minute is out of range [0-59]\r\n");
return ret;
}
if (second > 59)
{
os_kprintf("second is out of range [0-59]\r\n");
return ret;
}
/* get current time */
now = rtc_get();
/* lock scheduler. */
/* os_schedule_lock(); */
/* converts calendar time time into local time. */
p_tm = localtime(&now);
/* copy the statically located variable */
memcpy(&tm_new, p_tm, sizeof(struct tm));
/* unlock scheduler. */
/* os_schedule_unlock(); */
/* update time. */
tm_new.tm_hour = hour;
tm_new.tm_min = minute;
tm_new.tm_sec = second;
/* converts the local time in time to calendar time. */
now = mktime(&tm_new);
/* update to RTC device. */
ret = rtc_set(now);
return ret;
}分步详解:
第 1 步:参数校验(条件 时分秒越界)
if (hour > 23)
{
os_kprintf("hour is out of range [0-23]\r\n");
return ret;
}
if (minute > 59)
{
os_kprintf("minute is out of range [0-59]\r\n");
return ret;
}
if (second > 59)
{
os_kprintf("second is out of range [0-59]\r\n");
return ret;
}详细解释:校验时 [0,23]、分 [0,59]、秒 [0,59],越界返回 OS_FAILURE。注意这里的 ret 初始值就是 OS_FAILURE,所以直接 return ret 即返回失败。
第 2 步:取当前时间并只改时间字段(无条件执行)
/* get current time */
now = rtc_get();
/* lock scheduler. */
/* os_schedule_lock(); */
/* converts calendar time time into local time. */
p_tm = localtime(&now);
/* copy the statically located variable */
memcpy(&tm_new, p_tm, sizeof(struct tm));
/* unlock scheduler. */
/* os_schedule_unlock(); */
/* update time. */
tm_new.tm_hour = hour;
tm_new.tm_min = minute;
tm_new.tm_sec = second;
/* converts the local time in time to calendar time. */
now = mktime(&tm_new);
/* update to RTC device. */
ret = rtc_set(now);
return ret;
}详细解释:与 set_date 对称,只更新 tm_hour/tm_min/tm_sec 三个时间字段,保留日期,再 mktime + rtc_set。参数校验越界时直接 return ret(此时 ret 仍是初始值 OS_FAILURE)。
set_time() 条件走向总结:
set_time(hour, minute, second) 条件走向总结:
├── hour > 23(时越界)→ 打印 + 返回 OS_FAILURE
├── minute > 59(分越界)→ 打印 + 返回 OS_FAILURE
├── second > 59(秒越界)→ 打印 + 返回 OS_FAILURE
├── now = rtc_get()(读当前时间)
├── tm_new = localtime(now) 的拷贝
├── 只更新 tm_hour/tm_min/tm_sec,保留日期
├── now = mktime(&tm_new)(转回时间戳)
└── return rtc_set(now)2.6 set_rtc_alarm —— 设置闹钟
函数签名:
os_err_t set_rtc_alarm(time_t time);| 参数 | 说明 |
|---|---|
time | 闹钟触发的 Unix 时间戳 |
| 返回 | OS_SUCCESS 成功,OS_FAILURE 失败 |
完整源码(drivers/rtc/rtc.c,与项目一致):
os_err_t set_rtc_alarm(time_t time)
{
os_err_t ret = OS_FAILURE;
os_device_t *device = os_device_open_s(OS_RTC_DEV_NAME);
if (device == OS_NULL)
{
os_kprintf("find no rtc device\r\n");
return ret;
}
ret = os_device_control(device, OS_DEVICE_CTRL_RTC_SET_ALARM, &time);
os_device_close(device);
return ret;
}分步详解:
第 1 步:打开设备并发 SET_ALARM 命令(条件 打开失败)
os_err_t ret = OS_FAILURE;
os_device_t *device = os_device_open_s(OS_RTC_DEV_NAME);
if (device == OS_NULL)
{
os_kprintf("find no rtc device\r\n");
return ret;
}
ret = os_device_control(device, OS_DEVICE_CTRL_RTC_SET_ALARM, &time);
os_device_close(device);
return ret;
}详细解释:打开设备,用 OS_DEVICE_CTRL_RTC_SET_ALARM 命令下发闹钟时间戳。底层 HAL 会配置 RTC 闹钟,到点时触发中断并调用闹钟回调(应用可通过 5.15 LPMGR 或硬件中断进入唤醒)。注意:set_rtc_alarm 没有做任何时间合法性校验(如闹钟时间是否在当前时间之后),校验责任在调用方。
set_rtc_alarm() 条件走向总结:
set_rtc_alarm(time) 条件走向总结:
├── device = os_device_open_s("rtc")
├── device == OS_NULL(设备未注册)→ 打印 "find no rtc device" + 返回 ret(OS_FAILURE)
└── device != OS_NULL(打开成功)
├── ret = os_device_control(SET_ALARM, &time) ← HAL 配置闹钟,ret 记录结果
├── os_device_close(device)
└── 返回 ret2.7 NTP 自动校时(条件 RTC_SYNC_USING_NTP)
一句话定位:开启 NTP 同步时,启动一个后台任务周期性从 NTP 服务器校时到 RTC。
完整源码(drivers/rtc/rtc.c,与项目一致):
#ifdef RTC_SYNC_USING_NTP
static void ntp_sync_task_enrty(void *param)
{
extern time_t ntp_sync_to_rtc(const char *host_name);
os_task_msleep(RTC_NTP_FIRST_SYNC_DELAY * 1000);
while (1)
{
ntp_sync_to_rtc(NULL);
os_task_msleep(RTC_NTP_SYNC_PERIOD * 1000);
}
}
int os_rtc_ntp_sync_init(void)
{
static os_bool_t init_ok = OS_FALSE;
os_task_id task;
if (init_ok)
{
return 0;
}
task = os_task_create(OS_NULL, OS_NULL, 1536, "ntp_sync", ntp_sync_task_enrty, OS_NULL, 26);
if (task)
{
os_task_startup(task);
}
else
{
return OS_NOMEM;
}
init_ok = OS_TRUE;
return OS_SUCCESS;
}
OS_INIT_CALL(os_rtc_ntp_sync_init, OS_INIT_LEVEL_COMPONENT);
#endif /* RTC_SYNC_USING_NTP */分步详解:
第 1 步:创建 NTP 同步任务(条件 已初始化则直接返回)
if (init_ok)
{
return 0;
}
task = os_task_create(OS_NULL, OS_NULL, 1536, "ntp_sync", ntp_sync_task_enrty, OS_NULL, 26);
if (task)
{
os_task_startup(task);
}
else
{
return OS_NOMEM;
}
init_ok = OS_TRUE;
return OS_SUCCESS;
}
OS_INIT_CALL(os_rtc_ntp_sync_init, OS_INIT_LEVEL_COMPONENT);详细解释:init_ok 保证只创建一次。创建优先级 26、栈 1536 的 ntp_sync 任务,OS_INIT_CALL 让它在组件级别自动初始化。
第 2 步:任务体内周期校时(无条件执行)
os_task_msleep(RTC_NTP_FIRST_SYNC_DELAY * 1000);
while (1)
{
ntp_sync_to_rtc(NULL);
os_task_msleep(RTC_NTP_SYNC_PERIOD * 1000);
}详细解释:先等 RTC_NTP_FIRST_SYNC_DELAY(默认 30s,等网络就绪)再首次同步。之后循环:ntp_sync_to_rtc(NULL) 从 NTP 服务器取时间写入 RTC,然后等 RTC_NTP_SYNC_PERIOD(默认 1 小时)再次同步。
os_rtc_ntp_sync_init() 条件走向总结:
os_rtc_ntp_sync_init() 条件走向总结:
├── init_ok == OS_TRUE(已初始化过)→ 直接返回 0(防止重复创建任务)
├── init_ok == OS_FALSE(首次调用)
│ ├── task = os_task_create(NULL, NULL, 1536, "ntp_sync", ntp_sync_task_enrty, NULL, 26)
│ ├── task != OS_NULL(创建成功)→ os_task_startup(task)(启动 NTP 任务)
│ ├── task == OS_NULL(创建失败,栈/TCB 内存不足)→ 返回 OS_NOMEM
│ ├── init_ok = OS_TRUE(标记已初始化)
│ └── 返回 OS_SUCCESS
└── 由 OS_INIT_CALL(os_rtc_ntp_sync_init, OS_INIT_LEVEL_COMPONENT) 在组件初始化阶段自动调用NTP 同步任务的时间线:
RTC_NTP_FIRST_SYNC_DELAY(默认 30s,等网络连上)→ 首次ntp_sync_to_rtc→ 每RTC_NTP_SYNC_PERIOD(默认 1h)循环同步一次。这样 RTC 会周期性被 NTP 服务器校准,弥补 RTC 晶振漂移。
三、完整使用示例
示例:RTC 时间读写、日期/时间设置与闹钟
#include <rtc/rtc.h>
#include <os_stddef.h>
#include <os_util.h>
#include <time.h>
void rtc_demo(void)
{
time_t now;
os_err_t ret;
/* 1. 设置日期为 2026-08-19(只改日期,保留当前时间) */
ret = set_date(2026, 8, 19);
if (ret != OS_SUCCESS)
{
os_kprintf("set_date failed\r\n");
return;
}
/* 2. 设置时间为 10:30:00(只改时间,保留日期) */
ret = set_time(10, 30, 0);
if (ret != OS_SUCCESS)
{
os_kprintf("set_time failed\r\n");
return;
}
/* 3. 读取当前时间并打印(Unix 时间戳 → struct tm → 格式化) */
now = rtc_get();
{
struct tm *t = localtime(&now);
os_kprintf("Now: %04d-%02d-%02d %02d:%02d:%02d\r\n",
t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
t->tm_hour, t->tm_min, t->tm_sec);
}
/* 4. 设置 10 秒后的闹钟 */
ret = set_rtc_alarm(now + 10);
if (ret != OS_SUCCESS)
{
os_kprintf("set_rtc_alarm failed\r\n");
return;
}
os_kprintf("alarm set at now + 10s\r\n");
}说明:示例展示 RTC 标准用法:
set_date+set_time设日期时间,rtc_get读时间,set_rtc_alarm设闹钟。实际效果:日期和时间分别设置(互不影响),读取后格式化打印,10 秒后触发闹钟。
注意:
set_date只改日期、set_time只改时间,两者各保留对方字段。所有时间均用 Unix 时间戳(time_t)传递,展示时用localtime转struct tm。
四、RTC 的移植/适配(更换 MCU)
4.1 先认识 HAL 层:谁在真正读写 RTC 硬件
回顾 1.2 节,应用层所有 RTC 操作(rtc_get/rtc_set/set_rtc_alarm)都通过 os_device_control(device, cmd, &arg) 下发命令。这个命令最终落到谁的 control 回调上? 答案就是 HAL 层每个厂商的 drv_rtc.c。
关键认知:drivers/rtc/rtc.c 是跨平台统一的框架层,它只负责"打开 rtc 设备、发命令、关闭",不碰任何硬件。真正跟芯片相关的是 drivers/hal/<厂商>/drivers/drv_rtc.c——它为这个 MCU 实现"把 Unix 时间戳读写到 RTC 硬件寄存器"以及"配置 RTC 闹钟"。换 MCU 只需换这份 drv_rtc.c。
项目 drivers/hal/ 下每个 MCU 厂商都有一份 RTC 驱动(文件名一致、结构一致,内部换成各自寄存器/HAL 库):
| 厂商 | 文件 | 实现方式 |
|---|---|---|
| ST | drivers/hal/st/drivers/drv_rtc.c | 用 STM32 HAL(HAL_RTC_GetTime/HAL_RTC_SetTime/HAL_RTC_SetAlarm_IT) |
| GD | drivers/hal/gd/drivers/f4xx/drv_rtc.c | 用 GD32 库(rtc_*) |
| 华大 HDSC | drivers/hal/hdsc/drivers/lfxx/drv_rtc.c | 用华大库 |
| 全志 | drivers/hal/csky/cmc/drivers/drv_rtc.c | 用 CSKY 平台 RTC 驱动 |
这些
drv_rtc.c是并列的平级实现,一个工程只编译当前所选 MCU 的那一份。换 MCU 厂商 = 换一份drv_rtc.c,框架层与应用层代码一行不改。
4.2 移植的全景图:只有 HAL 层与 MCU 相关
┌──────────────────────────────────────────────────────────┐
│ 应用层(Application) │
│ rtc_get / rtc_set / set_date / set_time / set_rtc_alarm │
└──────────────────────────────┬───────────────────────────┘
│ os_device_control(device, cmd, &arg)
▼
┌──────────────────────────────────────────────────────────┐
│ RTC 框架层(drivers/rtc/rtc.c) │
│ 打开 "rtc" 设备 → 发 GET/SET TIME/ALARM 命令 → 关闭 │
│ (换 MCU 不用改) │
└──────────────────────────────┬───────────────────────────┘
│ ioctl cmd + &arg
▼
┌──────────────────────────────────────────────────────────┐
│ HAL 层(drivers/hal/<厂商>/drivers/drv_rtc.c) ★需适配★ │
│ os_rtc_control(dev, cmd, args) switch(cmd) 分发 │
│ get/set 时间戳、配置闹钟(操作具体 RTC 寄存器) │
└──────────────────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 硬件(MCU 的 RTC 外设,电池供电) │
└──────────────────────────────────────────────────────────┘移植的关键洞察:换 MCU,上面两层(应用层 + 框架层)一行都不用改,只需在 HAL 层提供"能读 RTC 时间"、"能写 RTC 时间"、"能配闹钟"的实现,并注册成名为 rtc 的设备。这就是 RTC 抽象成标准字符设备的解耦价值。
4.3 移植需要提供的"接口契约"
HAL 层 RTC 驱动要能用,必须实现一个 control 回调 + 一份设备信息 + 一次注册,契约核心是处理四个 IOCTL 命令(见 1.2 节的数值演算)。
源码参考——ST 的 os_rtc_control(drivers/hal/st/drivers/drv_rtc.c,与项目逐字一致,展示 switch(cmd) 分发骨架):
static os_err_t os_rtc_control(os_device_t *dev, int cmd, void *args)
{
os_err_t result = OS_FAILURE;
OS_ASSERT(dev != OS_NULL);
struct stm32_rtc *st_rtc = (struct stm32_rtc *)dev;
switch (cmd)
{
case OS_DEVICE_CTRL_RTC_GET_TIME:
*(time_t *)args = stm32_rtc_get_timestamp(st_rtc->hrtc);
LOG_D(DBG_TAG, "RTC: get rtc_time %x\r\n", *(time_t *)args);
result = OS_SUCCESS;
break;
case OS_DEVICE_CTRL_RTC_SET_TIME:
if (stm32_rtc_set_time_stamp(st_rtc->hrtc, *(time_t *)args))
{
result = OS_FAILURE;
}
else
{
LOG_D(DBG_TAG, "RTC: set rtc_time %x\r\n", *(time_t *)args);
result = OS_SUCCESS;
}
break;
case OS_DEVICE_CTRL_RTC_SET_ALARM:
if (stm32_rtc_set_alarm_stamp(st_rtc->hrtc, *(time_t *)args))
{
result = OS_FAILURE;
}
else
{
result = OS_SUCCESS;
}
break;
}
return result;
}接口契约要点:
| 项目 | 要求 |
|---|---|
control 回调 | 处理 GET_TIME/SET_TIME/GET_ALARM/SET_ALARM 四个命令(至少前三个) |
| 时间戳 ↔ 硬件 | 用 localtime/mktime 在 time_t 与 RTC 硬件时间寄存器(年月日时分秒)之间换算 |
args 参数 | 指向 time_t,GET_TIME 写回、SET_TIME/SET_ALARM 读出 |
| 设备 ops | const static struct os_device_ops rtc_ops = { .control = os_rtc_control }; |
| 注册 | 通过 OS_DRIVER_INFO + OS_DRIVER_DEFINE(或直接 os_device_register)注册为 rtc 设备 |
4.4 移植的标准流程(5 步)
第 1 步:确认 RTC 硬件能力——新 MCU 的 RTC 外设是否电池供电、支持闹钟、时间寄存器格式(BCD/二进制)、闰年处理。
第 2 步:定义设备控制块——封装 MCU 的 RTC 句柄:
struct my_rtc
{
os_device_t rtc; /* 内嵌标准设备 */
MY_RTC_HandleTypeDef *hrtc; /* MCU 的 RTC 句柄 */
};第 3 步:实现时间戳 ↔ 硬件换算(读):
static time_t my_rtc_get_timestamp(MY_RTC_HandleTypeDef *hrtc)
{
struct tm tm_new;
/* 1. 从硬件读年月日时分秒,填入 tm_new(注意 tm_year 要 +1900、tm_mon 要 +1) */
/* 2. 返回 mktime(&tm_new) 即 Unix 时间戳 */
}第 4 步:实现 os_rtc_control 并填 ops——按 4.3 的契约处理四个命令,其中 SET_TIME/SET_ALARM 要把 *(time_t*)args 转回年月日写硬件:
static os_err_t my_rtc_control(os_device_t *dev, int cmd, void *args)
{
struct my_rtc *rtc = (struct my_rtc *)dev;
switch (cmd)
{
case OS_DEVICE_CTRL_RTC_GET_TIME:
*(time_t *)args = my_rtc_get_timestamp(rtc->hrtc);
return OS_SUCCESS;
case OS_DEVICE_CTRL_RTC_SET_TIME:
return my_rtc_set_time_stamp(rtc->hrtc, *(time_t *)args);
case OS_DEVICE_CTRL_RTC_SET_ALARM:
return my_rtc_set_alarm_stamp(rtc->hrtc, *(time_t *)args);
default:
return OS_FAILURE;
}
}
const static struct os_device_ops rtc_ops = {
.control = my_rtc_control,
};第 5 步:注册设备——用 OS_DRIVER_INFO + OS_DRIVER_DEFINE(或板级注册)把 rtc 设备挂进系统,名字必须是 OS_RTC_DEV_NAME(即 "rtc"),否则 rtc_get/rtc_set 打开设备会失败:
static int my_rtc_probe(const os_driver_info_t *drv, const os_device_info_t *dev)
{
struct my_rtc *rtc = os_calloc(1, sizeof(struct my_rtc));
OS_ASSERT(rtc);
rtc->hrtc = (MY_RTC_HandleTypeDef *)dev->info;
rtc->rtc.type = OS_DEVICE_TYPE_RTC;
rtc->rtc.ops = &rtc_ops;
return os_device_register(&rtc->rtc, dev->name);
}
OS_DRIVER_INFO my_rtc_driver = {
.name = "MY_RTC_HandleTypeDef",
.probe = my_rtc_probe,
};
OS_DRIVER_DEFINE(my_rtc_driver, OS_INIT_LEVEL_PRE_DEVICE, OS_INIT_SUBLEVEL_LOW);提示:
dev->name来自板级配置的os_device_info(dev_rtc表),它决定了注册后的设备名。若该名不是"rtc",需让rtc.h中的OS_RTC_DEV_NAME与之一致。
4.5 芯片未适配怎么办(复用 vs 新建 BSP)
| 维度 | 已有厂商补型号 | 全新厂商新建 BSP |
|---|---|---|
| 判断 | 厂商目录已存在(如 hal/st/),只是型号没列 | 厂商目录完全不存在 |
是否新建 drv_rtc.c | 通常不改,只补型号的 RTC_HandleTypeDef/初始化 | 从零实现一份 drv_rtc.c |
是否改厂商宏/SRC_HAL | 否 | 是(加 MANUFACTOR_XXX + inq_table 映射) |
| 工作量 | 小 | 大 |
构建系统如何选这份 drv_rtc.c:与 5.2 GPIO、5.7 Timer 的移植章节一致,靠三层机制——①厂商宏(MANUFACTOR_XXX)→ ②SRC_HAL 目录 → ③weave.yaml 条件编译,最终只编译当前所选 MCU 的那份 drv_rtc.c。换 MCU 厂商 → 宏变 → SRC_HAL 变 → 编译的 drv_rtc.c 换成新厂商那份。
4.6 移植检查清单(快速自查)
| 检查项 | 说明 |
|---|---|
| 四个命令都处理? | GET_TIME/SET_TIME 必须有,SET_ALARM/GET_ALARM 视硬件 |
注册名是 "rtc"? | 必须与 OS_RTC_DEV_NAME 一致,否则框架打开失败 |
tm_year/tm_mon 换算正确? | 硬件读写时要 +1900/-1900、+1/-1 双向换算 |
| 首次上电校准? | HAL 层通常用备份寄存器标记是否已校准(如 ST 的 BKUP_REG_DATA) |
| 闹钟中断是否接线? | 若用闹钟,需使能对应中断并向 HAL 注册回调 |
📝 本节小结
核心要点回顾
本节围绕 RTC 设备展开,核心要点如下:
1. 时间模型
- OneOS RTC 统一使用
time_t(Unix 时间戳),底层 HAL 负责与硬件换算。 - IOCTL 命令:
GET_TIME/SET_TIME/GET_ALARM/SET_ALARM。
2. 五个核心 API
| 函数 | 作用 |
|---|---|
rtc_get | 读取当前 Unix 时间戳 |
rtc_set | 设置当前 Unix 时间戳 |
set_date | 只改日期(保留时间),参数校验 |
set_time | 只改时间(保留日期),参数校验 |
set_rtc_alarm | 设置闹钟时间戳 |
3. 关键设计
set_date/set_time先rtc_get读当前,localtime转struct tm,只改对应字段,再mktime+rtc_set,实现"改日期不动时间、改时间不动日期"。- 参数校验:年
[2000,2099]、月[1,12]、日[1,31]、时[0,23]、分/秒[0,59]。
4. NTP 校时
RTC_SYNC_USING_NTP开启时创建后台任务,首延 30s、周期 1h 自动从 NTP 校时到 RTC。
5. 关键结构体
time_t(Unix 时间戳)是 RTC 的"唯一真身",应用层传的就是秒数。struct tm把秒数拆成"年月日时分秒";tm_year是相对 1900 的偏移(+1900)、tm_mon是 0 起始(+1),是最大的坑。localtime把time_t拆成struct tm(返回静态共享区,需memcpy拷走);mktime把struct tm合回time_t。- HAL 层设备控制块
struct stm32_rtc内嵌os_device_t(首成员,保证dev与控制块指针数值相等)+ RTC 句柄指针。
6. 更换 MCU 的移植要点
- 框架层
rtc.c(应用 API)与 HAL 层drv_rtc.c(硬件实现)解耦,换 MCU 只换drv_rtc.c。 - HAL 层契约 = 处理四个 IOCTL 命令的
control回调 +os_device_ops+ 注册为"rtc"设备。 - 构建系统靠"厂商宏 →
SRC_HAL目录 →weave.yaml"三层机制选择要编译的那份drv_rtc.c。
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
| 时间值越界 | set_date/set_time 会校验范围并返回 OS_FAILURE,调用前自己再校验一次更稳妥 |
| 忘记初始校准时间 | RTC 掉电仍走时,首次上电需用 NTP(RTC_SYNC_USING_NTP)或外部源校准,否则时间不准 |
混淆 tm_year/tm_mon | struct tm.tm_year 需 +1900、tm_mon 需 +1 才是实际年月;源码里设置时反向 -1900/-1 |
localtime 返回静态共享区 | localtime 返回指向静态区,多次调用会互相覆盖;务必用 memcpy 拷到自己栈上的 tm_new 再改字段(源码正是这么做的) |
| 混淆本地时间与 UTC | time_t 本身是 UTC,localtime 转本地;展示给用户时需考虑时区 |
| 忽略 RTC 设备未注册 | 未注册 "rtc" 设备时 rtc_get/rtc_set 会打印 find no rtc device 并失败 |
set_date 想同时改时间 | set_date 只改日期,要改时间请再调 set_time |
误以为 set_date 会清空时间 | set_date/set_time 内部先 rtc_get 再只改对应字段,不会清空另一部分 |
| 换 MCU 改框架层 | 只改 HAL 层 drv_rtc.c,rtc.c 框架与应用层不动 |
注册名不是 "rtc" | 注册设备名必须与 OS_RTC_DEV_NAME 一致,否则 rtc_get/rtc_set 打开失败 |
HAL 里 tm_year/tm_mon 换算错 | 读写硬件时要 +1900/-1900、+1/-1 双向换算,方向相反时间就错 |
| 闹钟时间合法性 | set_rtc_alarm 不做校验,调用方要保证闹钟时间在未来 |
下一步
掌握了 RTC 实时时钟设备后,接下来学习 Watchdog 看门狗设备:
- 5.9 Watchdog设备:了解 OneOS 独立/窗口看门狗的配置与喂狗策略。