Linux RTC 驱动框架

目录

  • 一、实时时钟(RTC)介绍
    • [1.1 概述](#1.1 概述)
    • [1.2 功能](#1.2 功能)
    • [1.3 应用场景](#1.3 应用场景)
    • [1.4 工作原理](#1.4 工作原理)
    • [1.5 对外接口](#1.5 对外接口)
    • [1.6 常见 RTC 芯片](#1.6 常见 RTC 芯片)
    • [1.7 在 Linux 系统中的应用](#1.7 在 Linux 系统中的应用)
    • [1.8 注意事项](#1.8 注意事项)
  • [二、Linux 内核 RTC 驱动框架](#二、Linux 内核 RTC 驱动框架)
    • [2.1 相关源码文件介绍](#2.1 相关源码文件介绍)
    • [2.2 核心数据结构](#2.2 核心数据结构)
      • [2.2.1 struct rtc_device](#2.2.1 struct rtc_device)
      • [2.2.2 rtc_class_ops](#2.2.2 rtc_class_ops)
      • [2.2.3 struct rtc_timer](#2.2.3 struct rtc_timer)
    • [2.4 RTC驱动实例分析](#2.4 RTC驱动实例分析)
    • [2.5 提供带有中文注释的drivers/rtc/源码](#2.5 提供带有中文注释的drivers/rtc/源码)
    • [2.6 APP层操作RTC](#2.6 APP层操作RTC)
  • 三、参考资料

一、实时时钟(RTC)介绍

1.1 概述

实时时钟(Real-Time Clock,简称 RTC)是一种能够持续记录时间的电子设备。它通常用于计算机、嵌入式系统和其他需要准确时间记录的设备中。RTC 可以在系统关机或断电的情况下继续运行,因此即使在系统重启后也能保持准确的时间。

1.2 功能

  • 时间记录: 记录当前的日期和时间。
  • 闹钟功能: 可以设置特定的时间点触发中断,用于唤醒系统或执行特定任务。
  • 周期性中断:可以设置周期性的中断,用于定时任务。
  • 电源管理: 通常由电池供电,确保在主电源断开时仍能正常工作。

1.3 应用场景

  • 个人电脑: 用于记录 BIOS/UEFI 中的时间。
  • 嵌入式系统: 用于工业控制、医疗设备、汽车电子等需要高精度时间的应用。
  • 服务器:用于日志记录、定时任务调度等。 物联网设备: 用于时间同步和定时任务。

1.4 工作原理

  • 时钟源: RTC 通常使用低频晶体振荡器(如 32.768 kHz)作为时钟源,这种频率的振荡器功耗低且精度高。
  • 计数器:内部计数器根据时钟源的脉冲进行计数,记录秒、分钟、小时、日、月和年。
  • 寄存器: 时间和日期信息存储在寄存器中,可以通过 I2C、SPI 或其他接口读取和写入。
  • 中断: 当设置的闹钟时间到达或周期性中断条件满足时,RTC 会触发中断信号。

1.5 对外接口

  • I2C: 常见的通信接口,用于与主控制器通信。
  • SPI: 另一种常见的通信接口,适用于高速通信。
  • GPIO: 一些简单的 RTC设备可能使用 GPIO 进行通信。

1.6 常见 RTC 芯片

  • DS1307: 由 Maxim 生产,广泛用于各种嵌入式系统.
  • PCF8563: 由 NXP 生产,具有低功耗特性。
  • MCP79410:由 Microchip 生产,集成了 EEPROM 和时钟功能。
  • RV-1805: 由 Epson 生产,具有高精度和低功耗特性。

1.7 在 Linux 系统中的应用

  • RTC 驱动: Linux 内核提供了 RTC 驱动框架,用于管理和操作 RTC 设备。
  • 用户空间工具: hwclock命令用于读取和设置硬件时钟,date 命令用于读取和设置系统时间。
  • 系统启动: 在系统启动时,通常会从 RTC 读取时间并设置系统时间。

1.8 注意事项

  • 电池寿命: RTC 通常由纽扣电池供电,需要注意电池的寿命和更换。
  • 精度校准: 由于环境温度等因素的影响,RTC 的时间可能会有偏差,需要定期校准。
  • 中断处理: 闹钟和周期性中断需要正确处理,避免影响系统的正常运行。

二、Linux 内核 RTC 驱动框架

在内核源码中的路径:drivers/rtc

2.1 相关源码文件介绍

  • class.c:为底层驱动提供 register 与 unregister 接口用于 RTC 设备的注册/注销。初始化 RTC设备结构、sysfs、proc;
  • interface.c:提供用户程序与 RTC 的接口函数;
  • dev.c:将 RTC设备抽象为通用的字符设备,提供文件操作函数(struct file_operations rtc_dev_fops 的成员),可认为是一个字符设备驱动实现;
  • sysfs.c:管理 RTC 设备的 sysfs 属性,获取 RTC 设备名、日期、时间等;
  • proc.c:管理 RTC 设备的 procfs 属性,提供中断状态和标志查询;
  • lib.c:提供 RTC、Data 和 Time之间的转换函数;
  • rtc-xxx.c:不同 RTC 芯片的实际驱动;
  • rtc.h: 定义了 RTC 设备的数据结构和操作接口。

2.2 核心数据结构

2.2.1 struct rtc_device

c 复制代码
/**
 * struct rtc_device - 实时时钟设备结构体
 *
 * 该结构体表示实时时钟 (RTC) 设备的信息和状态。
 * 包括设备初始化、操作函数指针、中断处理和定时器相关信息。
 */
struct rtc_device {
	// 基本设备结构体
	struct device dev;
	// 设备所属模块的所有者
	struct module *owner;

	// 设备标识号
	int id;

	// 指向包含 RTC 设备操作函数的结构体的指针
	const struct rtc_class_ops *ops;
	// 保护操作函数的互斥锁,确保线程安全
	struct mutex ops_lock;

	// RTC 字符设备结构体
	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; 
	// 标记是否启用了周期性中断功能
	int pie_enabled;
	// 工作结构体,用于处理中断
	struct work_struct irqwork;
	// 有些硬件不支持 UIE 模式
	int uie_unsupported;

	// 设置 RTC 时钟所需的时间(纳秒)。这会影响设置操作的调用时间。偏移量:
	//   - 0.5 秒会在墙上时间 10.0 秒时在 9.5 秒调用 RTC 设置
	//   - 1.5 秒会在墙上时间 10.0 秒时在 8.5 秒调用 RTC 设置
	//   - -0.5 秒会在墙上时间 10.0 秒时在 10.5 秒调用 RTC 设置
	long set_offset_nsec;

	// 标记设备是否已注册
	bool registered;

	// 旧 ABI 支持
	bool nvram_old_abi;
	struct bin_attribute *nvram;

	// RTC 范围的最小值
	time64_t range_min;
	// RTC 范围的最大值
	timeu64_t range_max;
	// 开始秒数
	time64_t start_secs;
	// 偏移秒数
	time64_t offset_secs;
	// 标记是否设置了开始时间
	bool set_start_time;

#ifdef CONFIG_RTC_INTF_DEV_UIE_EMUL
	// 工作结构体,用于处理 UIE 任务
	struct work_struct uie_task;
	// UIE 定时器
	struct timer_list uie_timer;
	// 这些字段受 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

	ANDROID_KABI_RESERVE(1);
};

UIE 是 "Update Interrupt Enable" 的缩写,它是一种实时时钟(RTC)设备的功能。具体来说,UIE 模式用于在 RTC 时间更新时生成中断。以下是 UIE 模式的详细解释:

  1. 时间更新中断

    • 当 RTC 的时间每秒钟更新时,会触发一个中断。这个中断可以被用户空间程序捕获和处理。
    • 例如,某些应用程序可能需要在每一秒的边界上执行特定的操作,UIE 模式可以提供这种精确的时间同步。
  2. 应用场景

    • 日志记录:在需要精确时间戳的日志记录系统中,UIE 中断可以确保日志条目的时间戳非常准确。
    • 定时任务:需要在固定时间间隔内执行的任务,可以通过 UIE 中断来触发。
  3. 实现细节

    • struct rtc_device 结构体中,有几个与 UIE 相关的字段:
      • int uie_unsupported:标记某些硬件是否不支持 UIE 模式。
      • long set_offset_nsec:设置 RTC 时钟所需的时间偏移量,影响中断的触发时间。
      • struct work_struct uie_taskstruct timer_list uie_timer:用于处理 UIE 中断任务和定时器。
      • unsigned int uie_irq_active:1:标记 UIE 中断是否激活。
      • unsigned int stop_uie_polling:1:标记是否停止 UIE 轮询。
      • unsigned int uie_task_active:1:标记 UIE 任务是否激活。
      • unsigned int uie_timer_active:1:标记 UIE 定时器是否激活。
  4. 配置和启用

    • UIE 模式通常需要在内核配置中启用,例如通过 CONFIG_RTC_INTF_DEV_UIE_EMUL 配置选项。
    • 应用程序可以通过 RTC 设备文件接口(如 /dev/rtc0)来启用或禁用 UIE 模式。

总结来说,UIE 模式是为了在 RTC 时间更新时生成中断,以便应用程序能够精确地响应时间变化。这对于需要高精度时间同步的应用非常有用。

2.2.2 rtc_class_ops

c 复制代码
/*
 * RTC 类操作结构体,定义了与 RTC 设备交互的各种方法。
 * 这些方法中的 `device` 参数是指物理设备,该设备位于硬件所在的总线上(如 I2C、Platform、SPI 等),
 * 并且已传递给 `rtc_device_register()` 函数。通常,`driver_data` 包含设备状态,包括 RTC 的 `rtc_device` 指针。
 *
 * 大多数这些方法在调用时会持有 `rtc_device.ops_lock` 锁,通过 `rtc_*(struct rtc_device *, ...)` 调用。
 *
 * 当前的例外情况主要是文件系统钩子:
 *   - `proc()` 钩子用于 procfs
 */
struct rtc_class_ops {
	/*
	 * ioctl 方法,用于处理 RTC 设备的 I/O 控制命令。
	 * @param dev: RTC 设备
	 * @param cmd: 命令码
	 * @param arg: 命令参数
	 * @return: 成功返回 0,失败返回负错误码
	 */
	int (*ioctl)(struct device *dev, unsigned int cmd, unsigned long arg);

	/*
	 * read_time 方法,用于读取 RTC 设备的时间。
	 * @param dev: RTC 设备
	 * @param tm: 存储读取时间的结构体指针
	 * @return: 成功返回 0,失败返回负错误码
	 */
	int (*read_time)(struct device *dev, struct rtc_time *tm);

	/*
	 * set_time 方法,用于设置 RTC 设备的时间。
	 * @param dev: RTC 设备
	 * @param tm: 包含要设置时间的结构体指针
	 * @return: 成功返回 0,失败返回负错误码
	 */
	int (*set_time)(struct device *dev, struct rtc_time *tm);

	/*
	 * read_alarm 方法,用于读取 RTC 设备的闹钟设置。
	 * @param dev: RTC 设备
	 * @param alrm: 存储读取闹钟设置的结构体指针
	 * @return: 成功返回 0,失败返回负错误码
	 */
	int (*read_alarm)(struct device *dev, struct rtc_wkalrm *alrm);

	/*
	 * set_alarm 方法,用于设置 RTC 设备的闹钟。
	 * @param dev: RTC 设备
	 * @param alrm: 包含要设置闹钟的结构体指针
	 * @return: 成功返回 0,失败返回负错误码
	 */
	int (*set_alarm)(struct device *dev, struct rtc_wkalrm *alrm);

	/*
	 * proc 方法,用于处理 procfs 文件系统的请求。
	 * @param dev: RTC 设备
	 * @param seq: 序列文件指针
	 * @return: 成功返回 0,失败返回负错误码
	 */
	int (*proc)(struct device *dev, struct seq_file *seq);

	/*
	 * alarm_irq_enable 方法,用于启用或禁用 RTC 设备的闹钟中断。
	 * @param dev: RTC 设备
	 * @param enabled: 启用或禁用标志
	 * @return: 成功返回 0,失败返回负错误码
	 */
	int (*alarm_irq_enable)(struct device *dev, unsigned int enabled);

	/*
	 * read_offset 方法,用于读取 RTC 设备的时间偏移量。
	 * @param dev: RTC 设备
	 * @param offset: 存储读取时间偏移量的指针
	 * @return: 成功返回 0,失败返回负错误码
	 */
	int (*read_offset)(struct device *dev, long *offset);

	/*
	 * set_offset 方法,用于设置 RTC 设备的时间偏移量。
	 * @param dev: RTC 设备
	 * @param offset: 要设置的时间偏移量
	 * @return: 成功返回 0,失败返回负错误码
	 */
	int (*set_offset)(struct device *dev, long offset);

	ANDROID_KABI_RESERVE(1);
};

2.2.3 struct rtc_timer

c 复制代码
/**
 * @brief RTC定时器结构体
 * 
 * 该结构体用于表示RTC(实时时钟)定时器,包含了定时器所需的信息和配置,如定时周期、回调函数等。
 */
struct rtc_timer {
    /**
     * @brief 定时器队列节点
     * 
     * 该字段用于将定时器插入到定时器队列中,以管理多个定时器的到期时间。
     */
    struct timerqueue_node node;

    /**
     * @brief 定时周期
     * 
     * 该字段表示定时器的周期时间,使用ktime_t类型来存储时间间隔。
     */
    ktime_t period;

    /**
     * @brief 定时器回调函数指针
     * 
     * 当定时器到期时,将调用此字段指向的函数。该函数将接收一个指向RTC设备的指针作为参数。
     */
    void (*func)(struct rtc_device *rtc);

    /**
     * @brief 指向RTC设备的指针
     * 
     * 该字段用于关联定时器和特定的RTC设备,使得定时器可以操作或访问该设备。
     */
    struct rtc_device *rtc;

    /**
     * @brief 定时器启用状态
     * 
     * 该字段用于指示定时器是否已启用。当定时器被禁用时,其值为0;当定时器被启用时,其值为非0。
     */
    int enabled;
};

2.4 RTC驱动实例分析

drivers/rtc/rtc-rk808.c

c 复制代码
// SPDX-License-Identifier: GPL-2.0-only
/*
 * RTC driver for Rockchip RK808
 *
 * Copyright (c) 2014, Fuzhou Rockchip Electronics Co., Ltd
 *
 * Author: Chris Zhong <zyw@rock-chips.com>
 * Author: Zhang Qing <zhangqing@rock-chips.com>
 */

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/rtc.h>
#include <linux/bcd.h>
#include <linux/mfd/rk808.h>
#include <linux/platform_device.h>
#include <linux/i2c.h>

/* RTC_CTRL_REG bitfields */
#define BIT_RTC_CTRL_REG_STOP_RTC_M BIT(0)

/* RK808 has a shadowed register for saving a "frozen" RTC time.
 * When user setting "GET_TIME" to 1, the time will save in this shadowed
 * register. If set "READSEL" to 1, user read rtc time register, actually
 * get the time of that moment. If we need the real time, clr this bit.
 */
#define BIT_RTC_CTRL_REG_RTC_GET_TIME BIT(6)
#define BIT_RTC_CTRL_REG_RTC_READSEL_M BIT(7)
#define BIT_RTC_INTERRUPTS_REG_IT_ALARM_M BIT(3)
#define RTC_STATUS_MASK 0xFE
#define RTC_ALARM_STATUS BIT(6)

#define SECONDS_REG_MSK 0x7F
#define MINUTES_REG_MAK 0x7F
#define HOURS_REG_MSK 0x3F
#define DAYS_REG_MSK 0x3F
#define MONTHS_REG_MSK 0x1F
#define YEARS_REG_MSK 0xFF
#define WEEKS_REG_MSK 0x7

#define RTC_NEED_TRANSITIONS BIT(0)
/* REG_SECONDS_REG through REG_YEARS_REG is how many registers? */

#define NUM_TIME_REGS (RK808_WEEKS_REG - RK808_SECONDS_REG + 1)
#define NUM_ALARM_REGS (RK808_ALARM_YEARS_REG - RK808_ALARM_SECONDS_REG + 1)

struct rk_rtc_compat_reg {
	unsigned int ctrl_reg;
	unsigned int status_reg;
	unsigned int alarm_seconds_reg;
	unsigned int int_reg;
	unsigned int seconds_reg;
};

struct rk808_rtc {
	struct rk808 *rk808;
	struct rtc_device *rtc;
	struct rk_rtc_compat_reg *creg;
	int irq;
	unsigned int flag;
};

/*
 * 该函数用于处理 Rockchip 日历与公历(Gregorian calendar)之间的转换。
 * Rockchip 日历在 RK808 中将 11 月视为有 31 天。我们定义 2016 年 1 月 1 日为两个日历同步的基准日期,
 * 并根据该日期进行其他日期的相对转换。
 * 注意:其他系统软件(例如固件)在读取相同硬件时,必须实现完全相同的转换算法,并使用相同的基准日期。
 *
 * @param tm 指向 rtc_time 结构体的指针,包含待转换的时间信息
 *
 * @return 返回一个 time64_t 类型的值,表示从基准日期(2016年1月1日)开始的年份偏移量,
 *         如果当前月份大于11月,则额外加1,以补偿11月多出的一天。
 */
static time64_t nov2dec_transitions(struct rtc_time *tm)
{
	// 计算年份偏移量,并检查是否需要补偿11月多出的一天
	return (tm->tm_year + 1900) - 2016 + (tm->tm_mon + 1 > 11 ? 1 : 0);
}

/**
 * 将 Rockchip 日期表示转换为公历(Gregorian)日期。
 * 
 * 此函数用于处理从 Rockchip 特定日期表示法转换为标准公历的逻辑。
 * 特别地,它处理从11月31日转换为12月1日的特殊情况。
 * 转换过程分为两个主要步骤:
 * 1. 首先,使用 `rtc_tm_to_time64` 函数将输入的 Rockchip 日期转换为 Unix 时间戳。
 * 2. 然后,根据自输入日期以来发生的11月31日到12月1日的转换次数调整时间戳。
 *    这种调整是必要的,因为在 Rockchip 表示法中,11月31日被视为12月1日。
 *    调整后的时间戳再使用 `rtc_time64_to_tm` 函数转换回公历格式。
 * 
 * @param tm 指向 `rtc_time` 结构的指针,该结构包含 Rockchip 格式的日期和时间信息。
 *           经过转换后,此结构将更新为对应的公历日期和时间。
 */
static void rockchip_to_gregorian(struct rtc_time *tm)
{
	// 将 Rockchip 日期和时间转换为 Unix 时间戳
	time64_t time = rtc_tm_to_time64(tm);

	// 根据11月31日到12月1日的转换次数调整时间戳
	rtc_time64_to_tm(time + nov2dec_transitions(tm) * 86400, tm);
}

/**
 * 将公历日期转换为Rockchip格式的日期
 * 此函数旨在处理特定的日期转换问题,即将公历日期转换为Rockchip硬件时钟可以理解的格式
 * 其中包括处理从11月到12月的过渡,这是Rockchip硬件时钟处理日期的一种特殊需求
 * 
 * @param tm 指向RTC时间结构的指针,该结构包含日期和时间信息
 */
static void gregorian_to_rockchip(struct rtc_time *tm)
{
	// 计算从11月到12月的过渡天数
	time64_t extra_days = nov2dec_transitions(tm);
	// 将RTC时间结构转换为自1970年1月1日以来的秒数
	time64_t time = rtc_tm_to_time64(tm);
	// 根据过渡天数调整时间,并将结果转换回RTC时间结构
	rtc_time64_to_tm(time - extra_days * 86400, tm);

	/* 
	* 如果调整后的日期导致我们回到了11月(这可能发生在特定的年份),则进行补偿
	* 这种补偿机制可以确保日期正确地向前推进,即使在复杂的闰年情况下也是如此
	* (该补偿机制将在2381年之前有效)
	*/
	if (nov2dec_transitions(tm) < extra_days) {
		// 如果当前月份是11月,则简单地将日期推进一天
		if (tm->tm_mon + 1 == 11)
			tm->tm_mday++; /* This may result in 31! */
		// 否则,重新计算时间,确保日期正确地反映了从11月到12月的过渡
		else
			rtc_time64_to_tm(time - (extra_days - 1) * 86400, tm);
	}
}

/* Read current time and date in RTC */
/**
 * 从RTC设备读取当前时间。
 *
 * 此函数通过I2C总线与RTC芯片通信,读取当前的时间和日期,并将读取的数据转换为可使用的格式。
 *
 * @param dev RTC设备的device结构指针。
 * @param tm 用于存储读取到的时间和日期信息的rtc_time结构指针。
 *
 * @return 返回0表示成功,返回负值表示失败。
 */

static int rk808_rtc_readtime(struct device *dev, struct rtc_time *tm)
{
	struct rk808_rtc *rk808_rtc = dev_get_drvdata(dev);
	struct rk808 *rk808 = rk808_rtc->rk808;
	u8 rtc_data[NUM_TIME_REGS];
	int ret;

	/* 强制立即更新影子寄存器 */
	ret = regmap_update_bits(rk808->regmap, rk808_rtc->creg->ctrl_reg,
				 BIT_RTC_CTRL_REG_RTC_GET_TIME,
				 BIT_RTC_CTRL_REG_RTC_GET_TIME);
	if (ret) {
		dev_err(dev, "Failed to update bits rtc_ctrl: %d\n", ret);
		return ret;
	}

	/*
	 * 设置GET_TIME位后,不能立即读取RTC时间。需要等待大约31.25微秒,
	 * 这是32kHz时钟的一个周期。如果在这里清除GET_TIME位,则I2C传输时间
	 * 肯定超过31.25微秒:在400kHz总线频率下为16 * 2.5微秒。
	 */
	ret = regmap_update_bits(rk808->regmap, rk808_rtc->creg->ctrl_reg,
				 BIT_RTC_CTRL_REG_RTC_GET_TIME, 0);
	if (ret) {
		dev_err(dev, "Failed to update bits rtc_ctrl: %d\n", ret);
		return ret;
	}

	/* 批量读取RTC数据 */
	ret = regmap_bulk_read(rk808->regmap, rk808_rtc->creg->seconds_reg,
			       rtc_data, NUM_TIME_REGS);
	if (ret) {
		dev_err(dev, "Failed to bulk read rtc_data: %d\n", ret);
		return ret;
	}

	/* 将BCD编码的时间数据转换为二进制并填充到tm结构中 */
	tm->tm_sec = bcd2bin(rtc_data[0] & SECONDS_REG_MSK);
	tm->tm_min = bcd2bin(rtc_data[1] & MINUTES_REG_MAK);
	tm->tm_hour = bcd2bin(rtc_data[2] & HOURS_REG_MSK);
	tm->tm_mday = bcd2bin(rtc_data[3] & DAYS_REG_MSK);
	tm->tm_mon = (bcd2bin(rtc_data[4] & MONTHS_REG_MSK)) - 1;
	tm->tm_year = (bcd2bin(rtc_data[5] & YEARS_REG_MSK)) + 100;
	tm->tm_wday = bcd2bin(rtc_data[6] & WEEKS_REG_MSK);

	/* 如果需要转换,调用rockchip_to_gregorian进行转换 */
	if (rk808_rtc->flag & RTC_NEED_TRANSITIONS)
		rockchip_to_gregorian(tm);

	/* 打印调试信息 */
	dev_dbg(dev, "RTC date/time %4d-%02d-%02d(%d) %02d:%02d:%02d\n",
		1900 + tm->tm_year, tm->tm_mon + 1, tm->tm_mday, tm->tm_wday,
		tm->tm_hour, tm->tm_min, tm->tm_sec);

	return ret;
}

/**
 * rk808_rtc_set_time - 设置RTC(实时时钟)的时间和日期
 * @dev: 设备结构体指针,代表RTC设备
 * @tm: 指向rtc_time结构体的指针,包含要设置的日期和时间信息
 * 
 * 此函数将给定的日期和时间信息写入到RTC芯片中,以更新RTC的当前时间和日期设置
 * 它首先将日期和时间信息转换为BCD格式,然后通过regmap接口将这些信息写入到RTC的相应寄存器中
 * 
 * 返回值:
 * 成功时返回0,失败时返回负的错误代码
 */
static int rk808_rtc_set_time(struct device *dev, struct rtc_time *tm)
{
	// 获取RTC设备的驱动数据
	struct rk808_rtc *rk808_rtc = dev_get_drvdata(dev);
	// 获取RK808芯片的数据
	struct rk808 *rk808 = rk808_rtc->rk808;
	// 定义一个数组来存储RTC数据
	u8 rtc_data[NUM_TIME_REGS];
	// 定义返回值变量
	int ret;

	// 调试信息,显示正在设置的日期和时间
	dev_dbg(dev, "set RTC date/time %4d-%02d-%02d(%d) %02d:%02d:%02d\n",
		1900 + tm->tm_year, tm->tm_mon + 1, tm->tm_mday, tm->tm_wday,
		tm->tm_hour, tm->tm_min, tm->tm_sec);

	// 如果需要转换,则将格里高利日期转换为适合RTC的格式
	if (rk808_rtc->flag & RTC_NEED_TRANSITIONS)
		gregorian_to_rockchip(tm);

	// 将时间数据从二进制转换为BCD格式,并存储到rtc_data数组中
	rtc_data[0] = bin2bcd(tm->tm_sec);
	rtc_data[1] = bin2bcd(tm->tm_min);
	rtc_data[2] = bin2bcd(tm->tm_hour);
	rtc_data[3] = bin2bcd(tm->tm_mday);
	rtc_data[4] = bin2bcd(tm->tm_mon + 1);
	rtc_data[5] = bin2bcd(tm->tm_year - 100);
	rtc_data[6] = bin2bcd(tm->tm_wday);

	// 停止RTC,以便更新RTC寄存器
	ret = regmap_update_bits(rk808->regmap, rk808_rtc->creg->ctrl_reg,
				 BIT_RTC_CTRL_REG_STOP_RTC_M,
				 BIT_RTC_CTRL_REG_STOP_RTC_M);
	if (ret) {
		dev_err(dev, "Failed to update RTC control: %d\n", ret);
		return ret;
	}

	// 将rtc_data数组中的数据批量写入到RTC寄存器中
	ret = regmap_bulk_write(rk808->regmap, rk808_rtc->creg->seconds_reg,
				rtc_data, NUM_TIME_REGS);
	if (ret) {
		dev_err(dev, "Failed to bull write rtc_data: %d\n", ret);
		return ret;
	}
	// 再次启动RTC
	ret = regmap_update_bits(rk808->regmap, rk808_rtc->creg->ctrl_reg,
				 BIT_RTC_CTRL_REG_STOP_RTC_M, 0);
	if (ret) {
		dev_err(dev, "Failed to update RTC control: %d\n", ret);
		return ret;
	}
	return 0;
}

/**
 * rk808_rtc_readalarm - 读取RTC报警时间
 * @dev: 设备结构体指针
 * @alrm: RTC报警时间结构体指针
 *
 * 此函数从RTC中读取报警时间,并将其填充到alrm参数中。它首先读取报警时间寄存器,
 * 然后根据寄存器的值更新alrm结构体中的时间字段。此外,它还会读取中断寄存器以确定
 * 报警是否已启用。
 *
 * 返回值: 成功时返回0,失败时返回负错误代码
 */
static int rk808_rtc_readalarm(struct device *dev, struct rtc_wkalrm *alrm)
{
	// 获取RTC设备的驱动数据
	struct rk808_rtc *rk808_rtc = dev_get_drvdata(dev);
	// 获取RK808芯片结构体指针
	struct rk808 *rk808 = rk808_rtc->rk808;
	// 定义一个数组来存储报警时间寄存器的值
	u8 alrm_data[NUM_ALARM_REGS];
	// 定义一个变量来存储中断寄存器的值
	uint32_t int_reg;
	// 定义一个变量来存储函数执行结果
	int ret;

	// 从RTC中读取报警时间寄存器的值
	ret = regmap_bulk_read(rk808->regmap,
			       rk808_rtc->creg->alarm_seconds_reg, alrm_data,
			       NUM_ALARM_REGS);
	if (ret) {
		// 如果读取失败,打印错误信息并返回错误代码
		dev_err(dev, "Failed to read RTC alarm date REG: %d\n", ret);
		return ret;
	}

	// 将读取的寄存器值转换为报警时间
	alrm->time.tm_sec = bcd2bin(alrm_data[0] & SECONDS_REG_MSK);
	alrm->time.tm_min = bcd2bin(alrm_data[1] & MINUTES_REG_MAK);
	alrm->time.tm_hour = bcd2bin(alrm_data[2] & HOURS_REG_MSK);
	alrm->time.tm_mday = bcd2bin(alrm_data[3] & DAYS_REG_MSK);
	alrm->time.tm_mon = (bcd2bin(alrm_data[4] & MONTHS_REG_MSK)) - 1;
	alrm->time.tm_year = (bcd2bin(alrm_data[5] & YEARS_REG_MSK)) + 100;

	// 如果需要转换,将Rockchip日历时间转换为公历时间
	if (rk808_rtc->flag & RTC_NEED_TRANSITIONS)
		rockchip_to_gregorian(&alrm->time);

	// 读取中断寄存器的值以确定报警是否已启用
	ret = regmap_read(rk808->regmap, rk808_rtc->creg->int_reg, &int_reg);
	if (ret) {
		// 如果读取失败,打印错误信息并返回错误代码
		dev_err(dev, "Failed to read RTC INT REG: %d\n", ret);
		return ret;
	}

	// 打印调试信息,显示读取的报警时间
	dev_dbg(dev, "alrm read RTC date/time %ptRd(%d) %ptRt\n", &alrm->time,
		alrm->time.tm_wday, &alrm->time);

	// 根据中断寄存器的值设置报警启用状态
	alrm->enabled = (int_reg & BIT_RTC_INTERRUPTS_REG_IT_ALARM_M) ? 1 : 0;

	// 函数执行成功,返回0
	return 0;
}

/**
 * 停止RTC闹钟。
 * 
 * 该函数通过清除相应的中断使能位和闹钟状态位来停止RTC的闹钟功能。主要用于禁用RTC的闹钟功能。
 * 
 * @param rk808_rtc 指向rk808_rtc结构的指针,包含RTC操作所需的信息。
 * @return 成功返回0,失败返回负的错误码。
 */

static int rk808_rtc_stop_alarm(struct rk808_rtc *rk808_rtc)
{
	struct rk808 *rk808 = rk808_rtc->rk808;
	int ret;

	/* 禁用RTC闹钟中断 */
	ret = regmap_update_bits(rk808->regmap, rk808_rtc->creg->int_reg,
				 BIT_RTC_INTERRUPTS_REG_IT_ALARM_M, 0);

	/*
	 * 必须在闹钟触发1秒后或禁用闹钟后清除RTC闹钟状态(BIT(6))。
	 */
	ret = regmap_write(rk808->regmap, rk808_rtc->creg->status_reg,
			   RTC_ALARM_STATUS);
	return ret;
}

static int rk808_rtc_start_alarm(struct rk808_rtc *rk808_rtc)
{
	struct rk808 *rk808 = rk808_rtc->rk808;
	int ret;

	ret = regmap_update_bits(rk808->regmap, rk808_rtc->creg->int_reg,
				 BIT_RTC_INTERRUPTS_REG_IT_ALARM_M,
				 BIT_RTC_INTERRUPTS_REG_IT_ALARM_M);

	return ret;
}

/**
 * rk808_rtc_setalarm - 设置RTC闹钟
 * @dev: 设备结构体指针
 * @alrm: 闹钟数据结构指针,包含闹钟时间和是否启用闹钟的信息
 * 
 * 此函数负责将给定的闹钟时间设置到RTC芯片中,并根据alrm->enabled决定是否启用闹钟。
 * 它首先停止当前的闹钟,然后将时间数据从二进制转换为BCD格式,并写入到RTC的相关寄存器中。
 * 如果需要转换,会将时间从格里高利历转换为适合RTC芯片的格式。
 * 
 * 返回值: 0表示成功,负值表示出错
 */
static int rk808_rtc_setalarm(struct device *dev, struct rtc_wkalrm *alrm)
{
	// 获取RTC设备的私有数据结构
	struct rk808_rtc *rk808_rtc = dev_get_drvdata(dev);
	// 获取主设备结构体指针
	struct rk808 *rk808 = rk808_rtc->rk808;
	// 定义一个数组来存储闹钟数据
	u8 alrm_data[NUM_ALARM_REGS];
	int ret;

	// 停止当前的闹钟
	ret = rk808_rtc_stop_alarm(rk808_rtc);
	if (ret) {
		// 如果停止闹钟失败,打印错误信息并返回错误码
		dev_err(dev, "Failed to stop alarm: %d\n", ret);
		return ret;
	}
	// 打印设置的闹钟时间信息
	dev_dbg(dev, "alrm set RTC date/time %ptRd(%d) %ptRt\n", &alrm->time,
		alrm->time.tm_wday, &alrm->time);

	// 如果需要转换,将时间从格里高利历转换为适合RTC芯片的格式
	if (rk808_rtc->flag & RTC_NEED_TRANSITIONS)
		gregorian_to_rockchip(&alrm->time);

	// 将时间数据从二进制转换为BCD格式,并存储到数组中
	alrm_data[0] = bin2bcd(alrm->time.tm_sec);
	alrm_data[1] = bin2bcd(alrm->time.tm_min);
	alrm_data[2] = bin2bcd(alrm->time.tm_hour);
	alrm_data[3] = bin2bcd(alrm->time.tm_mday);
	alrm_data[4] = bin2bcd(alrm->time.tm_mon + 1);
	alrm_data[5] = bin2bcd(alrm->time.tm_year - 100);

	// 将闹钟数据写入到RTC的相关寄存器中
	ret = regmap_bulk_write(rk808->regmap,
				rk808_rtc->creg->alarm_seconds_reg, alrm_data,
				NUM_ALARM_REGS);
	if (ret) {
		// 如果写入失败,打印错误信息并返回错误码
		dev_err(dev, "Failed to bulk write: %d\n", ret);
		return ret;
	}
	// 如果闹钟被启用,启动闹钟
	if (alrm->enabled) {
		ret = rk808_rtc_start_alarm(rk808_rtc);
		if (ret) {
			// 如果启动闹钟失败,打印错误信息并返回错误码
			dev_err(dev, "Failed to start alarm: %d\n", ret);
			return ret;
		}
	}
	// 返回成功
	return 0;
}

static int rk808_rtc_alarm_irq_enable(struct device *dev, unsigned int enabled)
{
	struct rk808_rtc *rk808_rtc = dev_get_drvdata(dev);

	if (enabled)
		return rk808_rtc_start_alarm(rk808_rtc);

	return rk808_rtc_stop_alarm(rk808_rtc);
}

/*
 * We will just handle setting the frequency and make use the framework for
 * reading the periodic interupts.
 *
 * @freq: Current periodic IRQ freq:
 * bit 0: every second
 * bit 1: every minute
 * bit 2: every hour
 * bit 3: every day
 */
static irqreturn_t rk808_alarm_irq(int irq, void *data)
{
	struct rk808_rtc *rk808_rtc = data;
	struct rk808 *rk808 = rk808_rtc->rk808;
	struct i2c_client *client = rk808->i2c;
	int ret;

	ret = regmap_write(rk808->regmap, rk808_rtc->creg->status_reg,
			   RTC_STATUS_MASK);
	if (ret) {
		dev_err(&client->dev, "%s:Failed to update RTC status: %d\n",
			__func__, ret);
		return ret;
	}

	rtc_update_irq(rk808_rtc->rtc, 1, RTC_IRQF | RTC_AF);
	dev_dbg(&client->dev, "%s:irq=%d\n", __func__, irq);
	return IRQ_HANDLED;
}

static const struct rtc_class_ops rk808_rtc_ops = {
	.read_time = rk808_rtc_readtime,
	.set_time = rk808_rtc_set_time,
	.read_alarm = rk808_rtc_readalarm,
	.set_alarm = rk808_rtc_setalarm,
	.alarm_irq_enable = rk808_rtc_alarm_irq_enable,
};

#ifdef CONFIG_PM_SLEEP
/* Turn off the alarm if it should not be a wake source. */
static int rk808_rtc_suspend(struct device *dev)
{
	struct rk808_rtc *rk808_rtc = dev_get_drvdata(dev);

	if (device_may_wakeup(dev))
		enable_irq_wake(rk808_rtc->irq);

	return 0;
}

/* Enable the alarm if it should be enabled (in case it was disabled to
 * prevent use as a wake source).
 */
static int rk808_rtc_resume(struct device *dev)
{
	struct rk808_rtc *rk808_rtc = dev_get_drvdata(dev);

	if (device_may_wakeup(dev))
		disable_irq_wake(rk808_rtc->irq);

	return 0;
}
#endif

static SIMPLE_DEV_PM_OPS(rk808_rtc_pm_ops, rk808_rtc_suspend, rk808_rtc_resume);

static struct rk_rtc_compat_reg rk808_creg = {
	.ctrl_reg = RK808_RTC_CTRL_REG,
	.status_reg = RK808_RTC_STATUS_REG,
	.alarm_seconds_reg = RK808_ALARM_SECONDS_REG,
	.int_reg = RK808_RTC_INT_REG,
	.seconds_reg = RK808_SECONDS_REG,
};

static struct rk_rtc_compat_reg rk817_creg = {
	.ctrl_reg = RK817_RTC_CTRL_REG,
	.status_reg = RK817_RTC_STATUS_REG,
	.alarm_seconds_reg = RK817_ALARM_SECONDS_REG,
	.int_reg = RK817_RTC_INT_REG,
	.seconds_reg = RK817_SECONDS_REG,
};

/**
 * @brief 实现 RK808 芯片的 RTC 设备探测功能。
 * 
 * 此函数在加载相应的驱动程序时初始化 RTC 设备。
 * 主要任务包括:
 * - 检查设备树中是否启用了 RTC 设备。
 * - 为 RTC 设备结构分配内存。
 * - 根据不同的芯片变体设置 RTC 控制寄存器。
 * - 启动 RTC 并启用影子计时器。
 * - 注册 RTC 设备并请求报警中断。
 * 
 * @param pdev 平台设备指针
 * @return 成功返回 0,失败返回负的错误码
 */
static int rk808_rtc_probe(struct platform_device *pdev)
{
	struct rk808 *rk808 = dev_get_drvdata(pdev->dev.parent);
	struct rk808_rtc *rk808_rtc;
	struct device_node *np;
	int ret;

	// 根据芯片变体检查 RTC 设备是否启用
	switch (rk808->variant) {
	case RK805_ID:
	case RK808_ID:
	case RK816_ID:
	case RK818_ID:
		np = of_get_child_by_name(pdev->dev.parent->of_node, "rtc");
		if (np && !of_device_is_available(np)) {
			dev_info(&pdev->dev, "设备已禁用\n");
			return -EINVAL;
		}
		break;
	default:
		break;
	}

	// 为 RTC 设备结构分配内存
	rk808_rtc = devm_kzalloc(&pdev->dev, sizeof(*rk808_rtc), GFP_KERNEL);
	if (rk808_rtc == NULL)
		return -ENOMEM;

	// 根据不同的芯片变体设置控制寄存器
	switch (rk808->variant) {
	case RK808_ID:
	case RK818_ID:
		rk808_rtc->creg = &rk808_creg;
		rk808_rtc->flag |= RTC_NEED_TRANSITIONS;
		break;
	case RK805_ID:
	case RK816_ID:
		rk808_rtc->creg = &rk808_creg;
		break;
	case RK809_ID:
	case RK817_ID:
		rk808_rtc->creg = &rk817_creg;
		break;
	default:
		rk808_rtc->creg = &rk808_creg;
		break;
	}

	// 设置平台设备数据
	platform_set_drvdata(pdev, rk808_rtc);
	rk808_rtc->rk808 = rk808;

	// 启动 RTC 并启用影子计时器
	ret = regmap_update_bits(rk808->regmap, rk808_rtc->creg->ctrl_reg,
				 BIT_RTC_CTRL_REG_STOP_RTC_M |
					 BIT_RTC_CTRL_REG_RTC_READSEL_M,
				 BIT_RTC_CTRL_REG_RTC_READSEL_M);
	if (ret) {
		dev_err(&pdev->dev, "Failed to update RTC control: %d\n", ret);
		return ret;
	}

	ret = regmap_write(rk808->regmap, rk808_rtc->creg->status_reg,
			   RTC_STATUS_MASK);
	if (ret) {
		dev_err(&pdev->dev, "Failed to write RTC status: %d\n", ret);
		return ret;
	}

	// 启用设备唤醒功能
	device_init_wakeup(&pdev->dev, 1);

	// 分配 RTC 设备
	rk808_rtc->rtc = devm_rtc_allocate_device(&pdev->dev);
	if (IS_ERR(rk808_rtc->rtc))
		return PTR_ERR(rk808_rtc->rtc);

	// 设置 RTC 操作函数
	rk808_rtc->rtc->ops = &rk808_rtc_ops;

	// 获取 RTC 中断号
	rk808_rtc->irq = platform_get_irq(pdev, 0);
	if (rk808_rtc->irq < 0)
		return rk808_rtc->irq;

	// 请求报警中断
	ret = devm_request_threaded_irq(&pdev->dev, rk808_rtc->irq, NULL,
					rk808_alarm_irq, 0, "RTC alarm",
					rk808_rtc);
	if (ret) {
		dev_err(&pdev->dev, "Failed to request alarm IRQ %d: %d\n",
			rk808_rtc->irq, ret);
		return ret;
	}

	// 注册 RTC 设备
	return rtc_register_device(rk808_rtc->rtc);
}

static struct platform_driver rk808_rtc_driver = {
	.probe = rk808_rtc_probe,
	.driver = {
		.name = "rk808-rtc",
		.pm = &rk808_rtc_pm_ops,
	},
};

module_platform_driver(rk808_rtc_driver);

MODULE_DESCRIPTION("RTC driver for the rk808 series PMICs");
MODULE_AUTHOR("Chris Zhong <zyw@rock-chips.com>");
MODULE_AUTHOR("Zhang Qing <zhangqing@rock-chips.com>");
MODULE_LICENSE("GPL");
MODULE_ALIAS("platform:rk808-rtc");

2.5 提供带有中文注释的drivers/rtc/源码

在本文章的附带绑定资源中!

2.6 APP层操作RTC

c 复制代码
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/rtc.h>
#include <time.h>
#include <string.h>
#include <errno.h>

#define RTC_DEVICE "/dev/rtc0"

void print_time(struct rtc_time *rtc_tm)
{
	printf("RTC date/time: %04d-%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);
}

int main(int argc, char *argv[])
{
	int fd;
	struct rtc_time rtc_tm;
	time_t rawtime;
	struct tm *timeinfo;

	// 打开RTC设备
	fd = open(RTC_DEVICE, O_RDWR);
	if (fd == -1) {
		perror("打开RTC设备失败");
		return errno;
	}

	// 读取当前时间
	if (ioctl(fd, RTC_RD_TIME, &rtc_tm) == -1) {
		perror("读取RTC时间失败");
		close(fd);
		return errno;
	}

	printf("当前");
	print_time(&rtc_tm);

	// 设置新的时间
	time(&rawtime);
	timeinfo = localtime(&rawtime);
	rtc_tm.tm_year = timeinfo->tm_year;
	rtc_tm.tm_mon = timeinfo->tm_mon;
	rtc_tm.tm_mday = timeinfo->tm_mday;
	rtc_tm.tm_hour = timeinfo->tm_hour;
	rtc_tm.tm_min = timeinfo->tm_min;
	rtc_tm.tm_sec = timeinfo->tm_sec;

	if (ioctl(fd, RTC_SET_TIME, &rtc_tm) == -1) {
		perror("设置RTC时间失败");
		close(fd);
		return errno;
	}

	printf("设置后的");
	print_time(&rtc_tm);

	// 关闭RTC设备
	close(fd);
	return 0;
}

三、参考资料

相关推荐
chian-ocean19 分钟前
Linux环境变量:系统的隐形纽带
linux·运维·服务器
PigeonGuan32 分钟前
[Linux] 服务器CPU信息
java·linux·服务器
蜜獾云44 分钟前
linux tar 文件解压压缩
linux·运维·服务器·tar
码农不屈的一生2 小时前
ARM架构服务器安装部署KVM虚拟化环境
运维·服务器·arm开发·架构
vvw&2 小时前
如何在 Ubuntu 22.04 上部署 Nginx 并优化以应对高流量网站教程
linux·运维·服务器·后端·nginx·ubuntu·性能优化
网硕互联的小客服2 小时前
如何排查香港服务器上的权限问题
linux·运维·服务器·网络·云计算
果汁分你一半l2 小时前
shell脚本的使用
linux·vim
Peter4473 小时前
Linux上安装配置单节点zookeeper
linux·zookeeper·单节点
helloasimo3 小时前
Rocky Linux下安装meld
linux·运维·centos·rocky
vvw&4 小时前
如何在 Ubuntu 22.04 上优化 Apache 以应对高流量网站教程
linux·运维·服务器·前端·后端·ubuntu·apache