4.3.12. RTC 调试指南

4.3.12.1. RTC 概述

Linux RTC(Real-Time Clock)实时时钟驱动子系统内核管理硬件实时时钟设备标准化框架,其核心功能在于实现系统完全断电状态维持精准时基同步。Linux 内核提供一个通用的 RTC 框架,能够支持多种 RTC 芯片,包括通过 I²C、SPI 等总线进行通信设备。

在 X5 芯片中,内置一个 RTC 模块,该模块一个带有 AMBA APB 接口配置高精度计数器,能够系统提供稳定时间基准。此外,X5 EVB 的某些板型(例如 BOARD 1_B)还外置一个 PCF8563 模块,该模块支持外部电池供电,从而系统断电保持时间连续性。

RTC 特点

时间日期记录

RTC 最基本功能提供精确时间日期。它通常单位,从某个特定时间开始计数。RTC 可以提供以下时间信息:

  • 当前时间(小时、分钟、秒)

  • 当前日期(年、月、日)

  • 星期信息

闹钟中断

许多 RTC 设备支持设置一个多个闹钟。闹钟功能允许用户设置一个特定时间,当 RTC 达到时间时,可以触发一个中断,如唤醒系统、发送信号执行特定操作。

周期性唤醒

RTC 可以配置特定时间间隔触发中断,这种功能常用定时任务调度,如小时每天执行一次维护任务。

时间更新

RTC 允许用户更新当前时间,这对于系统启动同步时间手动校正时间非常有用。Linux 提供多种工具(如hwclock)来设置读取 RTC 时间。

低功耗模式

为了延长电池寿命,RTC 通常支持低功耗模式。在这种模式下,RTC 继续跟踪时间,但消耗电力非常少。

硬件接口

Linux RTC 框架支持多种硬件接口,包括 I²C、SPI、GPIO 等,这使得可以各种 RTC 芯片兼容,如 DS1307、DS3231、PCF8563等。

RTC 功能原理

RTC 通过精确晶振(通常采用 ​32.768 kHz 石英晶振)信号持续计算时间,并信息标准格式(如年、月、日、时、分、秒)存储内部寄存器中。它依靠备用电池电源关闭继续运行,确保时间连续性,并通过主控芯片通信接口进行时间读取设置。其主要功能包括精确计时、日期维护闹钟触发。

以下是 RTC 的工作原理简述:

  1. 时间计数器

    • RTC 内部集成一个计时电路,通常一个高精度晶振提供时钟信号。常见晶振频率为32.768 kHz,它频率非常稳定,适合用于长时间时间计量。

    • RTC 的计时电路通过晶振生成稳定时钟信号,通常脉冲(1Hz),然后累加。每秒钟,RTC 会增加计数,直到形成分钟、小时、日期等。

  2. 日期时间维护

    • RTC 将累加秒、分钟、小时、日期信息存储内部寄存器中。这些寄存器可以通过系统与 RTC 通信读取设置当前时间闹钟。

    • 一些 RTC 还具有存储年份、月份星期信息。

  3. 备用电池

    • 为了系统断电保持时间,RTC 通常配有一个备用电池。当主电源断开时,备用电池为 RTC 提供电力,确保内部时钟仍然继续工作,而不会丢失时间信息。

    • 这种情况下,RTC 会继续运行,但主控芯片可能处于低功耗关闭状态。

  4. 闹钟功能

    • RTC 模块通常内置闹钟功能,允许设置特定时间点,当计时达到设置时间时,RTC 会触发一个中断信号发出警告。

    • 主控芯片可以根据这个中断信号执行相应操作,如唤醒系统、启动某个任务等。

RTC 典型应用

RTC 时间保持

Linux 的系统时间系统关机时间丢失,而 RTC 可以系统关闭后,依靠外部电池继续工作,这样时间保存下来,待系统下次启动时候可以从 RTC 中恢复时间。其流程如下所示:

RTC_typical_application_2.png

流程详细说明如下:

  1. 系统关闭

    • 系统关闭当前系统时间设置到 RTC 中。

    • 内核通过 RTC 驱动时间写入 RTC 硬件。

    • RTC 硬件确认时间写入成功。

    • RTC 驱动确认信息返回内核。

    • 内核确认信息返回系统。

  2. 系统关闭

    • 系统关闭后,备用电池开始供电给 RTC 硬件。

    • RTC 硬件备用电池供电继续运行,持续计时。

  3. 系统下次上电时

    • 系统重新上电后,通过内核从 RTC 硬件获取时间。

    • 内核通过 RTC 驱动读取 RTC 硬件时间。

    • RTC 硬件返回当前时间给 RTC 驱动。

    • RTC 驱动时间返回内核。

    • 内核时间设置系统时间。

RTC 定时唤醒

RTC 的典型应用通过 RTC 定时唤醒系统,其流程如下所示:

RTC_typical_application.png

流程说明如下:

  1. 初始化阶段: 主控芯片首先与 RTC 模块进行初始化,设置当前时间闹钟(定时提醒)。这一步通常发生系统上电后,主控芯片通过与 RTC 模块通信设置时间闹钟参数。

  2. 切换低功耗模式: 完成初始化后,RTC 模块切换低功耗模式,依赖于外接电池持续保持时间闹钟信息。外接电池提供必要电源,以确保 RTC 模块主控芯片关闭进入低功耗状态时,依然保持运行。

  3. 持续运行阶段(Loop): RTC 模块进入一个循环模式,持续进行计时(计数器累加)。该过程低功耗的,RTC 模块定期更新内部计数器。

  4. 闹钟触发事件:特定时间点,RTC 模块闹钟触发条件满足时,RTC 模块主控芯片发送中断信号,通知主控芯片时间到达。 主控芯片接收中断信号后,会执行预定唤醒任务,比如唤醒系统、处理特定任务执行特定功能。

4.3.12.2. RTC 驱动代码

Linux RTC 驱动框架

在 Linux 中,RTC 设备驱动一个标准字符设备驱动,Linux 的 RTC 驱动框架可以抽象以下几个主要部分:

  1. 用户空间位于顶层,包含用户工具内核空间接口。

  2. 内核空间位于中间层,分为三个部分:

    • 接口:与用户空间直接交互。

    • RTC Core:管理 RTC 设备核心模块。

    • RTC 驱动:与硬件直接交互。

  3. 硬件位于最底层,表示具体的 RTC 硬件设备。

