imx6ull-驱动开发篇41——Linux RTC 驱动实验

目录

[I.MX6U 内部 RTC 驱动](#I.MX6U 内部 RTC 驱动)

[snvs_rtc 设备节点](#snvs_rtc 设备节点)

[snvs_rtc_probe 函数](#snvs_rtc_probe 函数)

snvs_rtc_ops操作集

[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_LPSRTCMRRTC_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 开发板底板接了纽扣电池,那么开发板即使断电了时间也不会丢失。

相关推荐
青草地溪水旁2 小时前
`lock()` 和 `unlock()` 线程同步函数
linux·c++·c
2401_858286112 小时前
OS26.【Linux】进程程序替换(下)
linux·运维·服务器·开发语言·算法·exec·进程
lkf197113 小时前
centos安装jenkins
linux·centos·jenkins
秦jh_4 小时前
【MySQL】基本查询
linux·数据库·c++·mysql
刃神太酷啦4 小时前
Linux 常用指令全解析:从基础操作到系统管理(1w字精简版)----《Hello Linux!》(2)
linux·运维·服务器·c语言·c++·算法·leetcode
正在努力的小河4 小时前
GPIO子系统自主实现(简单版)
linux·单片机·嵌入式硬件
小妖6664 小时前
centos 用 docker 方式安装 dufs
linux·docker·centos
qq_433888934 小时前
win11中系统的WSL安装Centos以及必要组件
linux·运维·centos
Aczone285 小时前
Linux 软件编程(十一)网络编程:TCP 机制与 HTTP 协议
linux·网络·tcp/ip