Linux 字符设备分析3

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设备驱动模型的架构。

  1. 在rtc设备驱动模型的最上层,针对procfs 文件、sysfs 属性文件、字符设备文件相关的处理接口,分别抽象出rtc-dev相关接口、rtc-sysfs相关接口、rtc-procfs相关接口,分别存储在rtc-dev.c、rtc-sysfs.c、rtc-proc.c这三个文件中;
  2. 在rtc-dev.c、rtc-sysfs.c、rtc-proc.c中定义了这三类文件的操作接口(open、read、write、close…);
  3. 上述三类文件的操作接口(open、read、write、close…),会借助rtc interface接口,调用各设备的操作接口,rtc interface接口可以理解为调用设备驱动的桥梁;
  4. 针对rtc设备驱动,均需要实现rtc class ops中的方法,以便被rtc上层接口调用,从而完成与rtc设备的通信操作。
  5. 应用程序通过系统调用接口、vfs相关接口、设备文件系统的inode的操作接口、sysfs文件系统的inode的操作接口、procfs文件系统的inode的操作接口,方才进入rtc设备驱动模型的处理接口中。

918f01f192d045fda4f58231dc0bc7e2.png

 

二、rtc字符设备驱动模型的特点

         rtc设备驱动模型与i2c/spi通用字符设备驱动模型与混杂字符设备驱动模型的特点来进行对比。

特性/模型RTC 设备驱动模型I2C/SPI 通用字符设备驱动模型Misc 字符设备驱动模型
驱动目的管理与硬件时钟(RTC)设备交互,提供时间管理功能管理通过 I2C/SPI 总线连接的外部硬件设备用于管理不属于其他特定类别的设备(例如自定义设备)
设备类型RTC(实时时钟)I2C 或 SPI 外设(传感器、存储、显示等)自定义设备(如 GPIO 控制器、简易硬件接口等)
总线协议无特定总线要求,通常是直接硬件接口使用 I2C 或 SPI 总线与外设通信无特定总线要求
驱动结构使用 rtc_device 结构使用 i2c_driverspi_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 驱动通常处理与时间相关的中断(如闹钟),数据交换以时间为主。
    • I2C/SPI 驱动需要处理从外设的中断或数据传输,数据交换通常涉及更复杂的外设交互。
    • Misc 驱动通常处理简单的数据交互,可能会有设备中断支持,但功能较为简化。

rtc字符设备与rtc_device以及与cdev_map关联如下图所示,主要内容如下:

  1. 针对每一个新注册的次设备,均需要创建对应的cdev,并注册至cdev_map中去;
  2. 每一个次设备,其文件操作接口指针均是相同的(指向rtc_dev_fops),而rtc_dev_fops则通过调用rtc_device中的class_ops,实现调用各rtc设备的驱动接口,实现与rtc设备的通信操作。

fc6d60291b334d2ca936253832c5f272.png

 

三、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设备,主要包括:

  1. device类型变量(实现与设备驱动模型、sysfs的关联);
  2. rtc_class_ops类型的成员变量用于操作rtc设备的接口;
  3. cdev类型的变量,用于表示该rtc设备对应的字符设备。
  4. 而剩下的成员变量包含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驱动架构图中已经说明:

  1. 应用程序通过rtc相关的字符设备文件,完成对rtc的操作;
  2. 应用程序通过sysfs中 的属性文件,可完成对rtc的操作;
  3. 应用程序通过procfs中的文件,可完成对rtc的操作。

分析下RTC设备驱动模型中字符设备处理相关的结构体关联、sysfs文件处理相关的结构体关联。

3.1、字符设备处理相关的结构体关联

首先分析下字符设备文件节点相关的结构体关联图,如下图所示,具体关联说明如下:

  1. rtc_device通过其cdev类型的成员变量,实现了与cdev_map的关联(这样当系统调用open进入至字符设备的open接口chrdev_open,根据设备号,即可在cdev_map中查找到该rtc设备对应的cdev,进而即可找到其文件操作接口指针rtc_dev_ops);
  2. 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“。

7836a1c5fc6f4f33a52b21606c477739.png

 

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,
};