Linux RTC 驱动框架如下所示:

RTC_Driver_Frame.png

下面各层分别进行介绍。

RTC 用户空间(User Space):

用户空间与 RTC 设备进行交互,主要通过以下几种方式:

  • 用户工具

    • hwclock:硬件时钟操作工具。

    • date:系统时间操作工具。

    • 测试工具:如 rtctest.c,用于测试 RTC 驱动ioctl 接口。

  • 字符设备接口

    • /dev/rtcN:字符设备节点,支持 openreadwriteioctl 操作。

  • sysfs 接口

    • /sys/class/rtc/rtcN:提供只读属性,如时间、闹钟等,允许用户空间访问 RTC 设备某些属性。

  • procfs 接口

    • /proc/driver/rtc:提供系统时钟 RTC 的信息,如果系统没有专用的 RTC,则默认使用 rtc0

RTC 内核空间(Kernel Space):

内核空间各个模块负责 RTC 驱动管理、设备注册、与用户空间交互等:

  • 接口层(Interface Layer)

    • 管理字符设备接口。

    • 管理 sysfs 和 procfs 属性。

  • RTC Core(核心层)

    • 设备管理

      • 设备注册注销:通过 registerunregister 函数进行 RTC 设备注册注销。

      • 字符设备抽象:通过 dev.c 将 RTC 设备抽象通用字符设备,提供文件操作函数。

      • sysfs 和 procfs 管理:通过 sysfs.cproc.c 管理 RTC 设备的 sysfs 和 procfs 属性。

    • 时间转换:通过 lib.c 提供 RTC 时间系统时间之间转换。

  • RTC 驱动层(Driver Layer)

    • 硬件抽象

      • 操作函数:通过 rtc_class_ops 结构定义函数集,提供对 RTC 硬件底层操作,如读取时间、设置时间、读取设置闹钟等。

      • 硬件初始化:初始化 RTC 硬件,配置时钟源、中断等。

      • 中断处理:处理 RTC 产生中断,如闹钟中断周期性中断。

  • 数据结构

    • struct rtc_device:描述 RTC 设备。

    • struct rtc_class_ops:定义底层操作函数。

RTC 硬件层(Hardware Layer):

  • RTC 硬件

    • 硬件时钟芯片(如 PCF8563)

    • 晶振

    • 外部电池

RTC 驱动代码说明

主要说明 RTC 子系统代码以下三个部分:

  1. rtc driver Layer:将 RTC 设备注册到 RTC 子系统,并提供针对 RTC 设备底层操作函数集。

  2. rtc core:负责 RTC 设备注册注销,向用户空间提供 RTC 字符设备文件,并实现 RTC 类的 sysfs 等接口。

  3. 用户空间接口:包括 ioctl 在内接口。

RTC driver Layer 代码说明

RTC 驱动代码主要负责直接操作 RTC 模块,在 Linux 系统中,内核将 RTC 设备抽象rtc_device结构体,RTC 驱动主要工作申请初始化rtc_device

Linux 内核中 RTC 设备抽象如下:

// kernel/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;
	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 硬件驱动依赖于一系列ops函数操作 RTC 模块,内核已经提供这些函数统一接口。这些接口上述rtc_device结构体中struct rtc_class_ops *ops成员变量中,rtc_class_ops是 RTC 设备最底层操作函数集合,包括读取 RTC 设备时间、设置 RTC 设备时间操作:

// kernel/include/linux/rtc.h
/*
 * For these RTC methods the device parameter is the physical device
 * on whatever bus holds the hardware (I2C, Platform, SPI, etc), which
 * was passed to rtc_device_register().  Its driver_data normally holds
 * device state, including the rtc_device pointer for the RTC.
 *
 * Most of these methods are called with rtc_device.ops_lock held,
 * through the rtc_*(struct rtc_device *, ...) calls.
 *
 * The (current) exceptions are mostly filesystem hooks:
 *   - the proc() hook for procfs
 */
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_class_ops具体操作需要根据使用的 RTC 设备实现。

以 BSP 源码的 PCF8563驱动为例进行说明:

// kernel/drivers/rtc/rtc-pcf8563.c
static const struct rtc_class_ops pcf8563_rtc_ops = {
	.ioctl		= pcf8563_rtc_ioctl,
	.read_time	= pcf8563_rtc_read_time,
	.set_time	= pcf8563_rtc_set_time,
	.read_alarm	= pcf8563_rtc_read_alarm,
	.set_alarm	= pcf8563_rtc_set_alarm,
	.alarm_irq_enable = pcf8563_irq_enable,
};

这些操作函数在 PCF8563驱动根据硬件具体接口实现,这些接口一般根据实际硬件直接操作寄存器,并通过rtc_class_ops结构指针提供给 RTC 子系统。通过这些函数,内核可以实现对 PCF8563 模块控制。

注意rtc_class_ops这些函数对 RTC 设备底层操作函数,并非提供应用层file_operations操作集。Linux 内核提供一个通用的 RTC 字符设备驱动文件drivers/rtc/rtc-dev.c,该文件实现所有 RTC 设备共用file_operations操作集。

rtc_init 函数实现了 RTC 子系统初始化,相关源码如下:

