RTC 调试指南
RTC 概述
Linux RTC(Real-Time Clock)实时时钟驱动子系统是内核中管理硬件实时时钟设备的标准化框架,其核心功能在于实现系统在完全断电状态下仍能维持精准的时基同步。Linux 内核提供了一个通用的 RTC 框架,能够支持多种 RTC 芯片,包括通过 I²C、SPI 等总线进行通信的设备。
在 S100 芯片中,内置了一个 RTC 模块,该模块是可配置高精度计数器,能够为系统提供稳定的时间基准。此外,S100 还外置了一个 YSN8130E 模块,该模块支持外部电池供电,从而在系统断电时仍能保持时间的连续性。
RTC 特点
时间与日期记录
RTC 最基本的功能是提供精确的时间和日期。它通常以秒为单位,从某个特定的时间点开始计数。RTC 可以提供以下时间信息:
- 当前时间(小时、分钟、秒)
- 当前日期(年、月、日)
- 星期信息
闹钟与中断
许多 RTC 设备支持设置一个或多个闹钟。闹钟功能允许用户设置一个特定的时间,当 RTC 达到该时间时,可以触发一个中断,如唤醒系统、发送信号或执行特定的操作。
周期性唤醒
RTC 可以配置为在特定的时间间隔触发中断,这种功能常用于定时任务调度,如每小时或每天执行一次的维护任务。
时间更新
RTC 允许用户更新当前时间,这对于在系统启动时同步时间或手动校正时间非常有用。Linux 提供了多种工具(如 hwclock)来设置和读取 RTC 时间。
低功耗模式
为了延长电池寿命,RTC 通常支持低功耗模式。在这种模式下,RTC 继续跟踪时间,但消耗的电力非常少。
硬件接口
Linux RTC 框架支持多种硬件接口,包括 I²C、SPI、GPIO 等,这使得它可以与各种 RTC 芯片兼容,如 DS1307、DS3231、YSN8130等。
RTC 功能原理
RTC 通过精确的晶振(通常采用 32.768 kHz 石英晶振)信号来持续计算时间,并将该信息以标准格式(如年、月、日、时、分、秒)存储在内部寄存器中。它依靠备用电池在主电源关闭时继续运行,确保时间的连续性,并通过与主控芯片的通信接口进行时间的读取和设置。其主要功能包括精确计 时、日期维护和闹钟触发。
以下是 RTC 的工作原理简述:
-
时间计数器:
- RTC 内部集成有一个计时电路,通常由一个高精度晶振提供时钟信号。常见的晶振频率为32.768 kHz,它的频率非常稳定,适合用于长时间的时间计量。
- RTC 的计时电路通过晶振生成稳定的时钟信号,通常是秒脉冲(1Hz),然后将其累加。每秒钟,RTC 会增加计数,直到形成分钟、小时、日期等。
-
日期和时间的维护:
- RTC 将累加的秒、分钟、小时、日期等信息存储在内部的寄存器中。这些寄存器可以通过系统与 RTC 通信来读取和设置当前时间或闹钟。
- 一些 RTC 还具有存储年份、月份和星期几等信息。
-
备用电池:
- 为了在主系统断电时保持时间,RTC 通常配有一个备用电池。当主电源断开时,备用电池为 RTC 提供电力,确保其内部时钟仍然继续工作,而不会丢失时间信息。
- 在这种情况下,RTC 会继续运行,但主控芯片可能处于低功耗或关闭状态。
-
闹钟功能:
- RTC 模块通常内置闹钟功能,允许设置特定的时间点,当计时达到设置的时间时,RTC 会触发一个中断信号或发出警告。
- 主控芯片可以根据这个中断信号执行相应的操作,如唤醒系统、启动某个任务等。
RTC 在 S100 上的应用
RTC 时间保持
Linux 的系统时间在系统关机时间就会丢失,而 RTC 可以在系统关闭后,依靠外部电池继续工作,这样就能将时间保存下来,待系统下次启动时候就可以从 RTC 中恢复时间。其流程如下所示:

流程详细说明如下:
-
系统关闭时:
- 系统在关闭前将当前的系统时间设置到 RTC 中。
- 内核通过 RTC 驱动将时间写入 RTC 硬件。
- RTC 硬件确认时间写入成功。
- RTC 驱动将确认信息返回给内核。
- 内核将确认信息返回给系统。
-
系统关闭后:
- 系统关闭后,备用电池开始供电给 RTC 硬件。
- RTC 硬件在备用电池供电下继续运行,持续计时。
-
系统下次上电时:
- 系统重新上电后,通过内核从 RTC 硬件获取时间。
- 内核通过 RTC 驱动读取 RTC 硬件中的时间。
- RTC 硬件返回当前时间给 RTC 驱动。
- RTC 驱动将时间返回给内核。
- 内核将时间设置为系统时间。
RTC 定时任务
RTC 的典型应用是通过 RTC 定时执行任务,该功能仅 YSN8130 支持,其流程如下所示:

