Linux 字符设备分析3(基于Linux6.6)---RTC设备介绍
一、RTC设备驱动模型的架构
在 Linux 内核中,RTC(Real-Time Clock)设备驱动模型用于管理硬件时钟(即实时时钟)。RTC 设备通常用于提供系统的当前时间,并且能够持续保持时间信息,即使在系统关闭电源的情况下。RTC 设备通常与电池供电的硬件时钟芯片(如 DS3231、PCF8563)结合使用,用于记录实际的日期和时间。
1.1、RTC 设备驱动概述
RTC 设备驱动是为了与硬件时钟交互而编写的,它提供了用于读取和设置系统时间的接口。Linux 内核为 RTC 提供了统一的接口,通过它可以访问和控制硬件时钟,支持时间的读取、设置、定时器功能等。
1.2、RTC 设备的主要功能
- 读取时间:从 RTC 设备中读取当前的日期和时间。
- 设置时间:设置 RTC 设备的当前日期和时间。
- 闹钟功能:一些 RTC 芯片提供闹钟功能,可以设置在特定时间触发中断。
- 周期性中断:某些 RTC 设备可以在固定的时间间隔触发中断,通常用于系统定时任务。
1.3、RTC 驱动的模型
RTC 设备驱动模型主要分为两个部分:RTC 子系统和RTC 设备驱动。
1. RTC 子系统
Linux 内核中的 RTC 子系统提供了一个统一的 API,供上层应用和其他内核模块访问硬件时钟。RTC 子系统会注册设备并暴露给用户空间应用,通常通过 /dev/rtc 或 /dev/rtcX 设备节点进行访问。
RTC 子系统的功能包括:
- 提供时间读取、设置接口。
- 支持 RTC 设备的开关机、闹钟功能。
- 支持电池供电的 RTC 芯片,以确保系统关机后仍能保持准确的时间。
2. RTC 设备驱动
RTC 设备驱动负责管理具体的 RTC 硬件。它实现了对 RTC 设备的访问、控制及相关操作,如设置时间、读取时间、设置闹钟等。设备驱动通过与硬件进行交互,提供给 RTC 子系统所需的接口。
RTC 设备驱动通常实现如下操作:
- 读取时间:读取 RTC 设备当前的时间,通常通过寄存器读取。
- 设置时间:向 RTC 设备写入当前的时间,以便硬件更新。
- 闹钟和中断:一些 RTC 设备支持设置闹钟并在特定时间生成中断。驱动需要设置中断并处理相应事件。
- 电池管理:确保 RTC 芯片在系统关机后仍然可以使用电池供电保持时间。
1.4、设备模型驱动
rtc设备驱动模型在应用层提供了三种访问方式:通过procfs下的文件实现对rtc设备的操作、通过sysfs下的属性文件实现对rtc设备的操作、通过字符设备文件实现对rtc设备的操作。
rtc设备驱动模型为了支持这三种访问方法,对驱动模型也做了相应的划分,如下图所示为rtc设备驱动模型的架构。
- 在rtc设备驱动模型的最上层,针对procfs 文件、sysfs 属性文件、字符设备文件相关的处理接口,分别抽象出rtc-dev相关接口、rtc-sysfs相关接口、rtc-procfs相关接口,分别存储在rtc-dev.c、rtc-sysfs.c、rtc-proc.c这三个文件中;
- 在rtc-dev.c、rtc-sysfs.c、rtc-proc.c中定义了这三类文件的操作接口(open、read、write、close…);
- 上述三类文件的操作接口(open、read、write、close…),会借助rtc interface接口,调用各设备的操作接口,rtc interface接口可以理解为调用设备驱动的桥梁;
- 针对rtc设备驱动,均需要实现rtc class ops中的方法,以便被rtc上层接口调用,从而完成与rtc设备的通信操作。
- 应用程序通过系统调用接口、vfs相关接口、设备文件系统的inode的操作接口、sysfs文件系统的inode的操作接口、procfs文件系统的inode的操作接口,方才进入rtc设备驱动模型的处理接口中。

