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_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核心代碼的組織方式也值得學習,不同功能的代碼放在不同的文件中,簡單明了。
最後更新:2017-04-03 18:51:44