// kernel/drivers/rtc/class.c
static int __init rtc_init(void)
{
    rtc_class = class_create(THIS_MODULE, "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 设备rtc_devt设备初始化。alloc_chrdev_region函数用于动态分配设备号。调用过程如下:

rtc_init
  ---> class_create(THIS_MODULE, "rtc")         // 创建 rtc_class 类。
    ---> rtc_dev_init()
      ---> alloc_chrdev_region(&rtc_devt, 0, RTC_DEV_MAX, "rtc")    //  rtc 设备分配子设备号范围0~15,主设备号随机分配,最终结果存储在 rtc_devt 中。

RTC core 代码说明

RTC core 层在 Linux 内核负责管理调度与 RTC 相关设备资源,并提供对 RTC 设备统一接口。

RTC 驱动准备rtc_class_ops 结构体后,即可在 RTC core 层通过接口 devm_rtc_device_register 向 Linux 内核注册 rtc 资源。

相关源码:

//kernel/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;
}
EXPORT_SYMBOL_GPL(devm_rtc_device_register);

这里rtc->ops = ops设置 rtc_class_ops 底层操作集。

下面主要分析__devm_rtc_register_device,该函数用于将 RTC 设备注册系统中:

//kernel/drivers/rtc/class.c
int __devm_rtc_register_device(struct module *owner, struct rtc_device *rtc)
{
	struct rtc_wkalrm alrm;
	int err;

	if (!rtc->ops) {
		dev_dbg(&rtc->dev, "no ops set\n");
		return -EINVAL;
	}

	if (!rtc->ops->set_alarm)
		clear_bit(RTC_FEATURE_ALARM, rtc->features);

	if (rtc->ops->set_offset)
		set_bit(RTC_FEATURE_CORRECTION, rtc->features);

	rtc->owner = owner;
	rtc_device_get_offset(rtc);

	/* Check to see if there is an ALARM already set in hw */
	err = __rtc_read_alarm(rtc, &alrm);
	if (!err && !rtc_valid_tm(&alrm.time))
		rtc_initialize_alarm(rtc, &alrm);

	rtc_dev_prepare(rtc);

	err = cdev_device_add(&rtc->char_dev, &rtc->dev);  // 将 RTC 设备添加为字符设备
	if (err) {
		set_bit(RTC_NO_CDEV, &rtc->flags);
		dev_warn(rtc->dev.parent, "failed to add char device %d:%d\n",
			 MAJOR(rtc->dev.devt), rtc->id);
	} else {
		dev_dbg(rtc->dev.parent, "char device (%d:%d)\n",
			MAJOR(rtc->dev.devt), rtc->id);
	}

	rtc_proc_add_device(rtc);  // 添加 RTC 设备到 proc 文件系统

	dev_info(rtc->dev.parent, "registered as %s\n",
		 dev_name(&rtc->dev));

#ifdef CONFIG_RTC_HCTOSYS_DEVICE
	if (!strcmp(dev_name(&rtc->dev), CONFIG_RTC_HCTOSYS_DEVICE))
		rtc_hctosys(rtc);
#endif

	return devm_add_action_or_reset(rtc->dev.parent,
					devm_rtc_unregister_device, rtc);
}
EXPORT_SYMBOL_GPL(__devm_rtc_register_device);

其中调用rtc_dev_prepare函数准备 RTC 设备资源,相关代码如下:

// kernel/drivers/rtc/dev.c
void rtc_dev_prepare(struct rtc_device *rtc)
{
  if (!rtc_devt)
    return;

  if (rtc->id >= RTC_DEV_MAX) {
    dev_dbg(&rtc->dev, "too many RTC devices\n");
    return;
  }

  rtc->dev.devt = MKDEV(MAJOR(rtc_devt), rtc->id);

#ifdef CONFIG_RTC_INTF_DEV_UIE_EMUL
  INIT_WORK(&rtc->uie_task, rtc_uie_task);
  timer_setup(&rtc->uie_timer, rtc_uie_timer, 0);
#endif

  cdev_init(&rtc->char_dev, &rtc_dev_fops);
  rtc->char_dev.owner = rtc->owner;
}  

rtc_dev_prepare 函数主要作用为 RTC 设备准备内核数据结构资源,以便设备可以系统识别正确用户空间通信,它在 Linux 内核的 RTC 驱动框架起到一个桥梁作用,将 RTC 硬件驱动用户空间连接起来。这个过程包括以下几个关键步骤:

  1. 初始化设备

    • 为 RTC 设备分配一个唯一设备号(devt),这是内核用来识别设备一个标识符。设备由主设备设备组成,其中设备通常用于标识设备类型,次设备用于区分同一类型多个设备实例。

  2. 初始化字符设备结构

    • 初始化 RTC 设备字符设备结构(cdev),这个结构包含文件操作函数(file_operations),这些函数定义用户空间如何设备文件交互。例如,当用户空间程序打开、读取、写入执行 I/O 控制操作(ioctl)时,内核调用这些函数。

  3. 设置文件操作

    • rtc_dev_fops(一个file_operations结构体)与 RTC 设备字符设备结构关联起来。这样,当用户空间程序设备文件进行操作时,内核调用这些预定函数执行相应硬件操作。

  4. 注册设备

    • 通过调用cdev_device_add函数将 RTC 设备字符设备添加系统中,这样用户空间程序可以通过设备文件(如/dev/rtcN)来访问 RTC 设备了。

  5. 初始化其他功能

    • 根据需要初始化其他功能,如定时器,一般用于处理特定的 RTC 功能。

用户接口代码说明

  • procfs 接口函数

    调用rtc_proc_add_device函数将 RTC 设备添加到 proc 文件系统。

    // kernel/drivers/rtc/proc.c
    void rtc_proc_add_device(struct rtc_device *rtc)
    {
      if (is_rtc_hctosys(rtc))
        proc_create_single_data("driver/rtc", 0, NULL, rtc_proc_show,
              rtc);
    }
    

    rtc_proc_add_device 函数主要作用将 RTC 设备信息暴露用户空间,通过 /proc 文件系统提供一个接口,使得用户空间程序可以方便读取 RTC 设备状态配置信息。这样,用户空间程序一种标准化方式获取内核设备信息,而无需直接访问设备驱动内部数据结构。

    rtc_proc_add_device函数执行成功/proc 目录创建一个名为 driver/rtc文件,这个文件与 rtc 设备关联的,其中内容一般如下:

    root@buildroot:~# cat /proc/driver/rtc
    rtc_time        : 00:01:00
    rtc_date        : 1970-01-01
    alrm_time       : 00:00:00
    alrm_date       : 1970-01-01
    alarm_IRQ       : no
    alrm_pending    : no
    update IRQ enabled      : no
    periodic IRQ enabled    : no
    periodic IRQ frequency  : 1
    max user IRQ frequency  : 64
    24hr            : yes
    

    /proc/driver/rtc 文件提供关于 RTC 设备状态详细信息,该文件信息解释如下:

    1. 查看 RTC 时间

      • 文件显示rtc_timertc_date 分别表示 RTC 设备当前小时、分钟、秒年、月、日。这对于验证 RTC 设备是否正确运行非常有用。

    2. 检查闹钟设置

      • alrm_timealrm_date 显示了 RTC 设备闹钟设置。如果这些不是预期值,可能需要重新配置闹钟。

      • alarm_IRQ 表示是否中断请求(IRQ)与闹钟相关联。如果设置yes,则表示 RTC 设备支持闹钟中断。

      • alrm_pending 表示是否有待处理闹钟事件。

    3. 监控中断状态

      • update IRQ enabledperiodic IRQ enabled 显示了 RTC 设备是否启用更新中断周期性中断。这些中断可以用于定时任务事件触发。

      • periodic IRQ frequencymax user IRQ frequency 提供关于周期性中断频率信息,这对于需要精确定时应用非常重要。

    4. 时间格式

      • 24hr 指示 RTC 设备是否使用24小时制。这对于需要在12小时和24小时之间转换应用来说一个重要信息。

    /proc/driver/rtc 文件常见使用场景如下:

    • 系统监控:用户可以使用这个文件监控 RTC 设备状态,确保时间同步闹钟功能正常工作。

    • 故障排除:如果 RTC 设备出现问题,如时间准确闹钟触发,这个文件可以提供故障排除线索。

    • 配置验证:用户可以配置 RTC 设备检查这个文件,以验证配置是否正确应用。

    • 应用开发:用户开发需要与 RTC 设备交互应用时,可以参考这个文件获取设备状态能力信息。


  • ioctl 接口函数

    调用 cdev_init 函数初始化 RTC 设备字符设备结构(rtc->char_dev),并文件操作函数指针设置rtc_dev_fops,这是一个结构体,包含了 RTC 设备文件操作函数,当用户程序打开、读写执行其他操作设备相关联文件时,内核调用这些函数。

    // kernel/drivers/rtc/dev.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,
    #ifdef CONFIG_COMPAT
      .compat_ioctl	= rtc_dev_compat_ioctl,
    #endif
      .open		= rtc_dev_open,
      .release	= rtc_dev_release,
      .fasync		= rtc_dev_fasync,
    };
    

    rtc_dev_fops用户态下使用的。在 Linux 内核中,struct file_operations(通常通过指向结构指针 fops 引用)定义一系列文件操作函数,这些函数实现设备文件操作。当用户程序通过 ioctl 函数打开、读写执行其他操作设备相关联文件时,内核调用这些函数。

    rtc_dev_fops 结构定义一组与 RTC 设备交互文件操作,包括:

    • .owner:指示哪个模块拥有这些文件操作。通常设置THIS_MODULE,表示当前模块。

    • .llseek:文件定位操作,no_llseek 表示设备支持常规文件定位操作。

    • .read:从 RTC 设备读取数据函数,用户程序调用 read() 系统调用时会使用。

    • .poll:轮询函数,用于阻塞 I/O 操作,例如,当使用 select()poll() 系统调用时。

    • .unlocked_ioctl:执行设备特定操作函数,如获取设置 RTC 时间。ioctl() 系统调用使用函数。

    • .compat_ioctl:兼容模式ioctl 函数,用于支持32位程序在64位系统运行。

    • .open:打开 RTC 设备文件调用函数。

    • .release:关闭 RTC 设备文件调用函数。

    • .fasync:用于异步 I/O 通知函数。

    • 这些操作函数用户程序通过文件系统与 RTC 设备交互内核调用。例如,当用户程序打开 /dev/rtcN 设备文件时,内核调用 rtc_dev_open 函数;当用户程序读取文件时,内核调用 rtc_dev_read 函数。

    • 这里需要说明 rtc_dev_ioctl 函数,它是 RTC 驱动处理 I/O 控制操作核心函数,主要负责根据传入命令参数执行相应的 RTC 操作:

      kernel/drivers/rtc/dev.c
      static long rtc_dev_ioctl(struct file *file,
              unsigned int cmd, unsigned long arg)
      {
        int err = 0;
        struct rtc_device *rtc = file->private_data;
        const struct rtc_class_ops *ops = rtc->ops;
        struct rtc_time tm;
        struct rtc_wkalrm alarm;
        struct rtc_param param;
        void __user *uarg = (void __user *)arg;
      
        err = mutex_lock_interruptible(&rtc->ops_lock);
        if (err)
          return err;
      
        /* check that the calling task has appropriate permissions
        * for certain ioctls. doing this check here is useful
        * to avoid duplicate code in each driver.
        */
        switch (cmd) {
        case RTC_EPOCH_SET:
        case RTC_SET_TIME:
        case RTC_PARAM_SET:
          if (!capable(CAP_SYS_TIME))
            err = -EACCES;
          break;
      
        case RTC_IRQP_SET:
          if (arg > rtc->max_user_freq && !capable(CAP_SYS_RESOURCE))
            err = -EACCES;
          break;
      
        case RTC_PIE_ON:
          if (rtc->irq_freq > rtc->max_user_freq &&
              !capable(CAP_SYS_RESOURCE))
            err = -EACCES;
          break;
        }
        ……
      }
      

      应用程序通过ioctl函数进行时间设置/读取、闹钟设置/读取操作时,rtc_dev_ioctl函数调用。rtc_dev_ioctl最终通过调用rtc_class_ops底层操作集中read_timeset_time函数,执行具体 RTC 设备读写操作。