二、rtc字符设备驱动模型的特点
rtc设备驱动模型与i2c/spi通用字符设备驱动模型与混杂字符设备驱动模型的特点来进行对比。
| 特性/模型 | RTC 设备驱动模型 | I2C/SPI 通用字符设备驱动模型 | Misc 字符设备驱动模型 |
|---|---|---|---|
| 驱动目的 | 管理与硬件时钟(RTC)设备交互,提供时间管理功能 | 管理通过 I2C/SPI 总线连接的外部硬件设备 | 用于管理不属于其他特定类别的设备(例如自定义设备) |
| 设备类型 | RTC(实时时钟) | I2C 或 SPI 外设(传感器、存储、显示等) | 自定义设备(如 GPIO 控制器、简易硬件接口等) |
| 总线协议 | 无特定总线要求,通常是直接硬件接口 | 使用 I2C 或 SPI 总线与外设通信 | 无特定总线要求 |
| 驱动结构 | 使用 rtc_device 结构 | 使用 i2c_driver 或 spi_driver 结构 | 使用 miscdevice 结构 |
| 设备注册方式 | 使用 rtc_register_device() 注册 | 使用 i2c_add_driver() 或 spi_register_driver() | 使用 misc_register_device() 注册 |
| 文件操作(File Operations) | 支持时间读取、设置和闹钟等操作 | 支持读写操作,如数据读写、配置操作等 | 支持简单的读写操作,通常是特定的控制命令 |
| 中断支持 | 支持闹钟或周期性中断 | 支持设备中断,如数据就绪、传感器触发等 | 支持设备中断(根据具体实现) |
| IOCTL 支持 | 支持获取或设置 RTC 时间、闹钟设置等 | 支持特定硬件配置操作或控制命令 | 支持特定硬件的控制命令 |
| 典型硬件 | RTC 芯片(如 DS3231、PCF8563) | I2C/SPI 外设(如传感器、显示器、存储芯片等) | 自定义硬件(如控制板、特殊功能硬件等) |
| 数据交换方式 | 通常是读取或设置时间、闹钟等小量数据 | 通过 I2C/SPI 总线进行数据读写 | 通常是通过字符设备进行简单的数据交互 |
| 内存映射(MMIO)支持 | 支持(如果硬件支持) | 支持(特别是 SPI 设备常常需要内存映射) | 不一定支持,通常不需要内存映射 |
| 驱动框架依赖性 | 依赖 RTC 子系统和相关 API | 依赖 I2C/SPI 子系统和相关 API | 依赖 Misc 子系统和简单的设备模型 |
| 用户空间访问 | 通过 /dev/rtc 或 /dev/rtcX 设备节点访问 | 通过 /dev/i2c-X 或 /dev/spi-X 设备节点访问 | 通过 /dev/misc 设备节点访问 |
主要异同点总结:
-
驱动目的和设备类型:
- RTC 设备驱动专注于管理 RTC 芯片(硬件时钟)并提供时间相关功能。
- I2C/SPI 设备驱动专注于通过 I2C 或 SPI 总线协议与外部设备通信,通常是传感器、显示器、存储等。
- Misc 设备驱动是一个通用模型,用于管理一些不属于特定类别的设备。
-
总线协议:
- RTC 驱动不依赖特定的总线协议,通常是直接与硬件时钟芯片交互。
- I2C/SPI 驱动使用 I2C 或 SPI 总线进行通信,强调数据传输与设备控制。
- Misc 驱动则没有固定的总线协议,适用于各种不依赖特定总线的设备。
-
设备注册方式:
- RTC 驱动使用
rtc_register_device()来注册设备。 - I2C/SPI 驱动分别使用
i2c_add_driver()或spi_register_driver()来注册。 - Misc 驱动则使用
misc_register_device()注册。
- RTC 驱动使用
-
中断与数据交换方式:
- RTC 驱动通常处理与时间相关的中断(如闹钟),数据交换以时间为主。
- I2C/SPI 驱动需要处理从外设的中断或数据传输,数据交换通常涉及更复杂的外设交互。
- Misc 驱动通常处理简单的数据交互,可能会有设备中断支持,但功能较为简化。
rtc字符设备与rtc_device以及与cdev_map关联如下图所示,主要内容如下:
- 针对每一个新注册的次设备,均需要创建对应的cdev,并注册至cdev_map中去;
- 每一个次设备,其文件操作接口指针均是相同的(指向rtc_dev_fops),而rtc_dev_fops则通过调用rtc_device中的class_ops,实现调用各rtc设备的驱动接口,实现与rtc设备的通信操作。

