在这里插入图片描述

[TOC]

drivers/rtc 实时时钟(Real Time Clock) 驱动框架与硬件驱动集合

历史与背景

这项技术是为了解决什么特定问题而诞生的?

RTC(Real Time Clock)驱动框架和相关驱动是为了解决计算机系统在**主电源关闭后维持和提供真实世界时间(“wall-clock time”)**这一根本问题而诞生的。

  • 时间持久化:计算机系统的主处理器和内存是易失性的,一旦断电,所有状态信息都会丢失。然而,系统需要一个持久化的时钟来记录日期和时间,以便在下次启动时能够知道当前的时间,这对于文件系统时间戳、日志记录、证书验证和网络协议至关重要。
  • 低功耗运行:这个持久化的时钟必须功耗极低,因为它通常仅由一块小型的纽扣电池供电,需要维持数年的运行。
  • 定时唤醒:除了计时,RTC硬件通常还具备闹钟(Alarm)功能。这允许系统进入深度休眠或关机状态,并由RTC在预设的时间点产生一个硬件中断信号,从而唤醒系统执行预定任务(如定时备份、系统维护等),这是实现高级电源管理的关键。

它的发展经历了哪些重要的里程碑或版本迭代?

Linux RTC子系统的发展反映了Linux从单一PC平台向多样化硬件平台演进的过程。

  • 早期PC时代 (rtc-cmos.c):最初,Linux的RTC支持主要围绕PC架构中的摩托罗拉MC146818兼容芯片。驱动 (rtc-cmos.c) 通过传统的I/O端口 (inb/outb) 直接访问硬件。用户空间接口也比较原始,只有一个固定的 /dev/rtc 设备。
  • RTC Class框架的诞生:随着Linux被广泛用于各种嵌入式系统,出现了成百上千种通过I2C、SPI或其他总线连接的RTC芯片。为每种芯片维护一套独立的用户空间接口是不可行的。因此,一个重要的里程碑是**RTC Class框架 (drivers/rtc/class.c)**的建立。它提供了一个统一的、抽象的接口给用户空间(如 /dev/rtc0, /dev/rtc1, …),并定义了一套标准的操作函数集 (struct rtc_class_ops),供底层硬件驱动来实现和注册。
  • Sysfs接口的引入:为了方便脚本和简单应用访问,RTC Class框架增加了sysfs接口(位于 /sys/class/rtc/rtcX/)。通过读写这些文件(如 date, time, wakealarm),可以无需复杂的ioctl调用就能获取时间或设置闹钟。
  • 与设备树(Device Tree)的集成:在现代嵌入式Linux中,RTC硬件的描述(如I2C地址、中断引脚等)都通过设备树来传递给内核,使得硬件驱动本身可以更加通用,无需硬编码平台相关信息。

目前该技术的社区活跃度和主流应用情况如何?

RTC子系统是Linux内核中一个非常成熟和稳定的部分。其核心Class框架改动很少,社区的活跃度主要体现在为层出不穷的新SoC或外围芯片添加新的硬件驱动。
该技术无处不在,是所有现代计算设备的基础组件,包括:

  • PC和服务器:用于在启动时初始化系统时钟。
  • 智能手机和移动设备:维持准确时间,并用于定时唤醒以处理通知等。
  • 嵌入式和物联网设备:在网络连接不可用时提供可靠的时间源,并用于低功耗场景下的定时唤醒。

核心原理与设计

它的核心工作原理是什么?

drivers/rtc 目录下的工作原理可以分为两个层面:框架层驱动层

  • 框架层 (class.c, interface.c)

    1. 提供统一接口:它向用户空间提供了标准化的接口,主要是字符设备 /dev/rtcX 和sysfs目录 /sys/class/rtc/rtcX/
    2. 定义操作集:它定义了 struct rtc_class_ops 结构体,其中包含了一系列函数指针,如 read_time, set_time, read_alarm, set_alarm, ioctl 等。这构成了硬件驱动必须遵守的“合同”。
    3. 管理和分发:当一个硬件驱动通过 rtc_device_register() 注册自己时,框架会为其分配一个rtc编号(如rtc0),创建相应的设备节点,并保存其提供的rtc_class_ops。当用户空间对 /dev/rtc0 进行操作(如 ioctl(fd, RTC_RD_TIME, ...))时,框架会捕获该请求,并调用 rtc0 对应硬件驱动注册的 read_time 函数来完成实际的硬件操作。
  • 驱动层 (各种 rtc-*.c 文件)

    1. 硬件通信:每个驱动文件都针对一款特定的RTC芯片。它包含了与该芯片通信的逻辑,例如通过I2C总线发送命令、通过SPI读写寄存器、或直接进行内存映射I/O。
    2. 实现操作集:驱动程序会实现 rtc_class_ops 中定义的函数。例如,它的 read_time 函数会包含一系列I2C传输指令,用于从芯片中读取年、月、日、时、分、秒等寄存器的值,并将其转换为内核标准的 struct rtc_time 格式。
    3. 注册与探测:驱动通过总线驱动模型(如I2C driver的.probe函数)被内核调用。在探测到对应的硬件设备后,它会初始化硬件,填充rtc_class_ops,并调用 rtc_device_register() 将自己“挂载”到RTC Class框架上。

它的主要优势体现在哪些方面?

  • 抽象与解耦:用户空间应用程序(如 hwclock 命令)使用完全相同的接口与任何RTC硬件交互,无需关心底层硬件是I2C接口还是SPI接口,是哪家厂商的芯片。
  • 模块化与可扩展性:支持新的RTC硬件变得非常简单。开发者只需编写一个新的驱动文件来实现 rtc_class_ops,而无需修改任何核心框架或上层应用代码。
  • 标准化:提供了统一的时间数据结构 (struct rtc_time) 和标准的 ioctl 命令,简化了应用开发。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 精度和分辨率有限:硬件RTC为了追求极低的功耗,通常使用32.768 kHz的晶振。这导致其精度相对较低,每天可能会有数秒的漂移。其分辨率通常也只有1秒,不适合进行高精度的时间测量。
  • 非单调性:RTC表示的是“墙上时钟”,它可以被用户或NTP服务任意修改,甚至可以向后调整(例如,当时区或夏令时改变时)。因此,它绝不能用于测量时间间隔。
  • 接口复杂性:传统的 ioctl 接口虽然功能强大,但对于编程者来说使用起来较为繁琐。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。

  • 系统时钟初始化:这是RTC最核心的场景。系统启动时,内核会读取RTC的时间来初始化系统的“墙上时钟”(CLOCK_REALTIME)。如果没有RTC,系统时钟将从1970年1月1日开始,直到NTP等网络时间服务同步为止。
  • 计划性系统唤醒:这是RTC的另一个关键功能。rtcwake 命令行工具就是其典型用户。例如,执行 rtcwake -m freeze -s 60 会让系统进入低功耗的冻结状态,并设置RTC闹钟在60秒后唤醒系统。这被广泛用于定时执行维护任务或在物联网设备中定时采集数据。
  • 无网络环境下的时间维持:在无法接入互联网的嵌入式设备或特定场景中,RTC是维持时间准确性的唯一可靠手段。

是否有不推荐使用该技术的场景?为什么?

  • 高精度时间间隔测量:当需要测量代码执行时间或事件间隔时,应使用内核的单调时钟(CLOCK_MONOTONIC)。该时钟从系统启动开始单调递增,不受墙上时钟变化的影响,且具有更高的分辨率。
  • 高精度时间同步:对于需要亚秒级时间精度的应用(如金融交易、科学计算),RTC只能作为初始参考。必须使用NTP(网络时间协议)或PTP(精确时间协议)来持续校准系统时钟,以补偿RTC和本地晶振的漂移。

对比分析

请将其 与 其他相似技术 进行详细对比。

Linux内核中有多种时间来源,RTC是其中特殊的一种。

特性 RTC (Real Time Clock) System Wall Clock (CLOCK_REALTIME) Monotonic Clock (CLOCK_MONOTONIC) 硬件高精度定时器 (HPET, TSC)
本质 独立的、电池供电的低功耗硬件。 内核维护的软件时钟,表示“墙上时间”。 内核维护的软件时钟,表示自启动以来的流逝时间。 CPU或主板上的高频硬件计数器。
持久性 断电后保持 断电后丢失。 断电后丢失。 断电后丢失。
单调性 (可任意设置,可回退)。 (可被date命令, NTP等改变)。 (保证永不回退)。 (通常情况下)。
分辨率 低 (通常为1秒)。 高 (通常为纳秒级)。 高 (通常为纳秒级)。 非常高 (与硬件频率相关)。
主要用途 在系统断电时维持时间;为系统启动提供初始时间;定时唤醒系统。 为应用程序和文件系统提供标准的日期和时间。 测量时间间隔、设置超时。 作为内核时钟子系统的基础,驱动CLOCK_REALTIMECLOCK_MONOTONIC的运转。

drivers/rtc/dev.c

drivers/rtc/class.c

RTC 子系统核心:硬件时钟抽象和管理

本代码片段是Linux内核RTC(Real-Time Clock)子系统的核心层。其主要功能是提供一个通用的、硬件无关的框架,用于管理系统中的RTC设备。它负责处理所有RTC设备的共性任务,包括:分配和管理设备ID;通过sysfs和字符设备节点向用户空间暴露标准接口;实现可选的、将硬件时钟同步到系统时钟的功能(hctosys);以及处理系统挂起/恢复期间的时间校准。具体的RTC硬件驱动(如针对STM32H750内部RTC的驱动)会在此框架之上构建,只需实现一组定义好的硬件操作回调函数(rtc_class_ops)。

实现原理分析

该RTC核心框架是一个典型的设备类驱动模型,它将设备驱动的实现与内核的通用接口解耦。

  1. 设备类与生命周期管理:

    • rtc_init函数注册一个名为rtcstruct class。这会在/sys/class/rtc/下为所有RTC设备提供一个统一的视图。
    • rtc_allocate_device是RTC设备的“构造函数”。它负责分配rtc_device结构体,并初始化所有内部组件,如互斥锁、自旋锁、定时器队列、工作队列等。这是一个纯软件的初始化,不涉及硬件。
    • __devm_rtc_register_device是注册的核心。它接收一个已分配并由具体驱动填充了opsrtc_device,然后执行以下操作:
      a. 通过ida_alloc从一个全局ID分配器中获取一个唯一的rtcX编号。
      b. 创建一个字符设备(cdev_device_add),这使得用户空间可以通过/dev/rtcX设备节点访问该RTC。
      c. 在/proc/driver/rtc下创建诊断文件。
    • rtc_device_release是“析构函数”,在设备的最后一个引用被释放时调用。它负责清理所有定时器、取消工作队列、释放ID、销毁锁并释放内存。
    • devm_*系列函数(如devm_rtc_allocate_device)是资源管理的封装,它们将设备的分配和释放与驱动程序的生命周期(probe/remove)绑定,极大地简化了驱动的错误处理和清理逻辑。
  2. 硬件到系统时钟同步 (hctosys):

    • 这是一个可选的、在启动时执行的功能,由CONFIG_RTC_HCTOSYS_DEVICE宏控制。
    • rtc_hctosys函数会读取指定RTC硬件的时间(通过rtc_read_time,它会调用具体驱动的.read_time回调),将RTC的tm格式时间转换为Unix时间戳,然后调用do_settimeofday64来设置系统(wall clock)时间。
    • 它包含一个重要的细节:由于RTC通常只存储整数秒,它在设置系统时间时会加上半秒(NSEC_PER_SEC >> 1),作为对被截断的亚秒部分的一个最佳猜测补偿。
  3. 挂起/恢复处理:

    • rtc_suspend: 在系统进入休眠前,它会记录下当前的系统时间和RTC时间。它还实现了一个巧妙的“delta-of-deltas”算法来补偿多次休眠/唤醒循环可能引入的微小漂移。
    • rtc_resume: 在系统从休眠中唤醒后,它再次读取当前的系统时间和RTC时间。通过比较休眠前后的两个时间快照,它可以精确地计算出系统“睡眠”了多长时间(sleep_time)。然后,它调用timekeeping_inject_sleeptime64将这段丢失的时间“注入”回内核的时间保持子系统,从而校准系统时间,防止因休眠导致的时间变慢。

代码分析

rtc-core 设备分配与注册链路:rtc_allocate_device / rtc_device_get_id / devm_rtc_allocate_device / __devm_rtc_register_device / devm_rtc_device_register / rtc_init


rtc_allocate_device:分配并初始化一个 rtc_device(不注册)

作用与关键点

  • 分配 struct rtc_device,初始化其 struct device 基础字段、锁、定时器队列、hrtimer、默认特性位等。
  • set_offset_nsec 的默认值用于描述“写入 RTC 后,秒翻转相对写入时刻的偏移模型”,它影响校准/对时相关逻辑,属于不直观点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/* SPDX-License-Identifier: GPL-2.0 */
#include <linux/bcd.h>
#include <linux/export.h>

/* 省略:头文件 include 与宏定义(不影响该链路的语义分析) */

const struct class rtc_class = {
.name = "rtc",
.pm = RTC_CLASS_DEV_PM_OPS,
};

/**
* @brief 分配并初始化 rtc_device 的内存与基础成员,但不分配 id,也不注册到系统。
*
* @return 成功返回 rtc_device 指针;失败返回 NULL。
*
* @note
* - device_initialize() 只做 device 对象初始化,不会触发 release;
* - 真正“注册”发生在后续 cdev_device_add()/class 体系中;
* - set_offset_nsec 的默认值表达了“RTC 秒边界相对写入时刻”的经验模型,
* 用于减少对时/校准路径中对亚秒精度的需求。
*/
static struct rtc_device *rtc_allocate_device(void)
{
struct rtc_device *rtc;

rtc = kzalloc(sizeof(*rtc), GFP_KERNEL);
if (!rtc)
return NULL;

device_initialize(&rtc->dev);

/**
* @brief 默认写入后的秒翻转偏移模型(不直观点)。
*
* 含义:多数 RTC 的行为接近“在写入动作完成后的下一个整秒边界翻转秒计数”,
* 因此可用 NSEC_PER_SEC + 5ms 作为默认偏移估计,降低对亚秒观测的依赖。
*/
rtc->set_offset_nsec = NSEC_PER_SEC + 5 * NSEC_PER_MSEC;

rtc->irq_freq = 1;
rtc->max_user_freq = 64;
rtc->dev.class = &rtc_class;
rtc->dev.groups = rtc_get_dev_attribute_groups();
rtc->dev.release = rtc_device_release;

mutex_init(&rtc->ops_lock);
spin_lock_init(&rtc->irq_lock);
init_waitqueue_head(&rtc->irq_queue);

timerqueue_init_head(&rtc->timerqueue);
INIT_WORK(&rtc->irqwork, rtc_timer_do_work);

rtc_timer_init(&rtc->aie_timer, rtc_aie_update_irq, rtc);
rtc_timer_init(&rtc->uie_rtctimer, rtc_uie_update_irq, rtc);
hrtimer_setup(&rtc->pie_timer, rtc_pie_update_irq,
CLOCK_MONOTONIC, HRTIMER_MODE_REL);
rtc->pie_enabled = 0;

/**
* @brief 默认宣称支持的特性位(驱动注册时可能根据 ops 再修正)。
* 例如:若硬件驱动未实现 set_alarm,会在注册阶段清掉 RTC_FEATURE_ALARM。
*/
set_bit(RTC_FEATURE_ALARM, rtc->features);
set_bit(RTC_FEATURE_UPDATE_INTERRUPT, rtc->features);

return rtc;
}