上述代码串连起来,用户程序与 RTC 设备交互时序如下所示:

RTC_user_interaction.png

具体说明如下:

  1. 用户程序

    • 发起 ioctl() 系统调用,请求对 RTC 设备进行操作。

  2. RTC 用户接口 (rtc_dev_fops)

    • 接收ioctl() 调用后,调用 rtc_dev_ioctl() 函数处理具体命令。

    • RTC 用户接口用户内核之间桥梁。

  3. RTC core

    • 如果首次使用 RTC 设备,调用 __devm_rtc_register_device() 进行设备注册。

    • 调用 rtc_dev_prepare() 准备设备资源。

    • 初始化 rtc_class_ops,设置底层操作函数集。

    • 根据用户程序命令,调用对应底层操作函数(如 read_timeset_timeset_alarm 等)。

  4. RTC 驱动

    • 提供底层操作函数集(rtc_class_ops),直接与 RTC 硬件交互。

    • 执行具体硬件操作,并结果返回给 RTC core。

  5. 返回结果

    • 操作结果逐层返回用户程序,用户程序根据返回结果继续执行。

4.3.12.3. 内核配置

通过 ./bd.sh boot menuconfig 命令进入 Linux 内核配置界面,在 menuconfig 界面中,按照以下路径导航到 RTC 配置选项:

