文章目錄
- 前言
- RTC簡介
- RTC驅動分析
- RTC驅動框架
- RTC驅動實現
- RTC應用
- 后續
前言
實時時鐘是很常用的一個外設,通過實時時鐘我們就可以知道年、月、日和時間等信息。 因此在需要記錄時間的場合就需要實時時鐘,可以使用專用的實時時鐘芯片來完成此功能,但 是現在大多數的 MCU 或者 MPU 內部就已經自帶了實時時鐘外設模塊。
RTC簡介
STM32 內部有一個 RTC 外設模塊,這個模塊需要一個 32.768KHz 的晶振,對這個 RTC 模塊進行初始化就可以得到一個實時時鐘。I.MX6U 內部也有 個 RTC 模塊,但是不叫作“RTC”,而是叫做“SNVS”,這一點要注意!
SNVS 直譯過來就是安全的非易性存儲,SNVS 里面主要是一些低功耗的外設,包括一個 安全的實時計數器(RTC)、一個單調計數器(monotonic counter)和一些通用的寄存器。SNVS 里面的外設在芯片掉電以后由電池供電繼續運行,這個紐扣電池就是在主電源關閉以后為 SNVS 供電的。
SNVS 分為兩個子模塊:SNVS_HP 和 SNVS_LP,也就是高功耗域(SNVS_HP)和低功耗域 (SNVS_LP),系統主電源斷電以后SNVS_HP也會斷電,但是在后備電源支持下,SNVS_LP 是不會斷電的,而且SNVS_LP是和芯片復位隔離開的,因此SNVS_LP相關的寄存器的值會一直保存著。
①、VDD_HIGH_IN 是系統(芯片)主電源,這個電源會同時供給給 SNVS_HP 和 SNVS_LP。
②、VDD_SNVS_IN 是紐扣電池供電的電源,這個電源只會供給給 SNVS_LP,保證在系 統主電源 VDD_HIGH_IN 掉電以后 SNVS_LP 會繼續運行。
③、SNVS_HP 部分。
④、SNVS_LP 部分,此部分有個 SRTC,這個就是我們本章要使用的 RTC。
其實不管是 SNVS_HP 還是 SNVS_LP,其內部都有一個 SRTC,但是因為 SNVS_HP 在系 統電源掉電以后就會關閉,所以我們使用的是 SNVS_LP內部的SRTC,不管是 SNVS_HP 里面的SRTC,還是 SNVS_LP 里面的 SRTC,其本質就是一個定時 器,SRTC需要外界提供一個32.768KHz 的時鐘,寄存器 SNVS_LPSRTCMR 和 SNVS_LPSRTCLR 保存著秒數,直接讀取這兩個寄存 器的值就知道過了多長時間了。一般以 1970 年 1 月 1 日為起點,加上經過的秒數即可得到現在 的時間和日期,原理還是很簡單的。SRTC 也是帶有鬧鐘功能的,可以在寄存器 SNVS_LPAR 中 寫入鬧鐘時間值,當時鐘值和鬧鐘值匹配的時候就會產生鬧鐘中斷,要使用時鐘功能的話還需要進行一些設置。
RTC驅動分析
RTC驅動框架
Linux內核中的RTC設備不僅用于獲取當前時間,還可以用于設置系統時間。內核提供了一套API,允許用戶空間程序與RTC設備進行交互,實現時間的讀取和設置。
RTC 設備驅動是一個標準的字符設備驅動,應用程序通過 open、release、read、write 和 ioctl 等函數完成對 RTC 設備的操作,如下圖所示:
內核將 RTC 設備抽象為 rtc_device 結構體,定義在include/linux/rtc.h:
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;…………
};
我們需要重點關注的是 ops 成員變量,這是一個 rtc_class_ops 類型的指針變量,定義在include/linux/rtc.h,rtc_class_ops 為 RTC 設備的最底層操作函數集合,包括從 RTC 設備中讀取時間、向 RTC 設備寫入新的時間值等。
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_mmss64)(struct device *, time64_t secs);int (*set_mmss)(struct device *, unsigned long secs);int (*read_callback)(struct device *, int data);int (*alarm_irq_enable)(struct device *, unsigned int enabled);
};
rtc_class_ops 中 的這 些 函數只最底層的RTC設備操作函數, 并不是提供給應用層的 file_operations 函數操作集。RTC 是個字符設備,那么肯定有字符設備的 file_operations 函數操作集,Linux 內核提供了一個 RTC 通用字符設備驅動文件,文件名為 drivers/rtc/rtc-dev.c,rtcdev.c 文件提供了所有 RTC 設備共用的 file_operations 函數操作集。之所以稱之為通用,是因為二次開發的驅動是建立在這個通用字符設備驅動文件和linux內核的基礎上開發的
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,
};
應用程序可以通過 ioctl 函 數來設置/讀取時間、設置/讀取鬧鐘的操作,那么對應的 rtc_dev_ioctl 函數就會執行, rtc_dev_ioctl 最終會通過操作 rtc_class_ops 中的 read_time、set_time 等函數來對具體 RTC 設備 的讀寫操作。具體的源碼詳細分析就不展示了,需要時可以自行查看。
字符設備的file_operations結構體設置好以后需要調用rtc_class_ops里的指針函數,但是rtc_class_ops的函數僅僅只是設置好了形式沒有注冊,因此需要將其注冊到 Linux 內核中,這里我們可以使用 rtc_device_register 函數完成注冊工作,rtc_device_unregister 函數來注銷注冊的 rtc_device。
struct rtc_device *rtc_device_register(const char *name, struct device *dev, const struct rtc_class_ops *ops, struct module *owner)
void rtc_device_unregister(struct rtc_device *rtc)
還有另外一對rtc_device注冊函數devm_rtc_device_register和devm_rtc_device_unregister, 分別為注冊和注銷 rtc_device。
RTC驅動實現
一般情況下,內部 RTC 驅動都不需要我們去編寫,半導體廠商會編寫好,這里就只學習原廠已經寫好的驅動是怎么寫的,以NXP的imx6ull為例,分析驅動,先從設備樹入手,打開 imx6ull.dtsi,在里面找到如下 snvs_rtc 設備節點,節點 內容如下所示:
/{soc{aips1: aips-bus@2000000{snvs: snvs@020cc000 {compatible = "fsl,sec-v4.0-mon", "syscon", "simple-mfd";reg = <0x020cc000 0x4000>;snvs_rtc: snvs-rtc-lp {compatible = "fsl,sec-v4.0-mon-rtc-lp";regmap = <&snvs>;offset = <0x34>;interrupts = <GIC_SPI 19 IRQ_TYPE_LEVEL_HIGH>, <GIC_SPI 20 IRQ_TYPE_LEVEL_HIGH>;};};}};
};
通過兼容屬性 compatible 的值“fsl,sec-v4.0-mon-rtc-lp”可以在Linux內核源碼中搜索到對應的驅動文件,此文件為 drivers/rtc/rtc-snvs.c,如下所示,可以看出該驅動對應的也是platform架構:
static const struct of_device_id snvs_dt_ids[] = {{ .compatible = "fsl,sec-v4.0-mon-rtc-lp", },{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, snvs_dt_ids);static struct platform_driver snvs_rtc_driver = {.driver = {.name = "snvs_rtc",.pm = SNVS_RTC_PM_OPS,.of_match_table = snvs_dt_ids,},.probe = snvs_rtc_probe,
};
在.snvs_rtc_probe函數中,主要做了以下幾個事件,代碼就不在展示了:
- platform_get_resource 函數從設備樹中獲取到 RTC 外設寄存器基地址
- devm_ioremap_resource 完成內存映射,得到 RTC 外設寄存器物理基 地址對應的虛擬地址
- devm_regmap_init_mmio 函數將 RTC 的硬件寄存器轉化為 regmap 形式,這樣 regmap 機制的 regmap_write、regmap_read 等 API 函數才能操作寄存器。==具體有關regmap機制的說明后面會出相關筆記
- 從設備樹中獲取 RTC 的中斷號
- 設置對應的寄存器
- 用 snvs_rtc_enable 函數使能 RTC
- devm_request_irq 函數請求 RTC 中斷
- devm_rtc_device_register 函數向系統注冊 rtc_devcie,此函數會設置snvs_rtc_ops操作集
最后需要原廠驅動開發人員把snvs_rtc_ops對應的函數完成:
static const struct rtc_class_ops snvs_rtc_ops = {.read_time = snvs_rtc_read_time,.set_time = snvs_rtc_set_time,.read_alarm = snvs_rtc_read_alarm,.set_alarm = snvs_rtc_set_alarm,.alarm_irq_enable = snvs_rtc_alarm_irq_enable,
};
RTC應用
- 如果要查看時間的話輸入“date”命令即可
date
- RTC 時間設置也是使用的date命令,輸入“date --help”命令即可查看 date 命令如何設置系統 時間,設置當前時間為 2019 年 8 月 31 日 18:13:00,因此輸入如下命令:
date -s "2019-08-31 18:13:00"
- “date -s”命令僅僅是將當前系統時間設置了,此時間還沒有寫入到 I.MX6U 內部 RTC 里面或其他的 RTC 芯片里面,因此系統重啟以后時間又會丟失。我們需要將 當前的時間寫入到 RTC 里面,這里要用到 hwclock 命令,輸入如下命令將系統時間寫入到 RTC 里面:
hwclock -w //將當前系統時間寫入到 RTC 里面
后續
到這里對RTC的筆記記錄大致結束了,后面有新的相關的重要的內容會繼續進行更新。