rtc_device_get_id:分配 rtc 设备号(优先用设备树别名 rtcX)

作用与关键点

  • 如果设备树 /aliases 里存在 rtc0/rtc1...,优先“强制”占用该固定 id,保证用户空间设备节点编号稳定。
  • 用 IDA(DEFINE_IDA(rtc_ida))实现动态/固定范围分配,避免并发冲突与重复。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static int rtc_device_get_id(struct device *dev)
{
int of_id = -1, id = -1;

if (dev->of_node)
of_id = of_alias_get_id(dev->of_node, "rtc");
else if (dev->parent && dev->parent->of_node)
of_id = of_alias_get_id(dev->parent->of_node, "rtc");

if (of_id >= 0) {
/**
* @brief 尝试占用别名指定的固定 id。
*
* 失败常见原因:该 id 已被其它 RTC 占用。
*/
id = ida_alloc_range(&rtc_ida, of_id, of_id, GFP_KERNEL);
if (id < 0)
dev_warn(dev, "/aliases ID %d not available\n", of_id);
}

if (id < 0)
id = ida_alloc(&rtc_ida, GFP_KERNEL);

return id;
}

devm_rtc_allocate_device:分配 rtc_device 并绑定到 parent 的 devres 生命周期

作用与关键点

  • 组合调用:rtc_device_get_id()rtc_allocate_device() → 设定 parent → devm_add_action_or_reset() 注册释放动作 → dev_set_name("rtc%d")
  • devm_add_action_or_reset() 是资源托管关键:如果后续 probe 失败,会自动执行释放动作,避免泄漏/半注册。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* @brief 资源托管方式分配 rtc_device,并设置 name/parent/id。
*
* @param dev 该 RTC 的父设备(通常是具体硬件驱动的 struct device)。
*
* @return 成功返回 rtc_device 指针;失败返回 ERR_PTR。
*/
struct rtc_device *devm_rtc_allocate_device(struct device *dev)
{
struct rtc_device *rtc;
int id, err;

id = rtc_device_get_id(dev);
if (id < 0)
return ERR_PTR(id);

rtc = rtc_allocate_device();
if (!rtc) {
ida_free(&rtc_ida, id);
return ERR_PTR(-ENOMEM);
}

rtc->id = id;
rtc->dev.parent = dev;

/**
* @brief 将 put_device 行为挂到 devres:probe 失败或卸载时自动调用。
* 这保证了 rtc->dev 的引用计数正确归零并触发 rtc_device_release。
*/
err = devm_add_action_or_reset(dev, devm_rtc_release_device, rtc);
if (err)
return ERR_PTR(err);

err = dev_set_name(&rtc->dev, "rtc%d", id);
if (err)
return ERR_PTR(err);

return rtc;
}
EXPORT_SYMBOL_GPL(devm_rtc_allocate_device);

__devm_rtc_register_device:注册字符设备 /proc 并根据 ops 修正特性位

作用与关键点

  • 检查 rtc->ops 合法性,并根据 ops 能力位修正 features(例如没有 set_alarm 就清掉 ALARM)。
  • 可能会读取硬件中的 alarm 并初始化(__rtc_read_alarm() + rtc_initialize_alarm()),避免内核与硬件状态不一致。
  • cdev_device_add() 失败并不算致命:会置 RTC_NO_CDEV,系统仍可通过 sysfs/其他路径使用部分功能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/**
* @brief 资源托管方式完成 rtc_device 的注册:char dev、/proc、初始化闹钟、并挂载自动反注册动作。
*
* @param owner 模块所有者。
* @param rtc 已分配并设置好 ops/parent/name 的 rtc_device。
*
* @return 0 成功;负 errno 失败。
*/
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;
}

/** 若硬件驱动不支持 set_alarm,则从 features 中移除闹钟特性。 */
if (!rtc->ops->set_alarm)
clear_bit(RTC_FEATURE_ALARM, rtc->features);

/** 支持 set_offset 则宣称支持校正特性。 */
if (rtc->ops->set_offset)
set_bit(RTC_FEATURE_CORRECTION, rtc->features);

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

/**
* @brief 尝试从硬件读取已存在的闹钟并做一致性初始化。
* 这用于处理“硬件已设闹钟,但内核刚启动/刚注册”的场景。
*/
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

/**
* @brief 将“反注册动作”挂到 devres:驱动卸载或 probe 失败时自动撤销 /proc/cdev/ops。
*/
return devm_add_action_or_reset(rtc->dev.parent,
devm_rtc_unregister_device, rtc);
}
EXPORT_SYMBOL_GPL(__devm_rtc_register_device);

devm_rtc_device_register:旧接口封装(已弃用)

作用与关键点

  • 这是历史接口:内部调用 devm_rtc_allocate_device() + 直接设置 rtc->ops + __devm_rtc_register_device()
  • 新推荐路径是:allocate → 自己填更多字段(range/offset 等)→ register。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* @brief 资源托管方式注册 RTC(旧接口封装)。
*
* @note 已标记为 deprecated,推荐新接口 devm_rtc_allocate_device + rtc_register_device。
*/
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_init:注册 rtc 类并初始化 rtc_dev 子模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @brief RTC 子系统初始化入口:注册 rtc_class,并初始化 rtc_dev(字符设备等子模块)。
*/
static int __init rtc_init(void)
{
int err;

err = class_register(&rtc_class);
if (err)
return err;

rtc_dev_init();

return 0;
}
subsys_initcall(rtc_init);

drivers/rtc/interface.c

RTC 工具函数集 (rtc_timer_init, rtc_tm_to_ktime, rtc_bound_alarmtime)

本代码片段包含一组 RTC(实时时钟)子系统的核心工具函数。它们提供了基础的数据结构初始化、时间格式转换以及硬件能力适配等功能。rtc_timer_init 用于初始化一个 rtc_timer 结构体,rtc_tm_to_ktime 负责将 RTC 硬件常用的日历时间格式转换为内核统一的 ktime_t 时间格式,而 rtc_bound_alarmtime 则用于将一个通用的闹钟请求时间调整到特定 RTC 硬件所能支持的最大范围内。

实现原理分析

  1. 时间基准的转换 (rtc_tm_to_time64): 该函数的核心是 mktime64,它实现了一个关键的算法:将格里高利历(公历)日期(年月日时分秒)转换为自 Unix 纪元(1970年1月1日 00:00:00 UTC)以来所经过的线性秒数。这个转换涉及到对年月日进行加权计算,并精确处理闰年的情况。函数在调用 mktime64 之前,对 tm->tm_year 加 1900 和 tm->tm_mon 加 1 进行了调整,这是因为 struct rtc_time 遵循 C 标准库 struct tm 的约定,即年份是从1900年起的偏移量,月份是 0-11。

  2. 硬件能力适配技巧 (rtc_bound_alarmtime): 此函数是一个典型的硬件抽象层适配技巧。它允许上层软件(如 alarmtimer_suspend)以统一的方式请求一个任意时长的闹钟,而无需关心底层硬件的具体限制。RTC 驱动在初始化时,可以根据其硬件手册,在 rtc->alarm_offset_max 字段中声明其能支持的最大闹钟偏移时长(以毫秒为单位)。rtc_bound_alarmtime 在设置闹钟前,会检查请求的时长是否超出了这个硬件限制。如果超出,它不会返回错误,而是“优雅降级”,将闹钟时长缩减到硬件所能支持的最大值。这种设计提高了系统的健壮性,确保即使在请求超出硬件能力时,系统仍能尽力设置一个最早可能被唤醒的闹钟。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// rtc_timer_init: 初始化一个 rtc_timer 结构体。
// @timer: 指向需要被初始化的 rtc_timer 结构体的指针。
// @f: 一个函数指针,当定时器触发时,该函数将被调用。其参数为触发该定时器的 rtc_device 指针。
// @rtc: 指向与此定时器关联的 rtc_device 结构体的指针。
void rtc_timer_init(struct rtc_timer *timer, void (*f)(struct rtc_device *r),
struct rtc_device *rtc)
{
// 初始化 timer 中的 timerqueue_node 成员,使其可以被添加到定时器队列中。
timerqueue_init(&timer->node);
// 将定时器的使能状态标志初始化为0(禁用)。
timer->enabled = 0;
// 将定时器触发时要执行的回调函数指针指向传入的函数 f。
timer->func = f;
// 将定时器关联的 rtc_device 指针指向传入的设备 rtc。
timer->rtc = rtc;
}

// rtc_tm_to_time64: 将 rtc_time 结构体表示的日历时间转换为 time64_t 类型的秒数。
// 该函数将公历日期转换为自1970年1月1日00:00:00 UTC以来的总秒数。
// @tm: 指向包含年月日时分秒信息的 rtc_time 结构体的指针。
// 返回值: time64_t 类型的秒数。
time64_t rtc_tm_to_time64(struct rtc_time *tm)
{
// 调用 mktime64 函数执行转换。
// tm->tm_year 是从1900年起的年数,故需加上1900。
// tm->tm_mon 是0-11的月份,故需加1以符合1-12的常规表示。
return mktime64(((unsigned int)tm->tm_year + 1900), tm->tm_mon + 1,
tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec);
}
// 将 rtc_tm_to_time64 函数导出,使其可以被其他内核模块调用。
EXPORT_SYMBOL(rtc_tm_to_time64);

// rtc_tm_to_ktime: 将 rtc_time 结构体表示的日历时间转换为内核标准的 ktime_t 时间格式。
// @tm: 包含年月日时分秒信息的 rtc_time 结构体(值传递)。
// 返回值: ktime_t 类型的时间值。
ktime_t rtc_tm_to_ktime(struct rtc_time tm)
{
// 首先调用 rtc_tm_to_time64 将日历时间转换为秒数,
// 然后使用 ktime_set 将秒数和0纳秒结合,创建一个 ktime_t 值。
return ktime_set(rtc_tm_to_time64(&tm), 0);
}
// 将 rtc_tm_to_ktime 函数导出,使其可以被其他内核模块调用。
EXPORT_SYMBOL_GPL(rtc_tm_to_ktime);

// rtc_bound_alarmtime: 根据RTC硬件的能力限制,调整并返回一个有效的闹钟时长。
// @rtc: 指向 rtc_device 结构体的指针。
// @requested: 上层软件请求的闹钟超时时长(ktime_t 格式)。
// 返回值: 一个不超过RTC硬件最大支持时长的 ktime_t 值。
static inline ktime_t rtc_bound_alarmtime(struct rtc_device *rtc,
ktime_t requested)
{
// 检查RTC驱动是否设置了最大闹钟偏移时长(alarm_offset_max,单位为毫秒)。
// 并且,检查请求的时长(转换为毫秒)是否大于硬件支持的最大时长。
if (rtc->alarm_offset_max &&
rtc->alarm_offset_max * MSEC_PER_SEC < ktime_to_ms(requested))
// 如果请求的时长超出了硬件限制,则返回硬件所能支持的最大时长。
return ms_to_ktime(rtc->alarm_offset_max * MSEC_PER_SEC);

// 如果未超出硬件限制,或者硬件没有设置限制,则直接返回原始请求的时长。
return requested;
}

RTC 定时器启动与入队 (rtc_timer_start, rtc_timer_enqueue)

本代码片段展示了 Linux 内核中 RTC(实时时钟)子系统用于启动和管理硬件定时器的核心逻辑。rtc_timer_start 提供了一个高层次的接口用于设置一个基于 RTC 的定时器,而 rtc_timer_enqueue 则是底层的实现函数,负责将定时器高效地加入管理队列,并根据需要对 RTC 硬件闹钟进行编程。

实现原理分析

此实现中包含了若干关键的技术和算法,以确保 RTC 定时器的高效、精确和健壮。

  1. 高效的定时器管理 (timerqueue): rtc_timer_enqueue 函数使用 timerqueue 数据结构来管理所有活动的 RTC 定时器。timerqueue 内部是基于红黑树实现的,这使得添加(timerqueue_add)和删除定时器的操作时间复杂度为 O(log N),而获取下一个即将到期的定时器(timerqueue_getnext)的操作时间复杂度为 O(1)。对于可能需要管理大量定时器的系统,这是一个至关重要的高效设计。

  2. 最小化硬件交互的优化: 在 rtc_timer_enqueue 函数中,if (!next || ktime_before(timer->node.expires, next->expires)) 语句是一个核心优化。它首先检查在添加新定时器 之前,队列中最早的定时器是哪一个(next)。只有当新加入的定时器比原来最早的定时器还要早到期时,它才会去调用 __rtc_set_alarm 来重新编程硬件。这种策略极大地减少了对 RTC 硬件寄存器的写操作次数,因为只有在更新“全局最早到期时间”时才需要与硬件交互,从而降低了系统开销和功耗。

  3. 过期闹钟的健壮处理 (-ETIME Erorr Handling): 当 __rtc_set_alarm 返回 -ETIME 错误时,意味着尝试设置的闹钟时间点已经过去。此时,代码并没有简单地返回失败,而是采取了一套恢复机制:

    • pm_stay_awake(): 调用此函数来创建一个唤醒锁,暂时阻止系统进入睡眠。
    • schedule_work(&rtc->irqwork): 将 RTC 的中断处理工作调度到一个工作队列(workqueue)中立即执行。
    • 这套机制确保了即使由于调度延迟等原因导致闹钟设置时间点已过,该闹钟事件也不会被丢失,而是会立即通过软件方式进行模拟触发和处理,保证了定时任务的可靠执行。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
// rtc_timer_start: 设置一个RTC定时器,使其在未来的某个时间点触发。
// @rtc: 指向要使用的 rtc_device 结构体的指针。
// @timer: 指向需要被设置的 rtc_timer 结构体的指针。
// @expires: 定时器到期的绝对时间(ktime_t 格式)。
// @period: 定时器循环触发的周期(ktime_t 格式),如果为0则不循环。
// 返回值: 成功则返回0,失败则返回负数错误码。
int rtc_timer_start(struct rtc_device *rtc, struct rtc_timer *timer,
ktime_t expires, ktime_t period)
{
// 初始化返回值为0。
int ret = 0;

// 获取与RTC设备操作相关的互斥锁,以保护对定时器队列的访问。
mutex_lock(&rtc->ops_lock);
// 检查此定时器是否已经被启用。
if (timer->enabled)
// 如果已启用,则先将其从队列中移除。
rtc_timer_remove(rtc, timer);

// 设置定时器的到期时间。
timer->node.expires = expires;
// 设置定时器的循环周期。
timer->period = period;

// 调用内部函数,将配置好的定时器加入队列并设置硬件闹钟。
ret = rtc_timer_enqueue(rtc, timer);

// 释放互斥锁。
mutex_unlock(&rtc->ops_lock);
// 返回操作结果。
return ret;
}