Device Drivers  --->
     Real Time Clock  --->

然后根据需要选择配置项,一般配置如下:

CONFIG_RTC_LIB=y
CONFIG_RTC_CLASS=y
CONFIG_RTC_HCTOSYS=y
CONFIG_RTC_HCTOSYS_DEVICE="rtc0"
CONFIG_RTC_SYSTOHC=y
CONFIG_RTC_SYSTOHC_DEVICE="rtc0"
CONFIG_RTC_DEBUG=y
CONFIG_RTC_NVMEM=y

CONFIG_RTC_INTF_SYSFS=y
CONFIG_RTC_INTF_PROC=y
CONFIG_RTC_INTF_DEV=y

这些配置解释如下:

核心配置

  1. CONFIG_RTC_LIB=y

    • 启用 RTC 库支持。该选项启用 RTC 的基本操作函数库,供其他模块驱动使用。

  2. CONFIG_RTC_CLASS=y

    • 启用 RTC 类。此选项创建一个 RTC 子系统,使得内核能够管理多个 RTC 设备,并用户提供相关接口。

  3. CONFIG_RTC_HCTOSYS=y

    • 启用系统从 RTC 设置时间功能。内核启动时会指定的 RTC 设备(由 CONFIG_RTC_HCTOSYS_DEVICE 配置指定)读取时间,并用作系统时间。

  4. CONFIG_RTC_HCTOSYS_DEVICE="rtc0"

    • 设置哪个 RTC 设备读取时间。此选项指定内核启动rtc0 设备时间作为系统时间。

  5. CONFIG_RTC_SYSTOHC=y

    • 启用系统时间写回 RTC 的功能。该选项内核当前系统时间保存指定的 RTC 设备。

  6. CONFIG_RTC_SYSTOHC_DEVICE="rtc0"

    • 设置哪个 RTC 设备用于保存系统时间。该选项指定系统时间写回 rtc0 设备。

  7. CONFIG_RTC_DEBUG=y

    • 启用 RTC 的调试信息输出。开启后,会输出调试信息,有助于调试 RTC 相关问题。

  8. CONFIG_RTC_NVMEM=y

    • 启用 RTC 的非易失性存储支持。该选项使 RTC 能够提供一个用于存储配置信息非易失性存储区域。

RTC 接口相关配置

  1. CONFIG_RTC_INTF_SYSFS=y

    • 启用 RTC 的 sysfs 接口。这会/sys/class/rtc/ 目录创建 RTC 设备相关信息,允许用户通过 sysfs 接口与 RTC 设备交互。

  2. CONFIG_RTC_INTF_PROC=y

    • 启用 RTC 的 proc 接口。内核创建与 RTC 相关/proc/rtc 文件,允许通过 proc 文件系统与 RTC 设备进行交互。

  3. CONFIG_RTC_INTF_DEV=y

    • 启用 RTC 的字符设备接口。该选项允许 RTC 设备出现/dev/rtcN 中,供用户空间应用程序(如 hwclock)进行读写操作。

另外, X5 芯片内置一个 RTC 模块 (/dev/rtc0), X5 EVB 当前外置一个 PCF8563 (/dev/rtc1),所以开启下面选项:

CONFIG_RTC_DRV_DWAPB=y     # /dev/rtc0
CONFIG_RTC_DRV_PCF8563=m   # /dev/rtc1

两个配置解释如下:

  1. CONFIG_RTC_DRV_DWAPB=y

    • 启用 DWAPB 驱动,该驱动支持 X5 芯片内置的 RTC 模块。此选项使内核识别驱动 /dev/rtc0 设备,使得 X5 芯片内置 RTC 可以作为系统时间使用。

  2. CONFIG_RTC_DRV_PCF8563=m

    • 启用 PCF8563 RTC 驱动,并作为模块加载(m 表示模块)。该驱动支持外置的 PCF8563 RTC 芯片(X5 EVB 中通过 I2C 接口连接)。通过加载这个模块,外置的 PCF8563 RTC 可以识别作为 /dev/rtc1 设备使用。

4.3.12.4. RTC 使用简介

RTC 测试方式

驱动加载成功后,会出现 /dev/rtcN 设备节点:

root@buildroot:~# ls /dev/rtc*
/dev/rtc  /dev/rtc0  /dev/rtc1

可以看到系统当前两个 RTC 设备, /dev/rtc0/dev/rtc1,分别对应内置 RTC 模块 rtc-dwapb 以及外置 RTCPCF8563,具体对应关系可以内核启动日志获知:

root@buildroot:~# dmesg | grep rtc
[    0.577125] dw-rtc 31010000.rtc: prescaler set to: 24000000
[    0.577462] dw-rtc 31010000.rtc: char device (253:0)
[    0.577474] dw-rtc 31010000.rtc: registered as rtc0
[    0.577491] dw-rtc 31010000.rtc: setting system clock to 1970-01-01T00:00:00 UTC (0)
[    1.306790] rtc-pcf8563 4-0051: pcf8563_probe
[    1.308632] rtc-pcf8563 4-0051: low voltage detected, date/time is not reliable.
[    1.309614] rtc rtc1: read_time: fail to read: -22
[    1.309838] rtc-pcf8563 4-0051: char device (253:1)
[    1.309854] rtc-pcf8563 4-0051: registered as rtc1

所以,/dev/rtc0内置 RTC-dwapb,而 /dev/rtc1外置 RTC PCF8563。

系统默认使用/dev/rtc0 作为主 RTC 设备 /dev/rtc

root@buildroot:~# ls -l  /dev/rtc
lrwxrwxrwx 1 root root 4 Jan  1 00:00 /dev/rtc -> rtc0