三、RTC设备驱动模型相关的结构体变量
针对该设备驱动模型架构而言,靠什么实现架构内层级间的关联呢?
那就是数据结构以及数据结构间的关联,针对rtc设备驱动模型而言,主要有两个结构体变量:struct rtc_device、struct rtc_class_ops 。其中struct rtc_class_ops 定义了各rtc设备的操作接口(包括读写时间、alarm的设置与读取等),即rtc设备对应的操作方法,也就是上面介绍的“RTC class ops”,这些接口是直接与rtc设备通信的。
include/linux/rtc.h
struct rtc_class_ops {
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 (*alarm_irq_enable)(struct device *, unsigned int enabled);
int (*read_offset)(struct device *, long *offset);
int (*set_offset)(struct device *, long offset);
int (*param_get)(struct device *, struct rtc_param *param);
int (*param_set)(struct device *, struct rtc_param *param);
};
而rtc_device用于表示一个rtc设备,主要包括:
- device类型变量(实现与设备驱动模型、sysfs的关联);
- rtc_class_ops类型的成员变量用于操作rtc设备的接口;
- cdev类型的变量,用于表示该rtc设备对应的字符设备。
- 而剩下的成员变量包含rtc设备名称、aie、uie、pie处理相关的变量(包括工作队列、等待队列等等)
include/linux/rtc.h
struct rtc_device {
struct device dev;
struct module *owner;
int id;
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;
int irq_freq;
int max_user_freq;
struct timerqueue_head timerqueue;
struct rtc_timer aie_timer;
struct rtc_timer uie_rtctimer;
struct hrtimer pie_timer; /* sub second exp, so needs hrtimer */
int pie_enabled;
struct work_struct irqwork;
/*
* This offset specifies the update timing of the RTC.
*
* tsched t1 write(t2.tv_sec - 1sec)) t2 RTC increments seconds
*
* The offset defines how tsched is computed so that the write to
* the RTC (t2.tv_sec - 1sec) is correct versus the time required
* for the transport of the write and the time which the RTC needs
* to increment seconds the first time after the write (t2).
*
* For direct accessible RTCs tsched ~= t1 because the write time
* is negligible. For RTCs behind slow busses the transport time is
* significant and has to be taken into account.
*
* The time between the write (t1) and the first increment after
* the write (t2) is RTC specific. For a MC146818 RTC it's 500ms,
* for many others it's exactly 1 second. Consult the datasheet.
*
* The value of this offset is also used to calculate the to be
* written value (t2.tv_sec - 1sec) at tsched.
*
* The default value for this is NSEC_PER_SEC + 10 msec default
* transport time. The offset can be adjusted by drivers so the
* calculation for the to be written value at tsched becomes
* correct:
*
* newval = tsched + set_offset_nsec - NSEC_PER_SEC
* and (tsched + set_offset_nsec) % NSEC_PER_SEC == 0
*/
unsigned long set_offset_nsec;
unsigned long features[BITS_TO_LONGS(RTC_FEATURE_CNT)];
time64_t range_min;
timeu64_t range_max;
timeu64_t alarm_offset_max;
time64_t start_secs;
time64_t offset_secs;
bool set_start_time;
#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驱动架构图中已经说明:
- 应用程序通过rtc相关的字符设备文件,完成对rtc的操作;
- 应用程序通过sysfs中 的属性文件,可完成对rtc的操作;
- 应用程序通过procfs中的文件,可完成对rtc的操作。
分析下RTC设备驱动模型中字符设备处理相关的结构体关联、sysfs文件处理相关的结构体关联。
3.1、字符设备处理相关的结构体关联
首先分析下字符设备文件节点相关的结构体关联图,如下图所示,具体关联说明如下:
- rtc_device通过其cdev类型的成员变量,实现了与cdev_map的关联(这样当系统调用open进入至字符设备的open接口chrdev_open,根据设备号,即可在cdev_map中查找到该rtc设备对应的cdev,进而即可找到其文件操作接口指针rtc_dev_ops);
- rtc_dev_ops中的操作接口(open、read、write、ioctl,即属于rtc dev if),一般是通过调用rtc interface层的接口,间接调用rtc设备的class_ops接口(当然针对open、ioctl、release接口是直接调用class_ops中对应的接口,而不需经过rtc interface层的接口)。
该结构体之间的关联图,RTC设备驱动模型的架构图中的“应用层<->设备文件系统接口<->rtc dev if<->rtc interface<->rtc class ops“。

3.2、sysfs文件处理相关的结构体关联
针对sysfs中的文件,一般是对应于device类型的一个属性,而针对rtc设备驱动模型,其device类型的属性定义如下:
drivers/rtc/sysfs.c
static struct attribute *rtc_attrs[] = {
&dev_attr_name.attr,
&dev_attr_date.attr,
&dev_attr_time.attr,
&dev_attr_since_epoch.attr,
&dev_attr_max_user_freq.attr,
&dev_attr_hctosys.attr,
&dev_attr_wakealarm.attr,
&dev_attr_offset.attr,
&dev_attr_range.attr,
NULL,
};
四、字符设备处理相关的结构体关联
- rtc_device的device成员通过其内部的kobject变量,链接至devices_kset的kset中的list上中;
- 针对每一个rtc设备,均会为其在sysfs的/sys/device目录下创建对应的目录,然后根据该device的属性(针对rtc设备而言,其属性为rtc设备对应的类rtc_class->dev_attrs,即rtc_attrs数组,针对该数组中的每一个成员均对应下图中的一个sysfs__dirent(attr类型));
- 针对rtc设备的sysfs属性文件的访问,主要借助dev_sysfs_ops的store/show接口进行访问,而在其store/show接口中,针对device_attribute属性,找到对应属性的store/show接口,从而即访问至rtc sysfs if中的接口;
- rtc_sysfs_if中的接口,基本上是调用rtc interface中的接口,与上图中的rtc_dev<->rtc_interface<->rtc_class_ops是一样的。
字符设备处理相关的结构体关联、sysfs文件处理相关的结构体关联:

五、RTC设备驱动模型的初始化接口
drivers/rtc/class.c
static int __init rtc_init(void)
{
rtc_class = class_create("rtc");
if (IS_ERR(rtc_class)) {
pr_err("couldn't create class\n");
return PTR_ERR(rtc_class);
}
rtc_class->pm = RTC_CLASS_DEV_PM_OPS;
rtc_dev_init();
return 0;
}
subsys_initcall(rtc_init);
针对rtc设备驱动模型的初始化,主要包括rtc模块的class申请、字符设备区域的申请等内容,其初始化接口为rtc_init,其实现流程如下,主要功能说明如下:
- 完成rtc模块class的创建,每一个rtc device均属于该类;
- 完成rtc模块字符设备区域的申请;
- 完成针对rtc模块class的dev_attr的设置,针对rtc_class上的设备,均会为其创建dev_attr上定义的属性值。

六、RTC设备的注册
drivers/rtc/class.c
/**
* devm_rtc_device_register - resource managed rtc_device_register()
* @dev: the device to register
* @name: the name of the device (unused)
* @ops: the rtc operations structure
* @owner: the module owner
*
* @return a struct rtc on success, or an ERR_PTR on error
*
* Managed rtc_device_register(). The rtc_device returned from this function
* are automatically freed on driver detach.
* This function is deprecated, use devm_rtc_allocate_device and
* rtc_register_device instead
*/
struct rtc_device *devm_rtc_device_register(struct device *dev,
const char *name,
const struct rtc_class_ops *ops,
struct module *owner)
{
struct rtc_device *rtc;
int err;
rtc = devm_rtc_allocate_device(dev);
if (IS_ERR(rtc))
return rtc;
rtc->ops = ops;
err = __devm_rtc_register_device(owner, rtc);
if (err)
return ERR_PTR(err);
return rtc;
}
针对rtc设备的创建,其实就是对rtc_device类型变量的设置,以及字符设备注册、device注册】主要包含如下几方面的内容:
- 提供针对rtc设备的访问接口(即实现rtc_class_ops中各接口);
- 将该rtc设备对应的字符设备注册至系统中(即注册至cdev_map中);
- 调用device_register,将rtc_device的dev成员变量注册至devices_kset中,并完成设备属性的创建;
- 调用proc_create_data,向系统挂载的procfs中注册rtc文件,并提供相应的操作方法。
- 设置该rtc设备的dev成员的class指针指向rtc_class。
通过以上几步,即完成了rtc设备的注册,rtc设备注册的接口为devm_rtc_device_register。
七、RTC时间到系统时间的更新机制
在系统启动后,完成RTC设备驱动的注册之后,LINUX系统会调用函数rtc_hctosys根据RTC时间更新系统时间(即墙上时间)。函数rtc_hctosys处理流程如下:

- 根据CONFIG_RTC_HCTOSYS_DEVICE,获取LINUX系统默认使用的RTC设备名称。针对CONFIG_RTC_HCTOSYS_DEVICE,其可通过make menuconfig进行配置,如下所示;
- 调用接口rtc_class_open,在rtc_class类上查找设备名称为CONFIG_RTC_HCTOSYS_DEVICE的rtc设备;
- 若上述步骤中找到rtc设备,则调用rtc_read_time接口获取rtc时间,并调用接口do_settimeofday设置系统时间,从而完成系统时间的更新。
八、如何实现一个RTC设备驱动
8.1、RTC 设备驱动的主要函数
rtc_read_time():读取 RTC 设备的当前时间。rtc_set_time():设置 RTC 设备的时间。rtc_alarm():设置 RTC 设备的闹钟或周期性中断。rtc_irq():处理 RTC 设备中断,如闹钟触发等。
8.2、典型的 RTC 设备驱动流程
-
定义
rtc_device结构: RTC 驱动需要定义一个rtc_device结构体,表示 RTC 设备的实例。这个结构体中包含设备的操作函数、时区信息、硬件时钟信息等。 -
实现文件操作(File Operations): RTC 驱动实现标准的文件操作接口,允许用户空间通过打开
/dev/rtc设备节点访问 RTC 设备。这些操作通常包括:open:打开 RTC 设备。read:读取当前时间。write:设置当前时间。ioctl:进行特定的控制操作,如设置闹钟等。
-
设备注册: 通过
rtc_register_device()函数将 RTC 设备注册到系统中。该函数会将设备实例与内核的 RTC 子系统连接。 -
处理硬件中断: 如果 RTC 设备支持中断(如闹钟功能),驱动需要实现相应的中断处理函数,处理 RTC 设备的中断请求(IRQ)。
示例代码
以下是一个简单的 RTC 设备驱动示例,演示了如何注册和使用 RTC 设备:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/rtc.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
static struct rtc_device *rtc;
static int rtc_example_read_time(struct file *file, unsigned long arg)
{
struct rtc_time rtc_tm;
// 读取 RTC 当前时间
rtc_read_time(rtc, &rtc_tm);
// 将时间复制到用户空间
if (copy_to_user((struct rtc_time *)arg, &rtc_tm, sizeof(struct rtc_time)))
return -EFAULT;
return 0;
}
static int rtc_example_set_time(struct file *file, unsigned long arg)
{
struct rtc_time rtc_tm;
// 从用户空间获取时间设置
if (copy_from_user(&rtc_tm, (struct rtc_time *)arg, sizeof(struct rtc_time)))
return -EFAULT;
// 设置 RTC 时间
rtc_set_time(rtc, &rtc_tm);
return 0;
}
static const struct file_operations rtc_example_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = rtc_example_read_time,
};
static int __init rtc_example_init(void)
{
int ret;
rtc = rtc_class_open(RTC_DEV_NAME);
if (IS_ERR(rtc)) {
pr_err("Failed to open RTC device\n");
return PTR_ERR(rtc);
}
ret = register_chrdev(0, "rtc_example", &rtc_example_fops);
if (ret < 0) {
pr_err("Failed to register RTC device\n");
return ret;
}
pr_info("RTC device registered successfully\n");
return 0;
}
static void __exit rtc_example_exit(void)
{
unregister_chrdev(0, "rtc_example");
rtc_class_close(rtc);
pr_info("RTC device unregistered\n");
}
module_init(rtc_example_init);
module_exit(rtc_example_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple RTC Device Driver");
1859

被折叠的 条评论
为什么被折叠?