// rtc_timer_enqueue: 将一个RTC定时器添加到设备的定时器队列中。
// @rtc: 指向 rtc_device 结构体的指针。
// @timer: 指向需要被添加的 rtc_timer 结构体的指针。
// 该函数在执行期间必须持有 rtc->ops_lock 锁。
// 返回值: 成功则返回0,失败则返回负数错误码。
static int rtc_timer_enqueue(struct rtc_device *rtc, struct rtc_timer *timer)
{
// 获取当前定时器队列中下一个(即最早)将到期的节点。
struct timerqueue_node *next = timerqueue_getnext(&rtc->timerqueue);
// 声明一个用于存储RTC硬件时间的 rtc_time 结构体。
struct rtc_time tm;
// 声明一个用于存储当前时间的 ktime_t 变量。
ktime_t now;
// 声明一个用于保存错误码的整型变量。
int err;

// 读取当前RTC硬件的精确时间。
err = __rtc_read_time(rtc, &tm);
// 如果读取失败,则直接返回错误码。
if (err)
return err;

// 将定时器的使能状态标志设为1。
timer->enabled = 1;
// 将读取到的RTC时间转换为内核标准的 ktime_t 格式。
now = rtc_tm_to_ktime(tm);

// 遍历并跳过队列中所有已经到期的定时器节点。
while (next) {
if (next->expires >= now)
break;
next = timerqueue_iterate_next(next);
}

// 将新的定时器节点添加到红黑树实现的定时器队列中。
timerqueue_add(&rtc->timerqueue, &timer->node);
// 记录定时器入队的追踪事件。
trace_rtc_timer_enqueue(timer);
// 检查新加入的定时器是否成为了队列中最早到期的那一个。
// (条件1: !next, 队列原为空;条件2: 新定时器的到期时间早于原队列中最早的那个)
if (!next || ktime_before(timer->node.expires, next->expires)) {
// 如果是,则需要重新设置RTC硬件闹钟。
struct rtc_wkalrm alarm;

// 将新定时器的ktime_t到期时间转换为RTC硬件所需的rtc_time格式。
alarm.time = rtc_ktime_to_tm(timer->node.expires);
// 使能闹钟。
alarm.enabled = 1;
// 调用底层函数,将闹钟时间编程到RTC硬件中。
err = __rtc_set_alarm(rtc, &alarm);
// 如果设置闹钟返回-ETIME,表示目标时间已经过去。
if (err == -ETIME) {
// 创建一个唤醒锁,暂时阻止系统进入睡眠状态。
pm_stay_awake(rtc->dev.parent);
// 调度RTC的中断处理工作队列,以软件方式立即处理这个“已到期”的闹钟。
schedule_work(&rtc->irqwork);
} else if (err) { // 如果是其他类型的错误。
// 从定时器队列中移除刚刚添加的节点,进行回滚。
timerqueue_del(&rtc->timerqueue, &timer->node);
// 记录定时器出队的追踪事件。
trace_rtc_timer_dequeue(timer);
// 将定时器的使能状态重新设为0。
timer->enabled = 0;
// 返回设置硬件闹钟时遇到的错误。
return err;
}
}
// 如果一切顺利,返回0表示成功。
return 0;
}

RTC 硬件闹钟设置与时间偏移校正 (__rtc_set_alarm, rtc_subtract_offset)

本代码片段揭示了 Linux 内核向 RTC(实时时钟)硬件编程闹钟的核心底层逻辑。__rtc_set_alarm 函数负责执行设置闹钟的完整流程,包括时间校验、防止设置过去时间、以及通过驱动操作硬件。rtc_subtract_offset 则是一个关键的辅助函数,用于处理那些时间表示范围有限的 RTC 硬件,通过软件偏移量实现对整个 time64_t 时间范围的“虚拟化”支持。

实现原理分析

此代码段包含了确保闹钟设置既健壮又兼容多种硬件的关键技术。

  1. 过去时间闹钟的预防与竞态窗口: __rtc_set_alarm 的一个核心设计原则是严禁设置一个已经过去的时间点作为闹钟。它通过以下步骤实现:
    a. 读取当前 RTC 的精确时间 now
    b. 将请求的闹钟时间 schedulednow 比较。
    c. 如果 scheduled <= now,则立即返回 -ETIME 错误。
    然而,代码注释(XXX - ... race window ...)明确指出了此机制的一个固有缺陷:在软件检查通过(scheduled > now)到真正通过 rtc->ops->set_alarm 写入硬件寄存器的这段极短时间内,真实时间可能恰好跨过下一秒的边界,使得 scheduled 变为过去时间。这是一个典型的竞态条件,虽然发生概率很低,但它揭示了在非原子操作中检查时间的理论局限性。

  2. 硬件时间范围的虚拟化 (rtc_subtract_offset): 许多(尤其是旧的)RTC 硬件芯片无法表示完整的 Unix 时间戳范围(例如,年份只用两位数表示)。rtc_subtract_offset 和与之配对的 rtc_add_offset 提供了一套精巧的软件解决方案来克服此限制:

    • 问题: 硬件只能表示例如从 1980 年到 2079 年的时间。
    • 解决方案: RTC 驱动在初始化时,可以声明其硬件的有效时间范围 [range_min, range_max],并计算出一个大的软件偏移量 offset_secs(例如,1980-01-01 相比 1970-01-01 的秒数)。
    • 写操作: 当 __rtc_set_alarm 准备向硬件写入一个时间 tm 时,它会调用 rtc_subtract_offset。该函数首先检查 tm 对应的秒数是否已经落在硬件的 [range_min, range_max] 范围内。如果是,则无需任何操作(这是一个优化)。如果超出了范围(例如,请求设置 2090 年的闹钟),函数就会从该时间的秒数中减去 offset_secs,将一个“绝对时间”转换为一个硬件可以理解的“相对时间”,然后再写入硬件。
    • 读操作: 反之,当从硬件读出时间时,rtc_add_offset 会被调用,将 offset_secs 加回去,从而将硬件的“相对时间”还原为内核使用的“绝对时间”。
      这套机制对上层是透明的,使其能够无差别地处理所有 RTC 硬件,无论其物理时间范围有多大。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// __rtc_set_alarm: 一个内部辅助函数,用于向RTC硬件设置一个唤醒闹钟。
// @rtc: 指向 rtc_device 结构体的指针。
// @alarm: 指向 rtc_wkalrm 结构体的指针,包含了要设置的闹钟时间和使能状态。
// 返回值: 成功则返回0,失败则返回负数错误码。
static int __rtc_set_alarm(struct rtc_device *rtc, struct rtc_wkalrm *alarm)
{
struct rtc_time tm; // 用于存储当前RTC时间的临时变量。
time64_t now, scheduled; // 用于存储当前时间和计划闹钟时间的64位秒数。
int err; // 用于存储错误码。

// 首先校验要设置的闹钟时间(alarm->time)是否是一个有效的日历时间。
err = rtc_valid_tm(&alarm->time);
if (err)
return err;

// 将要设置的闹钟时间转换为自Unix纪元以来的总秒数。
scheduled = rtc_tm_to_time64(&alarm->time);

// 关键检查:确保我们不会设置一个已经过去时间的闹钟。
// 读取当前RTC硬件的精确时间。
err = __rtc_read_time(rtc, &tm);
if (err)
return err;
// 将当前硬件时间也转换为总秒数。
now = rtc_tm_to_time64(&tm);

// 如果计划的闹钟时间早于或等于当前时间,则返回-ETIME错误。
if (scheduled <= now)
return -ETIME;
/*
* XXX - 这里存在一个竞态条件窗口:我们刚刚检查了闹钟时间不在过去,
* 但是,如果闹钟被设置为下一秒触发,而系统恰好在此时(在设置硬件之前)
* 发生了秒的翻转,那么设置的仍然会是一个过去的时间。
*/

// 如果RTC硬件的时间范围有限,则对闹钟时间应用一个软件偏移量进行校正。
rtc_subtract_offset(rtc, &alarm->time);

// 检查RTC设备的操作函数集是否存在。
if (!rtc->ops)
err = -ENODEV;
// 检查RTC设备的能力位图,确认其是否声明支持闹钟功能。
else if (!test_bit(RTC_FEATURE_ALARM, rtc->features))
err = -EINVAL;
else
// 调用底层硬件驱动提供的 set_alarm 函数来实际编程硬件闹钟。
err = rtc->ops->set_alarm(rtc->dev.parent, alarm);

// 记录设置闹钟的追踪事件,参数为闹钟时间的秒数和操作结果。
trace_rtc_set_alarm(rtc_tm_to_time64(&alarm->time), err);
// 返回最终的操作结果。
return err;
}

// rtc_subtract_offset: 在设置RTC时间之前,减去一个软件维护的偏移量。
// 这是为了兼容那些硬件时间表示范围有限的RTC芯片。
// @rtc: 指向 rtc_device 结构体的指针。
// @tm: 指向 rtc_time 结构体的指针,其时间值将被(可能)修改。
static void rtc_subtract_offset(struct rtc_device *rtc, struct rtc_time *tm)
{
time64_t secs; // 用于存储时间的64位秒数。

// 如果没有配置偏移量,则直接返回,不执行任何操作。
if (!rtc->offset_secs)
return;

// 将待处理的 rtc_time 转换为总秒数。
secs = rtc_tm_to_time64(tm);

/*
* 如果要设置的时间值已经落在RTC硬件的有效范围内,
* 那么在设置时间到RTC设备时就不需要减去偏移量。
* 否则,我们需要减去偏移量,以使时间值对于RTC硬件设备是有效的。
*/
if (secs >= rtc->range_min && secs <= rtc->range_max)
return;

// 如果时间超出了硬件的直接表示范围,则减去偏移量,
// 然后将新的秒数转换回 rtc_time 格式,更新传入的 tm 结构体。
rtc_time64_to_tm(secs - rtc->offset_secs, tm);
}

RTC 时间与闹钟的读取 (__rtc_read_time, rtc_read_alarm)

本代码片段包含两个 RTC(实时时钟)子系统的核心读取函数。__rtc_read_time 是一个内部函数,负责通过底层驱动从 RTC 硬件中读取当前的日历时间,并进行有效性校验。rtc_read_alarm 则是一个导出的外部接口,用于从 RTC 子系统的软件状态中查询当前设置的硬件闹钟信息。

实现原理分析

这两个函数展示了 Linux 设备驱动模型中的关键设计原则:硬件抽象和软件状态缓存。

  1. 驱动模型与硬件抽象 (__rtc_read_time): __rtc_read_time 的核心是通过 rtc->ops->read_time 这个函数指针来调用具体硬件驱动提供的功能。rtc->ops 指向一个 rtc_class_ops 结构体,其中包含了所有标准 RTC 操作的函数指针。这种设计将通用的 RTC 核心逻辑与特定硬件的实现完全解耦。RTC 核心不需要知道如何与某个具体的 RTC 芯片通信,它只需要调用这个标准的 read_time 接口。此外,函数在读取硬件数据后,会调用 rtc_valid_tm 进行严格的有效性检查(例如,确保月份在 0-11 之间,小时在 0-23 之间等),这增强了系统的健壮性,防止无效的硬件数据污染整个内核时间系统。

  2. 软件状态缓存与性能优化 (rtc_read_alarm): 与 __rtc_read_time 不同,rtc_read_alarm 并不直接调用硬件驱动的 read_alarm 函数来读取硬件寄存器。相反,它直接从 rtc->aie_timer 这个软件结构体中读取闹钟信息。aie_timer (Alarm Interrupt Enable timer) 是 RTC 核心层维护的一个软件副本,它代表了当前被编程到硬件中的那个闹钟。这种设计的原理是状态缓存:硬件只在必要时(即当一个新设置的闹钟比所有现有闹钟都更早到期时)才被更新。当外部需要查询闹钟时,直接读取这个软件缓存即可,避免了昂贵的、可能需要加锁的硬件I/O操作。这是一种重要的性能优化策略。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// __rtc_read_time: 一个内部辅助函数,用于从RTC硬件读取当前时间。
// @rtc: 指向 rtc_device 结构体的指针。
// @tm: 指向用于存储读取结果的 rtc_time 结构体的指针。
// 返回值: 成功则返回0,失败则返回负数错误码。
static int __rtc_read_time(struct rtc_device *rtc, struct rtc_time *tm)
{
int err; // 声明一个用于存储错误码的整型变量。

// 检查RTC设备的操作函数集是否存在。
if (!rtc->ops) {
err = -ENODEV; // 如果不存在,则表示设备或驱动未正确初始化。
} else if (!rtc->ops->read_time) { // 检查具体的read_time函数指针是否存在。
err = -EINVAL; // 如果不存在,则表示该驱动不支持读取时间操作。
} else {
// 将目标结构体清零,以确保没有残留数据。
memset(tm, 0, sizeof(struct rtc_time));
// 调用底层硬件驱动提供的 read_time 函数来实际读取硬件时间。
err = rtc->ops->read_time(rtc->dev.parent, tm);
// 检查驱动函数的返回值。
if (err < 0) {
// 如果读取失败,记录一条调试信息。
dev_dbg(&rtc->dev, "read_time: fail to read: %d\n",
err);
// 返回驱动报告的错误码。
return err;
}

// 对从硬件读取的时间应用一个软件偏移量(如果已配置)。
rtc_add_offset(rtc, tm);

// 校验读取并调整后的时间是否有效(例如,小时数是否在0-23之间)。
err = rtc_valid_tm(tm);
if (err < 0)
// 如果时间无效,记录一条调试信息。
dev_dbg(&rtc->dev, "read_time: rtc_time isn't valid\n");
}
// 返回最终的操作结果。
return err;
}

// rtc_read_alarm: 读取当前设置的RTC硬件闹钟信息。
// @rtc: 指向 rtc_device 结构体的指针。
// @alarm: 指向用于存储闹钟信息的 rtc_wkalrm 结构体的指针。
// 返回值: 成功则返回0,失败则返回负数错误码。
int rtc_read_alarm(struct rtc_device *rtc, struct rtc_wkalrm *alarm)
{
int err; // 声明一个用于存储错误码的整型变量。

// 以可被信号中断的方式获取与RTC设备操作相关的互斥锁。
err = mutex_lock_interruptible(&rtc->ops_lock);
// 如果获取锁时被中断,则直接返回。
if (err)
return err;
// 检查RTC设备的操作函数集是否存在。
if (!rtc->ops) {
err = -ENODEV; // 如果不存在,则表示设备或驱动未正确初始化。
// 检查RTC设备的能力位图,确认其是否支持闹钟功能。
} else if (!test_bit(RTC_FEATURE_ALARM, rtc->features)) {
err = -EINVAL; // 如果不支持,则返回无效参数错误。
} else {
// 将目标结构体清零。
memset(alarm, 0, sizeof(struct rtc_wkalrm));
// 从RTC核心维护的软件状态(aie_timer)中读取闹钟的使能状态。
alarm->enabled = rtc->aie_timer.enabled;
// 从软件状态中读取闹钟的到期时间,并将其从ktime_t格式转换为rtc_time格式。
alarm->time = rtc_ktime_to_tm(rtc->aie_timer.node.expires);
}
// 释放互斥锁。
mutex_unlock(&rtc->ops_lock);

// 记录读取闹钟的追踪事件,参数为闹钟时间的秒数和操作结果。
trace_rtc_read_alarm(rtc_tm_to_time64(&alarm->time), err);
// 返回操作结果。
return err;
}
// 将 rtc_read_alarm 函数导出,使其可以被其他内核模块调用。
EXPORT_SYMBOL_GPL(rtc_read_alarm);

RTC 定时器取消与硬件闹钟禁用 (rtc_timer_cancel, rtc_timer_remove)

本代码片段提供了 Linux 内核中用于安全地取消和移除 RTC(实时时钟)定时器的核心功能。rtc_timer_cancel 是一个高层次的外部接口,它通过加锁来保证操作的原子性。其核心逻辑由内部函数 rtc_timer_remove 实现,该函数不仅将指定的定时器从软件管理队列中移除,还智能地判断是否需要重新编程或禁用硬件闹钟,以确保系统状态的正确性。

