17| Linux RTC驱动

Linux RTC驱动

RTC 也就是实时时钟,用于记录当前系统时间,对于 Linux 系统而言时间是非常重要的,就和我们使用 Windows 电脑或手机查看时间一样,我们在使用 Linux 设备的时候也需要查看时间。本章我们就来学习一下如何编写 Linux 下的 RTC 驱动程序。

1| Linux 内核 RTC 驱动简介

RTC 设备驱动是一个标准的字符设备驱动,应用程序通过 open、release、read、write 和 ioctl等函数完成对 RTC 设备的操作,Linux 内核将 RTC 设备抽象为 rtc_device 结构体,因此 RTC 设备驱动就是申请并初始化 rtc_device,最后将 rtc_device 注册到 Linux 内核里面,这样 Linux 内核就有一个 RTC 设备的。

至于 RTC 设备的操作肯定是用一个操作集合(结构体)来表示的,我们先来看一下 rtc_device 结构体,此结构体定义在 include/linux/rtc.h 文件中,结构体内容如下(删除条件编译):

104 struct rtc_device
105 {
106 	struct device dev; /* 设备 */
107 	struct module *owner;
108
109 	int id; /* ID */
110 	char name[RTC_DEVICE_NAME_SIZE]; /* 名字 */
111
112 	const struct rtc_class_ops *ops; /* RTC 设备底层操作函数 */
113 	struct mutex ops_lock;
114
115 	struct cdev char_dev; /* 字符设备 */
116 	unsigned long flags;
117
118 	unsigned long irq_data;
119 	spinlock_t irq_lock;
120 	wait_queue_head_t irq_queue;
121 	struct fasync_struct *async_queue;
122
123 	struct rtc_task *irq_task;
124 	spinlock_t irq_task_lock;
125 	int irq_freq;
126 	int max_user_freq;
127
128 	struct timerqueue_head timerqueue;
129 	struct rtc_timer aie_timer;
130 	struct rtc_timer uie_rtctimer;
131 	struct hrtimer pie_timer; /* sub second exp, so needs hrtimer */
132 	int pie_enabled;
133 	struct work_struct irqwork;
134 	/* Some hardware can't support UIE mode */
135 	int uie_unsupported;
......
147 };

我们需要重点关注的是 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 };

看到示例代码是不是很熟悉了,标准的字符设备操作集。应用程序可以通过 ioctl 函数来设置/读取时间、设置/读取闹钟的操作,那么对应的 rtc_dev_ioctl 函数就会执行,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 函数内容如下:

23 static int __rtc_read_time(struct rtc_device *rtc, struct rtc_time *tm)
24 {
25 		int err;
26 		if (!rtc->ops)
27 			err = -ENODEV;
28 		else if (!rtc->ops->read_time)
29 			err = -EINVAL;
30 		else {
31 			memset(tm, 0, sizeof(struct rtc_time));
32 			err = rtc->ops->read_time(rtc->dev.parent, tm);
33 			if (err < 0) {
34	 			dev_dbg(&rtc->dev, "read_time: fail to read: %d\n",
35 																	err);
36 				return err;
37 			}
38
39 			err = rtc_valid_tm(tm);
40 			if (err < 0)
41 				dev_dbg(&rtc->dev, "read_time: rtc_time isn't valid\n");
42 		}
43 		return err;
44 }

从示例代码 中的 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 驱动调用流程就很清晰了,如图 所示:

image-20200830121424825


当 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,函数型如下:

void rtc_device_unregister(struct rtc_device *rtc)

函数参数和返回值含义如下:
rtc:要删除的 rtc_device。
返回值:无。


还有另外一对 rtc_device 注册函数 devm_rtc_device_register 和 devm_rtc_device_unregister,分别为注册和注销 rtc_device。


2| I.MX6U 内部 RTC 驱动分析

先直接告诉大家,I.MX6U 的 RTC 驱动我们不用自己编写,因为 NXP 已经写好了。其实对于大多数的 SOC 来讲,内部 RTC 驱动都不需要我们去编写,半导体厂商会编写好。但是这不代表我们就偷懒了,虽然不用编写 RTC 驱动,但是我们得看一下这些原厂是怎么编写 RTC 驱动的。

具体参考正点原子IMX6UL驱动开发手册 60.2节

3| RTC 时间查看与设置

1 、时间 RTC 查看

RTC 是用来计时的,因此最基本的就是查看时间,Linux 内核启动的时候可以看到系统时钟设置信息,如图所示:

image-20200830141549858

从图中可以看出,Linux 内核在启动的时候将 snvs_rtc 设置为 rtc0,大家的启动信息可能会和图中的不同,但是内容基本上都是一样的。如果要查看时间的话输入“date”命令即可,结果如图所示:

image-20200830141816507

从图可以看出,当前时间为 1970 年 1 月 1 日 00:01:19,很明显是时间不对,我们需要重新设置 RTC 时间。

2 、设置 RTC 时间

RTC 时间设置也是使用的 date 命令,输入“date –help”命令即可查看 date 命令如何设置系统时间,结果如图所示:

image-20200830141914426

现在我要设置当前时间为 2020年8月30日14:19:30,因此输入如下命令:

date -s "2020-08-30 14:20:00"

设置完成以后再次使用 date 命令查看一下当前时间就会发现时间改过来了,如图 所示:

image-20200830142017580

大家注意我们使用“date -s”命令仅仅是将当前系统时间设置了,此时间还没有写入到I.MX6U 内部 RTC 里面或其他的 RTC 芯片里面,因此系统重启以后时间又会丢失。我们需要将当前的时间写入到 RTC 里面,这里要用到 hwclock 命令,输入如下命令将系统时间写入到 RTC里面:

hwclock -w  //将当前系统时间写入到 RTC 里面

时间写入到 RTC 里面以后就不怕系统重启以后时间丢失了,如果 I.MX6U-ALPHA 开发板底板接了纽扣电池,那么开发板即使断电了时间也不会丢失。大家可以尝试一下不断电重启和断电重启这两种情况下开发板时间会不会丢失。