最近学习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_read_time函数差不多,同样是锁定信号量,同样是调用底层驱动函数。但是这里的设置时间提供了两个调用:一个是
set_time,一个是set_mmss。因为有的RTC硬件只计算秒数,不关心墙钟时间,所以如果是这样的RTC,必须实现set_mmss来设置时 间。
六. rtc-sysfs.c 部分
这个部分主要是有关sysfs的操作。rtc-sysfs.c中定义了这样一个设备属性组,如下:
static struct device_attribute rtc_attrs[] = { __ATTR(name, S_IRUGO, rtc_sysfs_show_name, NULL), __ATTR(date, S_IRUGO, rtc_sysfs_show_date, NULL), __ATTR(time, S_IRUGO, rtc_sysfs_show_time, NULL), __ATTR(since_epoch, S_IRUGO, rtc_sysfs_show_since_epoch, NULL), __ATTR(max_user_freq, S_IRUGO | S_IWUSR, rtc_sysfs_show_max_user_freq, rtc_sysfs_set_max_user_freq), __ATTR(hctosys, S_IRUGO, rtc_sysfs_show_hctosys, NULL), { }, };
这个属性组是在class.c的模块初始化函数中,由rtc_sysfs_init函数赋值给rtc_class->dev_attrs 的,以后属于这个类的设备都会有这些属性。但是我们知道要想一个设备结构拥有一种属性,必须调用device_create_file,这样才会使这个属 性出现在sysfs相关设备目录里。但是在这里的代码中只是给这个类的dev_attrs域赋值了这个属性组指针,而没有调用 device_create_file。我原来以为是在rtc_device_resgister函数中,由rtc_sysfs_add_device完
成这个工作,但是这个函数只是给设备添加了闹钟属性,并没有处理这个属性组。最后发现这个工作是由device_register来完成的。这里的调用关 系有点复杂:
device_register调用device_add
device_add调用 device_add_attrs
device_add_attrs调用device_add_attributes
device_add_attributes调用device_create_file来完成设备的属性设置的。
设置完属性后,在/sys/class/rtc/rtc(n)的目录下就会出现name,date,time等文件,用户读这些文件的时候就会调用相应的 函数。如读取name文件,就会调用rtc_sysfs_show_name函数,这个函数也是在rtc-sysfs.c中实现的,作用是读取并显示时 间。
七. rtc-proc.c
这个文件提供RTC的proc文件系统接口。proc文件系统是软件创建的文件系统,内核通过他向外界导出信息,下面的每一个文件都绑定一个函数,当用户读取这个文件的时候,这个函数会向文件写入信息。rtc-proc.c中初始化了一个文件操作:
static const struct file_operations rtc_proc_fops = { .open = rtc_proc_open, .read = seq_read, .llseek = seq_lseek, .release = rtc_proc_release, };
RTC驱动在向RTC核心注册自己的时候,由注册函数rtc_device_resgister调用rtc_proc_add_device来实现proc接口的初始化,这个函数如下定义:
void rtc_proc_add_device(struct rtc_device *rtc) { if (rtc->id == 0) proc_create_data("driver/rtc", 0, NULL, &rtc_proc_fops, rtc); }
他主要调用了proc_create_data。proc_create_data完成创建文件节点的作用,并将文件的操作函数与节点联系起来。调用这个
函数后,在/proc/driver目录下就会有一个文件rtc,应用程序打开这个文件就会调用rtc_proc_open函数,这个函数如下定义:
static int rtc_proc_open(struct inode *inode, struct file *file) { struct rtc_device *rtc = PDE(inode)->data; if (!try_module_get(THIS_MODULE)) return -ENODEV; return single_open(file, rtc_proc_show, rtc); }
我们知道一个proc的文件必须与一个操作函数组成一个proc入口项,这个文件才能正常工作。这个函数最主要作用就是调用single_open,创建
一个proc文件入口项,使其操作函数是rtc_proc_show,并初始化seq_file接口。rtc_proc_show函数如下定义:
static int rtc_proc_show(struct seq_file *seq, void *offset) { int err; struct rtc_device *rtc = seq->private; const struct rtc_class_ops *ops = rtc->ops; struct rtc_wkalrm alrm; struct rtc_time tm; err = rtc_read_time(rtc, &tm); if (err == 0) { seq_printf(seq, "rtc_time\t: %02d:%02d:%02d\n" "rtc_date\t: %04d-%02d-%02d\n", tm.tm_hour, tm.tm_min, tm.tm_sec, tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday); } err = rtc_read_alarm(rtc, &alrm); if (err == 0) { seq_printf(seq, "alrm_time\t: "); if ((unsigned int)alrm.time.tm_hour <= 24) seq_printf(seq, "%02d:", alrm.time.tm_hour); else seq_printf(seq, "**:"); if ((unsigned int)alrm.time.tm_min <= 59) seq_printf(seq, "%02d:", alrm.time.tm_min); else seq_printf(seq, "**:"); if ((unsigned int)alrm.time.tm_sec <= 59) seq_printf(seq, "%02d\n", alrm.time.tm_sec); else seq_printf(seq, "**\n"); seq_printf(seq, "alrm_date\t: "); if ((unsigned int)alrm.time.tm_year <= 200) seq_printf(seq, "%04d-", alrm.time.tm_year + 1900); else seq_printf(seq, "****-"); if ((unsigned int)alrm.time.tm_mon <= 11) seq_printf(seq, "%02d-", alrm.time.tm_mon + 1); else seq_printf(seq, "**-"); if (alrm.time.tm_mday && (unsigned int)alrm.time.tm_mday <= 31) seq_printf(seq, "%02d\n", alrm.time.tm_mday); else seq_printf(seq, "**\n"); seq_printf(seq, "alarm_IRQ\t: %s\n", alrm.enabled ? "yes" : "no"); seq_printf(seq, "alrm_pending\t: %s\n", alrm.pending ? "yes" : "no"); } seq_printf(seq, "24hr\t\t: yes\n"); if (ops->proc) ops->proc(rtc->dev.parent, seq); return 0; }
这个函数就是最后给用户显示信息的函数了,可以看出他通过调用rtc_deivce中的操作函数,读取时间,日期和一些其他的信息显示给用户。
六. 总结
RTC核心使底层硬件对用户来说是透明的,并且减少了编写驱动程序的工作量。RTC新的驱动接口提供了更多的功能,使系统可以同时存在多个RTC。 /dev,sysfs,proc这三种机制的实现使得应用程序能灵活的使用RTC,RTC核心虽然表面上看上去很简单,但是还是涉及到很多知识,有些东西 书上讲的还是不够详细,还需要通过分析代码加深理解。 另外RTC核心代码的组织方式也值得学习,不同功能的代码放在不同的文件中,简单明了。