实现原理分析

此代码段最关键的设计思想在于最小化硬件交互的优化策略,这对于降低系统功耗和开销至关重要。

  1. 条件性硬件重编程 (rtc_timer_remove): rtc_timer_remove 的核心在于 if (next == &timer->node) 这个判断。在移除一个定时器之前,它首先通过 timerqueue_getnext 获取了当前队列中最早到期的定时器节点 next。如果被移除的定时器 timer 正是这个 next 节点,这意味着被取消的正是当前已编程到硬件中的那个闹钟。只有在这种情况下,代码才会继续执行后续的硬件操作:

    • 查找下一个替代者: 它会再次调用 timerqueue_getnext 来找出队列中新的、最早的定时器。
    • 更新硬件: 如果找到了一个新的最早定时器,它就会调用 __rtc_set_alarm 将这个新的时间点编程到硬件中。
    • 禁用硬件: 如果移除后队列变空(!next),则调用 rtc_alarm_disable 彻底禁用硬件闹钟中断。
      如果被移除的定时器不是队列中最早的那个,那么硬件中编程的闹钟(即原先的 next)仍然是正确的,因此无需任何硬件操作,函数直接返回。这个策略避免了每次取消定时器都去访问硬件,极大地提升了效率。
  2. 状态一致性管理: timer->enabled 标志位虽然简单,但对于维护状态一致性至关重要。rtc_timer_cancel 在加锁后首先检查此标志,避免了对一个已经被取消的定时器执行重复的移除操作,防止了可能导致的队列损坏或内核错误。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// rtc_timer_cancel: 一个供外部调用的接口,用于停止一个正在运行的RTC定时器。
// @rtc: 指向要使用的 rtc_device 结构体的指针。
// @timer: 指向需要被取消的 rtc_timer 结构体的指针。
void rtc_timer_cancel(struct rtc_device *rtc, struct rtc_timer *timer)
{
// 获取与RTC设备操作相关的互斥锁,以确保对定时器队列的独占访问。
mutex_lock(&rtc->ops_lock);
// 检查此定时器当前是否处于启用状态。
if (timer->enabled)
// 如果已启用,则调用内部函数将其从队列中移除并处理硬件状态。
rtc_timer_remove(rtc, timer);
// 释放互斥锁。
mutex_unlock(&rtc->ops_lock);
}

// rtc_timer_remove: 从设备的定时器队列中移除一个RTC定时器。
// @rtc: 指向 rtc_device 结构体的指针。
// @timer: 指向需要被移除的 rtc_timer 结构体的指针。
// 该函数在执行期间必须持有 rtc->ops_lock 锁。
static void rtc_timer_remove(struct rtc_device *rtc, struct rtc_timer *timer)
{
// 获取当前定时器队列中下一个(即最早)将到期的节点。
struct timerqueue_node *next = timerqueue_getnext(&rtc->timerqueue);

// 从红黑树实现的定时器队列中删除指定的定时器节点。
timerqueue_del(&rtc->timerqueue, &timer->node);
// 记录定时器出队的追踪事件。
trace_rtc_timer_dequeue(timer);
// 将定时器的使能状态标志清零。
timer->enabled = 0;
// 关键优化:检查被移除的定时器是否就是刚才队列中最早的那一个。
if (next == &timer->node) {
// 如果是,则意味着硬件闹钟需要被重新编程或禁用。
struct rtc_wkalrm alarm;
int err;

// 再次获取移除后队列中新的最早到期节点。
next = timerqueue_getnext(&rtc->timerqueue);
// 如果移除后队列为空。
if (!next) {
// 调用函数禁用RTC硬件闹钟。
rtc_alarm_disable(rtc);
return;
}
// 如果队列不为空,则用新的最早到期时间来设置硬件闹钟。
// 将新的最早到期时间从ktime_t格式转换为rtc_time格式。
alarm.time = rtc_ktime_to_tm(next->expires);
// 使能闹钟。
alarm.enabled = 1;
// 调用底层函数,将新的闹钟时间编程到RTC硬件中。
err = __rtc_set_alarm(rtc, &alarm);
// 如果设置闹钟返回-ETIME,表示目标时间已经过去。
if (err == -ETIME) {
// 创建一个唤醒锁,暂时阻止系统进入睡眠。
pm_stay_awake(rtc->dev.parent);
// 调度RTC的中断处理工作队列,以软件方式立即处理这个“已到期”的闹钟。
schedule_work(&rtc->irqwork);
}
}
}

// rtc_alarm_disable: 禁用RTC设备的硬件闹钟中断。
// @rtc: 指向 rtc_device 结构体的指针。
static void rtc_alarm_disable(struct rtc_device *rtc)
{
// 检查RTC设备的操作函数集、闹钟功能支持位以及具体的alarm_irq_enable函数指针是否存在。
if (!rtc->ops || !test_bit(RTC_FEATURE_ALARM, rtc->features) || !rtc->ops->alarm_irq_enable)
// 如果有任何一个不存在,则直接返回,不做任何操作。
return;

// 调用底层硬件驱动提供的 alarm_irq_enable 函数,传入 false 以禁用中断。
rtc->ops->alarm_irq_enable(rtc->dev.parent, false);
// 记录禁用闹钟中断的追踪事件。
trace_rtc_alarm_irq_enable(0, 0);
}

rtc_update_irq

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* rtc_update_irq - Triggered when a RTC interrupt occurs.
* @rtc: the rtc device
* @num: how many irqs are being reported (usually one)
* @events: mask of RTC_IRQF with one or more of RTC_PF, RTC_AF, RTC_UF
* Context: any
*/
void rtc_update_irq(struct rtc_device *rtc,
unsigned long num, unsigned long events)
{
if (IS_ERR_OR_NULL(rtc))
return;

pm_stay_awake(rtc->dev.parent);
schedule_work(&rtc->irqwork);
}
EXPORT_SYMBOL_GPL(rtc_update_irq);

drivers/rtc/lib.c RTC 日期与时间转换及校验工具集 (rtc_time64_to_tm, rtc_valid_tm)

本代码片段是 Linux 内核 RTC 子系统的核心工具库,提供了一系列不依赖于具体硬件的、纯粹的日期和时间计算函数。其主要功能包括:在内核标准的 time64_t(自 Unix 纪元以来的秒数)和 RTC 硬件常用的 struct rtc_time(年月日时分秒的日历格式)之间进行精确转换;校验 struct rtc_time 的有效性;以及提供辅助计算(如计算某月天数)。

实现原理分析

此代码段的核心在于其高效且精确的日历算法,特别是从线性秒数到公历日期的转换。

  1. Neri and Schneider 历法转换算法 (rtc_time64_to_tm): 此函数没有采用传统的循环或试错法来确定年月日,而是实现了一个基于欧几里得仿射函数的精密算法。

    • 计算历法: 算法的核心思想是使用一个“计算历法”,其中年份从3月1日开始。这样做在数学上非常便利,因为闰年的额外一天(2月29日)被置于一年的末尾,从而使得在计算一年中的某一天时,无需预先判断当年是否为闰年。
    • 时间基准偏移: 算法首先将输入的 Unix 时间戳(从1970-01-01起)加上一个 719468 * 86400 秒的偏移量。719468 是从 0000-03-01 到 1970-01-01 的天数,这个操作将时间基准转换到了“计算历法”的起点。
    • 纯算术分解: 接下来,算法通过一系列精心选择的整数乘法、除法和取模运算,直接从总天数中解析出世纪、世纪中的年份、年内日、月和日。例如,146097 是一个400年周期(公历的完整闰年周期)的总天数 (97*366 + 303*365)。2939745ULL2141 等是根据月份长度分布推导出的“魔术数字”,用于通过乘法和移位来高效地计算出月份和日期。
    • 结果转换: 在计算历法下得到年月日后,算法最后通过简单的加减法将其转换回标准的公历表示。
  2. 查找表优化: rtc_month_daysrtc_year_days 函数使用了静态常量数组(rtc_days_in_monthrtc_ydays)作为查找表。这避免了复杂的 if-elseswitch 分支,使得计算某月天数或某日在一年中的序数成为一次高效的数组索引操作。

  3. 纳秒向上取整 (rtc_ktime_to_tm): 在将 ktime_t 转换为 rtc_time 时,代码中存在 if (ts.tv_nsec) ts.tv_sec++; 的逻辑。这是一个重要的取整策略:只要时间戳中包含任何非零的纳秒部分,就将其归入下一秒。这对于闹钟设置至关重要,它确保了闹钟的触发时间点不会早于请求的时间点,保证了行为的正确性。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
// rtc_days_in_month: 一个静态常量数组,存储非闰年情况下每个月的天数(月份从0开始索引)。
static const unsigned char rtc_days_in_month[] = {
31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
};

// rtc_ydays: 一个二维静态常量数组,用作查找表,存储每年中每个月第一天之前累计的总天数。
// 第一行用于非闰年,第二行用于闰年。
static const unsigned short rtc_ydays[2][13] = {
/* 普通年份 */
{ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 },
/* 闰年 */
{ 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366 }
};

static inline bool is_leap_year(unsigned int year)
{
return (!(year % 4) && (year % 100)) || !(year % 400);
}

// rtc_month_days: 计算给定年份和月份的总天数。
// @month: 月份 (0-11)。
// @year: 完整的年份 (例如 2024)。
// 返回值: 该月的总天数。
int rtc_month_days(unsigned int month, unsigned int year)
{
// 从查找表中获取该月的基本天数,并判断:如果年份是闰年且月份是1(二月),则天数加1。
return rtc_days_in_month[month] + (is_leap_year(year) && month == 1);
}
// 将 rtc_month_days 函数导出,使其可以被其他内核模块调用。
EXPORT_SYMBOL(rtc_month_days);

// rtc_year_days: 计算给定的日期是该年中的第几天 (从1开始计数)。
// @day: 该月的第几天 (1-31)。
// @month: 月份 (0-11)。
// @year: 完整的年份 (例如 2024)。
// 返回值: 该年中的第几天 (1-366)。
int rtc_year_days(unsigned int day, unsigned int month, unsigned int year)
{
// 根据是否为闰年选择正确的查找表行,获取该月之前所有月份的总天数,然后加上当月的天数,最后减1(因为day是从1开始的)。
return rtc_ydays[is_leap_year(year)][month] + day - 1;
}
// 将 rtc_year_days 函数导出,使其可以被其他内核模块调用。
EXPORT_SYMBOL(rtc_year_days);

// rtc_time64_to_tm: 将 time64_t 类型的秒数转换为 rtc_time 结构体表示的日历时间。
// @time: 自1970年1月1日00:00:00 UTC以来的总秒数。
// @tm: 指向用于存储结果的 rtc_time 结构体的指针。
void rtc_time64_to_tm(time64_t time, struct rtc_time *tm)
{
int secs; // 用于存储一天内的秒数。

u64 u64tmp; // 64位无符号整型临时变量,用于防止乘法溢出。
// 32位无符号整型临时变量,及用于存储计算结果的变量。
u32 u32tmp, udays, century, day_of_century, year_of_century, year,
day_of_year, month, day;
// 用于存储布尔状态的变量。
bool is_Jan_or_Feb, is_leap_year;

// 将时间基准从1970-01-01转换到0000-03-01,以简化后续的历法计算。
// 719468 是从 0000-03-01 到 1970-01-01 的天数。
time += (u64)719468 * 86400;

// 从总秒数中计算出总天数(udays)和当天剩余的秒数(secs)。
udays = div_s64_rem(time, 86400, &secs);

// 计算星期几。0000-03-01 在公历前推中是星期三,所以加3。
tm->tm_wday = (udays + 3) % 7;

// 以下是 Neri and Schneider 算法,用于将总天数转换为年月日。
// 该算法在一个以3月为年首的“计算历法”中进行。

// 计算一个中间值,用于后续分解。
u32tmp = 4 * udays + 3;
// 计算从0年3月1日起的世纪数。146097是400年的总天数。
century = u32tmp / 146097;
// 计算在当前400年周期内的天数(除以4是因为每4年有1个闰日)。
day_of_century = u32tmp % 146097 / 4;

// 计算另一个中间值。
u32tmp = 4 * day_of_century + 3;
// 使用一个大的64位乘法来同时计算年和年内日,这是一种高效的算术技巧。
// 2939745ULL 是一个用于此计算的“魔术数字”。
u64tmp = 2939745ULL * u32tmp;
// 64位乘法结果的高32位是世纪内的年份。
year_of_century = upper_32_bits(u64tmp);
// 低32位通过一系列运算得到年内日。
day_of_year = lower_32_bits(u64tmp) / 2939745 / 4;

// 计算完整的公历年份(在计算历法下)。
year = 100 * century + year_of_century;
// 判断计算出的年份是否为闰年。
is_leap_year = year_of_century != 0 ?
year_of_century % 4 == 0 : century % 4 == 0;

// 再次使用算术技巧从年内日计算出月份和日期。
u32tmp = 2141 * day_of_year + 132377;
// 结果的高16位代表月份。
month = u32tmp >> 16;
// 低16位除以一个常数得到日期。
day = ((u16) u32tmp) / 2141;

// 在计算历法中,1月1日是第306天。判断当前日期是否属于计算年的“年尾”(即公历的1月或2月)。
is_Jan_or_Feb = day_of_year >= 306;

// 将计算历法下的年月日转换回标准的公历表示。
// 如果是1月或2月,公历年份需要加1。
year = year + is_Jan_or_Feb;
// 月份进行相应调整。
month = is_Jan_or_Feb ? month - 12 : month;
// 日期加1(因为计算结果是从0开始的)。
day = day + 1;

// 计算在公历下的年内日。
day_of_year = is_Jan_or_Feb ?
day_of_year - 306 : day_of_year + 31 + 28 + is_leap_year;

// 将计算结果填充到 rtc_time 结构体中。
// tm_year 是自1900年起的年份。
tm->tm_year = (int) (year - 1900);
tm->tm_mon = (int) month;
tm->tm_mday = (int) day;
// tm_yday 是从1开始计数的年内日。
tm->tm_yday = (int) day_of_year + 1;

// 从当天剩余的秒数中解析出时、分、秒。
tm->tm_hour = secs / 3600;
secs -= tm->tm_hour * 3600;
tm->tm_min = secs / 60;
tm->tm_sec = secs - tm->tm_min * 60;

// 默认不考虑夏令时。
tm->tm_isdst = 0;
}
// 将 rtc_time64_to_tm 函数导出。
EXPORT_SYMBOL(rtc_time64_to_tm);

// rtc_valid_tm: 校验一个 rtc_time 结构体中的日期和时间是否有效。
// @tm: 指向待校验的 rtc_time 结构体的指针。
// 返回值: 如果有效则返回0,否则返回-EINVAL。
int rtc_valid_tm(struct rtc_time *tm)
{
// 检查年月日时分秒的各个字段是否在其合法范围内。
// tm->tm_year 从70年 (Unix纪元) 开始有效。
if (tm->tm_year < 70 ||
tm->tm_year > (INT_MAX - 1900) || // 防止年份溢出。
((unsigned int)tm->tm_mon) >= 12 || // 月份必须是 0-11。
tm->tm_mday < 1 || // 日期必须大于等于1。
// 日期不能超过当月的实际天数。
tm->tm_mday > rtc_month_days(tm->tm_mon,
((unsigned int)tm->tm_year + 1900)) ||
((unsigned int)tm->tm_hour) >= 24 || // 小时必须是 0-23。
((unsigned int)tm->tm_min) >= 60 || // 分钟必须是 0-59。
((unsigned int)tm->tm_sec) >= 60) // 秒数必须是 0-59。
return -EINVAL;

return 0;
}
// 将 rtc_valid_tm 函数导出。
EXPORT_SYMBOL(rtc_valid_tm);