四、字符设备处理相关的结构体关联

  1. rtc_device的device成员通过其内部的kobject变量,链接至devices_kset的kset中的list上中;
  2. 针对每一个rtc设备,均会为其在sysfs的/sys/device目录下创建对应的目录,然后根据该device的属性(针对rtc设备而言,其属性为rtc设备对应的类rtc_class->dev_attrs,即rtc_attrs数组,针对该数组中的每一个成员均对应下图中的一个sysfs__dirent(attr类型));
  3. 针对rtc设备的sysfs属性文件的访问,主要借助dev_sysfs_ops的store/show接口进行访问,而在其store/show接口中,针对device_attribute属性,找到对应属性的store/show接口,从而即访问至rtc sysfs if中的接口;
  4. rtc_sysfs_if中的接口,基本上是调用rtc interface中的接口,与上图中的rtc_dev<->rtc_interface<->rtc_class_ops是一样的。

 字符设备处理相关的结构体关联、sysfs文件处理相关的结构体关联:

58d027f47ea940d4ac6953353667928b.png

 

五、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,其实现流程如下,主要功能说明如下:

  1. 完成rtc模块class的创建,每一个rtc device均属于该类;
  2. 完成rtc模块字符设备区域的申请;
  3. 完成针对rtc模块class的dev_attr的设置,针对rtc_class上的设备,均会为其创建dev_attr上定义的属性值。

d7c3175ae443416c816f63cd6b4e042b.png

 

六、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注册】主要包含如下几方面的内容:

  1. 提供针对rtc设备的访问接口(即实现rtc_class_ops中各接口);
  2. 将该rtc设备对应的字符设备注册至系统中(即注册至cdev_map中);
  3. 调用device_register,将rtc_device的dev成员变量注册至devices_kset中,并完成设备属性的创建;
  4. 调用proc_create_data,向系统挂载的procfs中注册rtc文件,并提供相应的操作方法。
  5. 设置该rtc设备的dev成员的class指针指向rtc_class。

通过以上几步,即完成了rtc设备的注册,rtc设备注册的接口为devm_rtc_device_register。

七、RTC时间到系统时间的更新机制

在系统启动后,完成RTC设备驱动的注册之后,LINUX系统会调用函数rtc_hctosys根据RTC时间更新系统时间(即墙上时间)。函数rtc_hctosys处理流程如下:

b0dcfdf59f7044cd8b3472fb4a3c09c5.png

 

  1. 根据CONFIG_RTC_HCTOSYS_DEVICE,获取LINUX系统默认使用的RTC设备名称。针对CONFIG_RTC_HCTOSYS_DEVICE,其可通过make menuconfig进行配置,如下所示;
  2. 调用接口rtc_class_open,在rtc_class类上查找设备名称为CONFIG_RTC_HCTOSYS_DEVICE的rtc设备;
  3. 若上述步骤中找到rtc设备,则调用rtc_read_time接口获取rtc时间,并调用接口do_settimeofday设置系统时间,从而完成系统时间的更新。

 

八、如何实现一个RTC设备驱动

8.1、RTC 设备驱动的主要函数

  1. rtc_read_time():读取 RTC 设备的当前时间。
  2. rtc_set_time():设置 RTC 设备的时间。
  3. rtc_alarm():设置 RTC 设备的闹钟或周期性中断。
  4. rtc_irq():处理 RTC 设备中断,如闹钟触发等。

8.2、典型的 RTC 设备驱动流程

  1. 定义 rtc_device 结构: RTC 驱动需要定义一个 rtc_device 结构体,表示 RTC 设备的实例。这个结构体中包含设备的操作函数、时区信息、硬件时钟信息等。

  2. 实现文件操作(File Operations): RTC 驱动实现标准的文件操作接口,允许用户空间通过打开 /dev/rtc 设备节点访问 RTC 设备。这些操作通常包括:

    • open:打开 RTC 设备。
    • read:读取当前时间。
    • write:设置当前时间。
    • ioctl:进行特定的控制操作,如设置闹钟等。
  3. 设备注册: 通过 rtc_register_device() 函数将 RTC 设备注册到系统中。该函数会将设备实例与内核的 RTC 子系统连接。

  4. 处理硬件中断: 如果 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");

 

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值