这里就是对应内置 rtc-dwapb,可以使用以下命令进行测试。

# 测试命令
date -s "2024/01/01 17:00:00"				# 设置系统时间
hwclock -w						# 将系统时间写入 RTC
hwclock -r						# 读取 RTC 时间,确认时间是否写入成功
date							# 读取系统时间

此时可以通过 /proc 接口验证配置结果:

root@buildroot:~# cat /proc/driver/rtc
rtc_time        : 00:15:09
rtc_date        : 1970-01-01
alrm_time       : 00:00:00
alrm_date       : 1970-01-01
alarm_IRQ       : no
alrm_pending    : no
update IRQ enabled      : no
periodic IRQ enabled    : no
periodic IRQ frequency  : 1
max user IRQ frequency  : 64
24hr            : yes
root@buildroot:~# date -s "2024/01/01 17:00:00"
Mon Jan  1 17:00:00 UTC 2024
root@buildroot:~# hwclock -w
root@buildroot:~# hwclock -r
Mon Jan  1 17:00:11 2024  0.000000 seconds
root@buildroot:~# date
Mon Jan  1 17:00:14 UTC 2024
root@buildroot:~# cat /proc/driver/rtc
rtc_time        : 17:00:20
rtc_date        : 2024-01-01
alrm_time       : 00:00:00
alrm_date       : 1970-01-01
alarm_IRQ       : no
alrm_pending    : no
update IRQ enabled      : no
periodic IRQ enabled    : no
periodic IRQ frequency  : 1
max user IRQ frequency  : 64
24hr            : yes

可以看到 rtc_time 已经成功配置了。

外置 RTC 模块 PCF8563 的基本测试方法可以参考 RTC 接口章节。

RTC 测试接口

下面介绍一些用户空间 APP 中 RTC 的常见接口函数,这些接口函数搭建基本框架实现与 RTC 设备交互,用户可以根据具体硬件需求进行调整完善。

set_rtc_time 函数

  • 功能:设置 RTC 的时间。

  • 参数int fd,文件描述符;struct rtc_time rtc_tm,包含设置时间。

  • 实现:通过 ioctl 系统调用使用 RTC_SET_TIME 命令时间写入 RTC。

  • 错误处理:如果 ioctl 调用失败,输出错误信息关闭文件描述符。

    代码示例

    int set_rtc_time(int fd, struct rtc_time rtc_tm) 
    {
        int ret;
        ret = ioctl(fd, RTC_SET_TIME, &rtc_tm);
        if (ret < 0) {
            printf("<%s %d> ERR: set rtc time failed!\n", __func__, __LINE__);
            close(fd);
            return -1;
        }
        return 0;
    }
    

read_rtc_time 函数

  • 功能:读取 RTC 的当前时间。

  • 参数int fd,文件描述符;struct rtc_time *rtc_tm,存储读取时间。

  • 实现:通过 ioctl 系统调用使用 RTC_RD_TIME 命令读取 RTC 的时间,并调用 print_rtc_time 输出时间。

  • 错误处理:如果 ioctl 调用失败,输出错误信息关闭文件描述符。

    代码示例

    int read_rtc_time(int fd, struct rtc_time *rtc_tm) 
    {
        int ret;
        ret = ioctl(fd, RTC_RD_TIME, rtc_tm);
        if (ret < 0) {
            printf("<%s %d> ERR: read rtc time failed!\n", __func__, __LINE__);
            close(fd);
            return -1;
        }
        print_rtc_time(rtc_tm);
        return 0;
    }
    

alm_set_rtc 函数

  • 功能:设置 RTC 的闹钟时间。

  • 参数int fd,文件描述符;struct rtc_time rtc_tm,要设置闹钟时间。

  • 实现:通过 ioctl 系统调用使用 RTC_ALM_SET 命令设置闹钟时间。

  • 错误处理:如果 ioctl 调用失败,输出错误信息关闭文件描述符。

    代码示例

    int alm_set_rtc(int fd, struct rtc_time rtc_tm) 
    {
        int ret;
        ret = ioctl(fd, RTC_ALM_SET, &rtc_tm);
        if (ret < 0) {
            printf("<%s %d> ERR: set alarm failed!\n", __func__, __LINE__);
            close(fd);
            return -1;
        }
        return 0;
    }
    

alm_read_rtc 函数

  • 功能:读取 RTC 的闹钟时间。

  • 参数int fd,文件描述符;struct rtc_time *rtc_tm,存储读取闹钟时间。

  • 实现:通过 ioctl 系统调用使用 RTC_ALM_READ 命令读取闹钟时间,并调用 print_rtc_time 输出时间。

  • 错误处理:如果 ioctl 调用失败,输出错误信息关闭文件描述符。

    代码示例

    int alm_read_rtc(int fd, struct rtc_time *rtc_tm) 
    {
        int ret;
        ret = ioctl(fd, RTC_ALM_READ, rtc_tm);
        if (ret < 0) {
            printf("<%s %d> ERR: read alarm failed!\n", __func__, __LINE__);
            close(fd);
            return -1;
        }
        print_rtc_time(rtc_tm);
        return 0;
    }
    

alm_rtc_enable 函数

  • 功能:启用 RTC 闹钟中断。

  • 参数int fd,文件描述符。

  • 实现:通过 ioctl 系统调用使用 RTC_AIE_ON 命令启用闹钟中断。

  • 错误处理:如果 ioctl 调用失败,输出错误信息关闭文件描述符。

    代码示例

    int alm_rtc_enable(int fd) 
    {
        int ret;
        ret = ioctl(fd, RTC_AIE_ON, 0);
        if (ret < 0) {
            printf("<%s %d> ERR: enable alarm failed!\n", __func__, __LINE__);
            close(fd);
            return -1;
        }
        return 0;
    }
    

alm_rtc_disable 函数

  • 功能:禁用 RTC 闹钟中断。

  • 参数int fd,文件描述符。

  • 实现:通过 ioctl 系统调用使用 RTC_AIE_OFF 命令禁用闹钟中断。

  • 错误处理:如果 ioctl 调用失败,输出错误信息关闭文件描述符。

    代码示例

    int alm_rtc_disable(int fd) 
    {
        int ret;
        ret = ioctl(fd, RTC_AIE_OFF, 0);
        if (ret < 0) {
            printf("<%s %d> ERR: disable alarm failed!\n", __func__, __LINE__);
            close(fd);
            return -1;
        }
        return 0;
    }
    