// rtc_tm_to_time64: 将 rtc_time 结构体表示的日历时间转换为 time64_t 类型的秒数。
// @tm: 指向包含日历时间信息的 rtc_time 结构体的指针。
// 返回值: time64_t 类型的秒数。
time64_t rtc_tm_to_time64(struct rtc_time *tm)
{
// 调用内核提供的 mktime64 函数来执行转换。
// 注意,tm->tm_mon 需要加1来匹配 mktime64 的参数要求 (1-12)。
return mktime64(((unsigned int)tm->tm_year + 1900), tm->tm_mon + 1,
tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec);
}
// 将 rtc_tm_to_time64 函数导出。
EXPORT_SYMBOL(rtc_tm_to_time64);

// rtc_tm_to_ktime: 将 rtc_time 结构体转换为内核标准的 ktime_t 时间格式。
// @tm: 包含日历时间信息的 rtc_time 结构体(值传递)。
// 返回值: ktime_t 类型的时间值。
ktime_t rtc_tm_to_ktime(struct rtc_time tm)
{
// 首先将日历时间转换为秒数,然后使用 ktime_set 将其包装为 ktime_t 类型(纳秒部分为0)。
return ktime_set(rtc_tm_to_time64(&tm), 0);
}
// 将 rtc_tm_to_ktime 函数导出。
EXPORT_SYMBOL_GPL(rtc_tm_to_ktime);

// rtc_ktime_to_tm: 将 ktime_t 时间格式转换为 rtc_time 结构体。
// @kt: ktime_t 类型的时间值。
// 返回值: 转换后的 rtc_time 结构体。
struct rtc_time rtc_ktime_to_tm(ktime_t kt)
{
struct timespec64 ts; // 用于存储转换后的秒和纳秒。
struct rtc_time ret; // 用于存储最终的 rtc_time 结果。

// 将 ktime_t 转换为 timespec64 结构。
ts = ktime_to_timespec64(kt);
// 向上取整:如果纳秒部分不为零,则秒数加1。
// 这确保了闹钟等事件不会早于请求的时间触发。
if (ts.tv_nsec)
ts.tv_sec++;
// 调用 rtc_time64_to_tm 将总秒数转换为 rtc_time 格式。
rtc_time64_to_tm(ts.tv_sec, &ret);
return ret;
}
// 将 rtc_ktime_to_tm 函数导出。
EXPORT_SYMBOL_GPL(rtc_ktime_to_tm);

drivers/rtc/rtc-stm32.c

stm32_rtc_probe / stm32_rtc_init / stm32_rtc_enter_init_mode / stm32_rtc_wait_sync / stm32_rtc_wpr_unlock / stm32_rtc_wpr_lock:STM32 RTC 驱动初始化主路径与日历时钟分频配置

  • 该驱动的并发关注点主要来自中断上下文寄存器写保护/初始化模式的硬件状态机,与多核无关。
  • rtc_ck 频率需要通过 PRED_A/PRED_S 分频得到 1Hz 日历时钟;这属于硬件约束,配置必须在 RTC 初始化模式下完成。
  • 写保护寄存器 WPR 采用特定 key 序列解锁/上锁;未解锁时对关键寄存器写入会被硬件忽略或拒绝。

作用与实现原理

  • stm32_rtc_probe() 是平台驱动入口:完成资源映射、时钟使能、备份域写使能(DBP)、可选安全访问检查(RIF)、RTC 分频与格式初始化、RTC 设备注册、告警中断注册、以及 RTC 专用 pinctrl 注册。

  • stm32_rtc_init() 的核心是计算 (pred_a + 1) * (pred_s + 1) ≈ rate 以产生 1Hz:

    • need_accuracy=true,优先寻找严格整除的分频组合(得到精确 1Hz)。
    • 否则偏向选择更大的 pred_a(通常意味着更低功耗/更低内部切换频率的配置倾向),并在找不到严格 1Hz 时做降级处理。
  • stm32_rtc_enter_init_mode()/exit_init_mode()/wait_sync() 实现对 RTC 硬件状态机的进入/退出与同步等待,避免读写日历寄存器时处于未同步状态。


stm32_rtc_wpr_unlock / stm32_rtc_wpr_lock:写保护开关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* @brief 解除 RTC 写保护。
*
* @param rtc 驱动私有结构体,包含基址与寄存器偏移表。
*
* @note 必须按硬件规定的两次 key 写入顺序执行。
*/
static void stm32_rtc_wpr_unlock(struct stm32_rtc *rtc)
{
const struct stm32_rtc_registers *regs = &rtc->data->regs; /**< 使用 SoC 变体提供的寄存器偏移表。 */

writel_relaxed(RTC_WPR_1ST_KEY, rtc->base + regs->wpr); /**< 写入第 1 个 key,进入解锁序列。 */
writel_relaxed(RTC_WPR_2ND_KEY, rtc->base + regs->wpr); /**< 写入第 2 个 key,完成解锁。 */
}

/**
* @brief 启用 RTC 写保护。
*
* @param rtc 驱动私有结构体。
*
* @note 写入错误 key 触发硬件重新上锁。
*/
static void stm32_rtc_wpr_lock(struct stm32_rtc *rtc)
{
const struct stm32_rtc_registers *regs = &rtc->data->regs;

writel_relaxed(RTC_WPR_WRONG_KEY, rtc->base + regs->wpr); /**< 重新上锁,防止非预期写入关键寄存器。 */
}

stm32_rtc_enter_init_mode / stm32_rtc_exit_init_mode / stm32_rtc_wait_sync:初始化模式与寄存器同步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* @brief 进入 RTC 初始化模式,使能对日历/分频等寄存器的安全更新。
*
* @param rtc 驱动私有结构体。
* @return 0 成功;负 errno 表示超时或硬件异常。
*/
static int stm32_rtc_enter_init_mode(struct stm32_rtc *rtc)
{
const struct stm32_rtc_registers *regs = &rtc->data->regs;
unsigned int isr = readl_relaxed(rtc->base + regs->isr); /**< 读取 ISR/ICSR 当前状态。 */

if (!(isr & STM32_RTC_ISR_INITF)) { /**< INITF=0 表示尚未处于初始化阶段。 */
isr |= STM32_RTC_ISR_INIT; /**< 置位 INIT 请求进入初始化阶段。 */
writel_relaxed(isr, rtc->base + regs->isr);

/** 轮询等待 INITF 置位,表示硬件已进入初始化阶段。 */
return readl_relaxed_poll_timeout_atomic(rtc->base + regs->isr, isr,
(isr & STM32_RTC_ISR_INITF),
10, 100000);
}

return 0; /**< 已在初始化阶段则直接成功返回。 */
}

/**
* @brief 退出 RTC 初始化模式。
*
* @param rtc 驱动私有结构体。
*/
static void stm32_rtc_exit_init_mode(struct stm32_rtc *rtc)
{
const struct stm32_rtc_registers *regs = &rtc->data->regs;
unsigned int isr = readl_relaxed(rtc->base + regs->isr);

isr &= ~STM32_RTC_ISR_INIT; /**< 清除 INIT 位,结束初始化阶段。 */
writel_relaxed(isr, rtc->base + regs->isr);
}

/**
* @brief 等待日历寄存器同步完成。
*
* @param rtc 驱动私有结构体。
* @return 0 成功;负 errno 表示超时或硬件异常。
*/
static int stm32_rtc_wait_sync(struct stm32_rtc *rtc)
{
const struct stm32_rtc_registers *regs = &rtc->data->regs;
unsigned int isr = readl_relaxed(rtc->base + regs->isr);

isr &= ~STM32_RTC_ISR_RSF; /**< 通过清零 RSF 触发重新同步过程。 */
writel_relaxed(isr, rtc->base + regs->isr);

/** 轮询等待 RSF 再次置位,表示日历寄存器与 RTC 时钟域完成同步。 */
return readl_relaxed_poll_timeout_atomic(rtc->base + regs->isr,
isr,
(isr & STM32_RTC_ISR_RSF),
10, 100000);
}

stm32_rtc_init:计算分频并强制 24 小时制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/**
* @brief 配置 RTC 预分频器以得到 1Hz 日历时钟,并强制 24 小时制。
*
* @param pdev 平台设备。
* @param rtc 驱动私有结构体。
* @return 0 成功;负 errno 失败。
*
* @note 目标关系为:1Hz = rtc_ck / ((pred_a + 1) * (pred_s + 1))。
*/
static int stm32_rtc_init(struct platform_device *pdev,
struct stm32_rtc *rtc)
{
const struct stm32_rtc_registers *regs = &rtc->data->regs;
unsigned int prer, pred_a, pred_s, pred_a_max, pred_s_max, cr;
unsigned int rate;
int ret;

rate = clk_get_rate(rtc->rtc_ck); /**< rtc_ck 输入时钟频率。 */

pred_a_max = STM32_RTC_PRER_PRED_A >> STM32_RTC_PRER_PRED_A_SHIFT; /**< 异步分频器最大可配置值。 */
pred_s_max = STM32_RTC_PRER_PRED_S >> STM32_RTC_PRER_PRED_S_SHIFT; /**< 同步分频器最大可配置值。 */

if (rate > (pred_a_max + 1) * (pred_s_max + 1)) { /**< 若频率过高则无法分频到 1Hz。 */
dev_err(&pdev->dev, "rtc_ck rate is too high: %dHz\n", rate);
return -EINVAL;
}

if (rtc->data->need_accuracy) {
for (pred_a = 0; pred_a <= pred_a_max; pred_a++) {
pred_s = (rate / (pred_a + 1)) - 1; /**< 令 (pred_s+1) = rate/(pred_a+1),尝试严格整除。 */
if (pred_s <= pred_s_max && ((pred_s + 1) * (pred_a + 1)) == rate)
break; /**< 找到精确 1Hz 的组合。 */
}
} else {
for (pred_a = pred_a_max; pred_a + 1 > 0; pred_a--) { /**< 反向遍历倾向更大 pred_a。 */
pred_s = (rate / (pred_a + 1)) - 1;
if (((pred_s + 1) * (pred_a + 1)) == rate)
break; /**< 仍优先精确 1Hz;未命中则继续降 pred_a。 */
}
}

if (pred_s > pred_s_max || pred_a > pred_a_max) { /**< 仍未得到可用精确组合时进行降级。 */
pred_a = pred_a_max;
pred_s = (rate / (pred_a + 1)) - 1;
dev_warn(&pdev->dev, "rtc_ck is %s\n",
(rate < ((pred_a + 1) * (pred_s + 1))) ? "fast" : "slow");
}

cr = readl_relaxed(rtc->base + regs->cr);
prer = readl_relaxed(rtc->base + regs->prer);
prer &= STM32_RTC_PRER_PRED_S | STM32_RTC_PRER_PRED_A; /**< 仅保留分频字段用于比较。 */

pred_s = (pred_s << STM32_RTC_PRER_PRED_S_SHIFT) & STM32_RTC_PRER_PRED_S;
pred_a = (pred_a << STM32_RTC_PRER_PRED_A_SHIFT) & STM32_RTC_PRER_PRED_A;

if ((cr & STM32_RTC_CR_FMT) == 0 && prer == (pred_s | pred_a)) /**< 已是 24 小时制且分频不变则无需初始化。 */
return 0;

stm32_rtc_wpr_unlock(rtc);

ret = stm32_rtc_enter_init_mode(rtc);
if (ret) {
dev_err(&pdev->dev, "Can't enter in init mode. Prescaler config failed.\n");
goto end;
}

writel_relaxed(pred_s, rtc->base + regs->prer); /**< 先写入同步分频字段。 */
writel_relaxed(pred_a | pred_s, rtc->base + regs->prer); /**< 再写入完整分频组合。 */

cr &= ~STM32_RTC_CR_FMT; /**< 强制 24 小时制。 */
writel_relaxed(cr, rtc->base + regs->cr);

stm32_rtc_exit_init_mode(rtc);

ret = stm32_rtc_wait_sync(rtc); /**< 等待寄存器同步,避免后续读到未同步日历值。 */
end:
stm32_rtc_wpr_lock(rtc);

return ret;
}