S100 芯片上 RTC 定时任务流程说明如下:
- 初始化阶段: 主控芯片首先与 RTC 模块进行初始化,设置当前时间和闹钟(定时提醒)。其次是建立和 MCU 之间的 IPC 通信,因为 RTC 模块的中断引脚是接在 MCU 上的。
- 持续运行阶段(Loop): RTC 模块进入一个循环模式,持续地进行计时(计数器累加)。该过程是低功耗的,RTC 模块会定期更新其内部计数器。
- 闹钟触发事件: 在特定时间点,RTC 模块的闹钟触发条件被满足时,RTC 模块向 MCU 发送中断信号。MCU 接收到中断信号后,会处理特定任务或执行特定功能。
- 清除标志位:
在执行完任务之后,MCU 通过 IPC 通道通知 Acore,随后 Acore 再通过写寄存器的方式清除相关的中断标志位。
RTC 驱动代码
RTC 模块的驱动代码位于 /hobot-drivers/rtc。
Linux RTC 驱动框架
在 Linux 中,RTC 设备驱动是一个标准的字符设备驱动,Linux 的 RTC 驱动框架可以抽象为以下几个主要部分:
- 用户空间位于最顶层,包含用户工具和内核空间的接口。
- 内核空间位于中间层,分为三个部分:
- 接口层:与用户空间直接交互。
- RTC Core:管理 RTC 设备 的核心模块。
- RTC 驱动层:与硬件层直接交互。
- 硬件层位于最底层,表示具体的 RTC 硬件设备。
Linux RTC 驱动框架如下图所示:

下面对各层分别进行介绍。
RTC 用户空间(User Space):
用户空间与 RTC 设备进行交互,主要通过以下几种方式:
- 用户工具:
hwclock:硬件时钟操作工具。date:系统时间操作工具。- 测试工具:如
rtctest.c,用于测试 RTC 驱动的ioctl接口。
- 字符设备接口:
/dev/rtcN:字符设备节点,支持open、read、write和ioctl操作。
- sysfs 接口:
/sys/class/rtc/rtcN:提供只读属性,如时间、闹钟等,允许用户空间访问 RTC 设备的某些属性。
- procfs 接口:
/proc/driver/rtc:提供系统时钟 RTC 的信息,如果系统没有专用的 RTC,则默认使用rtc0。
RTC 内核空间(Kernel Space):
内核空间的各个模块负责 RTC 驱动的管理、设备注册、与用户空间的交互等:
- 接口层(Interface Layer):
- 管理字符设备接口。
- 管理 sysfs 和 procfs 属性。
- RTC Core(核心层):
- 设备管理:
- 设备注册与注销:通过
register和unregister函数进行 RTC 设备的注册和注销。 - 字符设备抽象:通过
dev.c将 RTC 设备抽象为通用的字符设备,提供文件操作函数。 - sysfs 和 procfs 管理:通过
sysfs.c和proc.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 硬件:
- 硬件时钟芯片(如 YSN8130)
- 晶振
- 外部电池
RTC 驱动代码说明
本节将主要说明 RTC 子系统代码的以下三个部分:
- rtc driver Layer:将 RTC 设备注册到 RTC 子系统,并提供针对 RTC 设备的底层操作函数集。
- rtc core:负责 RTC 设备的注册与注销,向用户空间提供 RTC 字符设备文件,并实现 RTC 类的 sysfs 等接口。
- 用户空间接口:包括
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 设备来实现。
以源码中的 YSN8130 驱动为例进行说明:
// hobot-drivers/rtc/rtc-ysn8130.c
static const struct rtc_class_ops ysn8130_rtc_ops = {
.read_time = ysn8130_rtc_read_time,
.set_time = ysn8130_rtc_set_time,
.read_alarm = ysn8130_rtc_read_alarm,
.set_alarm = ysn8130_rtc_set_alarm,
.alarm_irq_enable = ysn8130_irq_enable,
.ioctl = ysn8130_rtc_ioctl,
.proc = ysn8130_rtc_proc,
};
这些操作函数在 YSN8130 驱动中根据硬件的具体接口实现,这些接口一般会根据实际的硬件直接操作寄存器,并通过 rtc_class_ops结构体指针提供给 RTC 子系统。通过这些函数,内核可以实现对 YSN8130 模块的控制。
注意: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 资源。
相关源码:
/**
* 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);
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);
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 硬件驱动层和用户空间层连接起来。这个过程包括以下几个关键步骤:
-
初始化设备号:
- 为 RTC 设备分配一个唯一的设备号(
devt),这是内核用来识别设备的一个标识符。设备号由主设备号和次设备号组成,其中主设备号通常用于标识设备类型,次设备号用于区分同一类型的多个设备实例。
- 为 RTC 设备分配一个唯一的设备号(
-
初始化字符设备结构:
- 初始化 RTC 设备的字符设备结构(
cdev),这个结构包含了文件操作函数(file_operations),这些函数定义了用户空间如何与设备文件交互。例如,当用户空间程序打开、读取、写入或执行 I/O 控制操作(ioctl)时,内核会调用这些函数。
- 初始化 RTC 设备的字符设备结构(
-
设置文件操作:
- 将
rtc_dev_fops(一个file_operations结构体)与 RTC 设备的字符设备结构关联起来。这样,当用户空间程序对设备文件进行操作时,内核就会调用这些预定义的函数来执行相应的硬件操作。
- 将
-
注册设备:
- 通过调用
cdev_device_add函数将 RTC 设备的字符设备添加到系统中,这样用户空间程序就可以通过设备文件(如/dev/rtcN)来访问 RTC 设备了。
- 通过调用
-
初始化其他功能:
- 根据需要初始化其他功能,如定时器,一般用于处理特定的 RTC 功能。