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

发布于:2025-08-30 ⋅ 阅读:(21) ⋅ 点赞:(0)

目录

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


    网站公告

    今日签到

    点亮在社区的每一天
    去签到