stm32_rtc_probe:平台驱动入口(资源/时钟/DBP/RIF/注册)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
/**
* @brief STM32 RTC 平台驱动 probe:建立 RTC 设备、初始化硬件并注册中断与 pinctrl。
*
* @param pdev 平台设备。
* @return 0 成功;负 errno 失败。
*/
static int stm32_rtc_probe(struct platform_device *pdev)
{
struct stm32_rtc *rtc;
const struct stm32_rtc_registers *regs;
struct pinctrl_dev *pctl;
int ret;

rtc = devm_kzalloc(&pdev->dev, sizeof(*rtc), GFP_KERNEL);
if (!rtc)
return -ENOMEM;

rtc->base = devm_platform_ioremap_resource(pdev, 0); /**< 映射 RTC 寄存器基址。 */
if (IS_ERR(rtc->base))
return PTR_ERR(rtc->base);

rtc->data = (struct stm32_rtc_data *)of_device_get_match_data(&pdev->dev); /**< 选择 SoC 变体数据表。 */
regs = &rtc->data->regs;

if (rtc->data->need_dbp) {
unsigned int args[2];

rtc->dbp = syscon_regmap_lookup_by_phandle_args(pdev->dev.of_node,
"st,syscfg",
2, args); /**< 获取备份域写使能寄存器的 regmap 与参数。 */
if (IS_ERR(rtc->dbp)) {
dev_err(&pdev->dev, "no st,syscfg\n");
return PTR_ERR(rtc->dbp);
}

rtc->dbp_reg = args[0]; /**< DBP 控制寄存器偏移。 */
rtc->dbp_mask = args[1]; /**< DBP 位掩码。 */
}

if (!rtc->data->has_pclk) {
rtc->pclk = NULL;
rtc->rtc_ck = devm_clk_get(&pdev->dev, NULL); /**< 旧变体:只有一个时钟输入。 */
} else {
rtc->pclk = devm_clk_get(&pdev->dev, "pclk"); /**< H7 类:需要 pclk 访问寄存器。 */
if (IS_ERR(rtc->pclk))
return dev_err_probe(&pdev->dev, PTR_ERR(rtc->pclk), "no pclk clock");

rtc->rtc_ck = devm_clk_get(&pdev->dev, "rtc_ck"); /**< 独立的 rtc_ck 用于日历计数域。 */
}
if (IS_ERR(rtc->rtc_ck))
return dev_err_probe(&pdev->dev, PTR_ERR(rtc->rtc_ck), "no rtc_ck clock");

if (rtc->data->has_pclk) {
ret = clk_prepare_enable(rtc->pclk); /**< 使能 pclk 以保证寄存器访问时钟有效。 */
if (ret)
return ret;
}

ret = clk_prepare_enable(rtc->rtc_ck); /**< 使能 rtc_ck,后续 init/sync 依赖该时钟。 */
if (ret)
goto err_no_rtc_ck;

if (rtc->data->need_dbp)
regmap_update_bits(rtc->dbp, rtc->dbp_reg,
rtc->dbp_mask, rtc->dbp_mask); /**< 解除备份域写保护,使能写 RTC 关键寄存器。 */

if (rtc->data->rif_protected) {
ret = stm32_rtc_check_rif(rtc, STM32_RTC_RES_INIT);
if (!ret)
ret = stm32_rtc_check_rif(rtc, STM32_RTC_RES_ALRA);
if (ret) {
dev_err(&pdev->dev, "Failed to probe RTC due to RIF configuration\n");
goto err;
}
}

ret = stm32_rtc_init(pdev, rtc); /**< 配置分频与 24 小时制,并完成同步。 */
if (ret)
goto err;

rtc->irq_alarm = platform_get_irq(pdev, 0);
if (rtc->irq_alarm <= 0) {
ret = rtc->irq_alarm;
goto err;
}

ret = devm_device_init_wakeup(&pdev->dev);
if (ret)
goto err;

ret = devm_pm_set_wake_irq(&pdev->dev, rtc->irq_alarm);
if (ret)
goto err;

platform_set_drvdata(pdev, rtc);

rtc->rtc_dev = devm_rtc_device_register(&pdev->dev, pdev->name,
&stm32_rtc_ops, THIS_MODULE);
if (IS_ERR(rtc->rtc_dev)) {
ret = PTR_ERR(rtc->rtc_dev);
dev_err(&pdev->dev, "rtc device registration failed, err=%d\n", ret);
goto err;
}

ret = devm_request_threaded_irq(&pdev->dev, rtc->irq_alarm, NULL,
stm32_rtc_alarm_irq, IRQF_ONESHOT,
pdev->name, rtc); /**< 告警中断采用线程化处理,避免硬中断做复杂工作。 */
if (ret) {
dev_err(&pdev->dev, "IRQ%d (alarm interrupt) already claimed\n", rtc->irq_alarm);
goto err;
}

stm32_rtc_clean_outs(rtc); /**< 初始化输出复用相关寄存器到“干净状态”。 */

ret = devm_pinctrl_register_and_init(&pdev->dev, &stm32_rtc_pdesc, rtc, &pctl);
if (ret)
return dev_err_probe(&pdev->dev, ret, "pinctrl register failed");

ret = pinctrl_enable(pctl);
if (ret)
return dev_err_probe(&pdev->dev, ret, "pinctrl enable failed");

if (!(readl_relaxed(rtc->base + regs->isr) & STM32_RTC_ISR_INITS)) /**< INITS 未置位常见于备份域未初始化。 */
dev_warn(&pdev->dev, "Date/Time must be initialized\n");

if (regs->verr != UNDEF_REG) {
u32 ver = readl_relaxed(rtc->base + regs->verr);
dev_info(&pdev->dev, "registered rev:%d.%d\n",
(ver >> STM32_RTC_VERR_MAJREV_SHIFT) & 0xF,
(ver >> STM32_RTC_VERR_MINREV_SHIFT) & 0xF);
}

return 0;

err:
clk_disable_unprepare(rtc->rtc_ck);
err_no_rtc_ck:
if (rtc->data->has_pclk)
clk_disable_unprepare(rtc->pclk);

if (rtc->data->need_dbp)
regmap_update_bits(rtc->dbp, rtc->dbp_reg, rtc->dbp_mask, 0); /**< probe 失败回滚 DBP 使能。 */

return ret;
}

stm32_rtc_alarm_irq / stm32_rtc_alarm_irq_enable / stm32_rtc_clear_event_flags / stm32_rtc_clear_events:告警中断处理与使能/清除链路

平台关注:单核、无 MMU、ARMv7-M(STM32H750)

  • 该驱动在 probe() 中用 线程化中断devm_request_threaded_irq(..., NULL, stm32_rtc_alarm_irq, IRQF_ONESHOT, ...))注册告警中断,因此 stm32_rtc_alarm_irq() 运行在线程上下文,允许使用 rtc_lock() 这类可能睡眠的锁原语。
  • 单核下 IRQF_ONESHOT 的关键意义是:线程处理函数运行期间,内核会保持该 IRQ 线处于屏蔽状态,避免同一告警中断在处理未完成时重入导致重复上报或清旗竞争。

stm32_rtc_alarm_irq:告警中断线程处理函数(确认条件→上报→清旗)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* @brief RTC 告警中断线程处理函数。
*
* @param irq 中断号。
* @param dev_id probe 时传入的驱动私有数据指针。
*
* @return IRQ_HANDLED 表示中断已处理。
*/
static irqreturn_t stm32_rtc_alarm_irq(int irq, void *dev_id)
{
struct stm32_rtc *rtc = (struct stm32_rtc *)dev_id; /**< 私有数据:包含寄存器基址、变体表、rtc_dev 等。 */
const struct stm32_rtc_registers *regs = &rtc->data->regs;
const struct stm32_rtc_events *evts = &rtc->data->events;
unsigned int status, cr;

rtc_lock(rtc->rtc_dev); /**< 串行化 rtc 核心与本驱动的并发访问,避免与 enable/disable 等路径交错。 */

status = readl_relaxed(rtc->base + regs->sr); /**< 读取告警状态来源寄存器;不同变体可能映射到 ISR 或 SR。 */
cr = readl_relaxed(rtc->base + regs->cr); /**< 读取控制寄存器,用于确认中断使能位。 */

/**
* 触发条件必须同时满足:
* 1) 状态寄存器中对应告警标志已置位;
* 2) 控制寄存器中对应告警中断已使能。
* 这样可避免“历史遗留标志”在中断未使能时被误上报。
*/
if ((status & evts->alra) &&
(cr & STM32_RTC_CR_ALRAIE)) {

rtc_update_irq(rtc->rtc_dev, 1, RTC_IRQF | RTC_AF); /**< 向 RTC 核心上报一次告警事件。 */

stm32_rtc_clear_event_flags(rtc, evts->alra); /**< 清除告警标志,否则后续告警可能无法再次触发。 */
}

rtc_unlock(rtc->rtc_dev);

return IRQ_HANDLED;
}

stm32_rtc_alarm_irq_enable:告警中断开关(同时控制 ALRAE 与 ALRAIE,并清历史标志)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* @brief 使能或关闭 Alarm A 以及其对应中断输出。
*
* @param dev 设备对象。
* @param enabled 1 表示使能;0 表示关闭。
*
* @return 0 成功。
*/
static int stm32_rtc_alarm_irq_enable(struct device *dev, unsigned int enabled)
{
struct stm32_rtc *rtc = dev_get_drvdata(dev);
const struct stm32_rtc_registers *regs = &rtc->data->regs;
const struct stm32_rtc_events *evts = &rtc->data->events;
unsigned int cr;

cr = readl_relaxed(rtc->base + regs->cr);

stm32_rtc_wpr_unlock(rtc); /**< 修改 CR 等关键寄存器前必须解锁 WPR,否则写入可能无效。 */

/**
* 同时操作两类位:
* - ALRAE:告警比较功能本身使能;
* - ALRAIE:告警中断输出使能。
* 若只开其中之一,会导致“告警发生但无中断”或“中断开但告警比较未运行”等非预期行为。
*/
if (enabled)
cr |= (STM32_RTC_CR_ALRAIE | STM32_RTC_CR_ALRAE);
else
cr &= ~(STM32_RTC_CR_ALRAIE | STM32_RTC_CR_ALRAE);

writel_relaxed(cr, rtc->base + regs->cr);

/**
* 启停过程中统一清除告警标志:
* - 防止 enable 后立刻因历史标志触发一次“伪告警”;
* - 防止 disable/enable 交替导致标志残留影响后续边沿触发路径。
*/
stm32_rtc_clear_event_flags(rtc, evts->alra);

stm32_rtc_wpr_lock(rtc);

return 0;
}

stm32_rtc_clear_event_flags:事件清除的统一入口(变体分发)

1
2
3
4
5
6
7
8
9
10
11
/**
* @brief 清除 RTC 事件标志(根据 SoC 变体选择正确的清除方式)。
*
* @param rtc 驱动私有数据。
* @param flags 需要清除的事件位掩码(例如 Alarm A 标志位)。
*/
static void stm32_rtc_clear_event_flags(struct stm32_rtc *rtc,
unsigned int flags)
{
rtc->data->clear_events(rtc, flags); /**< 通过变体回调适配“写 0 清除”或“写 1 清除”等硬件差异。 */
}

stm32_rtc_clear_events:部分变体的清旗方式(写回 ISR 清零对应位)

对 STM32H750 典型匹配项 st,stm32h7-rtc,该回调用于清除 RTC_ISR 中的事件位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @brief 通过向 ISR 写回“清零后的值”来清除事件标志。
*
* @param rtc 驱动私有数据。
* @param flags 需要清除的事件位掩码。
*
* @note 此类硬件要求:对应标志位通过写 0 清除(写 1 保持或无效),因此采用 read-modify-write。
*/
static void stm32_rtc_clear_events(struct stm32_rtc *rtc,
unsigned int flags)
{
const struct stm32_rtc_registers *regs = &rtc->data->regs;

writel_relaxed(readl_relaxed(rtc->base + regs->isr) & ~flags,
rtc->base + regs->isr); /**< 将 flags 对应位清零后写回,实现事件清除。 */
}

stm32mp1_rtc_clear_events:另一类变体的清旗方式(写 SCR 置 1 清除)

该回调展示了“同名事件,不同清除寄存器/语义”的硬件差异:通过写 RTC_SCR 的 1 来清除标志。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @brief 通过向 SCR 写 1 来清除事件标志(适用于部分 STM32MP 变体)。
*
* @param rtc 驱动私有数据。
* @param flags 需要清除的事件位掩码。
*/
static void stm32mp1_rtc_clear_events(struct stm32_rtc *rtc,
unsigned int flags)
{
struct stm32_rtc_registers regs = rtc->data->regs;

writel_relaxed(flags, rtc->base + regs.scr); /**< SCR 的“写 1 清除”语义下,直接写 flags 即可。 */
}

stm32_rtc_read_time / stm32_rtc_set_time / tm2bcd / bcd2tm:时间读取、设置与 BCD 编码转换链路

  • 读写路径的关键约束来自 RTC 硬件:日历寄存器 TR/DR 的字段以 BCD 存储,写入必须在写保护解锁且通常需要进入初始化模式(INIT/INITF)后进行,写完还要等待同步标志 RSF
  • 单核下主要并发来源仍是中断/线程化上下文,不涉及多核一致性;这里用“进入 init 模式 + 等待同步”来避免读到未同步的日历值。

tm2bcd:把内核 rtc_time(二进制)转换为硬件需要的 BCD 字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* @brief 将 rtc_time 从二进制转换为 RTC 寄存器期望的 BCD 表示。
*
* @param tm 内核 rtc_time(输入为二进制;输出字段就地改为 BCD 格式/RTC 侧语义)。
*
* @note STM32 RTC 的 weekday 语义与内核不同,需要额外映射。
*/
static void tm2bcd(struct rtc_time *tm)
{
tm->tm_sec = bin2bcd(tm->tm_sec); /**< 秒:二进制→BCD。 */
tm->tm_min = bin2bcd(tm->tm_min); /**< 分:二进制→BCD。 */
tm->tm_hour = bin2bcd(tm->tm_hour); /**< 时:二进制→BCD。 */

tm->tm_mday = bin2bcd(tm->tm_mday); /**< 日:二进制→BCD。 */
tm->tm_mon = bin2bcd(tm->tm_mon + 1); /**< 月:内核 0~11,硬件 1~12,先 +1 再转 BCD。 */
tm->tm_year = bin2bcd(tm->tm_year - 100); /**< 年:内核以 1900 为基准,STM32 常存 0~99 表示 2000~2099,因此减 100。 */

/**
* weekday 映射:
* - 内核:0=周日...6=周六
* - RTC:0=无效,1=周一...7=周日
* 因此:内核 0(周日) 映射为 7;其余保持 1~6(对应周一~周六)。
*/
tm->tm_wday = (!tm->tm_wday) ? 7 : tm->tm_wday;
}

bcd2tm:把硬件寄存器读出的 BCD 字段转换为内核 rtc_time(二进制)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @brief 将 rtc_time 从 BCD/RTC 侧语义转换为内核二进制语义。
*
* @param tm 内核 rtc_time(输入为 BCD 字段;输出为二进制字段/内核语义)。
*/
static void bcd2tm(struct rtc_time *tm)
{
tm->tm_sec = bcd2bin(tm->tm_sec); /**< 秒:BCD→二进制。 */
tm->tm_min = bcd2bin(tm->tm_min); /**< 分:BCD→二进制。 */
tm->tm_hour = bcd2bin(tm->tm_hour); /**< 时:BCD→二进制。 */

tm->tm_mday = bcd2bin(tm->tm_mday); /**< 日:BCD→二进制。 */
tm->tm_mon = bcd2bin(tm->tm_mon) - 1; /**< 月:硬件 1~12 → 内核 0~11。 */
tm->tm_year = bcd2bin(tm->tm_year) + 100; /**< 年:硬件 0~99 → 内核 tm_year=100..199 (2000..2099)。 */

/**
* weekday 映射回内核:
* 硬件:1=周一...7=周日;内核:0=周日...6=周六
* 这里用 “% 7” 将 7 映射为 0,其余 1..6 保持不变(对应周一..周六)。
*/
tm->tm_wday %= 7;
}

stm32_rtc_read_time:从 TR/DR 读取 BCD 字段并转换为内核 rtc_time

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* @brief 读取 RTC 当前时间。
*
* @param dev 设备对象。
* @param tm 输出:内核 rtc_time(最终为二进制语义)。
*
* @return 0 成功。
*/
static int stm32_rtc_read_time(struct device *dev, struct rtc_time *tm)
{
struct stm32_rtc *rtc = dev_get_drvdata(dev);
const struct stm32_rtc_registers *regs = &rtc->data->regs;
unsigned int tr, dr;

tr = readl_relaxed(rtc->base + regs->tr); /**< TR:时分秒字段(BCD)。 */
dr = readl_relaxed(rtc->base + regs->dr); /**< DR:年月日星期字段(BCD)。 */

/** 从寄存器位域提取 BCD 值(仍保持 BCD 编码,待 bcd2tm() 再转二进制)。 */
tm->tm_sec = (tr & STM32_RTC_TR_SEC) >> STM32_RTC_TR_SEC_SHIFT;
tm->tm_min = (tr & STM32_RTC_TR_MIN) >> STM32_RTC_TR_MIN_SHIFT;
tm->tm_hour = (tr & STM32_RTC_TR_HOUR) >> STM32_RTC_TR_HOUR_SHIFT;

tm->tm_mday = (dr & STM32_RTC_DR_DATE) >> STM32_RTC_DR_DATE_SHIFT;
tm->tm_mon = (dr & STM32_RTC_DR_MONTH) >> STM32_RTC_DR_MONTH_SHIFT;
tm->tm_year = (dr & STM32_RTC_DR_YEAR) >> STM32_RTC_DR_YEAR_SHIFT;
tm->tm_wday = (dr & STM32_RTC_DR_WDAY) >> STM32_RTC_DR_WDAY_SHIFT;

bcd2tm(tm); /**< 将 BCD/硬件语义转换为内核二进制语义。 */

return 0;
}

备注:这段实现没有在读前做“寄存器一致性读”的双读/锁存处理;其正确性依赖于 STM32 RTC 的 TR/DR 读出语义(硬件通常提供内部锁存/同步机制),以及上层在需要时通过同步流程保证读到稳定值。


