• Linux RTC驱动分析及应用


    一、RTC简介

    RTC(real-time clock)简称实时时钟,主要是用来计时,产生闹钟等。RTC一般有个备份电池,所以即使设备关机掉电,RTC也能在备份电池的供电下继续正常计时,这样在每次系统开机上电时就可以从RTC设备中读取到准确的时间。RTC时间在每次系统启动的时候会使用,在需要的时候也可以由系统将要设置的时间写入到RTC设备。Linux系统中,RTC可以使用周期性的中断来产生闹钟,也可以在系统suspend的时候作为系统的唤醒源使用。内核为RTC设备设计了一套驱动模型,如果驱动工程师想增加某一种新RTC硬件的驱动,只需要去编写芯片相关的代码,然后调用内核提供函数注册到RTC核心层即可。

    二、RTC设备描述

    在介绍内核RTC驱动结构之前,先来看RTC设备在内核中是如何描述的

    rtc_device结构体

    struct rtc_device
    {
        struct device dev;                      //代表是一个设备
        struct module *owner;
    
        int id;                                 //rtc设备编号
        char name[RTC_DEVICE_NAME_SIZE];        //rtc设备的名称
    
        const struct rtc_class_ops *ops;        //rtc操作函数集,由驱动程序实现
        struct mutex ops_lock;                  //操作函数集的互斥锁
    
        struct cdev char_dev;                   //cdev结构体,代表rtc是字符设备
        unsigned long flags;                    //rtc的状态标志,例如RTC_DEV_BUSY
    
        unsigned long irq_data;                 //rtc中断数据
        spinlock_t irq_lock;                    //互斥访问数据
        wait_queue_head_t irq_queue;            //数据查询中用到的rtc队列
        struct fasync_struct *async_queue;      //异步队列
    
        struct rtc_task *irq_task;              //在中断中使用task传输数据
        spinlock_t irq_task_lock;               //task传输互斥
        int irq_freq;                           //rtc的中断频率
        int max_user_freq;                      //rtc的最大中断频率
    
        struct timerqueue_head timerqueue;      //定时器队列
        struct rtc_timer aie_timer;             //报警中断定时器
        struct rtc_timer uie_rtctimer;          //更新中断定时器
        struct hrtimer pie_timer;               //周期中断高精度定时器
        int pie_enabled;                        //周期中断使能标志
        struct work_struct irqwork;
        /* Some hardware can't support UIE mode */
        int uie_unsupported;
        ...
    };

    RTC设备在内核中是以字符设备形式存在的,内核中使用rtc_device结构体来抽象一个rtc设备。rtc_device结构体屏蔽了不同RTC硬件之间的差异,通过rtc_class_ops结构体为上层提供了访问硬件设备的统一接口,该结构体中包含了对硬件操作的相关函数。

    rtc_class_ops结构体

    struct rtc_class_ops {
        int (*open)(struct device *);
        void (*release)(struct device *);
        int (*ioctl)(struct device *, unsigned int, unsigned long);
        int (*read_time)(struct device *, struct rtc_time *);
        int (*set_time)(struct device *, struct rtc_time *);
        int (*read_alarm)(struct device *, struct rtc_wkalrm *);
        int (*set_alarm)(struct device *, struct rtc_wkalrm *);
        int (*proc)(struct device *, struct seq_file *);
        int (*set_mmss)(struct device *, unsigned long secs);
        int (*read_callback)(struct device *, int data);
        int (*alarm_irq_enable)(struct device *, unsigned int enabled);
    };

    rtc_class_ops结构体中的操作函数需要驱动程序实现,比如open, read_time, set_time等。这些函数大多数都是和RTC芯片的操作有关,上层通过这些函数进行对硬件设备的操作,比如调用set_time设置时间、read_time获取时间等等。

    三、内核源代码分析

    好了,有了上面的知识作为铺垫,现在我们开始来从源码角度去详细分析内核中RTC驱动程序的框架。这里以s3c24xx芯片外设RTC为例,一步步推出内核中RTC设备驱动是如何实现的。

    RTC设备驱动程序存放在内核源码树的drivers/rtc目录,s3c24xx芯片的RTC驱动程序对应的是rtc-s3c.c文件。下面我们就从这个文件中的入口函数进行分析

    入口函数

    module_platform_driver(s3c_rtc_driver);
    static struct platform_driver s3c_rtc_driver = {
        .probe        = s3c_rtc_probe,
        .remove        = __devexit_p(s3c_rtc_remove),
        .suspend    = s3c_rtc_suspend,
        .resume        = s3c_rtc_resume,
        .id_table    = s3c_rtc_driver_ids,
        .driver        = {
            .name    = "s3c-rtc",
            .owner    = THIS_MODULE,
            .of_match_table    = s3c_rtc_dt_match,
        },
    };

    modules_platform_driver是一个宏,这个宏在includelinuxdevice.h文件中定义,内容如下:

    #define module_platform_driver(__platform_driver) 
        module_driver(__platform_driver, platform_driver_register, 
                platform_driver_unregister)
                
    #define module_driver(__driver, __register, __unregister, ...) 
    static int __init __driver##_init(void) 
    { 
        return __register(&(__driver) , ##__VA_ARGS__); 
    } 
    module_init(__driver##_init); 
    static void __exit __driver##_exit(void) 
    { 
        __unregister(&(__driver) , ##__VA_ARGS__); 
    } 
    module_exit(__driver##_exit);

    根据上述宏定义,将module_platform_device(s3c_rtc_driver)展开,展开内容如下所示:

    static int __init s3c_rtc_driver_init(void) 
    { 
        return platform_driver_register(&(s3c_rtc_driver)); 
    } 
    module_init(s3c_rtc_driver_init); 
    static void __exit s3c_rtc_driver_exit(void) 
    { 
        platform_driver_unregister(&(s3c_rtc_driver)); 
    } 
    module_exit(s3c_rtc_driver_exit);

    通过上面的展开操作,我们可以看出module_platform_driver(s3c_rtc_driver)即实现了注册、卸载一个platform驱动程序的入口和出口函数。其实和我们平时编写驱动程序一样,不过这里使用的是内核定义好的宏,看上去更加简介。

    我们知道platform驱动在注册时,会遍历platform总线上所有的挂载的platform设备,并调用platform总线的match函数进行比较,如果匹配成功将调用platform驱动的probe函数。

    下面来看s3c_rtc_driver的probe函数s3c_rtc_probe

    static int __devinit s3c_rtc_probe(struct platform_device *pdev)
    {
        ...
        res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
        ...
        s3c_rtc_mem = request_mem_region(res->start, resource_size(res),
                         pdev->name);
        ...
        s3c_rtc_base = ioremap(res->start, resource_size(res));
        ...
        rtc_clk = clk_get(&pdev->dev, "rtc");
        ...
        clk_enable(rtc_clk);
    
        s3c_rtc_enable(pdev, 1);
        ...
        device_init_wakeup(&pdev->dev, 1);
    
        rtc = rtc_device_register("s3c", &pdev->dev, &s3c_rtcops,
                      THIS_MODULE);
        ...
    }

    s3c_rtc_probe函数实现的基本上是硬件相关的操作,首先获取与之匹配的platform_device中描述的硬件相关的资源,如s3c24xx芯片RTC外设寄存器地址,完成寄存器物理地址到虚拟地址映射、使能RTC外设时钟、初始化RTC外设寄存器,然后调用rtc_device_register函数向内核注册一个rtc设备。

    rtc_device_register函数是驱动程序找到匹配的设备后,向内核注册一个rtc_device,来详细分析这个函数

    rtc_device_register函数

    struct rtc_device *rtc_device_register(const char *name, struct device *dev,
                        const struct rtc_class_ops *ops,
                        struct module *owner)
    {
        struct rtc_device *rtc;
        struct rtc_wkalrm alrm;
        int id, err;
    
        id = ida_simple_get(&rtc_ida, 0, 0, GFP_KERNEL); ------------------------->①
        ...
    
        rtc = kzalloc(sizeof(struct rtc_device), GFP_KERNEL);--------------------->②
        ...
    
        rtc->id = id;
        rtc->ops = ops;
        ...
    
        mutex_init(&rtc->ops_lock);
        spin_lock_init(&rtc->irq_lock);
        spin_lock_init(&rtc->irq_task_lock);
        init_waitqueue_head(&rtc->irq_queue);
    
        /* Init timerqueue */
        timerqueue_init_head(&rtc->timerqueue);
        INIT_WORK(&rtc->irqwork, rtc_timer_do_work);
        ...
    
        /* Check to see if there is an ALARM already set in hw */
        err = __rtc_read_alarm(rtc, &alrm);
        ...
    
        strlcpy(rtc->name, name, RTC_DEVICE_NAME_SIZE);
        dev_set_name(&rtc->dev, "rtc%d", id);
    
        rtc_dev_prepare(rtc);------------------------------------------------>③
    
        err = device_register(&rtc->dev);
        ...
        rtc_dev_add_device(rtc);-------------------------------------------->④
        ...
    }

    ① 给注册的rtc_device分配一个新的设备id,创建的字符设备的设备节点会使用到这个id, /dev/rtc(id)

    ② 分配一个rtc_device结构体,初始化rtc_device结构体中的成员变量,比如使用到的锁,初始化定时器等等

    注:rtc_device结构体中的rtc设备操作函数指针rtc_class_ops指针指向s3c_rtcops,这部分内容在后面再进行展开分析

    ③ 调用rtc_dev_prepare初始化cdev结构体

    void rtc_dev_prepare(struct rtc_device *rtc)
    {
        ...
        rtc->dev.devt = MKDEV(MAJOR(rtc_devt), rtc->id);
        ...
        cdev_init(&rtc->char_dev, &rtc_dev_fops);
        rtc->char_dev.owner = rtc->owner;
    }

    ④ 调用ret_dev_add_device向内核添加一个字符设备

    void rtc_dev_add_device(struct rtc_device *rtc)
    {
        if (cdev_add(&rtc->char_dev, rtc->dev.devt, 1))
        ...
    }

    rtc_device_register函数动态分配了一个rtc_device结构体,并初始化分配的rtc_device结构体成员。最后向内核注册了一个字符设备,注册的字符设备的file_operations结构体指针指向rtc_dev_fops结构体。当系统访问RTC设备时,对RTC设备的操作是通过注册的字符设备的file_operations结构体的成员函数实现的。

    注册的RTC设备使用的file_operations结构体

    static const struct file_operations rtc_dev_fops = {
        .owner        = THIS_MODULE,
        .llseek        = no_llseek,
        .read        = rtc_dev_read,
        .poll        = rtc_dev_poll,
        .unlocked_ioctl    = rtc_dev_ioctl,
        .open        = rtc_dev_open,
        .release    = rtc_dev_release,
        .fasync        = rtc_dev_fasync,
    };

    以open函数进行分析,rtc_dev_open

    static int rtc_dev_open(struct inode *inode, struct file *file)
    {
        int err;
        struct rtc_device *rtc = container_of(inode->i_cdev,
                        struct rtc_device, char_dev);
        const struct rtc_class_ops *ops = rtc->ops;
    
        ...
    
        file->private_data = rtc;
    
        err = ops->open ? ops->open(rtc->dev.parent) : 0;
        ...
    }

    rtc_dev_open函数在打开rtc设备节点时会被调用,首先根据inode->i_cdev使用container_of获取对应的rtc_device结构体(inode->i_cdev = &rtc->char_dev)。

    获取rtc_device结构体后,找到其成员rtc_class_ops结构体指针,调用rtc_class_ops结构体指针指向对象的open函数。

    由此可见,访问设备的file_operations函数最终会调用到对应的rtc_device结构体中对RTC硬件进行操作的函数。

    前面讲过,rtc_device结构体中的RTC设备操作函数指针rtc_class_ops指针指向s3c_rtcops,我们来看下s3c_rtcops结构体内容

    static const struct rtc_class_ops s3c_rtcops = {
        .read_time    = s3c_rtc_gettime,
        .set_time    = s3c_rtc_settime,
        .read_alarm    = s3c_rtc_getalarm,
        .set_alarm    = s3c_rtc_setalarm,
        .proc        = s3c_rtc_proc,
        .alarm_irq_enable = s3c_rtc_setaie,
    };

    s3c_rtcops结构体中实现了.read_time、.set_time等成员函数,这些成员函数在系统调用使用ioctl读取时间或设置时间时被调用,我们进一步分析看它是如何返回时间信息的

    static long rtc_dev_ioctl(struct file *file,
            unsigned int cmd, unsigned long arg)
    {
        
        struct rtc_device *rtc = file->private_data;
        const struct rtc_class_ops *ops = rtc->ops;
        struct rtc_time tm;
        ...
        switch (cmd) {
        ...
    
        case RTC_RD_TIME:
            mutex_unlock(&rtc->ops_lock);
    
            err = rtc_read_time(rtc, &tm);
                    |-->__rtc_read_time(rtc, tm);
                        |-->memset(tm, 0, sizeof(struct rtc_time));
                        |-->err = rtc->ops->read_time(rtc->dev.parent, tm);    
                        
            if (copy_to_user(uarg, &tm, sizeof(tm)))
                err = -EFAULT;
            return err;
    
        ...
    }

    应用程序使用ioctl函数执行获取设备时间操作,最终会调用到rtc_dev_ioctl,执行rtc_read_time获取当前时间,将获取时间存放到rtc_time结构体中,最后使用copy_to_user拷贝到用户空间。

    rtc_read_time函数最终会调用到s3c_rtcops.read_time,即最终调用到s3c_rtc_gettime函数

    s3c_rtc_gettime函数

    static int s3c_rtc_gettime(struct device *dev, struct rtc_time *rtc_tm)
    {
        ...
        rtc_tm->tm_min  = readb(base + S3C2410_RTCMIN);
        rtc_tm->tm_hour = readb(base + S3C2410_RTCHOUR);
        rtc_tm->tm_mday = readb(base + S3C2410_RTCDATE);
        rtc_tm->tm_mon  = readb(base + S3C2410_RTCMON);
        rtc_tm->tm_year = readb(base + S3C2410_RTCYEAR);
        rtc_tm->tm_sec  = readb(base + S3C2410_RTCSEC);
        ...
    }

    s3c_rtc_gettime函数通过读取s3c24xx芯片RTC外设当前时间寄存器值来获取时间,将读取时间信息填充到一个rtc_time结构体中.

    由以上的分析,我们知道了加载一个RTC驱动程序,需要去初始化RTC硬件设备,并调用rtc_device_register函数构建一个rtc设备。RTC驱动程序中需要实例化一个rtc_class_op类型成员对象,该成员对象包含了获取时间信息、设置时间、闹钟时间等硬件相关的操作函数。

    调用rtc_device_register函数构造的rtc_device结构体时会将其rtc_class_op类型指针指向实例化的rtc_class_op类型成员对象,最后初始化、创建一个字符设备。应用程序通过注册的/dev/rtc(n)设备节点对RTC设备进行访问,对字符设备的操作最后会调用到注册字符设备的file_operations成员函数。RTC设备的file_operation成员函数是访问所有注册的RTC设备的统一入口,file_operation成员函数函数中再根据打开字符设备节点,找到对应的rtc_device,最终调用到rtc_device中的rtc_class_op类型指针指向内容的成员函数。

    前面看到了使用rtc_dev_prepare函数调用cdev_init去初始化一个cdev结构体,使用rtc_dev_add_device函数调用cdev_add去向内核添加一个字符设备,但注册的RTC字符设备主设备号和可用此设备号区域是在哪里分配的呢?我们还需要继续来看

    /drivers/rtc/class.c的入口函数中分配了注册的RTC字符设备主设备号和可用此设备号区域

    static int __init rtc_init(void)
    {
        rtc_class = class_create(THIS_MODULE, "rtc");
        ...
        rtc_dev_init();
        rtc_sysfs_init(rtc_class);
        return 0;
    }
    void __init rtc_dev_init(void)
    {
        ...
        err = alloc_chrdev_region(&rtc_devt, 0, RTC_DEV_MAX, "rtc");
        ...
    }

    rtc_init函数中创建了一个rtc_class,最后调用rtc_dev_init函数,rtc_dev_init函数调用alloc_chrdev_region为rtc设备动态分配了主设备号区域。创建一个rtc_class,在驱动程序中匹配到设备时去这rtc_class下创建设备,这样mdev机制获取rtc_class类下设备信息,自动创建/dev/rtc(n)设备节点。

    至此,我们的内核RTC设备驱动程序框架分析完毕。

  • 相关阅读:
    UrlRewriter
    PortalBasic Web 应用开发框架
    首页静态化和定时执行
    细谈在C#中读写Excel系列文章之四
    Ubuntu安装SVN及SvnManager
    Select语句导致瓶颈
    策略模式
    抽象工厂
    递归优化之快速排序
    从安装git开始,一步一步把程序发布到AppHarbor
  • 原文地址:https://www.cnblogs.com/053179hu/p/14130300.html
Copyright © 2020-2023  润新知