linux RTC 驱动模型分析

    xiaoxiao2021-04-19  68

    摘要: 最近学习RTC(real time clock)实时时钟,RTC实时时钟主要作用是给Linux系统提供时间。RTC因为是电池供电的,所以掉电后时间不丢失。Linux内核把RTC用作“离线”的时间 与日期维护器。

    最近学习RTC(real time clock)实时时钟,RTC实时时钟主要作用是给Linux系统提供时间。RTC因为是电池供电的,所以掉电后时间不丢失。Linux内核把RTC用作“离线”的时间 与日期维护器。当Linux内核启动时,它从RTC中读取时间与日期,作为基准值。在运行期间内核完全抛开RTC,以软件的形式维护系统的当前时间与日 期,并在需要时将时间回写RTC芯片。另外如果RTC提供了IRQ中断并且可以定时,那么RTC还可以作为内核睡眠时唤醒内核的闹钟。应用程序可以用 RTC提供的周期中断做一些周期的任务。 linux有两种rtc驱动的接口,一个是老的接口,专门用在PC机上的。另外一钟新接口是基于linux设备 驱动程序的。这个新的接口创建了一个RTC驱动模型,实现了RTC的大部分基本功能。而底层驱动无须考虑一些功能的实现,只需将自己注册的RTC核心中, 其他工作由RTC核心来完成。下面分析RTC新接口的驱动模型。

    一. 驱动模型结构

            与RTC核心有关的文件有:         /drivers/rtc/class.c          这个文件向linux设备模型核心注册了一个类RTC,然后向驱动程序提供了注册/注销接口         /drivers/rtc/rtc-dev.c       这个文件定义了基本的设备文件操作函数,如:open,read等         /drivers/rtc/interface.c     顾名思义,这个文件主要提供了用户程序与RTC驱动的接口函数,用户程序一般通过ioctl与RTC驱动交互,这里定义了每个ioctl命令需要调用的函数         /drivers/rtc/rtc-sysfs.c     与sysfs有关         /drivers/rtc/rtc-proc.c      与proc文件系统有关         /include/linux/rtc.h         定义了与RTC有关的数据结构

            RTC驱动模型结构如下图:

    二. 基本数据结构

      1. struct rtc_device 结构

    struct rtc_device { struct device dev; struct module *owner; int id; char name[RTC_DEVICE_NAME_SIZE]; const struct rtc_class_ops *ops; struct mutex ops_lock; struct cdev char_dev; unsigned long flags; unsigned long irq_data; spinlock_t irq_lock; wait_queue_head_t irq_queue; struct fasync_struct *async_queue; struct rtc_task *irq_task; spinlock_t irq_task_lock; int irq_freq; int max_user_freq; #ifdef CONFIG_RTC_INTF_DEV_UIE_EMUL struct work_struct uie_task; struct timer_list uie_timer; /* Those fields are protected by rtc->irq_lock */ unsigned int oldsecs; unsigned int uie_irq_active:1; unsigned int stop_uie_polling:1; unsigned int uie_task_active:1; unsigned int uie_timer_active:1; #endif }; 这个结构是RTC驱动程序的基本数据结构,但是他不像其他核心的基本结构一样,驱动程序以他为参数调用注册函数注册到核心。这个结构是由注册函数返回给驱动程序的。

      2. struct 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 (*irq_set_state)(struct device *, int enabled); int (*irq_set_freq)(struct device *, int freq); int (*read_callback)(struct device *, int data); int (*alarm_irq_enable)(struct device *, unsigned int enabled); int (*update_irq_enable)(struct device *, unsigned int enabled); }; 这个结构是RTC驱动程序要实现的基本操作函数,注意这里的操作不是文件操作。驱动程序通过初始化这样一个结构,将自己实现的函数与RTC核心联系起来。这里面的大部分函数都要驱动程序来实现。而且这些函数都是操作底层硬件的,属于最底层的函数。

      3. struct rtc_time 结构

    struct rtc_time { int tm_sec; int tm_min; int tm_hour; int tm_mday; int tm_mon; int tm_year; int tm_wday; int tm_yday; int tm_isdst; };    代表了时间与日期,从RTC设备读回的时间和日期就保存在这个结构体中

    三. class.c 

      1. 模块初始化函数:rtc_init static int __init rtc_init(void) { rtc_class = class_create(THIS_MODULE, "rtc"); if (IS_ERR(rtc_class)) { printk(KERN_ERR "%s: couldn't create class\n", __FILE__); return PTR_ERR(rtc_class); } rtc_class->suspend = rtc_suspend; rtc_class->resume = rtc_resume; rtc_dev_init(); rtc_sysfs_init(rtc_class); return 0; } rtc_init首先调用class_create创建了一个类--rtc。我们知道类是一个设备的高层视图,他抽象出了底层的实现细节。类的作用就是向用户空间提供设备的信息,驱动程序不需要直接处理类。然后初始化类结构的相应成员,rtc_suspend,rtc_resume这两个函数也是在class.c中实现的。接下来调用rtc_dev_init(),这个函数为RTC设备动态分配设备号,保存在rtc_devt中。最后调用rtc_sysfs_init,初始化rtc_class的属性。   2. 为底层驱动提供接口:rtc_device_register,rtc_device_unregister 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; int id, err; if (idr_pre_get(&rtc_idr, GFP_KERNEL) == 0) { err = -ENOMEM; goto exit; } mutex_lock(&idr_lock); err = idr_get_new(&rtc_idr, NULL, &id); mutex_unlock(&idr_lock); /*--------------------(1)---------------------*/ if (err < 0) goto exit; id = id & MAX_ID_MASK; rtc = kzalloc(sizeof(struct rtc_device), GFP_KERNEL); if (rtc == NULL) { err = -ENOMEM; goto exit_idr; } rtc->id = id; rtc->ops = ops; rtc->owner = owner; rtc->max_user_freq = 64; rtc->dev.parent = dev; rtc->dev.class = rtc_class; rtc->dev.release = rtc_device_release; mutex_init(&rtc->ops_lock); spin_lock_init(&rtc->irq_lock); spin_lock_init(&rtc->irq_task_lock); init_waitqueue_head(&rtc->irq_queue); strlcpy(rtc->name, name, RTC_DEVICE_NAME_SIZE); dev_set_name(&rtc->dev, "rtc%d", id); /*-------------------(2)--------------------*/ rtc_dev_prepare(rtc); err = device_register(&rtc->dev); if (err) goto exit_kfree; /*-------------------(3)--------------------*/ rtc_dev_add_device(rtc); rtc_sysfs_add_device(rtc); rtc_proc_add_device(rtc); dev_info(dev, "rtc core: registered %s as %s\n", rtc->name, dev_name(&rtc->dev)); /*-------------------(4)--------------------*/ return rtc; exit_kfree: kfree(rtc); exit_idr: mutex_lock(&idr_lock); idr_remove(&rtc_idr, id); mutex_unlock(&idr_lock); exit: dev_err(dev, "rtc core: unable to register %s, err = %d\n", name, err); return ERR_PTR(err); }  (1):处理一个idr的结构,idr在linux内核中指的就是整数ID管理机制,从本质上来说,idr是一种将整数ID号和特定指针关联在一起的机制。这个机制最早是在2003年2月加入内核的,当时是作为POSIX定时器的一个补丁。现在,在内核的很多地方都可以找到idr的身影。详细实现请参照相关内核代码。这里从内核中获取一个idr结构,并与id相关联。     (2):分配了一个rtc_device的结构--rtc,并且初始化了相关的成员:id, rtc_class_ops等等。     (3):首先调用rtc_dev_prepare(在rtc-dev.c中定义)。因为RTC设备本质来讲还是字符设备,所以这里初始化了字符设备相关的结构:设备号以及文件操作。然后调用device_register将设备注册到linux设备模型核心。这样在模块加载的时候,udev daemon就会自动为我们创建设备文件rtc(n)。     (4):先后调用rtc_dev_add_device,rtc_sysfs_add_device,rtc_proc_add_device三个函数。rtc_dev_add_device注册字符设备,rtc_sysfs_add_device只是为设备添加了一个闹钟属性,rtc_proc_add_device 创建proc文件系统接口。 四. rtc-dev.c          rtc-dev.c 初始化了一个file_operations结构--rtc_dev_fops,并定义了这些操作函数。   1. rtc_dev_fops rtc基本的文件操作

    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, };

    2. 函数的实现(以rtc_dev_read为例)   

    rtc_dev_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { struct rtc_device *rtc = file->private_data; DECLARE_WAITQUEUE(wait, current); unsigned long data; ssize_t ret; if (count != sizeof(unsigned int) && count < sizeof(unsigned long)) return -EINVAL; add_wait_queue(&rtc->irq_queue, &wait); do { __set_current_state(TASK_INTERRUPTIBLE); spin_lock_irq(&rtc->irq_lock); data = rtc->irq_data; rtc->irq_data = 0; spin_unlock_irq(&rtc->irq_lock); if (data != 0) { ret = 0; break; } if (file->f_flags & O_NONBLOCK) { ret = -EAGAIN; break; } if (signal_pending(current)) { ret = -ERESTARTSYS; break; } schedule(); } while (1); set_current_state(TASK_RUNNING); remove_wait_queue(&rtc->irq_queue, &wait); if (ret == 0) { /* Check for any data updates */ if (rtc->ops->read_callback) data = rtc->ops->read_callback(rtc->dev.parent, data); if (sizeof(int) != sizeof(long) && count == sizeof(unsigned int)) ret = put_user(data, (unsigned int __user *)buf) ?: sizeof(unsigned int); else ret = put_user(data, (unsigned long __user *)buf) ?: sizeof(unsigned long); } return ret; } 这里的read不是应用程序用来获取时间的,而是有其他的作用,他帮助应用程序周期性的完成一些工作。如果要使用这个功能,应用程序首先保证RTC驱动程序提供这样的功能。这个功能是这样实现的:进程读取/dev/rtc(n),进程睡眠直到RTC中断将他唤醒。我们可以发现,这里的睡眠是ldd3中提到的手工睡眠。这个函数的手工休眠过程如下:首先调用DECLARE_WAITQUEUE(wait, current),声明一个等待队列入口,然后调用add_wait_queue将这个入口加入到RTC的irq等待队列里,然后进入循环。在循环里首先把进程的状态改成TASK_INTERRUPTIBLE,这样进程就不能再被调度运行。但是现在进程还在运行,没有进入睡眠状态。程序然后读取RTC里面的irq_data,如果不是零,那么程序跳出这个循环,进程不会睡眠。因为这个irq_data在rtc的中断处理程序会被赋值,而读过之后就会清零,所以如果数据不是零的话说明发生过一次中断。如果是零那么没有发生中断,调用schedule,进程会被调度出可运行队列,从而让出处理器,真正进入睡眠。跳出循环代表被唤醒,然后将进程状态改变为可运行,移除等待队列入口。最后将读回的数据传给用户空间。 五. interface.c          interface.c里的所有函数的实现都对应于rtc-dev.c 中ioctl相应的命令。对应关系如下: RTC_ALM_READ                     rtc_read_alarm           读取闹钟时间 RTC_ALM_SET                      rtc_set_alarm            设置闹钟时间 RTC_RD_TIME                      rtc_read_time            读取时间与日期 RTC_SET_TIME                     rtc_set_time             设置时间与日期 RTC_PIE_ON RTC_PIE_OFF           rtc_irq_set_state              开关RTC全局中断的函数 RTC_AIE_ON RTC_AIE_OFF           rtc_alarm_irq_enable     使能禁止RTC闹钟中断 RTC_UIE_OFF RTC_UIE_ON           rtc_update_irq_enable    使能禁止RTC更新中断 RTC_IRQP_SET                     rtc_irq_set_freq         设置中断的频率        以上就是所有ioctl的命令与实现的对应关系。其中如果不涉及中断的话,有两个命令需要我们特别关心一下,就是RTC_RD_TIME与RTC_SET_TIME。因为RTC最基本的功能就是提供时间与日期。这两个命令恰恰是获取时间和设置时间。下面分析一下这两个命令的实现,也就是rtc_set_alarm与rtc_read_time函数的实现:   1. 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 err; 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函数,这个函数是由具体的驱动程序实现的,操作底层硬件。读回的时间存放在rtc_time结构里面的。   2. rtc_set_time 函数

    int rtc_set_time(struct rtc_device *rtc, struct rtc_time *tm) { int err; err = rtc_valid_tm(tm); if (err != 0) return err; err = mutex_lock_interruptible(&rtc->ops_lock); if (err) return err; if (!rtc->ops) err = -ENODEV; else if (rtc->ops->set_time) err = rtc->ops->set_time(rtc->dev.parent, tm); else if (rtc->ops->set_mmss) { unsigned long secs; err = rtc_tm_to_time(tm, &secs); if (err == 0) err = rtc->ops->set_mmss(rtc->dev.parent, secs); } else err = -EINVAL; mutex_unlock(&rtc->ops_lock); return err; }

    六. 总结

            RTC核心使底层硬件对用户来说是透明的,并且减少了编写驱动程序的工作量。RTC新的驱动接口提供了更多的功能,使系统可以同时存在多个RTC。/dev,sysfs,proc这三种机制的实现使得应用程序能灵活的使用RTC,RTC核心虽然表面上看上去很简单,但是还是涉及到很多知识,有些东西书上讲的还是不够详细,还需要通过分析代码加深理解。 另外RTC核心代码的组织方式也值得学习,不同功能的代码放在不同的文件中,简单明了

    转载请注明原文地址: https://ju.6miu.com/read-675769.html

    最新回复(0)