RK3568 RTC驱动实验
1. RTC简介
RTC 也就是实时时钟,用于记录当前系统时间,对于 Linux 系统而言时间是非常重要的,使用 Linux 设备的时候也需要查看时间。RTC是Linux的时间系统。
RTC 设备驱动是一个标准的字符设备驱动,应用程序通过 open、 release、 read、 write 和 ioctl 等函数完成对 RTC 设备的操作。
2. RTC相关结构体
// Linux内核将 RTC 设备抽象为 rtc_device 结构体
struct rtc_device {
struct device dev; // 设备
struct module *owner;
int id; // ID
const struct rtc_class_ops *ops; // RTC 设备底层操作函数
struct mutex ops_lock;
struct cdev char_dev; // 字符设备
unsigned long flags;
unsigned long irq_data;
spinlock_t irq_lock;
wait_queue_head_t irq_queue;
struct fasync_struct *async_queue;
int irq_freq;
int max_user_freq;
struct timerqueue_head timerqueue;
struct rtc_timer aie_timer;
struct rtc_timer uie_rtctimer;
struct hrtimer pie_timer; /* sub second exp, so needs hrtimer */
int pie_enabled;
struct work_struct irqwork;
/* Some hardware can't support UIE mode */
int uie_unsupported;
/* Number of nsec it takes to set the RTC clock. This influences when
* the set ops are called. An offset:
* - of 0.5 s will call RTC set for wall clock time 10.0 s at 9.5 s
* - of 1.5 s will call RTC set for wall clock time 10.0 s at 8.5 s
* - of -0.5 s will call RTC set for wall clock time 10.0 s at 10.5 s
*/
long set_offset_nsec;
bool registered;
struct nvmem_device *nvmem;
/* Old ABI support */
bool nvram_old_abi;
struct bin_attribute *nvram;
time64_t range_min;
timeu64_t range_max;
time64_t start_secs;
time64_t offset_secs;
bool set_start_time;
#ifdef CONFIG_RTC_INTF_DEV_UIE_EMUL
struct work_struct uie_task;
struct timer_list uie_timer;
/* Those fields are protected by rtc->irq_lock */
unsigned int oldsecs;
unsigned int uie_irq_active:1;
unsigned int stop_uie_polling:1;
unsigned int uie_task_active:1;
unsigned int uie_timer_active:1;
#endif
};
// RTC 设备的最底层操作函数集合,用户编写
// 只是最底层的 RTC 设备操作函数,并不是提供给应用层的file_operations 函数操作集。
struct rtc_class_ops {
int (*ioctl)(struct device *, unsigned int, unsigned long);
int (*read_time)(struct device *, struct rtc_time *);
int (*set_time)(struct device *, struct rtc_time *);
int (*read_alarm)(struct device *, struct rtc_wkalrm *);
int (*set_alarm)(struct device *, struct rtc_wkalrm *);
int (*proc)(struct device *, struct seq_file *);
int (*set_mmss64)(struct device *, time64_t secs);
int (*set_mmss)(struct device *, unsigned long secs);
int (*read_callback)(struct device *, int data);
int (*alarm_irq_enable)(struct device *, unsigned int enabled);
int (*read_offset)(struct device *, long *offset);
int (*set_offset)(struct device *, long offset);
};
// 提供给应用层的file_operations 函数操作集 drivers/rtc/rtc-dev.c
static const struct file_operations rtc_dev_fops = {
.owner = THIS_MODULE,
.llseek = no_llseek,
.read = rtc_dev_read,
.poll = rtc_dev_poll,
.unlocked_ioctl = rtc_dev_ioctl,
.open = rtc_dev_open,
.release = rtc_dev_release,
.fasync = rtc_dev_fasync,
};
3. RTC整体调用框架
4. RTC代码解析
涉及到的目录功能总结
class.c:向linux设备模型核心注册了一个类RTC,提供了RTC子系统的一些公共函数,让各个RTC驱动注册集成到我们的linux内核中,向驱动程序提供了注册/注销接口。
rtc-dev.c:定义了基本的设备文件操作函数,用户程序与RTC驱动的接口函数,这里定义了每个ioctl命令需要调用的函数,还有open,read等。
interface.c:提供了ioctl各个命令需要调用的函数。
rtc-sysfs.c:与sysfs有关,提供通过sys文件系统操作pcf8563。
rtc-proc.c:与proc文件系统有关,提供通过proc文件系统操作pcf8563。
hctosys.c:系统起来之后会调用到这个文件中的rtc_hctosys()函数,主要功能是系统起来的时候去读RTC硬件中的时间,然后更新我们的系统时间。
rtc.h:定义了与RTC有关的数据结构。
5. RTC驱动注册函数解析
// 驱动路径:drivers/rtc/class.c
struct class *rtc_class;
rtc_init
-> rtc_class = class_create(THIS_MODULE, "rtc"); // 创建名为rtc的class
-> rtc_class->pm = RTC_CLASS_DEV_PM_OPS; // 提供休眠唤醒相关接口suspend/resume
-> rtc_dev_init(); // 动态申请/dev/rtcN的设备号
-> alloc_chrdev_region(&rtc_devt, 0, RTC_DEV_MAX, "rtc"); // RTC_DEV_MAX=16
subsys_initcall(rtc_init); // rtc_sysfs_init():rtc类具有的device_attribute属性
rtc_device_register
-> id = rtc_device_get_id(dev); // Linux支持多个RTC设备,所以需要为每一个设备分配一个ID
-> ida_simple_get(&rtc_ida, 0, 0, GFP_KERNEL); // 对应与/dev/rtc0 /dev/rtc1 /dev/rtcN
-> rtc = rtc_allocate_device();
-> struct rtc_device *rtc;
-> rtc = kzalloc(sizeof(*rtc), GFP_KERNEL); // 创建rtc_device设备(对象)并初始化
-> ...... rtc->dev.class = rtc_class; ...... // rtc_init创建的rtc_class
// rtc设备中相关锁,等待队列的初始化
-> mutex_init(&rtc->ops_lock);
-> spin_lock_init(&rtc->irq_lock);
-> init_waitqueue_head(&rtc->irq_queue);
// 初始化工作队列rtc_timer_do_work
-> timerqueue_init_head(&rtc->timerqueue);
-> INIT_WORK(&rtc->irqwork, rtc_timer_do_work);
// 初始化rtc闹钟中断
-> rtc_timer_init(&rtc->aie_timer, rtc_aie_update_irq, (void *)rtc);
// RTC更新中断
-> rtc_timer_init(&rtc->uie_rtctimer, rtc_uie_update_irq, (void *)rtc);
// RTC周期性中断
-> hrtimer_init(&rtc->pie_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
-> rtc->pie_timer.function = rtc_pie_update_irq;
-> rtc->pie_enabled = 0;
-> rtc->id = id;
-> rtc->ops = ops; // 对应RTC驱动填充的底层操作函数
-> rtc->owner = owner;
-> rtc->dev.parent = dev;
-> dev_set_name(&rtc->dev, "rtc%d", id); // 设置rtc的dev成员中的name域
-> __rtc_read_alarm(rtc, &alrm); // 检查是否设置闹钟
-> if (!err && !rtc_valid_tm(&alrm.time)) // 如果RTC芯片中设置有效的Alarm,则初始化,加入队列中
rtc_initialize_alarm(rtc, &alrm);
-> rtc_dev_prepare(rtc); // /dev/rtc0的rtc作为字符设备进行初始化
-> cdev_init(&rtc->char_dev, &rtc_dev_fops); // rtc_dev_fops接口操作函数结构体
-> cdev->ops = fops;
-> cdev_device_add(&rtc->char_dev, &rtc->dev);
-> cdev_add(cdev, dev->devt, 1); // rtc设备作为字符设备添加到系统 生成/dev/rtc0
-> rtc_proc_add_device(rtc); // /proc/rtc
-> proc_create_single_data("driver/rtc", 0, NULL, rtc_proc_show, rtc);
// 提供给用户层的接口
static const struct file_operations rtc_dev_fops = {
.owner = THIS_MODULE,
.llseek = no_llseek,
.read = rtc_dev_read,
.poll = rtc_dev_poll,
.unlocked_ioctl = rtc_dev_ioctl,
.open = rtc_dev_open,
.release = rtc_dev_release,
.fasync = rtc_dev_fasync,
};
kernel中__init类型函数都位于.init.text段中,对应的在.initcall.init段中保存相应的函数指针。系统在启动过程中,根据定义在段中的等级值(0~7)从低到高依次执行。定义:
// include/linux/init.h
#define pure_initcall(fn) __define_initcall(fn, 0)
#define core_initcall(fn) __define_initcall(fn, 1)
#define core_initcall_sync(fn) __define_initcall(fn, 1s)
#define postcore_initcall(fn) __define_initcall(fn, 2)
#define postcore_initcall_sync(fn) __define_initcall(fn, 2s)
#define arch_initcall(fn) __define_initcall(fn, 3)
#define arch_initcall_sync(fn) __define_initcall(fn, 3s)
#define subsys_initcall(fn) __define_initcall(fn, 4)
#define subsys_initcall_sync(fn) __define_initcall(fn, 4s)
#define fs_initcall(fn) __define_initcall(fn, 5)
#define fs_initcall_sync(fn) __define_initcall(fn, 5s)
#define rootfs_initcall(fn) __define_initcall(fn, rootfs)
#define device_initcall(fn) __define_initcall(fn, 6)
#define device_initcall_sync(fn) __define_initcall(fn, 6s)
#define late_initcall(fn) __define_initcall(fn, 7)
#define late_initcall_sync(fn) __define_initcall(fn, 7s)
6. 应用层调用驱动流程解析
rtc_dev_ioctl (struct file *file, unsigned int cmd, unsigned long arg) // drivers/rtc/rtc-dev.c
-> struct rtc_device *rtc = file->private_data; // 获取到rtc设备
-> switch (cmd)
-> case RTC_RD_TIME:
-> rtc_read_time // drivers/rtc/interface.c
-> __rtc_read_time
-> rtc->ops->read_time(rtc->dev.parent, tm);
-> case RTC_SET_TIME:
-> rtc_set_time // drivers/rtc/interface.c
-> rtc_valid_tm(tm); // 参数检查
// 调用rtc_device中ops结构体的函数指针
// ops结构体的函数指针已经在RTC驱动中被赋值
-> if (!rtc->ops)
err = -ENODEV;
else if (rtc->ops->set_time)
err = rtc->ops->set_time(rtc->dev.parent, tm);
else if (rtc->ops->set_mmss64) {
time64_t secs64 = rtc_tm_to_time64(tm);
err = rtc->ops->set_mmss64(rtc->dev.parent, secs64);
7. RTC时间查看与设置
RK809 内部 RTC 的使能需要先使能 RK809,默认已经使能,我们打开设备树 rk3568-evb.dtsi:
rk809: pmic@20 {
compatible = "rockchip,rk809";
reg = <0x20>;
interrupt-parent = <&gpio0>;
interrupts = <3 IRQ_TYPE_LEVEL_LOW>;
pinctrl-names = "default", "pmic-sleep",
"pmic-power-off", "pmic-reset";
pinctrl-0 = <&pmic_int>;
pinctrl-1 = <&soc_slppin_slp>, <&rk817_slppin_slp>;
pinctrl-2 = <&soc_slppin_gpio>, <&rk817_slppin_pwrdn>;
pinctrl-3 = <&soc_slppin_gpio>, <&rk817_slppin_rst>;
rockchip,system-power-controller;
wakeup-source;
#clock-cells = <1>;
clock-output-names = "rk808-clkout1", "rk808-clkout2";
//fb-inner-reg-idxs = <2>;
/* 1: rst regs (default in codes), 0: rst the pmic */
pmic-reset-func = <0>;
/* not save the PMIC_POWER_EN register in uboot */
not-save-power-en = <1>;
vcc1-supply = <&vcc3v3_sys>;
vcc2-supply = <&vcc3v3_sys>;
vcc3-supply = <&vcc3v3_sys>;
vcc4-supply = <&vcc3v3_sys>;
vcc5-supply = <&vcc3v3_sys>;
vcc6-supply = <&vcc3v3_sys>;
vcc7-supply = <&vcc3v3_sys>;
vcc8-supply = <&vcc3v3_sys>;
vcc9-supply = <&vcc3v3_sys>;
上面 status 状态没写,默认就是“okay”的。
同时我们需要在 menuconfg 里对应的宏配置为 CONFIG RTC DRV RK808。> Device
Drivers > Real Time Clock 选中 CONFIG RTC DRV RK808。如下图。
8. 查看时间
如果要查看时间的话输入“date”命令即可,结果如图:
从上面可看到内核启动 RTC 时间采用的是 UTC 标准,而系统启动后采用的是 CST 标准
时间恰好相差8个小时。UTC(协调世界时)和CST(中部标准时间)是两个不同的时间标准,
在中国,CST 通常被解释为"China Standard Time"(中国标准时间),而不是"Central Standard Time
(中部标准时间)。中国 CST 与协调世界时(UTC)相差8小时,即 UTC+8。
RTC 时间设置也是使用的 date 命令,输入“date --help”命令即可査看 date 命令如何设置
系统时间,结果如图所示:
root@RK356X:/# date --help
Usage: date [OPTION]... [+FORMAT]
or: date [-u|--utc|--universal] [MMDDhhmm[[CC]YY][.ss]]
Display the current time in the given FORMAT, or set the system date.
Mandatory arguments to long options are mandatory for short options too.
-d, --date=STRING display time described by STRING, not 'now'
--debug annotate the parsed date,
and warn about questionable usage to stderr
-f, --file=DATEFILE like --date; once for each line of DATEFILE
-I[FMT], --iso-8601[=FMT] output date/time in ISO 8601 format.
FMT='date' for date only (the default),
'hours', 'minutes', 'seconds', or 'ns'
for date and time to the indicated precision.
14T02:34:56-06:00
-R, --rfc-email output date and Example: Mon, 14 Aug 2006 02:34:56 -0600
--rfc-3339= FMT='date', 'seconds', or 'ns'
Example: 2006-08-14 02:34:56-06:00
-r, -s, --set=STRING set time described by STRING
-u, - --help display this help and exit
--version outrpreted sequences are:
%% a literal %
%a locale's ab(e.g., Sunday)
%b locale's abbreviated month name (e.g., Ja date and time (e.g., Thu Mar 3 23:05:25 2005)
%C century;(e.g., 01)
%D date; same as %m/%d/%y
%e day of month, last two digits of year of ISO week number (see %G)
%G yearme as %b
%H hour (00..23)
%I hour (01..12)
%j day
%l hour, space padded ( 1..12); same as %_I
%m month ((000000000..999999999)
%p locale's equivalent of either AM rter of year (1..4)
%r locale's 12-hour clock time (e.g., 1conds since 1970-01-01 00:00:00 UTC
%S second (00..60)
% is Monday
%U week number of year, with Sunday as first dayf week (01..53)
%w day of week (0..6); 0 is Sunday
%W locale's date representation (e.g., 12/31/99)
%X locale's(00..99)
%Y year
%z +hhmm numeric time zone (e.g., -04s numeric time zone (e.g., -04:00:00)
%:::z numeric time zon time zone abbreviation (e.g., EDT)
By default, date pads num:
- (hyphen) do not pad the field
_ (underscore) pad wle
# use opposite case if possible
After any flags comes er, which is either
E to use the locale's alternate representats if available.
Examples:
Convert seconds since the epoch (1ime on the west coast of the US (use tzselect(1) to find TZ)
iday on the west coast of the US
$ date --date='TZ="America/Lww.gnu.org/software/coreutils/>
Report date translation bugs to://www.gnu.org/software/coreutils/date>
比如现在设置当前时间为2024年4月1日11:40:00,因此输入如下:
date -s "2024-04-01 11:40:00"
设置完成后再次使用date
命令查看一下当前时间就会发现时间改过来了
大家注意我们使用date -s
命令仅仅是修改了当前时间,此时间还没有写入到RK809内部 RTC 里面或其他的 RTC 芯片里面,因此系统重启以后时间又会丢失。我们需要将当前的时间写入到 RTC 里面,这里要用到hwclock命令,输入如下命令将系统时间写入到 RTC 里面:
hwclock -w /将当前系统时间写入RTC里面/
时间写入到 RTC 里面以后就不怕系统重启以后时间丢失了
间隔时间输入如下命令:
hwclock -r /读取当前系统时间/
发现当前系统时间在走动,系统时间正常。