stm32_rtc_set_time:进入初始化模式写 TR/DR,并等待同步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* @brief 设置 RTC 当前时间。
*
* @param dev 设备对象。
* @param tm 输入:内核 rtc_time(二进制语义);函数内部会转换为 BCD 并写入寄存器。
*
* @return 0 成功;负 errno 表示进入 init 模式或同步等待失败。
*/
static int stm32_rtc_set_time(struct device *dev, struct rtc_time *tm)
{
struct stm32_rtc *rtc = dev_get_drvdata(dev);
const struct stm32_rtc_registers *regs = &rtc->data->regs;
unsigned int tr, dr;
int ret = 0;

tm2bcd(tm); /**< 就地转换为 BCD/RTC 语义,便于拼装寄存器位域。 */

tr = ((tm->tm_sec << STM32_RTC_TR_SEC_SHIFT) & STM32_RTC_TR_SEC) |
((tm->tm_min << STM32_RTC_TR_MIN_SHIFT) & STM32_RTC_TR_MIN) |
((tm->tm_hour << STM32_RTC_TR_HOUR_SHIFT) & STM32_RTC_TR_HOUR); /**< 按位域拼装 TR(BCD)。 */

dr = ((tm->tm_mday << STM32_RTC_DR_DATE_SHIFT) & STM32_RTC_DR_DATE) |
((tm->tm_mon << STM32_RTC_DR_MONTH_SHIFT) & STM32_RTC_DR_MONTH) |
((tm->tm_year << STM32_RTC_DR_YEAR_SHIFT) & STM32_RTC_DR_YEAR) |
((tm->tm_wday << STM32_RTC_DR_WDAY_SHIFT) & STM32_RTC_DR_WDAY); /**< 按位域拼装 DR(BCD)。 */

stm32_rtc_wpr_unlock(rtc); /**< 解除写保护,允许更新 TR/DR 等关键寄存器。 */

ret = stm32_rtc_enter_init_mode(rtc); /**< 进入初始化模式,硬件在该模式下允许一致性更新日历寄存器。 */
if (ret) {
dev_err(dev, "Can't enter in init mode. Set time aborted.\n");
goto end;
}

writel_relaxed(tr, rtc->base + regs->tr); /**< 写入时间寄存器(BCD)。 */
writel_relaxed(dr, rtc->base + regs->dr); /**< 写入日期寄存器(BCD)。 */

stm32_rtc_exit_init_mode(rtc); /**< 退出初始化模式,日历开始按新值运行。 */

ret = stm32_rtc_wait_sync(rtc); /**< 等待同步,保证后续读取路径观察到一致的日历寄存器状态。 */
end:
stm32_rtc_wpr_lock(rtc); /**< 恢复写保护,降低误写风险。 */

return ret;
}

stm32_rtc_read_alarm / stm32_rtc_set_alarm / stm32_rtc_valid_alrm:闹钟时间链路(解析/配置 ALRMAR 与“未来一个月窗口”约束)


stm32_rtc_read_alarm:从 ALRMAR 解码为 rtc_wkalrm(处理 mask/WDSEL/PM)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/**
* @brief 读取 Alarm A 配置与状态,并转换为内核 rtc_wkalrm。
*
* @param dev 设备对象。
* @param alrm 输出:闹钟结构体,包含 time/enabled/pending。
*
* @return 0 成功。
*/
static int stm32_rtc_read_alarm(struct device *dev, struct rtc_wkalrm *alrm)
{
struct stm32_rtc *rtc = dev_get_drvdata(dev);
const struct stm32_rtc_registers *regs = &rtc->data->regs;
const struct stm32_rtc_events *evts = &rtc->data->events;
struct rtc_time *tm = &alrm->time;
unsigned int alrmar, cr, status;

alrmar = readl_relaxed(rtc->base + regs->alrmar); /**< 读取闹钟寄存器(BCD 字段 + mask + 选择位)。 */
cr = readl_relaxed(rtc->base + regs->cr); /**< 读取使能位(ALRAE/ALRAIE 等)。 */
status = readl_relaxed(rtc->base + regs->sr); /**< 读取告警事件标志(不同变体映射不同寄存器位)。 */

/**
* DATE_MASK=1 表示“日期/星期不参与比较”,即每天都能匹配到该闹钟。
* 由于内核 rtc_time 需要表达“不关心”,此处用 -1 作为哨兵值。
*/
if (alrmar & STM32_RTC_ALRMXR_DATE_MASK) {
tm->tm_mday = -1;
tm->tm_wday = -1;
} else {
if (alrmar & STM32_RTC_ALRMXR_WDSEL) {
/** WDSEL=1:日期字段解释为“星期几”比较。 */
tm->tm_mday = -1;
tm->tm_wday = (alrmar & STM32_RTC_ALRMXR_WDAY) >>
STM32_RTC_ALRMXR_WDAY_SHIFT;
tm->tm_wday %= 7; /**< RTC:1..7(周日);内核:0..6(周日=0)。 */
} else {
/** WDSEL=0:日期字段解释为“每月第几日”比较。 */
tm->tm_wday = -1;
tm->tm_mday = (alrmar & STM32_RTC_ALRMXR_DATE) >>
STM32_RTC_ALRMXR_DATE_SHIFT;
}
}

/**
* HOUR_MASK=1 表示“小时不参与比较”。否则提取 BCD 小时。
* PM 位只对 12 小时制有意义;本驱动通常配置 24h,但读路径仍做兼容处理。
*/
if (alrmar & STM32_RTC_ALRMXR_HOUR_MASK) {
tm->tm_hour = -1;
} else {
tm->tm_hour = (alrmar & STM32_RTC_ALRMXR_HOUR) >>
STM32_RTC_ALRMXR_HOUR_SHIFT;
if (alrmar & STM32_RTC_ALRMXR_PM)
tm->tm_hour += 12;
}

if (alrmar & STM32_RTC_ALRMXR_MIN_MASK) {
tm->tm_min = -1;
} else {
tm->tm_min = (alrmar & STM32_RTC_ALRMXR_MIN) >>
STM32_RTC_ALRMXR_MIN_SHIFT;
}

if (alrmar & STM32_RTC_ALRMXR_SEC_MASK) {
tm->tm_sec = -1;
} else {
tm->tm_sec = (alrmar & STM32_RTC_ALRMXR_SEC) >>
STM32_RTC_ALRMXR_SEC_SHIFT;
}

bcd2tm(tm); /**< 将提取到的 BCD 字段转换为内核二进制语义(对 -1 哨兵字段不应使用该转换)。 */

alrm->enabled = (cr & STM32_RTC_CR_ALRAE) ? 1 : 0; /**< 闹钟比较功能是否使能。 */
alrm->pending = (status & evts->alra) ? 1 : 0; /**< 是否已有告警事件挂起(标志未清)。 */

return 0;
}

stm32_rtc_valid_alrm:为什么限制“只能设到未来一个月窗口”

硬件 Alarm A 不支持“指定月份和年份”。如果允许用户设置一个绝对时间戳,软件无法把它无歧义地映射到硬件比较字段:

  • 例如用户设置“明年 1 月 15 日 08:00”,硬件里没有“年/月”比较,单靠“日=15、时=08:00”会导致每个月 15 日都触发,语义被破坏。

因此驱动采用一个可验证的约束:

允许的闹钟时间满足:now < alarm <= now + (最多跨到下个月同日)

