Linux RTC 驱动实验
目錄
- Linux 內核RTC 驅動簡介
- I.MX6U 內部RTC 驅動分析
- RTC 時間查看與設置
RTC 也就是實時時鐘,用于記錄當前系統(tǒng)時間,對于Linux 系統(tǒng)而言時間是非常重要的,就和我們使用Windows 電腦或手機查看時間一樣,我們在使用Linux 設備的時候也需要查看時間。本章我們就來學習一下如何編寫Linux 下的RTC 驅動程序。
Linux 內核RTC 驅動簡介
RTC 設備驅動是一個標準的字符設備驅動,應用程序通過open、release、read、write 和ioctl等函數完成對RTC 設備的操作,關于RTC 硬件原理部分我們已經在裸機篇中的第二十五章進行了詳細的講解。
Linux 內核將RTC 設備抽象為rtc_device 結構體,因此RTC 設備驅動就是申請并初始化rtc_device,最后將rtc_device 注冊到Linux 內核里面,這樣Linux 內核就有一個RTC 設備的。
至于RTC 設備的操作肯定是用一個操作集合(結構體)來表示的,我們先來看一下rtc_device 結構體,此結構體定義在include/linux/rtc.h 文件中,結構體內容如下(刪除條件編譯):
我們需要重點關注的是ops 成員變量,這是一個rtc_class_ops 類型的指針變量,rtc_class_ops為RTC 設備的最底層操作函數集合,包括從RTC 設備中讀取時間、向RTC 設備寫入新的時間值等。因此,rtc_class_ops 是需要用戶根據所使用的RTC 設備編寫的,此結構體定義在include/linux/rtc.h 文件中,內容如下:
71 struct rtc_class_ops { 72 int (*open)(struct device *); 73 void (*release)(struct device *); 74 int (*ioctl)(struct device *, unsigned int, unsigned long); 75 int (*read_time)(struct device *, struct rtc_time *); 76 int (*set_time)(struct device *, struct rtc_time *); 77 int (*read_alarm)(struct device *, struct rtc_wkalrm *); 78 int (*set_alarm)(struct device *, struct rtc_wkalrm *); 79 int (*proc)(struct device *, struct seq_file *); 80 int (*set_mmss64)(struct device *, time64_t secs); 81 int (*set_mmss)(struct device *, unsigned long secs); 82 int (*read_callback)(struct device *, int data); 83 int (*alarm_irq_enable)(struct device *, unsigned int enabled); 84 };看名字就知道rtc_class_ops 操作集合中的這些函數是做什么的了,但是我們要注意,rtc_class_ops 中的這些函數只是最底層的RTC 設備操作函數,并不是提供給應用層的file_operations 函數操作集。RTC 是個字符設備,那么肯定有字符設備的file_operations 函數操作集,Linux 內核提供了一個RTC 通用字符設備驅動文件,文件名為drivers/rtc/rtc-dev.c,rtc-dev.c 文件提供了所有RTC 設備共用的file_operations 函數操作集,如下所示:
448 static const struct file_operations rtc_dev_fops = { 449 .owner = THIS_MODULE, 450 .llseek = no_llseek, 451 .read = rtc_dev_read, 452 .poll = rtc_dev_poll, 453 .unlocked_ioctl = rtc_dev_ioctl, 454 .open = rtc_dev_open, 455 .release = rtc_dev_release, 456 .fasync = rtc_dev_fasync, 457 };看到示例代碼60.1.3 是不是很熟悉了,標準的字符設備操作集。應用程序可以通過ioctl 函數來設置/讀取時間、設置/讀取鬧鐘的操作,那么對應的rtc_dev_ioctl 函數就會執(zhí)行,rtc_dev_ioctl 最終會通過操作rtc_class_ops 中的read_time、set_time 等函數來對具體RTC 設備的讀寫操作。我們簡單來看一下rtc_dev_ioctl 函數,函數內容如下(有省略):
218 static long rtc_dev_ioctl(struct file *file, 219 unsigned int cmd, unsigned long arg) 220 { 221 int err = 0; 222 struct rtc_device *rtc = file->private_data; 223 const struct rtc_class_ops *ops = rtc->ops; 224 struct rtc_time tm; 225 struct rtc_wkalrm alarm; 226 void __user *uarg = (void __user *) arg; 227 228 err = mutex_lock_interruptible(&rtc->ops_lock); 229 if (err) 230 return err; ...... 269 switch (cmd) { ...... 333 case RTC_RD_TIME: /* 讀取時間*/ 334 mutex_unlock(&rtc->ops_lock); 335 336 err = rtc_read_time(rtc, &tm); 337 if (err < 0) 338 return err; 339 340 if (copy_to_user(uarg, &tm, sizeof(tm))) 341 err = -EFAULT; 342 return err; 343 344 case RTC_SET_TIME: /* 設置時間*/ 345 mutex_unlock(&rtc->ops_lock); 346 347 if (copy_from_user(&tm, uarg, sizeof(tm))) 348 return -EFAULT; 349 350 return rtc_set_time(rtc, &tm); ...... 401 default: 402 /* Finally try the driver's ioctl interface */ 403 if (ops->ioctl) { 404 err = ops->ioctl(rtc->dev.parent, cmd, arg); 405 if (err == -ENOIOCTLCMD) 406 err = -ENOTTY; 407 } else 408 err = -ENOTTY; 409 break; 410 } 411 412 done: 413 mutex_unlock(&rtc->ops_lock); 414 return err; 415 }第333 行,RTC_RD_TIME 為時間讀取命令。
第336 行,如果是讀取時間命令的話就調用rtc_read_time 函數獲取當前RTC 時鐘,rtc_read_time 函數,rtc_read_time 會調用__rtc_read_time 函數,__rtc_read_time 函數內容如下:
從示例代碼60.1.5 中的32 行可以看出,__rtc_read_time 函數會通過調用rtc_class_ops 中的read_time 來從RTC 設備中獲取當前時間。rtc_dev_ioctl 函數對其他的命令處理都是類似的,比如RTC_ALM_READ 命令會通過rtc_read_alarm 函數獲取到鬧鐘值,而rtc_read_alarm 函數經過層層調用,最終會調用rtc_class_ops 中的read_alarm 函數來獲取鬧鐘值。
至此,Linux 內核中RTC 驅動調用流程就很清晰了,如圖60.1.1 所示:
當rtc_class_ops 準備好以后需要將其注冊到Linux 內核中,這里我們可以使用rtc_device_register 函數完成注冊工作。此函數會申請一個rtc_device 并且初始化這個rtc_device,最后向調用者返回這個rtc_device,此函數原型如下:
struct rtc_device *rtc_device_register(const char *name, struct device *dev, const struct rtc_class_ops *ops, struct module *owner)函數參數和返回值含義如下:
name:設備名字。
dev:設備。
ops:RTC 底層驅動函數集。
owner:驅動模塊擁有者。
返回值:注冊成功的話就返回rtc_device,錯誤的話會返回一個負值。
當卸載RTC 驅動的時候需要調用rtc_device_unregister 函數來注銷注冊的rtc_device,函數原型如下:
函數參數和返回值含義如下:
rtc:要刪除的rtc_device。
返回值:無。
還有另外一對rtc_device 注冊函數devm_rtc_device_register 和devm_rtc_device_unregister,分別為注冊和注銷rtc_device。
I.MX6U 內部RTC 驅動分析
先直接告訴大家,I.MX6U 的RTC 驅動我們不用自己編寫,因為NXP 已經寫好了。其實對于大多數的SOC 來講,內部RTC 驅動都不需要我們去編寫,半導體廠商會編寫好。但是這不代表我們就偷懶了,雖然不用編寫RTC 驅動,但是我們得看一下這些原廠是怎么編寫RTC 驅動的。
分析驅動,先從設備樹入手,打開imx6ull.dtsi,在里面找到如下snvs_rtc 設備節(jié)點,節(jié)點內容如下所示:
1 snvs_rtc: snvs-rtc-lp { 2 compatible = "fsl,sec-v4.0-mon-rtc-lp"; 3 regmap = <&snvs>; 4 offset = <0x34>; 5 interrupts = <GIC_SPI 19 IRQ_TYPE_LEVEL_HIGH>, <GIC_SPI 20 IRQ_TYPE_LEVEL_HIGH>; 6 };第2 行設置兼容屬性compatible 的值為“fsl,sec-v4.0-mon-rtc-lp”,因此在Linux 內核源碼中搜索此字符串即可找到對應的驅動文件,此文件為drivers/rtc/rtc-snvs.c,在rtc-snvs.c 文件中找到如下所示內容:
380 static const struct of_device_id snvs_dt_ids[] = { 381 { .compatible = "fsl,sec-v4.0-mon-rtc-lp", }, 382 { /* sentinel */ } 383 }; 384 MODULE_DEVICE_TABLE(of, snvs_dt_ids); 385 386 static struct platform_driver snvs_rtc_driver = { 387 .driver = { 388 .name = "snvs_rtc", 389 .pm = SNVS_RTC_PM_OPS, 390 .of_match_table = snvs_dt_ids, 391 }, 392 .probe = snvs_rtc_probe, 393 }; 394 module_platform_driver(snvs_rtc_driver);第380~383 行,設備樹ID 表,有一條compatible 屬性,值為“fsl,sec-v4.0-mon-rtc-lp”,因此imx6ull.dtsi 中的snvs_rtc 設備節(jié)點會和此驅動匹配。
第386~393 行,標準的platform 驅動框架,當設備和驅動匹配成功以后snvs_rtc_probe 函數就會執(zhí)行。我們來看一下snvs_rtc_probe 函數,函數內容如下(有省略):
第253 行,調用platform_get_resource 函數從設備樹中獲取到RTC 外設寄存器基地址。
第255 行,調用函數devm_ioremap_resource 完成內存映射,得到RTC 外設寄存器物理基地址對應的虛擬地址。
第259 行,Linux3.1 引入了一個全新的regmap 機制,regmap 用于提供一套方便的API 函數去操作底層硬件寄存器,以提高代碼的可重用性。snvs-rtc.c 文件會采用regmap 機制來讀寫RTC 底層硬件寄存器。這里使用devm_regmap_init_mmio 函數將RTC 的硬件寄存器轉化為regmap 形式,這樣regmap 機制的regmap_write、regmap_read 等API 函數才能操作寄存器。
第270 行,從設備樹中獲取RTC 的中斷號。
第289 行,設置RTC_ LPPGDR 寄存器值為SNVS_LPPGDR_INIT= 0x41736166,這里就是用的regmap 機制的regmap_write 函數完成對寄存器進行寫操作。
第292 行,設置RTC_LPSR 寄存器,寫入0xffffffff,LPSR 是RTC 狀態(tài)寄存器,寫1 清零,因此這一步就是清除LPSR 寄存器。
第295 行,調用snvs_rtc_enable 函數使能RTC,此函數會設置RTC_LPCR 寄存器。
第299 行,調用devm_request_irq 函數請求RTC 中斷,中斷服務函數為snvs_rtc_irq_handler,用于RTC 鬧鐘中斷。
第307 行,調用devm_rtc_device_register 函數向系統(tǒng)注冊rtc_devcie,RTC 底層驅動集為snvs_rtc_ops。snvs_rtc_ops操作集包含了讀取/設置RTC 時間,讀取/設置鬧鐘等函數。snvs_rtc_ops內容如下:
我們就以第201 行的snvs_rtc_read_time 函數為例講解一下rtc_class_ops 的各個RTC 底層操作函數該如何去編寫。snvs_rtc_read_time 函數用于讀取RTC 時間值,此函數內容如下所示:
126 static int snvs_rtc_read_time(struct device *dev, struct rtc_time *tm) 127 { 128 struct snvs_rtc_data *data = dev_get_drvdata(dev); 129 unsigned long time = rtc_read_lp_counter(data); 130 131 rtc_time_to_tm(time, tm); 132 133 return 0; 134 }第129 行,調用rtc_read_lp_counter 獲取RTC 計數值,這個時間值是秒數。
第131 行,調用rtc_time_to_tm 函數將獲取到的秒數轉換為時間值,也就是rtc_time 結構體類型,rtc_time 結構體定義如下:
最后我們來看一下rtc_read_lp_counter 函數,此函數用于讀取RTC 計數值,函數內容如下(有省略):
50 static u32 rtc_read_lp_counter(struct snvs_rtc_data *data) 51 { 52 u64 read1, read2; 53 u32 val; 54 55 do { 56 regmap_read(data->regmap, data->offset + SNVS_LPSRTCMR, &val); 57 read1 = val; 58 read1 <<= 32; 59 regmap_read(data->regmap, data->offset + SNVS_LPSRTCLR, &val); 60 read1 |= val; 61 62 regmap_read(data->regmap, data->offset + SNVS_LPSRTCMR, &val); 63 read2 = val; 64 read2 <<= 32; 65 regmap_read(data->regmap, data->offset + SNVS_LPSRTCLR, &val); 66 read2 |= val; 67 /* 68 * when CPU/BUS are running at low speed, there is chance that 69 * we never get same value during two consecutive read, so here 70 * we only compare the second value. 71 */ 72 } while ((read1 >> CNTR_TO_SECS_SH) != (read2 >> CNTR_TO_SECS_SH)); 73 74 /* Convert 47-bit counter to 32-bit raw second count */ 75 return (u32) (read1 >> CNTR_TO_SECS_SH); 76 }第56~72 行,讀取RTC_LPSRTCMR 和RTC_LPSRTCLR 這兩個寄存器,得到RTC 的計數值,單位為秒,這個秒數就是當前時間。這里讀取了兩次RTC 計數值,因為要讀取兩個寄存器,因此可能存在讀取第二個寄存器的時候時間數據更新了,導致時間不匹配,因此這里連續(xù)讀兩次,如果兩次的時間值相等那么就表示時間數據有效。
第75 行,返回時間值,注意這里將前面讀取到的RTC 計數值右移了15 位。
這個就是snvs_rtc_read_time 函數讀取RTC 時間值的過程,至于其他的底層操作函數大家自行分析即可,都是大同小異的,這里就不再分析了。關于I.MX6U 內部RTC 驅動源碼就講解到這里。
RTC 時間查看與設置
1、時間RTC 查看
RTC 是用來計時的,因此最基本的就是查看時間,Linux 內核啟動的時候可以看到系統(tǒng)時鐘設置信息,如圖60.3.1 所示:
從圖60.3.1 中可以看出,Linux 內核在啟動的時候將snvs_rtc 設置為rtc0,大家的啟動信息可能會和圖60.3.1 中的不同,但是內容基本上都是一樣的。
如果要查看時間的話輸入“date”命令即可,結果如圖60.3.2 所示:
從圖60.3.2 可以看出,當前時間為1970 年1 月1 日00:06:11,很明顯是時間不對,我們需要重新設置RTC 時間。
2、設置RTC 時間
RTC 時間設置也是使用的date 命令,輸入“date --help”命令即可查看date 命令如何設置系統(tǒng)時間,結果如圖60.3.3 所示
現在我要設置當前時間為2019 年8 月31 日18:13:00,因此輸入如下命令:
設置完成以后再次使用date 命令查看一下當前時間就會發(fā)現時間改過來了,如圖60.3.4 所示:
大家注意我們使用“date -s”命令僅僅是將當前系統(tǒng)時間設置了,此時間還沒有寫入到I.MX6U 內部RTC 里面或其他的RTC 芯片里面,因此系統(tǒng)重啟以后時間又會丟失。我們需要將當前的時間寫入到RTC 里面,這里要用到hwclock 命令,輸入如下命令將系統(tǒng)時間寫入到RTC里面:
時間寫入到RTC 里面以后就不怕系統(tǒng)重啟以后時間丟失了,如果I.MX6U-ALPHA 開發(fā)板底板接了紐扣電池,那么開發(fā)板即使斷電了時間也不會丟失。大家可以嘗試一下不斷電重啟和斷電重啟這兩種情況下開發(fā)板時間會不會丟失。
總結
以上是生活随笔為你收集整理的Linux RTC 驱动实验的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android项目使用Eclipse进行
- 下一篇: Matlab之取整函数