RTC驱动分析

作者: __来日方长__ | 来源:发表于2019-11-04 17:12 被阅读0次

    学号:19021211263

    linux中的rtc驱动位于drivers/rtc下,里面包含了许多开发平台的RTC驱动,我们这里是以S3C24xx为主,所以它的RTC驱动为rtc-s3c.c

    1、入口函数s3c_rtc_init分析

    进入./drivers/rtc/rtc-s3c.c,找到入口函数,如下所示:

    static struct platform_driver s3c2410_rtcdrv = {
        .probe      = s3c_rtc_probe,//.probe函数
        .remove     = s3c_rtc_remove,
        .suspend    = s3c_rtc_suspend,
        .resume     = s3c_rtc_resume,
        .driver     = {
            .name   = "s3c2410-rtc",
            .owner  = THIS_MODULE,
        },
    };
    
    static char __initdata banner[] = "S3C24XX RTC, (c) 2004,2006 Simtec Electronics\n";
    
    static int __init s3c_rtc_init(void)
    {
        printk(banner);
        return platform_driver_register(&s3c2410_rtcdrv);
    }
    
    module_init(s3c_rtc_init);
    

    这里注册了一个“s3c2410-rtc”名称的平台设备驱动

    而“s3c2410-rtc”的平台设备,在./arch/arm/plat-s3c24xx/devs.c里定义了,只不过这里没有注册,如下所示:

    /* RTC */
    
    static struct resource s3c_rtc_resource[] = {
        [0] = {
            .start = S3C24XX_PA_RTC,//RTC寄存器地址
            .end   = S3C24XX_PA_RTC + 0xff,
            .flags = IORESOURCE_MEM,//内存资源
        },
        [1] = {
            .start = IRQ_RTC,//RTC闹钟中断
            .end   = IRQ_RTC,
            .flags = IORESOURCE_IRQ,//IRQ资源
        },
        [2] = {
            .start = IRQ_TICK,//RTC时钟节拍中断
            .end   = IRQ_TICK,
            .flags = IORESOURCE_IRQ//IRQ资源
        }
    };
    
    struct platform_device s3c_device_rtc = {
        .name         = "s3c2410-rtc",
        .id       = -1,
        .num_resources    = ARRAY_SIZE(s3c_rtc_resource),
        .resource     = s3c_rtc_resource,
    };
    
    EXPORT_SYMBOL(s3c_device_rtc);
    

    当内核匹配到有与它名称同名的平台设备,就会调用.probe函数,接下来我们便进入s3c2410_rtcdrv->probe函数(即s3c_rtc_probe函数)中看看,做了什么:

    static int s3c_rtc_probe(struct platform_device *pdev)
    {
        struct rtc_device *rtc;           //rtc设备结构体
        struct resource *res;
        int ret;
    
        s3c_rtc_tickno = platform_get_irq(pdev, 1);//获取IRQ_TICK节拍中断资源
        s3c_rtc_alarmno = platform_get_irq(pdev, 0);//获取IRQ_RTC闹钟中断资源
        res = platform_get_resource(pdev, IORESOURCE_MEM, 0);//获取内存资源
    
        s3c_rtc_mem = request_mem_region(res->start,res->end-res->start+1,pdev->name);//申请内存资源
    
        s3c_rtc_base = ioremap(res->start, res->end - res->start + 1);//对内存进行重映射
    
        s3c_rtc_enable(pdev, 1);//设置硬件相关设置,使能RTC寄存器
    
        s3c_rtc_setfreq(s3c_rtc_freq);//设置TICONT寄存器,使能节拍中断,设置节拍计数值
    
        /*1.注册RTC设备*/
        rtc = rtc_device_register("s3c", &pdev->dev, &s3c_rtcops,THIS_MODULE);
    
        rtc->max_user_freq = 128;
        platform_set_drvdata(pdev, rtc);
        return 0;
    }
    

    显然最终会调用rtc_device_register()函数来向内核注册rtc_device设备,注册成功会返回一个已注册好的rtc_device,而s3c_rtcops是一个rtc_class_ops结构体,里面就是保存如何操作这个rtc设备的函数,比如读写RTC时间,读写闹钟时间等,注册后,会保存在rtc_device->ops里。

    rtc_device_register()函数在drivers/rtc/Class.c文件内被定义。Class.c文件主要定义了RTC子系统,而内核初始化,便会进入Class.c。


    再从Class.c中的初始化函数看:

    先进入rtc_init()在创建了相关的类之后会调用rtc_dev_init(),在rtc_dev_init()中会通过alloc_chrdev_region()函数来注册字符设备:

    err = alloc_chrdev_region(&rtc_devt, 0, RTC_DEV_MAX, "rtc");// RTC_DEV_MAX=16,表示只注册0~15个次设备号,设备编号保存在rtc_devt中 
    

    2、rtc_device_register()函数分析

    Class.c中的alloc_chrdev_region()函数和./arch/arm/plat-s3c24xx/devs.c中通过rtc_device_register()函数注册RTC设备,会有什么关系?

    接下来便来看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;//定义一个rtc_device结构体
        ... ...
        rtc = kzalloc(sizeof(struct rtc_device), GFP_KERNEL);      //分配rtc_device结构体为全局变量
    
    
        /*设置rtc_device*/
        rtc->id = id;
        rtc->ops = ops;//将s3c_rtcops保存在rtc_device->ops里
        rtc->owner = owner;
        rtc->max_user_freq = 64;
        rtc->dev.parent = dev;
        rtc->dev.class = rtc_class;
        rtc->dev.release = rtc_device_release;
        ... ...
    
        rtc_dev_prepare(rtc);   //1.做提前准备,初始化cdev结构体
        ... ...
        rtc_dev_add_device(rtc);//2.在/dev下创建rtc相关文件,将cdev添加到系统中
    
        rtc_sysfs_add_device(rtc);//在/sysfs下创建rtc相关文件
        rtc_proc_add_device(rtc); //在/proc下创建rtc相关文件
        ... ...
        return rtc;
    }
    

    上面的rtc_dev_prepare(rtc)和rtc_dev_add_device(rtc)主要做了以下两件事(位于./drivers/rtc/rtc-dev.c):

    • rtc_dev_prepare(rtc)中:

      cdev_init(&rtc->char_dev, &rtc_dev_fops);//绑定file_operations  
      
    • rtc_dev_add_device(rtc)中:

      cdev_add(&rtc->char_dev, rtc->dev.devt, 1);//注册rtc->char_dev字符设备,添加一个从设备到系统中
      

    显然这里就是利用新方法注册字符设备


    .probe函数总结

    所以“s3c2410-rtc”平台设备驱动的.probe主要做了以下几件事:

    • 1.设置RTC相关寄存器

    • 2.分配rtc_device结构体

    • 3.设置rtc_device结构体

      • 3.1 通过rtc_device_register函数,将struct rtc_class_ops s3c_rtcops放入rtc_device->ops,实现对RTC读写时间等操作
    • 4.注册rtc->char_dev字符设备,通过cdev_init函数将该字符设备的操作结构体设为:struct file_operations rtc_dev_fops


    3、file_operations结构体分析

    综上所述,rtc->char_dev字符设备中绑定的file_operations结构体rtc_dev_fops为:

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

    3.1、open函数

    当应用层open(”/dev/rtcXX”)时,就会调用rtc_dev_fops->rtc_dev_open(),我们来看看如何open的:

    static int rtc_dev_open(struct inode *inode, struct file *file)
    {
        struct rtc_device *rtc = container_of(inode->i_cdev,struct rtc_device, char_dev);//获取对应的rtc_device结构体
        const struct rtc_class_ops *ops = rtc->ops;//最终等于s3c_rtcops
    
        file->private_data = rtc;//设置file结构体的私有成员等于rtc_device,再次执行ioctl等函数时,直接就可以提取file->private_data即可
    
        err = ops->open ? ops->open(rtc->dev.parent) : 0;//调用s3c_rtcops->open
    
        mutex_unlock(&rtc->char_lock);
        return err;
    }
    

    container_of:

    #define container_of(ptr, type, member) ({              \         
    const typeof( ((type *)0)->member ) *__mptr = (ptr);    \         
    (type *)( (char *)__mptr - offsetof(type,member) );})
    //ptr:返回的type类型的结构体首地址
    //type:该结构体类型
    //member:结构体中的某一个成员
    

    通过一个结构变量中一个成员的地址member找到这个结构体变量的首地址ptr。

    显然最终还是调用rtc_device下的s3c_rtcops->open函数:

    static const struct rtc_class_ops s3c_rtcops = {
        .open       = s3c_rtc_open,
        .release    = s3c_rtc_release,
        .ioctl      = s3c_rtc_ioctl,
        .read_time  = s3c_rtc_gettime,
        .set_time   = s3c_rtc_settime,
        .read_alarm = s3c_rtc_getalarm,
        .set_alarm  = s3c_rtc_setalarm,
        .proc           = s3c_rtc_proc,
    };
    

    s3c_rtc_open()函数,而s3c_rtc_open()函数里主要是申请了两个中断,一个闹钟中断,一个计时中断:

    static int s3c_rtc_open(struct device *dev)
    {     
        struct platform_device *pdev = to_platform_device(dev);    
        struct rtc_device *rtc_dev = platform_get_drvdata(pdev);      
        int ret;
    
        ret = request_irq(s3c_rtc_alarmno, s3c_rtc_alarmirq,IRQF_DISABLED,  "s3c2410-rtc alarm", rtc_dev);//申请闹钟中断                      
        if (ret) {
            dev_err(dev, "IRQ%d error %d\n", s3c_rtc_alarmno, ret);
            return ret;
        }
    
        
        ret = request_irq(s3c_rtc_tickno, s3c_rtc_tickirq,IRQF_DISABLED,  "s3c2410-rtc tick", rtc_dev);//申请计时中断   
        if (ret) {
            dev_err(dev, "IRQ%d error %d\n", s3c_rtc_tickno, ret);
            goto tick_err;
        }
    
        return ret;
    
        tick_err:
        free_irq(s3c_rtc_alarmno, rtc_dev);
        return ret;
    }
    

    3.2、ioctl函数

    当我们应用层open后,使用 ioctl(int fd, unsigned long cmd, ...)时,就会调用rtc_dev_fops-> rtc_dev_ioctl ():

    static int rtc_dev_ioctl(struct inode *inode, struct file *file,unsigned int cmd, unsigned long arg)
    {
        struct rtc_device *rtc = file->private_data;//提取rtc_device
        void __user *uarg = (void __user *) arg;
        ... ...
    
        switch (cmd) {
            case RTC_EPOCH_SET:
            case RTC_SET_TIME://设置时间
                if (!capable(CAP_SYS_TIME))
                    return -EACCES;
                break;
            case RTC_IRQP_SET://改变中断触发速度
                ... ...
        ... ...}
    
        switch (cmd) {
            case RTC_ALM_READ://读闹钟时间
                err = rtc_read_alarm(rtc, &alarm);//调用s3c_rtcops-> read_alarm
                if (err < 0)
                    return err;
    
                if (copy_to_user(uarg, &alarm.time, sizeof(tm)))//长传时间数据
                    return -EFAULT;
                break;
    
            case RTC_ALM_SET://设置闹钟时间 , 调用s3c_rtcops-> set_alarm
                ... ...
    
            case RTC_RD_TIME://读RTC时间, 调用s3c_rtcops-> read_alarm
                err = rtc_read_time(rtc, &tm);
                ... ...
    
            case RTC_SET_TIME://写RTC时间,调用s3c_rtcops-> set_time
                ... ...
    
            case RTC_IRQP_SET://改变中断触发频率,调用s3c_rtcops-> irq_set_freq
                ... ...
        }
    

    这里我们假设是读RTC时间,即会进入rtc_read_time函数:

    int rtc_read_time(struct rtc_device *rtc, struct rtc_time *tm)
    {
        int err;
    
        err = mutex_lock_interruptible(&rtc->ops_lock);
        if (err)
            return -EBUSY;
    
        if (!rtc->ops)
            err = -ENODEV;
        else if (!rtc->ops->read_time)
            err = -EINVAL;
        else {
            memset(tm, 0, sizeof(struct rtc_time));
            err = rtc->ops->read_time(rtc->dev.parent, tm);//最终调用该函数
        }
    
        mutex_unlock(&rtc->ops_lock);
        return err;
    }
    

    从上面可以看出,rtc->ops->read_time即为rtc_device结构体下的ops成员s3c_rtcops下的read_time函数,即s3c_rtc_getalarm函数

    调用了半天,最终还是调用s3c_rtcops下的成员函数

    继续分析s3c_rtc_getalarm函数,看看如何读出时间:

    static int s3c_rtc_gettime(struct device *dev, struct rtc_time *rtc_tm)
    {
        unsigned int have_retried = 0;
        void __iomem *base = s3c_rtc_base;//获取RTC相关寄存器基地址
    
    
    retry_get_time:
    
        /*获取年,月,日,时,分,秒寄存器*/
        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);
    
    
        /*  判断秒寄存器中是0,则表示过去了一分钟,那么小时,天,月,等寄存器中的值都可能已经变化,需要重新读取这些寄存器的值*/
        if (rtc_tm->tm_sec == 0 && !have_retried) {
            have_retried = 1;
            goto retry_get_time;
        }
    
        /*将获取的寄存器值,转换为真正的时间数据*/
        BCD_TO_BIN(rtc_tm->tm_sec);
        BCD_TO_BIN(rtc_tm->tm_min);
        BCD_TO_BIN(rtc_tm->tm_hour);
        BCD_TO_BIN(rtc_tm->tm_mday);
        BCD_TO_BIN(rtc_tm->tm_mon);
        BCD_TO_BIN(rtc_tm->tm_year);
    
        rtc_tm->tm_year += 100;//存储器中存放的是从1900年开始的时间,所以加上100 
        rtc_tm->tm_mon -= 1;
        return 0;
    }
    

    同样, 在s3c_rtc_gettime函数下(即s3c_rtcops-> set_time()函数),也是向相关寄存器写入RTC时间


    总结

    • rtc_device->char_dev :字符设备,与应用层、以及更底层的函数打交道
    • rtc_device->ops    :更底层的操作函数,直接操作硬件相关的寄存器,被rtc_device->char_dev调用

    4、修改内核

    我们单板上使用ls /dev/rtc*,找不到该字符设备, 因为内核里只定义了s3c_device_rtc这个RTC平台设备,没有注册,所以平台驱动没有被匹配上,接下来我们来修改内核里的注册数组

    4.1进入arch/arm/plat-s3c24xx/Common-smdk.c

    如下所示,在smdk_devs[]里,添加RTC的平台设备即可,当内核启动时,就会调用该数组,将里面的platform_device统统注册一遍

    
    static struct platform_device __initdata *smdk_devs[] = {
        &s3c_device_nand,
        &smdk_led4,
        &smdk_led5,
        &smdk_led6,
        &smdk_led7,
        &s3c_device_rtc,//加入这一行
    #if defined(CONFIG_DM9000) || defined(CONFIG_DM9000_MODULE)
        &s3c_device_dm9k,
    #endif    
    #ifdef CONFIG_SERIAL_EXTEND_S3C24xx
        &s3c_device_8250,
    #endif
    #ifdef CONFIG_TOUCHSCREEN_S3C2410
        &s3c_device_ts,
    #endif
    };
    

    然后将Common-smdk.c代替虚拟机的内核目录下的Common-smdk.c,重新make uImage编译内核即可

    5、测试运行

    启动后,如下所示, 使用ls /dev/rtc*,就找到了rtc0这个字符设备

    # ls /dev/rtc*
    /dev/rtc0
    #
    

    5.1、设置RTC时间

    在linux里有两个时钟:

    硬件时钟(2440里寄存器的时钟)、系统时钟(内核中的时钟)

    所以有两个不同的命令: date命令、hwclock命令

    5.2、date命令

    输入date查看系统时钟:

    # date
    wed Nov  3 14:50:24 UTC 2021
    

    如果觉得不方便也可以指定格式显示日期,需要在字符串前面加”+”

    如下所示,输入了 date "+ %Y/%m/%d %H:%M:%S"

    # date
    wed Nov 3 14:54:30 UTC 2021
    #
    #
    # data "+ %Y/%m/%d %H:%M:%S"
     2021/11/03 14:54:33
    
    • %M:表示秒
    • %m:表示月
    • %Y:表示年,当只需要最后两位数字,输入%y即可

    date命令设置时间格式如下:

    date 月日时分年.秒

    如下所示,输入date 111515292017.20,即可设置好系统时钟

    # date 111515292017.20
    wed Nov 15 15:29:20 UTC 2017
    #
    

    5.3、hwclock命令

    常用参数如下所示

    • -r, --show 读取并打印硬件时钟(read hardware clock and print result )
    • -s, --hctosys 将硬件时钟同步到系统时钟(set the system time from the hardware clock )
    • -w, --systohc 将系统时钟同步到硬件时钟(set the hardware clock to the current system time )

    如下所示,使用hwclock -w,即可同步硬件时钟

    # hwclock -r
    Wed Nov  3 15:20:46 2021 0.000000 seconds 未同步之前的时间
    # hwclock -w
    # hwclock -r
    Wed Nov 15 15:30:06 2017 0.000000 seconds 同步后的时间
    #
    

    然后重启后,使用date命令,看到时间正常

    更多有趣内容欢迎访问我的个人博客

    相关文章

      网友评论

        本文标题:RTC驱动分析

        本文链接:https://www.haomeiwen.com/subject/odbdbctx.html