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是一个宏,这个宏在include\linux\device.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设备驱动程序框架分析完毕。

posted on 2020-12-14 23:47  quinoa  阅读(1208)  评论(0编辑  收藏  举报