上述这些接口函数的 IOCTL 命令(RTC_SET_TIMERTC_RD_TIMERTC_AIE_OFF 等)都驱动代码章节提及rtc_dev_ioctl 函数定义的。

RTC 测试用例

下面给出一个简单的 RTC 测试用例。

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <time.h>
#include <linux/rtc.h>

#define RTC_DEVICE "/dev/rtc0"

int set_rtc_time(int fd, struct rtc_time rtc_tm) 
{
    int ret;

    ret = ioctl(fd, RTC_SET_TIME, &rtc_tm);
    if (ret < 0) {
        printf("<%s %d> ERR: set rtc time failed!\n", __func__, __LINE__);
        close(fd);
        return -1;
    }

    return 0;
}

int read_rtc_time(int fd, struct rtc_time *rtc_tm) 
{
    int ret;

    ret = ioctl(fd, RTC_RD_TIME, rtc_tm);
    if (ret < 0) {
        printf("<%s %d> ERR: read rtc time failed!\n", __func__, __LINE__);
        close(fd);
        return -1;
    }

    return 0;
}

int alm_set_rtc(int fd, struct rtc_time rtc_tm) 
{
    int ret;

    ret = ioctl(fd, RTC_ALM_SET, &rtc_tm);
    if (ret < 0) {
        printf("<%s %d> ERR: set alarm failed!\n", __func__, __LINE__);
        close(fd);
        return -1;
    }

    return 0;
}

int alm_read_rtc(int fd, struct rtc_time *rtc_tm) 
{
    int ret;

    ret = ioctl(fd, RTC_ALM_READ, rtc_tm);
    if (ret < 0) {
        printf("<%s %d> ERR: read alarm failed!\n", __func__, __LINE__);
        close(fd);
        return -1;
    }

    return (0);
}

int alm_rtc_enable(int fd) 
{
    int ret;

    ret = ioctl(fd, RTC_AIE_ON, 0);
    if (ret < 0) {
        printf("<%s %d> ERR: enable alarm failed!\n", __func__, __LINE__);

        close(fd);
        return -1;
    }

    return 0;
}

int alm_rtc_disable(int fd) 
{
    int ret;

    ret = ioctl(fd, RTC_AIE_OFF, 0);
    if (ret < 0) {
        printf("<%s %d> ERR: disable alarm failed!\n", __func__, __LINE__);

        close(fd);
        return -1;
    }

    return (0);    
}

int main() {
    int fd;
    struct rtc_time rtc_tm;
    struct rtc_wkalrm alarm_tm;
    int ret;
    int choice;

    // 打开 RTC 设备文件,通常是"/dev/rtc0"
    printf("Opening RTC device...\n");
    fd = open(RTC_DEVICE, O_RDWR);
    if (fd == -1) {
        perror("Failed to open RTC device");
        return -1;
    }
    printf("RTC device opened successfully.\n");

    // 用户选择操作
    while (1) {
        printf("\nPlease choose an option:\n");
        printf("1. Set RTC time\n");
        printf("2. Read RTC time\n");
        printf("3. Set Alarm time\n");
        printf("4. Read Alarm time\n");
        printf("5. Enable Alarm\n");
        printf("6. Disable Alarm\n");
        printf("7. Exit\n");
        printf("Enter your choice: ");
        scanf("%d", &choice);

        switch (choice) {
            case 1:
                // 设置 RTC 时间
                printf("Enter year (e.g., 2025): ");
                scanf("%d", &rtc_tm.tm_year);
                rtc_tm.tm_year -= 1900;  // 年份需要减去1900
                printf("Enter month (1-12): ");
                scanf("%d", &rtc_tm.tm_mon);
                rtc_tm.tm_mon -= 1;  // 月份是0-11的范围
                printf("Enter day (1-31): ");
                scanf("%d", &rtc_tm.tm_mday);
                printf("Enter hour (0-23): ");
                scanf("%d", &rtc_tm.tm_hour);
                printf("Enter minute (0-59): ");
                scanf("%d", &rtc_tm.tm_min);
                printf("Enter second (0-59): ");
                scanf("%d", &rtc_tm.tm_sec);

                printf("Setting RTC time to: %d-%02d-%02d %02d:%02d:%02d\n", 
                       rtc_tm.tm_year + 1900, rtc_tm.tm_mon + 1, rtc_tm.tm_mday,
                       rtc_tm.tm_hour, rtc_tm.tm_min, rtc_tm.tm_sec);
                ret = set_rtc_time(fd, rtc_tm);
                if (ret < 0) {
                    printf("Failed to set RTC time.\n");
                } else {
                    printf("RTC time set successfully.\n");
                }
                break;

            case 2:
                // 读取 RTC 时间
                printf("Reading RTC time...\n");
                ret = read_rtc_time(fd, &rtc_tm);
                if (ret < 0) {
                    printf("Failed to read RTC time.\n");
                } else {
                    printf("RTC time is: %d-%02d-%02d %02d:%02d:%02d\n", 
                           rtc_tm.tm_year + 1900, rtc_tm.tm_mon + 1, rtc_tm.tm_mday,
                           rtc_tm.tm_hour, rtc_tm.tm_min, rtc_tm.tm_sec);
                }
                break;

            case 3:
                // 设置闹钟时间
                printf("Enter alarm year (e.g., 2025): ");
                scanf("%d", &alarm_tm.time.tm_year);
                alarm_tm.time.tm_year -= 1900;  // 年份需要减去1900
                printf("Enter alarm month (1-12): ");
                scanf("%d", &alarm_tm.time.tm_mon);
                alarm_tm.time.tm_mon -= 1;  // 月份是0-11的范围
                printf("Enter alarm day (1-31): ");
                scanf("%d", &alarm_tm.time.tm_mday);
                printf("Enter alarm hour (0-23): ");
                scanf("%d", &alarm_tm.time.tm_hour);
                printf("Enter alarm minute (0-59): ");
                scanf("%d", &alarm_tm.time.tm_min);
                printf("Enter alarm second (0-59): ");
                scanf("%d", &alarm_tm.time.tm_sec);

                printf("Setting alarm time to: %d-%02d-%02d %02d:%02d:%02d\n", 
                       alarm_tm.time.tm_year + 1900, alarm_tm.time.tm_mon + 1, alarm_tm.time.tm_mday,
                       alarm_tm.time.tm_hour, alarm_tm.time.tm_min, alarm_tm.time.tm_sec);
                ret = alm_set_rtc(fd, alarm_tm.time);
                if (ret < 0) {
                    printf("Failed to set alarm time.\n");
                } else {
                    printf("Alarm time set successfully.\n");
                }
                break;

            case 4:
                // 读取闹钟时间
                printf("Reading alarm time...\n");
                ret = alm_read_rtc(fd, &rtc_tm);
                if (ret < 0) {
                    printf("Failed to read alarm time.\n");
                } else {
                    printf("Alarm time is: %d-%02d-%02d %02d:%02d:%02d\n", 
                           rtc_tm.tm_year + 1900, rtc_tm.tm_mon + 1, rtc_tm.tm_mday,
                           rtc_tm.tm_hour, rtc_tm.tm_min, rtc_tm.tm_sec);
                }
                break;

            case 5:
                // 启用闹钟
                printf("Enabling RTC alarm...\n");
                ret = alm_rtc_enable(fd);
                if (ret < 0) {
                    printf("Failed to enable RTC alarm.\n");
                } else {
                    printf("RTC alarm enabled successfully.\n");
                }
                break;

            case 6:
                // 禁用闹钟
                printf("Disabling RTC alarm...\n");
                ret = alm_rtc_disable(fd);
                if (ret < 0) {
                    printf("Failed to disable RTC alarm.\n");
                } else {
                    printf("RTC alarm disabled successfully.\n");
                }
                break;

            case 7:
                // 退出程序
                printf("Exiting program...\n");
                close(fd);
                return 0;

            default:
                printf("Invalid choice. Please try again.\n");
                break;
        }
    }

    return 0;
}

