文章目录
前言
实时时钟是很常用的一个外设,通过实时时钟我们就可以知道年、月、日和时间等信息。 因此在需要记录时间的场合就需要实时时钟,可以使用专用的实时时钟芯片来完成此功能,但 是现在大多数的 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:
c
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 设备写入新的时间值等。
c
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内核的基础上开发的
c
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。
c
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 设备节点,节点 内容如下所示:
c
/{
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架构:
c
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对应的函数完成:
c
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"命令即可
sh
date
- RTC 时间设置也是使用的date命令,输入"date --help"命令即可查看 date 命令如何设置系统 时间,设置当前时间为 2019 年 8 月 31 日 18:13:00,因此输入如下命令:
sh
date -s "2019-08-31 18:13:00"
- "date -s"命令仅仅是将当前系统时间设置了,此时间还没有写入到 I.MX6U 内部 RTC 里面或其他的 RTC 芯片里面,因此系统重启以后时间又会丢失。我们需要将 当前的时间写入到 RTC 里面,这里要用到 hwclock 命令,输入如下命令将系统时间写入到 RTC 里面:
sh
hwclock -w //将当前系统时间写入到 RTC 里面
后续
到这里对RTC的笔记记录大致结束了,后面有新的相关的重要的内容会继续进行更新。