imx6ull-驱动开发篇41——Linux RTC 驱动实验
目录
I.MX6U 内部 RTC 驱动
snvs_rtc 设备节点
snvs_rtc_probe 函数
snvs_rtc_ops操作集
snvs_rtc_read_time 函数
RTC 时间查看与设置
时间 RTC 查看date
设置 RTC 时间
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 文件中找到如下所示内容:
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 函数,函数内容如下(有省略):
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 外设寄存器基地址。
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
调用函数 devm_ioremap_resource 完成内存映射,得到 RTC 外设寄存器物理基地址对应的虚拟地址。
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 函数才能操作寄存器。
/* 2.2 手动创建寄存器映射 */data->regmap = devm_regmap_init_mmio(&pdev->dev, mmio, &snvs_rtc_config);
调用platform_get_irq函数,从设备树中获取 RTC 的中断号。
data->irq = platform_get_irq(pdev, 0);
调用regmap 机制的 regmap_write 函数,设置 RTC_ LPPGDR 寄存器值为 SNVS_LPPGDR_INIT= 0x41736166。
regmap_write(data->regmap, data->offset + SNVS_LPPGDR, SNVS_LPPGDR_INIT);
调用regmap_write函数,设置 RTC_LPSR 寄存器,写入 0xffffffff, LPSR 是 RTC 状态寄存器,写 1 清零,因此这一步就是清除 LPSR 寄存器。
regmap_write(data->regmap, data->offset + SNVS_LPSR, 0xffffffff);
调用 snvs_rtc_enable 函数,使能 RTC,此函数会设置 RTC_LPCR 寄存器。
snvs_rtc_enable(data, true);
调用devm_request_irq函数,请求RTC中断,中断服务函数为snvs_rtc_irq_handler,用于 RTC 闹钟中断。
/* 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。
/* 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操作集内容如下:
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 时间值,函数内容如下所示:
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 结构体定义如下:
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 函数内容如下(有省略):
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 命令如何设置系统时间,结果如图:
按照图中说明,示例用法如下:
设置系统日期:
# 使用标准格式设置日期(时间默认为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"
显示日期
# 显示指定日期的默认格式
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里面:
hwclock -w //将当前系统时间写入到 RTC 里面
时间写入到 RTC 里面以后,就不怕系统重启以后时间丢失了,如果 I.MX6U-ALPHA 开发板底板接了纽扣电池,那么开发板即使断电了时间也不会丢失。