设置 RTC 时间为例,测试日志如下:

root@buildroot:/userdata# chmod +x rtc_test 
root@buildroot:/userdata# ./rtc_test 
Opening RTC device...
RTC device opened successfully.

Please choose an option:
1. Set RTC time
2. Read RTC time
3. Set Alarm time
4. Read Alarm time
5. Enable Alarm
6. Disable Alarm
7. Exit
Enter your choice: 1
Enter year (e.g., 2025): 2025
Enter month (1-12): 3
Enter day (1-31): 21
Enter hour (0-23): 15
Enter minute (0-59): 46
Enter second (0-59): 0
Setting RTC time to: 2025-03-21 15:46:00
RTC time set successfully.

Please choose an option:
1. Set RTC time
2. Read RTC time
3. Set Alarm time
4. Read Alarm time
5. Enable Alarm
6. Disable Alarm
7. Exit
Enter your choice: 2
Reading RTC time...
RTC time is: 2025-03-21 15:46:03

Please choose an option:
1. Set RTC time
2. Read RTC time
3. Set Alarm time
4. Read Alarm time
5. Enable Alarm
6. Disable Alarm
7. Exit
Enter your choice: 7
Exiting program...
root@buildroot:/userdata# cat /proc/driver/rtc 
rtc_time        : 15:46:23
rtc_date        : 2025-03-21
alrm_time       : 00:00:00
alrm_date       : 1970-01-01
alarm_IRQ       : no
alrm_pending    : no
update IRQ enabled      : no
periodic IRQ enabled    : no
periodic IRQ frequency  : 1
max user IRQ frequency  : 64
24hr            : yes

可以看出已经成功设置了 RTC 时间。

4.3.12.5. RTC 常见问题

使用 Linux RTC 模块过程中,可能遇到一些问题,以下这些问题对应解决方法:

  1. RTC 时间丢失

    • 现象:重启后,RTC 时间变成1970-01-01 00:00:00。

    • 原因分析:RTC 需要持续供电才能保持时间信息。可能原因包括电源线断开、未安装电池电池电量不足。可以通过内核启动日志看到相关内容:

      root@buildroot:/userdata# dmesg | grep rtc
      [    0.577125] dw-rtc 31010000.rtc: prescaler set to: 24000000
      [    0.577462] dw-rtc 31010000.rtc: char device (253:0)
      [    0.577474] dw-rtc 31010000.rtc: registered as rtc0
      [    0.577491] dw-rtc 31010000.rtc: setting system clock to 1970-01-01T00:00:00 UTC (0)
      [    1.306790] rtc-pcf8563 4-0051: pcf8563_probe
      [    1.308632] rtc-pcf8563 4-0051: low voltage detected, date/time is not reliable.
      [    1.309614] rtc rtc1: read_time: fail to read: -22
      [    1.309838] rtc-pcf8563 4-0051: char device (253:1)
      [    1.309854] rtc-pcf8563 4-0051: registered as rtc1
      

      注意:这里可以看到 rtc-pcf8563 4-0051: low voltage detected, date/time is not reliable. ,一般出现这样日志说明没有正确接入外部电池或者电池电量不足。

    • 解决方法:确认电源线没有断开过,检查是否安装电池以及电池电量是否充足。

  2. 访问 RTC 设备遇到权限问题

    • 现象:用户空间应用程序可能没有足够权限访问 RTC 设备文件。

    • 解决方法:确保应用程序具有适当权限用户身份运行。

  3. RTC 闹钟设置失败

    • 现象:尝试设置 RTC 闹钟操作失败。

    • 解决方法:检查闹钟时间是否在 RTC 支持范围内,确认闹钟功能是否硬件支持,以及是否足够权限设置闹钟。

  4. 系统没有启用 RTC 驱动

    • 现象:Linux 系统可能没有启用 RTC 驱动,导致无法使用 RTC 设备。

    • 解决方法:确保内核配置文件启用了 RTC 支持。如果没有启用,可以重新编译内核启用 RTC 驱动。如果 RTC 驱动加载,可以手动加载。