实现策略:

  1. 先读当前时间 now(通过 stm32_rtc_read_time()
  2. 计算“下个月”和“下个月所在年份”
  3. 计算从当前日期到“下个月同日(若下个月天数不够,则取下个月最后一天)”的最大前进天数 max_day_forward
  4. now 转为时间戳,加上 max_day_forward * SEC_PER_DAY 得到上限,再比较目标 tm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* @brief 校验闹钟时间是否落在“未来一个月窗口”内,以适配硬件不支持年/月比较的限制。
*
* @param dev 设备对象。
* @param tm 目标闹钟时间(内核二进制语义)。
*
* @return 0 表示有效;-EINVAL 表示超出可表示范围。
*/
static int stm32_rtc_valid_alrm(struct device *dev, struct rtc_time *tm)
{
static struct rtc_time now;
time64_t max_alarm_time64;
int max_day_forward;
int next_month;
int next_year;

stm32_rtc_read_time(dev, &now); /**< 获取当前时间作为窗口起点。 */

next_month = now.tm_mon + 1; /**< 内核月份 0..11。 */
if (next_month == 12) {
next_month = 0;
next_year = now.tm_year + 1;
} else {
next_year = now.tm_year;
}

/**
* max_day_forward:
* - 本月剩余天数:month_days(now_mon, now_year) - now_mday
* - 加上“下个月允许到达的天数”:min(下个月总天数, now_mday)
* 其含义是:最多允许到达“下个月的同一日”,若下个月没有该日则到下个月最后一日。
*/
max_day_forward = rtc_month_days(now.tm_mon, now.tm_year)
- now.tm_mday
+ min(rtc_month_days(next_month, next_year), now.tm_mday);

max_alarm_time64 = rtc_tm_to_time64(&now) + max_day_forward * SEC_PER_DAY;

return rtc_tm_to_time64(tm) <= max_alarm_time64 ? 0 : -EINVAL;
}

stm32_rtc_set_alarm:写入 ALRMAR(仅写 d/h/m/s),并通过 ALRAWF 确认可写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
* @brief 设置 Alarm A:写入 ALRMAR,并根据 alrm->enabled 使能/关闭告警中断。
*
* @param dev 设备对象。
* @param alrm 输入:目标闹钟时间与 enabled 标志。
*
* @return 0 成功;负 errno 表示不可写或校验失败。
*/
static int stm32_rtc_set_alarm(struct device *dev, struct rtc_wkalrm *alrm)
{
struct stm32_rtc *rtc = dev_get_drvdata(dev);
const struct stm32_rtc_registers *regs = &rtc->data->regs;
struct rtc_time *tm = &alrm->time;
unsigned int cr, isr, alrmar;
int ret = 0;

/** 软件层约束:避免设置无法被硬件唯一表示的“远未来”闹钟。 */
if (stm32_rtc_valid_alrm(dev, tm) < 0) {
dev_err(dev, "Alarm can be set only on upcoming month.\n");
return -EINVAL;
}

tm2bcd(tm); /**< 目标时间转换为 BCD,便于写入 ALRMAR 字段。 */

alrmar = 0;

/** 注意:这里不写 year/month,因为硬件不支持。 */
alrmar |= (tm->tm_mday << STM32_RTC_ALRMXR_DATE_SHIFT) &
STM32_RTC_ALRMXR_DATE;

alrmar &= ~STM32_RTC_ALRMXR_PM; /**< 以 24 小时制配置闹钟。 */
alrmar |= (tm->tm_hour << STM32_RTC_ALRMXR_HOUR_SHIFT) &
STM32_RTC_ALRMXR_HOUR;
alrmar |= (tm->tm_min << STM32_RTC_ALRMXR_MIN_SHIFT) &
STM32_RTC_ALRMXR_MIN;
alrmar |= (tm->tm_sec << STM32_RTC_ALRMXR_SEC_SHIFT) &
STM32_RTC_ALRMXR_SEC;

stm32_rtc_wpr_unlock(rtc);

/** 更新闹钟寄存器前先关闭闹钟功能,符合多数 STM32 RTC 的更新流程要求。 */
cr = readl_relaxed(rtc->base + regs->cr);
cr &= ~STM32_RTC_CR_ALRAE;
writel_relaxed(cr, rtc->base + regs->cr);

/**
* 轮询 ALRAWF:写入允许标志。
* 若未就绪直接写 ALRMAR,硬件可能丢弃写入或产生不一致。
*/
ret = readl_relaxed_poll_timeout_atomic(rtc->base + regs->isr,
isr,
(isr & STM32_RTC_ISR_ALRAWF),
10, 100000);
if (ret) {
dev_err(dev, "Alarm update not allowed\n");
goto end;
}

writel_relaxed(alrmar, rtc->base + regs->alrmar); /**< 写入闹钟寄存器。 */

stm32_rtc_alarm_irq_enable(dev, alrm->enabled); /**< 根据用户需求重新使能/关闭 ALRAE+ALRAIE,并清标志。 */
end:
stm32_rtc_wpr_lock(rtc);

return ret;
}

stm32_rtc_pinmux_action_alarm / stm32_rtc_pinmux_action_lsco / stm32_rtc_pinmux_lsco_available / stm32_rtc_pinmux_set_mux:RTC 输出复用的 pinctrl/pinmux 链路


stm32_rtc_pinctrl_get_*:把“out1/out2/out2_rmp”作为 pin group 暴露

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* @brief 返回 pin group 数量(由静态 pin 表决定)。
*/
static int stm32_rtc_pinctrl_get_groups_count(struct pinctrl_dev *pctldev)
{
return ARRAY_SIZE(stm32_rtc_pinctrl_pins); /**< group 数量等于 out1/out2/out2_rmp 三个逻辑 pin。 */
}

/**
* @brief 根据 selector 返回 group 名称("out1"/"out2"/"out2_rmp")。
*/
static const char *stm32_rtc_pinctrl_get_group_name(struct pinctrl_dev *pctldev,
unsigned int selector)
{
return stm32_rtc_pinctrl_pins[selector].name; /**< 名称来自 pin 描述表。 */
}

/**
* @brief 返回某个 group 对应的 pin 列表(每个 group 只含 1 个逻辑 pin)。
*/
static int stm32_rtc_pinctrl_get_group_pins(struct pinctrl_dev *pctldev,
unsigned int selector,
const unsigned int **pins,
unsigned int *num_pins)
{
*pins = &stm32_rtc_pinctrl_pins[selector].number; /**< pin number 用 enum stm32_rtc_pin_name 表达。 */
*num_pins = 1;
return 0;
}

stm32_rtc_pinmux_set_mux:pinmux 框架回调,选择功能并执行 action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @brief pinmux 回调:为某个 group 选择某个 function,并执行对应寄存器配置动作。
*
* @param pctldev pinctrl 设备。
* @param selector function 索引("lsco" 或 "alarm-a")。
* @param group group 索引(out1/out2/out2_rmp)。
*
* @return 0 成功;负 errno 表示不支持或资源冲突。
*/
static int stm32_rtc_pinmux_set_mux(struct pinctrl_dev *pctldev, unsigned int selector,
unsigned int group)
{
struct stm32_rtc_pinmux_func selected_func = stm32_rtc_pinmux_functions[selector]; /**< 被选中的功能描述。 */
struct pinctrl_pin_desc pin = stm32_rtc_pinctrl_pins[group]; /**< 被选中的输出 pin。 */

/** 通过 action 回调把“选择功能”落到 RTC 寄存器配置上。 */
if (selected_func.action)
return selected_func.action(pctldev, pin.number);

return -EINVAL;
}

stm32_rtc_pinmux_action_alarm:把 Alarm A 事件路由到 out1/out2/out2_rmp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* @brief 将 RTC 输出复用为 Alarm A 输出,并选择输出管脚路径(out1/out2/out2_rmp)。
*
* @param pctldev pinctrl 设备。
* @param pin 逻辑输出选择(OUT1/OUT2/OUT2_RMP)。
*
* @return 0 成功;-EPERM 表示硬件不支持 alarm 输出;-EINVAL 表示 pin 不合法。
*/
static int stm32_rtc_pinmux_action_alarm(struct pinctrl_dev *pctldev, unsigned int pin)
{
struct stm32_rtc *rtc = pinctrl_dev_get_drvdata(pctldev); /**< 取回驱动私有数据。 */
struct stm32_rtc_registers regs = rtc->data->regs;
unsigned int cr = readl_relaxed(rtc->base + regs.cr);
unsigned int cfgr = readl_relaxed(rtc->base + regs.cfgr);

if (!rtc->data->has_alarm_out)
return -EPERM; /**< 仅 STM32MP1/MP25 等变体支持将 alarm 输出到引脚。 */

/** OSEL 选择输出源为 Alarm A,同时关闭可能占用同一路径的其他输出源。 */
cr &= ~STM32_RTC_CR_OSEL;
cr |= STM32_RTC_CR_OSEL_ALARM_A;
cr &= ~STM32_RTC_CR_TAMPOE; /**< 关闭 tamper 输出使能,避免与 alarm 输出竞争。 */
cr &= ~STM32_RTC_CR_COE; /**< 关闭校准输出(calib out),避免与 alarm 输出竞争。 */
cr &= ~STM32_RTC_CR_TAMPALRM_TYPE; /**< 清除 tamper/alarm 类型配置,确保输出语义一致。 */

/**
* 根据 pin 决定走 OUT1 还是 OUT2,及 OUT2 是否使用 RMP 重映射:
* - OUT1:禁用 OUT2EN,并清 OUT2_RMP;
* - OUT2:启用 OUT2EN,清 OUT2_RMP;
* - OUT2_RMP:启用 OUT2EN,并置 OUT2_RMP。
*/
switch (pin) {
case OUT1:
cr &= ~STM32_RTC_CR_OUT2EN;
cfgr &= ~STM32_RTC_CFGR_OUT2_RMP;
break;
case OUT2:
cr |= STM32_RTC_CR_OUT2EN;
cfgr &= ~STM32_RTC_CFGR_OUT2_RMP;
break;
case OUT2_RMP:
cr |= STM32_RTC_CR_OUT2EN;
cfgr |= STM32_RTC_CFGR_OUT2_RMP;
break;
default:
return -EINVAL;
}

stm32_rtc_wpr_unlock(rtc); /**< 修改 CR/CFGR 前解锁写保护。 */
writel_relaxed(cr, rtc->base + regs.cr);
writel_relaxed(cfgr, rtc->base + regs.cfgr);
stm32_rtc_wpr_lock(rtc); /**< 配置完成后重新上锁。 */

return 0;
}

stm32_rtc_pinmux_lsco_available:LSCO 输出资源冲突判定(避免与校准/防拆/其它输出重叠)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
* @brief 检查指定输出 pin 是否可用于 LSCO(低速时钟输出)。
*
* @param pctldev pinctrl 设备。
* @param pin OUT1 或 OUT2_RMP(LSCO 仅支持这两种路径)。
*
* @return 0 可用;-EBUSY 表示该路径已被其他输出占用;-ERANGE 表示 rtc_ck 非 32768Hz;
* -EINVAL 表示 pin 不支持。
*/
static int stm32_rtc_pinmux_lsco_available(struct pinctrl_dev *pctldev, unsigned int pin)
{
struct stm32_rtc *rtc = pinctrl_dev_get_drvdata(pctldev);
struct stm32_rtc_registers regs = rtc->data->regs;
unsigned int cr = readl_relaxed(rtc->base + regs.cr);
unsigned int cfgr = readl_relaxed(rtc->base + regs.cfgr);
unsigned int calib = STM32_RTC_CR_COE; /**< 校准输出使能位。 */
unsigned int tampalrm = STM32_RTC_CR_TAMPOE | STM32_RTC_CR_OSEL; /**< tamper 输出与输出选择相关位组合,用于粗粒度冲突判定。 */

switch (pin) {
case OUT1:
/**
* OUT1 路径冲突判定:
* - OUT2 未使能时,若已开启 calib 或 tampalrm,则 OUT1 被占用;
* - 或者 calib 与 tampalrm 同时开启时也视为冲突(避免混合输出状态)。
*/
if ((!(cr & STM32_RTC_CR_OUT2EN) &&
((cr & calib) || cr & tampalrm)) ||
((cr & calib) && (cr & tampalrm)))
return -EBUSY;
break;

case OUT2_RMP:
/**
* OUT2_RMP 路径冲突判定:
* 仅当 OUT2EN 且 OUT2_RMP 已选择该路径时,若 calib 或 tampalrm 已占用则冲突。
*/
if ((cr & STM32_RTC_CR_OUT2EN) &&
(cfgr & STM32_RTC_CFGR_OUT2_RMP) &&
((cr & calib) || (cr & tampalrm)))
return -EBUSY;
break;

default:
return -EINVAL; /**< LSCO 不支持 OUT2(未重映射)路径。 */
}

/** LSCO 仅在 rtc_ck=32768Hz 时有意义,否则输出频率不符合预期。 */
if (clk_get_rate(rtc->rtc_ck) != 32768)
return -ERANGE;

return 0;
}

stm32_rtc_pinmux_action_lsco:选择 LSCO 输出,并注册 gate 时钟供系统引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* @brief 将 RTC 输出复用为 LSCO,并在内核时钟框架中注册 rtc_lsco 门控时钟。
*
* @param pctldev pinctrl 设备。
* @param pin OUT1 或 OUT2_RMP。
*
* @return 0 成功;负 errno 表示不支持、冲突或注册失败。
*/
static int stm32_rtc_pinmux_action_lsco(struct pinctrl_dev *pctldev, unsigned int pin)
{
struct stm32_rtc *rtc = pinctrl_dev_get_drvdata(pctldev);
struct stm32_rtc_registers regs = rtc->data->regs;
struct device *dev = rtc->rtc_dev->dev.parent;
u8 lscoen;
int ret;

if (!rtc->data->has_lsco)
return -EPERM; /**< 仅部分变体提供 LSCO 输出能力。 */

ret = stm32_rtc_pinmux_lsco_available(pctldev, pin); /**< 检查输出路径资源冲突与频率前提。 */
if (ret)
return ret;

/** 选择 CFGR 中的 LSCOEN 位:OUT1 对应 OUT1 使能;OUT2_RMP 对应 OUT2_RMP 使能。 */
lscoen = (pin == OUT1) ? STM32_RTC_CFGR_LSCOEN_OUT1 : STM32_RTC_CFGR_LSCOEN_OUT2_RMP;

/**
* 将 LSCO 输出抽象为一个 gate 时钟:
* - 父时钟名为 rtc_ck;
* - 通过写 CFGR 的 lscoen 位门控输出。
*/
rtc->clk_lsco = clk_register_gate(dev, "rtc_lsco", __clk_get_name(rtc->rtc_ck),
CLK_IGNORE_UNUSED | CLK_IS_CRITICAL,
rtc->base + regs.cfgr, lscoen, 0, NULL);
if (IS_ERR(rtc->clk_lsco))
return PTR_ERR(rtc->clk_lsco);

of_clk_add_provider(dev->of_node, of_clk_src_simple_get, rtc->clk_lsco); /**< 将 rtc_lsco 暴露给设备树时钟消费者。 */

return 0;
}

stm32_rtc_clean_outs / stm32_rtc_check_rif:RTC 输出状态清理与 RIF 访问权限检查


stm32_rtc_clean_outs:清理 RTC 输出选择位,避免与后续 pinmux 冲突

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* @brief 清理 RTC 输出相关配置,使 RTC 不主动驱动 OUT1/OUT2 路径。
*
* @param rtc 驱动私有数据。
*
* @note 该函数用于 probe 阶段,把输出复用寄存器恢复到“无输出源/无校准输出/无防拆输出”的基线,
* 避免历史配置导致后续 pinmux 申请资源时出现冲突或不可预期行为。
*/
static void stm32_rtc_clean_outs(struct stm32_rtc *rtc)
{
struct stm32_rtc_registers regs = rtc->data->regs;
unsigned int cr = readl_relaxed(rtc->base + regs.cr);

cr &= ~STM32_RTC_CR_OSEL; /**< 清除输出源选择,避免 ALARM/其它源被路由到输出。 */
cr &= ~STM32_RTC_CR_TAMPOE; /**< 关闭 tamper 输出使能,释放输出路径。 */
cr &= ~STM32_RTC_CR_COE; /**< 关闭校准输出(calib out),释放输出路径。 */
cr &= ~STM32_RTC_CR_TAMPALRM_TYPE; /**< 清理 tamper/alarm 类型相关配置,避免混合状态。 */
cr &= ~STM32_RTC_CR_OUT2EN; /**< 禁用 OUT2 输出路径(若存在)。 */

stm32_rtc_wpr_unlock(rtc); /**< CR 属于受写保护寄存器,修改前必须解锁。 */
writel_relaxed(cr, rtc->base + regs.cr);
stm32_rtc_wpr_lock(rtc);

/**
* 部分 SoC 变体存在 CFGR(例如 STM32MP 系列),用于控制 LSCO、OUT2_RMP 等。
* 若该寄存器未实现(UNDEF_REG),则不做访问以避免非法地址读写。
*/
if (regs.cfgr != UNDEF_REG) {
unsigned int cfgr = readl_relaxed(rtc->base + regs.cfgr);

cfgr &= ~STM32_RTC_CFGR_LSCOEN; /**< 关闭 LSCO 输出使能(两种路径的组合字段)。 */
cfgr &= ~STM32_RTC_CFGR_OUT2_RMP; /**< 清除 OUT2 重映射选择,恢复默认路径。 */
writel_relaxed(cfgr, rtc->base + regs.cfgr);
}
}

stm32_rtc_check_rif:检查资源隔离配置(CID 与 secure world)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* @brief 检查某个 RTC 资源是否允许当前执行环境访问(RIF 资源隔离)。
*
* @param stm32_rtc 驱动私有数据(包含 RTC 基址)。
* @param res 资源描述:num 用于定位 RXCIDCFGR(n),bit 表示该资源在 SECCFGR 中的安全控制位。
*
* @return 0 表示允许访问;-EACCES 表示拒绝访问。
*
* @note 该检查仅对 data->rif_protected=true 的变体生效(例如带安全隔离的 STM32MP25 等)。
*/
static int stm32_rtc_check_rif(struct stm32_rtc *stm32_rtc,
struct stm32_rtc_rif_resource res)
{
u32 rxcidcfgr = readl_relaxed(stm32_rtc->base + STM32_RTC_RXCIDCFGR(res.num));
u32 seccfgr;

/**
* 若 CFEN 置位,表示启用 CID 过滤:
* 只有 CID 匹配预期值(此处为 CID1)才允许访问该资源,否则拒绝。
*/
if ((rxcidcfgr & STM32_RTC_RXCIDCFGR_CFEN) &&
(FIELD_GET(STM32_RTC_RXCIDCFGR_CID, rxcidcfgr) != STM32_RTC_RXCIDCFGR_CID1))
return -EACCES;

/**
* SECCFGR 的含义是“该资源是否属于 secure world / 是否对非安全世界不可见”。
* - STM32_RTC_SECCFGR_SEC:RTC 整体处于 secure 配置
* - res.bit:某具体资源(如 INIT、ALRA)被标为 secure
*
* 这里用按位或组合判断:只要任一条件成立,就拒绝非安全世界访问。
*/
seccfgr = readl_relaxed(stm32_rtc->base + STM32_RTC_SECCFGR);
if ((seccfgr & STM32_RTC_SECCFGR_SEC) | (seccfgr & res.bit))
return -EACCES;

return 0;
}

stm32_rtc_remove / stm32_rtc_suspend / stm32_rtc_resume:卸载与系统休眠/唤醒链路


stm32_rtc_remove:驱动卸载时释放 LSCO gate、关闭告警中断、关时钟、恢复 DBP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* @brief 平台驱动 remove:撤销运行期副作用并释放硬件资源占用。
*
* @param pdev 平台设备。
*/
static void stm32_rtc_remove(struct platform_device *pdev)
{
struct stm32_rtc *rtc = platform_get_drvdata(pdev);
const struct stm32_rtc_registers *regs = &rtc->data->regs;
unsigned int cr;

/**
* 若 LSCO 功能被 pinmux action 注册过 gate 时钟,这里需要注销,
* 否则时钟框架中会残留该 provider。
*/
if (!IS_ERR_OR_NULL(rtc->clk_lsco))
clk_unregister_gate(rtc->clk_lsco);

/**
* 禁用告警中断输出:
* 这里只清 ALRAIE(中断使能),不必一定清 ALRAE(告警比较功能),
* 因为 remove 之后设备不再向内核上报事件,优先确保 IRQ 不再触发。
*/
stm32_rtc_wpr_unlock(rtc);
cr = readl_relaxed(rtc->base + regs->cr);
cr &= ~STM32_RTC_CR_ALRAIE; /**< 关闭 Alarm A 中断使能。 */
writel_relaxed(cr, rtc->base + regs->cr);
stm32_rtc_wpr_lock(rtc);

/** 关闭 RTC 内部时钟。 */
clk_disable_unprepare(rtc->rtc_ck);

/** H7 变体存在 pclk:RTC 寄存器接口时钟,关闭以降低功耗并释放资源。 */
if (rtc->data->has_pclk)
clk_disable_unprepare(rtc->pclk);

/**
* 若该变体要求 DBP(备份域写使能)由驱动打开,则在 remove 时关闭,
* 以恢复默认的写保护策略,降低其他模块误写备份域寄存器风险。
*/
if (rtc->data->need_dbp)
regmap_update_bits(rtc->dbp, rtc->dbp_reg, rtc->dbp_mask, 0);
}

stm32_rtc_suspend:系统休眠前关闭 pclk(若存在)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @brief 系统休眠路径:关闭 pclk(若存在)。
*
* @param dev 设备对象。
* @return 0 成功。
*
* @note 这里不关闭 rtc_ck:因为 rtc_ck 通常是 LSE/LSI 等低速时钟,
* 需要在睡眠中保持 RTC 计时与闹钟唤醒能力。
*/
static int stm32_rtc_suspend(struct device *dev)
{
struct stm32_rtc *rtc = dev_get_drvdata(dev);

if (rtc->data->has_pclk)
clk_disable_unprepare(rtc->pclk); /**< 关闭寄存器接口时钟,减少睡眠功耗。 */

return 0;
}

stm32_rtc_resume:系统唤醒后开启 pclk,并等待日历寄存器同步完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* @brief 系统唤醒路径:恢复 pclk 并等待 RTC 日历寄存器同步。
*
* @param dev 设备对象。
* @return 0 成功;负 errno 表示同步失败。
*
* @note wait_sync 的目的:清 RSF 再等待 RSF 置位,确保日历寄存器与 RTC 域同步完成。
*/
static int stm32_rtc_resume(struct device *dev)
{
struct stm32_rtc *rtc = dev_get_drvdata(dev);
int ret = 0;

if (rtc->data->has_pclk) {
ret = clk_prepare_enable(rtc->pclk); /**< 恢复寄存器接口时钟,否则无法可靠访问 RTC 寄存器。 */
if (ret)
return ret;
}

ret = stm32_rtc_wait_sync(rtc); /**< 确认日历寄存器同步,避免唤醒后读到未同步的 TR/DR。 */
if (ret < 0) {
if (rtc->data->has_pclk)
clk_disable_unprepare(rtc->pclk); /**< 同步失败时回滚 pclk 状态,避免半初始化状态。 */
return ret;
}

return ret;
}