目录
[I.MX6U 内部 RTC 驱动](#I.MX6U 内部 RTC 驱动)
[snvs_rtc 设备节点](#snvs_rtc 设备节点)
[snvs_rtc_probe 函数](#snvs_rtc_probe 函数)
[snvs_rtc_read_time 函数](#snvs_rtc_read_time 函数)
[RTC 时间查看与设置](#RTC 时间查看与设置)
[时间 RTC 查看date](#时间 RTC 查看date)
[设置 RTC 时间](#设置 RTC 时间)
[hwclock 命令](#hwclock 命令)
在上一讲内容里:Linux RTC 驱动简介,我们简单了解了一些linux下RTC驱动相关的结构体变量和函数。
本讲内容里,我们学习正点原子I.MX6U 开发板的内部 RTC 驱动,掌握RTC时间查看与设置的方法。
I.MX6U 内部 RTC 驱动
从设备树开始,打开我们自己移植的linux源码路径下的**/arch/arm/boot/dts/imx6ull.dtsi**,在里面找到如下 snvs_rtc 设备节点。
snvs_rtc 设备节点
snvs_rtc 设备节点内容如下所示

其中,设置兼容属性 compatible 的值为"fsl,sec-v4.0-mon-rtc-lp",在 Linux 内核源码中搜索此字符串即可找到对应的驱动文件,此文件为 drivers/rtc/rtc-snvs.c,
在 rtc-snvs.c 文件中找到如下所示内容:
cpp
static const struct of_device_id snvs_dt_ids[] = {
{ .compatible = "fsl,sec-v4.0-mon-rtc-lp", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, snvs_dt_ids);
static struct platform_driver snvs_rtc_driver = {
.driver = {
.name = "snvs_rtc",
.pm = SNVS_RTC_PM_OPS,
.of_match_table = snvs_dt_ids,
},
.probe = snvs_rtc_probe,
};
module_platform_driver(snvs_rtc_driver);
其中,设备树 ID 表的compatible 属性,值为"fsl,sec-v4.0-mon-rtc-lp",因此 imx6ull.dtsi 中的 snvs_rtc 设备节点会和此驱动匹配。
当设备和驱动匹配成功以后, snvs_rtc_probe 函数就会执行。
snvs_rtc_probe 函数
snvs_rtc_probe 函数,函数内容如下(有省略):
cpp
static int snvs_rtc_probe(struct platform_device *pdev)
{
struct snvs_rtc_data *data;
struct resource *res;
int ret;
void __iomem *mmio;
/* 1. 分配设备私有数据结构 */
data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
/* 2. 获取寄存器映射 - 新/旧设备树兼容处理 */
data->regmap = syscon_regmap_lookup_by_phandle(pdev->dev.of_node, "regmap");
if (IS_ERR(data->regmap)) {
/* 旧设备树兼容路径 */
dev_warn(&pdev->dev, "snvs rtc: you use old dts file,please update it\n");
/* 2.1 获取传统内存资源 */
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
mmio = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(mmio))
return PTR_ERR(mmio);
/* 2.2 手动创建寄存器映射 */
data->regmap = devm_regmap_init_mmio(&pdev->dev, mmio, &snvs_rtc_config);
} else {
/* 新设备树路径 */
data->offset = SNVS_LPREGISTER_OFFSET;
of_property_read_u32(pdev->dev.of_node, "offset", &data->offset);
}
/* 3. 寄存器映射最终检查 */
if (!data->regmap) {
dev_err(&pdev->dev, "Can't find snvs syscon\n");
return -ENODEV;
}
/* 4. 获取中断资源 */
data->irq = platform_get_irq(pdev, 0);
if (data->irq < 0)
return data->irq;
/* 5. 保存设备私有数据 */
platform_set_drvdata(pdev, data);
/* 6. 硬件初始化序列 */
/* 6.1 初始化毛刺检测寄存器 */
regmap_write(data->regmap, data->offset + SNVS_LPPGDR, SNVS_LPPGDR_INIT);
/* 6.2 清除中断状态寄存器 */
regmap_write(data->regmap, data->offset + SNVS_LPSR, 0xffffffff);
/* 6.3 使能RTC功能 */
snvs_rtc_enable(data, true);
/* 7. 配置设备唤醒功能 */
device_init_wakeup(&pdev->dev, true);
/* 8. 注册中断处理程序 */
ret = devm_request_irq(&pdev->dev, data->irq, snvs_rtc_irq_handler,
IRQF_SHARED, "rtc alarm", &pdev->dev);
if (ret) {
dev_err(&pdev->dev, "failed to request irq %d: %d\n", data->irq, ret);
goto error_rtc_device_register;
}
/* 9. 注册RTC设备 */
data->rtc = devm_rtc_device_register(&pdev->dev, pdev->name,
&snvs_rtc_ops, THIS_MODULE);
if (IS_ERR(data->rtc)) {
ret = PTR_ERR(data->rtc);
dev_err(&pdev->dev, "failed to register rtc: %d\n", ret);
goto error_rtc_device_register;
}
return 0;
error_rtc_device_register:
/* 错误恢复路径 */
if (data->clk)
clk_disable_unprepare(data->clk);
return ret;
}
关键代码分析如下:
调用 platform_get_resource 函数,从设备树中获取到 RTC 外设寄存器基地址。
cpp
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
调用函数 devm_ioremap_resource完成内存映射,得到 RTC 外设寄存器物理基地址对应的虚拟地址。
cpp
mmio = devm_ioremap_resource(&pdev->dev, res);
Linux3.1 引入了一个全新的 regmap 机制, regmap 用于提供一套方便的 API 函数去操作底层硬件寄存器,以提高代码的可重用性。 snvs-rtc.c 文件会采用 regmap 机制来读写RTC 底层硬件寄存器。
使用 devm_regmap_init_mmio 函数,将 RTC 的硬件寄存器转化为regmap 形式,这样 regmap 机制的 regmap_write、 regmap_read 等 API 函数才能操作寄存器。
cpp
/* 2.2 手动创建寄存器映射 */
data->regmap = devm_regmap_init_mmio(&pdev->dev, mmio, &snvs_rtc_config);
调用platform_get_irq函数,从设备树中获取 RTC 的中断号。
cpp
data->irq = platform_get_irq(pdev, 0);
调用regmap 机制的 regmap_write 函数,设置 RTC_ LPPGDR 寄存器值为 SNVS_LPPGDR_INIT= 0x41736166。
cpp
regmap_write(data->regmap, data->offset + SNVS_LPPGDR, SNVS_LPPGDR_INIT);
调用regmap_write函数,设置 RTC_LPSR寄存器,写入 0xffffffff, LPSR 是 RTC 状态寄存器,写 1 清零,因此这一步就是清除 LPSR 寄存器。
cpp
regmap_write(data->regmap, data->offset + SNVS_LPSR, 0xffffffff);
调用snvs_rtc_enable 函数,使能 RTC,此函数会设置RTC_LPCR寄存器。
cpp
snvs_rtc_enable(data, true);
调用devm_request_irq函数,请求RTC中断,中断服务函数为snvs_rtc_irq_handler,用于 RTC 闹钟中断。
cpp
/* 8. 注册中断处理程序 */
ret = devm_request_irq(&pdev->dev, data->irq, snvs_rtc_irq_handler,
IRQF_SHARED, "rtc alarm", &pdev->dev);
调用 devm_rtc_device_register 函数,向系统注册 rtc_devcie。
cpp
/* 9. 注册RTC设备 */
data->rtc = devm_rtc_device_register(&pdev->dev, pdev->name,
&snvs_rtc_ops, THIS_MODULE);
snvs_rtc_ops操作集
RTC 底层驱动集为snvs_rtc_ops,snvs_rtc_ops操作集包含了读取/设置RTC时间,读取/设置闹钟等函数。
snvs_rtc_ops操作集内容如下:
cpp
static const struct rtc_class_ops snvs_rtc_ops = {
/* 基础时间操作 */
.read_time = snvs_rtc_read_time, // 读取当前RTC时间(必须实现)
.set_time = snvs_rtc_set_time, // 设置RTC时间(必须实现)
/* 闹钟功能 */
.read_alarm = snvs_rtc_read_alarm, // 读取闹钟设置
.set_alarm = snvs_rtc_set_alarm, // 设置闹钟时间
.alarm_irq_enable = snvs_rtc_alarm_irq_enable, // 控制闹钟中断使能
};
以snvs_rtc_read_time 函数为例,讲解一下 rtc_class_ops 的各个 RTC 底层操作函数,该如何去编写。
snvs_rtc_read_time 函数
snvs_rtc_read_time 函数用于读取 RTC 时间值,函数内容如下所示:
cpp
static int snvs_rtc_read_time(struct device *dev, struct rtc_time *tm)
{
/* 1. 获取设备私有数据 */
struct snvs_rtc_data *data = dev_get_drvdata(dev);
/* 2. 读取硬件计数器值 */
unsigned long time = rtc_read_lp_counter(data);
/* 3. 将秒数转换为RTC时间结构 */
rtc_time_to_tm(time, tm);
/* 4. 返回成功状态 */
return 0;
}
- 调用 rtc_read_lp_counter 函数,获取 RTC 计数值,这个时间值是秒数。
- 调用 rtc_time_to_tm 函数,将获取到的秒数转换为时间值,也就是 rtc_time 结构体类型。
- 调用rtc_read_lp_counter 函数,用于读取 RTC 计数值。
rtc_time 结构体定义如下:
cpp
struct rtc_time {
int tm_sec; // 秒 [0-59] (可能包含闰秒至60)
int tm_min; // 分 [0-59]
int tm_hour; // 时 [0-23]
int tm_mday; // 月中的日 [1-31]
int tm_mon; // 月 [0-11] (注意:比实际月份小1)
int tm_year; // 年 - 1900的偏移量(如2023年存储为123)
int tm_wday; // 周几 [0-6] (0=周日)
int tm_yday; // 年中的日 [0-365]
int tm_isdst; // 夏令时标志(通常RTC不维护此字段)
};
rtc_read_lp_counter 函数内容如下(有省略):
cpp
static u32 rtc_read_lp_counter(struct snvs_rtc_data *data)
{
u64 read1, read2; // 用于存储两次读取的64位组合值
u32 val; // 临时存储32位寄存器值
/* 硬件同步读取循环 */
do {
/* 第一次完整读取 */
// 读取高32位计数器
regmap_read(data->regmap, data->offset + SNVS_LPSRTCMR, &val);
read1 = val;
read1 <<= 32;
// 读取低32位计数器
regmap_read(data->regmap, data->offset + SNVS_LPSRTCLR, &val);
read1 |= val;
/* 第二次完整读取(用于验证) */
regmap_read(data->regmap, data->offset + SNVS_LPSRTCMR, &val);
read2 = val;
read2 <<= 32;
regmap_read(data->regmap, data->offset + SNVS_LPSRTCLR, &val);
read2 |= val;
/*
* 由于低速总线可能导致读取撕裂(tearing),这里采用宽松验证策略:
* 只比较两个读数的有效秒数部分(忽略低位的亚秒计数)
*/
} while ((read1 >> CNTR_TO_SECS_SH) != (read2 >> CNTR_TO_SECS_SH));
/* 将47位计数器转换为32位秒数 */
return (u32)(read1 >> CNTR_TO_SECS_SH);
}
读取RTC_LPSRTCMR 和 RTC_LPSRTCLR 这两个寄存器,得到 RTC 的计数值,单位为秒,这个秒数就是当前时间。
这里读取了两次 RTC 计数值,因为要读取两个寄存器,因此可能存在读取第二个寄存器的时候时间数据更新了,导致时间不匹配,因此这里连续读两次,如果两次的时间值相等那么就表示时间数据有效。
RTC 时间查看与设置
时间 RTC 查看date
Linux 内核启动的时候,可以看到系统时钟设置信息,如图

Linux 内核在启动的时候将 snvs_rtc 设置为 rtc0。
如果要查看时间的话输入"date"命令即可,结果如图:

可以看出,当前时间和现实不一致,我们需要重新设置 RTC 时间。
设置 RTC 时间
RTC 时间设置也是使用的 date 命令,输入"date --help"命令即可查看 date 命令如何设置系统时间,结果如图:

按照图中说明,示例用法如下:
设置系统日期:
cpp
# 使用标准格式设置日期(时间默认为00:00:00)
date -s "2025-08-25"
# 设置2025年8月25日15:30:45
date -s "2025.08.25-15:30:45"
# 使用点分隔格式(TIME formats格式)
date -s "2025.08.25"
# 使用紧凑格式(MMDDhhmmYYYY格式)
date -s "082515302025.45"
显示日期
cpp
# 显示指定日期的默认格式
date -d "2025.08.25"
# 输出:Mon Aug 25 00:00:00 CST 2025
# 显示ISO-8601格式
date -I -d "2025-08-25"
# 输出:2025-08-25
# 显示RFC-2822格式
date -R -d "2025.08.25"
# 输出:Mon, 25 Aug 2025 00:00:00 +0800
用" date -s"命令仅仅是将当前系统时间设置了,此时间还没有写入到I.MX6U 内部 RTC 里面或其他的 RTC 芯片里面,因此系统重启以后时间又会丢失。
hwclock 命令
我们需要将当前的时间写入到 RTC 里面,这里要用到 hwclock 命令。
输入如下命令将系统时间写入到 RTC里面:
cpp
hwclock -w //将当前系统时间写入到 RTC 里面
时间写入到 RTC 里面以后,就不怕系统重启以后时间丢失了,如果 I.MX6U-ALPHA 开发板底板接了纽扣电池,那么开发板即使断电了时间也不会丢失。