15. Linux内核RTC子系统

在系统运行过程中,时间是系统调度、日志打印、定时任务、业务逻辑执行的基础核心基准。系统上电运行后,内核会维护一套软件系统时间, 但软件时间完全依赖CPU运行计时,一旦设备断电、重启或系统复位,软件时间就会清零丢失,无法保证时间的连续性和准确性。 为了解决断电保时间、永久计时的需求,设备普遍搭载独立的RTC(Real Time Clock,实时时钟)硬件外设。RTC芯片自带独立后备电池, 设备主电源断电后仍可独立运行、持续计时,上电后为Linux系统提供精准的初始时间基准,保障系统时间不掉电、不重置。

Linux内核专门为各类RTC硬件外设抽象封装了RTC子系统,统一规范了RTC时间读写、闹钟配置、中断唤醒、电源休眠等核心功能接口, 屏蔽不同厂商、不同型号RTC芯片的硬件寄存器差异,驱动开发者只需适配底层硬件寄存器读写逻辑,无需重复编写上层时间管理、sysfs节点、应用层交互逻辑, 大幅降低RTC驱动开发难度,提升驱动通用性与可移植性。

15.1. RTC核心概念

15.1.1. RTC定义

RTC全称为Real Time Clock,实时时钟,是一种专用的硬件计时外设,核心功能为独立不间断计时、断电时间保存、定时闹钟中断、系统休眠唤醒。 区别于Linux内核软件定时器,RTC硬件不依赖CPU主频和系统调度,拥有独立时钟源和独立供电回路,是嵌入式系统唯一可靠的永久时间基准。

Linux系统启动流程中,内核优先通过RTC驱动读取硬件RTC时间,同步为系统软件时间; 系统运行期间,软件时间正常计时;系统关机断电后,仅RTC硬件持续走时,实现时间永久同步。

15.1.2. RTC时间编码格式(BCD码)

绝大多数工业级RTC硬件芯片,均采用BCD码格式存储时间数据,而非二进制原生格式。 BCD码即二进制编码十进制,用4bit存储1位十进制数字,便于硬件寄存器存储与读取。

Linux内核提供标准转换工具函数:

  • bcd2bin():BCD码转二进制,用于读取RTC硬件时间后转换为内核标准时间格式;

  • bin2bcd():二进制转BCD码,用于将内核设置的时间转换后写入RTC硬件寄存器。

RTC芯片寄存器不会直接存普通二进制数值,而是用高4位存十位、低4位存个位的格式存储时间, BCD码转换举例:

  1. 示例1:秒值转换

如十进制秒数35:数值十位是3,个位是5

  • bin2bcd(35):二进制0011 0101,对应十六进制0x35,RTC寄存器实际存储值为0x35;

  • bcd2bin(0x35):把高4位0x3作为十位、低4位0x5作为个位,计算3x10+5 = 十进制35。

  1. 示例2:年份低两位转换(十进制数值25,根据世纪位判断是1925/2025年)

十进制年份尾数25:数值十位2,个位5

  • bin2bcd(25):二进制0010 0101,对应十六进制0x25,RTC寄存器实际存储值为0x25;

  • bcd2bin(0x25):把高4位0x2作为十位、低4位0x5作为个位,计算2x10+5 = 十进制25。

15.2. RTC子系统核心结构体

15.2.1. 内核标准时间结构体

内核标准时间结构体(struct rtc_time)用于统一规范Linux内核所有RTC设备的时间存储格式,所有RTC驱动读写时间必须通过该结构体交互,是内核与驱动时间数据的统一标准。

rtc_time结构体(内核源码/usr/include/linux/rtc.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct rtc_time {
    int tm_sec;    /* 秒:取值范围0~59 */
    int tm_min;    /* 分:取值范围0~59 */
    int tm_hour;   /* 时:取值范围0~23,24小时制 */
    int tm_mday;   /* 日期:当月第几天,1~31 */
    int tm_mon;    /* 月份:内核规范0~11,硬件寄存器月份需-1适配 */
    int tm_year;   /* 年份:基于1900年偏移,如2026年填写126 */
    int tm_wday;   /* 星期:0~6,对应周日~周六 */
    int tm_yday;   /* 年内天数:1~365,一般RTC驱动无需配置 */
    int tm_isdst;  /* 夏令时标记:0关闭,1开启,RTC驱动默认关闭 */
};

15.2.2. 内核闹钟配置结构体

内核闹钟配置结构体(struct rtc_wkalrm)用于统一存储RTC闹钟时间、闹钟使能状态、闹钟 pending 触发状态,用于闹钟读取、闹钟设置、中断状态管理。

rtc_wkalrm结构体(内核源码/usr/include/linux/rtc.h)
1
2
3
4
5
struct rtc_wkalrm {
    unsigned char enabled; /* 闹钟使能开关:1开启闹钟中断,0关闭闹钟中断 */
    unsigned char pending; /* 闹钟触发标志:1闹钟已触发待处理,0未触发 */
    struct rtc_time time;  /* 闹钟触发时间,复用标准rtc_time时间格式 */
};

15.2.3. RTC设备管理结构体

RTC设备管理结构体(struct rtc_device)用于内核RTC核心层管理每一个注册的RTC设备实例,包含设备名称、操作回调、中断号、电源状态、设备节点等核心信息,驱动注册RTC设备后由内核自动分配管理。

rtc_device结构体(内核源码/include/linux/rtc.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
struct rtc_device {
    struct device dev;               /* 内嵌标准设备结构体,内核设备模型基础 */
    struct module *owner;            /* 所属内核模块指针,绑定驱动模块归属 */

    int id;                          /* RTC设备编号,系统自动分配,如rtc0、rtc1 */

    const struct rtc_class_ops *ops; /* 绑定底层RTC硬件读写、闹钟操作回调函数集 */
    struct mutex ops_lock;           /* 操作互斥锁,防止多线程并发访问RTC硬件寄存器 */

    struct cdev char_dev;            /* 字符设备核心结构体,对应/dev/rtc设备文件节点 */
    unsigned long flags;             /* RTC设备工作状态标记位 */

    unsigned long irq_data;          /* 中断私有数据,存储RTC闹钟中断相关信息 */
    spinlock_t irq_lock;             /* 中断处理自旋锁,保护中断上下文临界资源 */
    wait_queue_head_t irq_queue;     /* 中断等待队列,阻塞唤醒应用层RTC读取进程 */

    bool registered;                 /* 设备注册标记,标识RTC设备是否注册成功生效 */
    /* 其他成员省略 */
};

15.2.4. RTC驱动操作回调结构体

RTC驱动操作回调结构体(struct rtc_class_ops)是RTC驱动核心操作接口集,驱动开发者必须实现该结构体中的底层硬件回调函数,内核RTC核心层通过该结构体调用驱动底层硬件读写逻辑。

rtc_device结构体(内核源码/include/linux/rtc.h)
 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
struct rtc_class_ops {
    /* 自定义设备ioctl控制接口,适配特殊芯片私有指令 */
    int (*ioctl)(struct device *, unsigned int, unsigned long);
    /* 读取硬件RTC当前时间,填充到rtc_time结构体 */
    int (*read_time)(struct device *, struct rtc_time *);
    /* 设置硬件RTC标准时间,写入硬件寄存器 */
    int (*set_time)(struct device *, struct rtc_time *);
    /* 读取硬件闹钟配置时间与中断状态 */
    int (*read_alarm)(struct device *, struct rtc_wkalrm *);
    /* 设置硬件闹钟触发时间与配置参数 */
    int (*set_alarm)(struct device *, struct rtc_wkalrm *);
    /* proc文件系统节点打印信息回调,调试查看设备状态 */
    int (*proc)(struct device *, struct seq_file *);
    /* 64位秒级时间设置接口,适配大容量时间戳设备 */
    int (*set_mmss64)(struct device *, time64_t secs);
    /* 32位秒级时间兼容设置接口,旧内核设备兼容适配 */
    int (*set_mmss)(struct device *, unsigned long secs);
    /* 读取完成回调函数,硬件读取结束后置回调处理 */
    int (*read_callback)(struct device *, int data);
    /* 闹钟中断单独使能/禁用控制,开关闹钟中断源 */
    int (*alarm_irq_enable)(struct device *, unsigned int enabled);
    /* 读取RTC硬件时间偏移量,用于时间校准补偿 */
    int (*read_offset)(struct device *, long *offset);
    /* 设置RTC硬件时间偏移量,修正硬件走时误差 */
    int (*set_offset)(struct device *, long offset);
};

15.3. RTC子系统驱动核心API函数

15.3.1. RTC设备注册

15.3.1.1. devm_rtc_device_register函数

devm_rtc_device_register函数采用devres资源自动管理机制,向内核RTC核心层注册一个新的RTC设备,自动创建设备文件、sysfs节点、字符设备,驱动卸载时自动释放资源,无需手动注销。

函数原型:

1
2
3
4
struct rtc_device *devm_rtc_device_register(struct device *dev,
                                            const char *name,
                                            const struct rtc_class_ops *ops,
                                            struct module *owner);

参数说明:

  • dev:设备结构体指针,一般传入I2C客户端或Platform设备的dev成员;

  • name:RTC设备名称,内核会根据名称生成rtc设备节点;

  • ops:RTC底层硬件操作回调结构体指针,绑定驱动实现的read_time/set_time等函数;

  • owner:模块所属指针,固定传入THIS_MODULE。

返回值:

  • 成功:返回struct rtc_device类型有效设备指针;

  • 失败:返回NULL或错误指针,需通过IS_ERR判断。

15.3.2. RTC闹钟中断上报

15.3.2.1. rtc_update_irq函数

rtc_update_irq函数是RTC闹钟中断服务函数中核心调用API,用于向内核RTC子系统上报闹钟中断事件,唤醒等待的应用进程、触发系统闹钟回调。

函数原型:

1
void rtc_update_irq(struct rtc_device *rtc, unsigned long num, unsigned int events);

参数说明:

  • rtc:已注册的RTC设备结构体指针;

  • num:中断事件次数,固定填1即可;

  • events:中断事件类型,闹钟中断固定传RTC_IRQF | RTC_AF。

15.3.3. 时间格式转换

15.3.3.1. rtc_tm_to_time64函数

rtc_tm_to_time64函数用于将struct rtc_time结构体格式时间,转换为64位标准时间戳,用于时间运算、时间比较、闹钟时间校准。

函数原型:

1
time64_t rtc_tm_to_time64(const struct rtc_time *tm);

参数说明:

  • tm:输入参数,内核标准rtc_time时间结构体指针。

返回值:返回64位time64_t类型时间戳,单位秒。

15.3.4. 时间戳转时间结构体

15.3.4.1. rtc_time64_to_tm函数

rtc_time64_to_tm函数将64位时间戳反向转换为struct rtc_time标准时间格式,用于闹钟时间修正、时间格式标准化。

函数原型:

1
void rtc_time64_to_tm(time64_t time, struct rtc_time *tm);

参数说明:

  • time:输入64位时间戳;

  • tm:输出参数,存储转换后的标准rtc_time时间。

15.3.5. 设备唤醒能力设置

15.3.5.1. device_set_wakeup_capable函数

device_set_wakeup_capable函数用于配置RTC设备具备系统休眠唤醒能力,允许RTC闹钟触发系统从休眠状态唤醒。

函数原型:

1
void device_set_wakeup_capable(struct device *dev, bool capable);

参数说明:

  • dev:RTC设备所属设备结构体指针;

  • capable:true开启唤醒能力,false关闭。

15.3.6. BCD码与二进制互转

15.3.6.1. BCD码转二进制函数(bcd2bin)

bcd2bin函数是内核内置纯运算内联逻辑,自动拆分BCD格式字节的高4位十位、低4位个位,通过数学运算合并为标准十进制二进制数值, 专门用于RTC硬件寄存器读取后的数据解码,把硬件存储的BCD时间还原为内核可识别的正常时间数字。

函数原型:

1
static inline u8 bcd2bin(u8 val);

参数说明:

  • val:从RTC硬件寄存器读取的BCD格式十六进制数值,如0x35、0x59。

返回值:返回转换后的十进制二进制数值,供内核rtc_time结构体赋值使用。

15.3.6.2. 二进制转BCD码函数(bin2bcd)

bin2bcd函数是内核内置纯运算内联逻辑,将正常十进制时间数值,分别计算出十位和个位,重新组合成高4位存十位、低4位存个位的BCD格式字节, 专门用于RTC时间设置编码,把内核标准时间转为硬件寄存器可直接存储的BCD格式数据。

函数原型:

1
static inline u8 bin2bcd(u8 val);

参数说明:

  • val:内核rtc_time结构体中的十进制时间数值,如35、59、25。

返回值:返回转换后的BCD格式十六进制数值,可直接写入RTC硬件寄存器。

15.4. LubanCat系列板卡板载RTC情况

15.4.1. rk809

RK809是一款高性能PMIC,RK809 集成5个大电流DCDC、9个LDO、2个开关SWITCH、 1个RTC、1个高性能CODEC、可调上电时序等功能。

LubanCat-RK3562/RK3566/RK3568系列板卡均使用RK809作为pmic使用,但是真正使用RK809的RTC功能的有:

  • LubanCat-1(V1、V2)

  • LubanCat-2(V1、V2)

  • LubanCat-2N(V1、V2),

以上板卡从V3版本开始均使用外部低功耗RTC。

15.4.2. rx8010

RX8010是爱普生推出的一款低功耗、高性能的实时时钟(RTC)芯片,由于其高精度、低功耗和多功能特性, 广泛应用于智能家居设备、工业控制器、物联网设备等各种需要精确时间信息的嵌入式系统中。

使用rx8010的RTC的有:

  • LubanCat-1IO

  • LubanCat-2IO

15.4.3. HYM8563S/BM8563

HYM8563S和BM8563都是低功耗、高性能的实时时钟(RTC)芯片,芯片具有一个可编程的时钟输出、一个中断输出、一个掉电检测器以及片内电源复位功能, 还支持设置闹钟和周期性报警,可根据用户设定的时间间隔发出报警信号。

使用HYM8563S/BM8563的有:

  • LubanCat-1(V3)

  • LubanCat-1h

  • LubanCat-1hs

  • LubanCat-2(V3)

  • LubanCat-2n(V3)

  • LubanCat-3

  • LubanCat-3IO

  • LubanCat-4

  • LubanCat-4IO

  • LubanCat-5

  • LubanCat-5IO

15.4.4. 无RTC

以下板卡没有板载RTC,需要外挂RTC模块:

  • LubanCat-0

  • LubanCat-1N

  • LubanCat-Q1

15.5. 内核自带RTC驱动详解

15.5.1. Rockchip PMIC RTC驱动详解

Rockchip的RK805/RK809/RK816/RK817/RK818等多款同系列PMIC芯片都带有RTC功能,都使用内核rtc-rk808.c驱动, 驱动基于Linux平台总线和RTC子系统开发,专为瑞芯微PMIC电源管理芯片内置RTC模块适配开发。 核心实现硬件RTC实时时间读写、闹钟定时配置、闹钟中断响应处理、休眠唤醒电源适配、特殊日历格式双向转换功能, 通过标准RTC子系统向用户态提供统一的时间配置、闹钟唤醒、休眠定时接口,保障时钟计时与定时唤醒的稳定性与可靠性。

驱动源码位于: 内核源码/drivers/rtc/rtc-rk808.c

以下以瑞芯微6.1.99内核版本的rtc-rk808驱动进行分析:

核心定义与数据结构

驱动通过宏定义封装RTC控制寄存器、中断寄存器、状态寄存器的关键位掩码与寄存器数量常量,自定义兼容寄存器结构体适配多型号PMIC寄存器差异, 私有数据结构体统一管理驱动运行时硬件资源、中断号与兼容标记,实现多芯片硬件适配。

核心定义与数据结构(位于内核源码/drivers/rtc/rtc-rk808.c)
 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
/* RTC控制寄存器位域定义 */
#define BIT_RTC_CTRL_REG_STOP_RTC_M         BIT(0)     /* RTC时钟停止位:置1暂停计数,用于时间写入 */
#define BIT_RTC_CTRL_REG_RTC_GET_TIME       BIT(6)     /* 快照锁存位:锁存当前实时时间到影子寄存器 */
#define BIT_RTC_CTRL_REG_RTC_READSEL_M      BIT(7)     /* 读取选择位:选择读取快照时间/实时时间 */

/* RTC中断寄存器闹钟中断使能位 */
#define BIT_RTC_INTERRUPTS_REG_IT_ALARM_M   BIT(3)     /* 闹钟中断总使能开关 */

/* 状态与寄存器掩码常量 */
#define RTC_STATUS_MASK     0xFE                 /* RTC状态寄存器清理掩码 */
#define RTC_ALARM_STATUS    BIT(6)               /* 闹钟触发状态标志位 */

/* 时间/闹钟寄存器数值掩码(BCD码有效位限制) */
#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)

/* 时间/闹钟寄存器组数量定义 */
#define NUM_TIME_REGS   (RK808_WEEKS_REG - RK808_SECONDS_REG + 1)
#define NUM_ALARM_REGS  (RK808_ALARM_YEARS_REG - RK808_ALARM_SECONDS_REG + 1)

/* 多芯片兼容寄存器映射结构体:适配RK808/RK817等不同PMIC寄存器地址差异 */
struct rk_rtc_compat_reg {
    unsigned int ctrl_reg;         /* RTC控制寄存器地址 */
    unsigned int status_reg;       /* RTC状态寄存器地址 */
    unsigned int alarm_seconds_reg;/* 闹钟秒数寄存器起始地址 */
    unsigned int int_reg;          /* RTC中断配置寄存器地址 */
    unsigned int seconds_reg;      /* 实时时间秒数寄存器起始地址 */
};

/* 驱动私有数据结构体:存储驱动运行时核心资源 */
struct rk808_rtc {
    struct rk808 *rk808;                /* PMIC主设备句柄,用于regmap寄存器读写 */
    struct rtc_device *rtc;             /* RTC子系统设备句柄,对接内核RTC框架 */
    struct rk_rtc_compat_reg *creg;     /* 当前芯片兼容寄存器配置指针 */
    int irq;                            /* 闹钟中断号 */
    unsigned int flag;                  /* 功能标记:是否需要日历格式转换 */
};

/* RK805/RK808/RK816/RK818芯片通用寄存器配置实例:绑定基础RTC硬件寄存器物理地址 */
static struct rk_rtc_compat_reg rk808_creg = {
    .ctrl_reg = RK808_RTC_CTRL_REG,         /* 绑定RK808系列RTC控制寄存器物理地址 */
    .status_reg = RK808_RTC_STATUS_REG,     /* 绑定RK808系列RTC状态寄存器物理地址 */
    .alarm_seconds_reg = RK808_ALARM_SECONDS_REG, /* 绑定RK808系列闹钟秒数起始寄存器地址 */
    .int_reg = RK808_RTC_INT_REG,           /* 绑定RK808系列RTC中断配置寄存器物理地址 */
    .seconds_reg = RK808_SECONDS_REG,       /* 绑定RK808系列实时时间秒数起始寄存器地址 */
};

/* RK809/RK817芯片专属寄存器配置实例:适配该系列芯片RTC寄存器地址偏移差异 */
static struct rk_rtc_compat_reg rk817_creg = {
    .ctrl_reg = RK817_RTC_CTRL_REG,         /* 绑定RK817系列RTC控制寄存器专属物理地址 */
    .status_reg = RK817_RTC_STATUS_REG,     /* 绑定RK817系列RTC状态寄存器专属物理地址 */
    .alarm_seconds_reg = RK817_ALARM_SECONDS_REG, /* 绑定RK817系列闹钟秒数起始专属寄存器地址 */
    .int_reg = RK817_RTC_INT_REG,           /* 绑定RK817系列RTC中断配置专属寄存器地址 */
    .seconds_reg = RK817_SECONDS_REG,       /* 绑定RK817系列实时时间秒数起始专属寄存器地址 */
};

关键解析:

  • 多芯片兼容寄存器结构体:适配RK805/RK808/RK809/RK817等多款瑞芯微PMIC,不同芯片RTC寄存器地址不同,通过该结构体统一映射,一套驱动无需改硬件逻辑即可适配多型号设备。

  • 私有数据结构体:集中管理PMIC设备句柄、RTC设备节点、兼容寄存器配置、中断号和功能标记,所有底层寄存器读写、中断操作、子系统注册均通过该结构体传递资源。

日历转换函数

RK808系列 RTC硬件采用 自研特殊日历计数规则 ,11月份默认按31天计数,与标准公历11月30天不符,驱动实现双向日历转换函数, 完成硬件特殊日历与系统标准公历的互相适配,保证时间读写前后日期精准一致。

日历转换函数(位于内核源码/drivers/rtc/rtc-rk808.c)
 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
/* 计算日历转换偏移天数:以2016年1月1日为同步基准日 */
static time64_t nov2dec_transitions(struct rtc_time *tm)
{
    return (tm->tm_year + 1900) - 2016 + (tm->tm_mon + 1 > 11 ? 1 : 0);
}

/* RK硬件特殊日历 -> 系统标准公历 */
static void rockchip_to_gregorian(struct rtc_time *tm)
{
    /* 先按硬件时间转时间戳,叠加偏移天数修正为公历 */
    time64_t time = rtc_tm_to_time64(tm);
    rtc_time64_to_tm(time + nov2dec_transitions(tm) * 86400, tm);
}

/* 系统标准公历 -> RK硬件特殊日历 */
static void gregorian_to_rockchip(struct rtc_time *tm)
{
    time64_t extra_days = nov2dec_transitions(tm);
    time64_t time = rtc_tm_to_time64(tm);
    /* 减去偏移天数适配硬件日历规则 */
    rtc_time64_to_tm(time - extra_days * 86400, tm);

    /* 跨月份边界补偿:避免11月31日临界时间转换出错 */
    if (nov2dec_transitions(tm) < extra_days) {
        if (tm->tm_mon + 1 == 11)
            tm->tm_mday++;  /* 适配硬件11月31天特殊计数 */
        else
            rtc_time64_to_tm(time - (extra_days - 1) * 86400, tm);
    }
}

关键解析:

  • nov2dec_transitions函数:定义日历偏移计算函数,用于修正RK808硬件特殊日历与标准公历的天数差

    • 第4行:以2016年为基准,计算年份偏移+12月额外天数补偿。

  • rockchip_to_gregorian函数:定义硬件时间转系统公历函数,适配RK808非常规日历规则

    • 第11行:将硬件RTC时间转换为内核64位时间戳,为天数偏移做准备;

    • 第12行:叠加偏移天数(86400秒 = 1天),将硬件时间修正为标准公历。

  • gregorian_to_rockchip函数:定义系统公历转硬件特殊日历函数

    • 第18行:预计算需要扣除的偏移天数,避免重复调用函数;

    • 第19行:将系统标准时间转为时间戳,准备反向转换;

    • 第21行:扣除偏移天数,将公历转换为RK808硬件可识别的特殊日历;

    • 第24行:判断跨月份临界场景,防止时间转换出现日期异常;

    • 第26行:RK808硬件bug适配,11月强制支持31天,日期自增补偿;

    • 第28行:非11月临界场景,减少1天偏移量,避免日期错乱。

时间读取函数

rk808_rtc_readtime函数是RTC核心读接口,通过regmap批量读取RTC年月日时分秒星期寄存器,将硬件BCD码时间转为系统标准rtc_time格式, 触发硬件时间快照锁存确保读取时间一致性,按需调用日历转换函数修正为标准公历,向上层子系统输出精准时间数据。

rk808_rtc_readtime函数(位于内核源码/drivers/rtc/rtc-rk808.c)
 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
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;

    /* 第一步:置位GET_TIME锁存位,冻结当前实时时间到影子寄存器 */
    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位,允许时间继续计数 */
    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;
    }

    /* 批量读取7个时间寄存器,一次性获取完整时间数据 */
    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码转二进制,填充系统rtc_time结构体 */
    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);

    /* 标记需转换则:RK硬件日历转标准公历 */
    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;
}

关键解析:

  • 第9行:通过regmap置位RTC控制寄存器GET_TIME位,冻结硬件时间防止读取错乱;

  • 第18行:清除GET_TIME控制位,解除时间冻结,恢复硬件正常计时;

  • 第27行:批量连续读取秒、分、时、日、月、年、星期所有时间寄存器;

  • 第35行:硬件秒数据BCD转二进制,掩码过滤无效位;

  • 第39行:月份数据解码后减1,适配Linux内核月份从0开始计数规则;

  • 第40行:年份数据解码后加100,适配根据2000年基准年份偏移;

  • 第44行:判断硬件是否需要特殊日历转换;

  • 第45行:调用转换函数,将RK硬件特殊日历转为标准公历;

  • 第47行:打印调试日志,输出格式化的标准时间信息。

时间设置函数

rk808_rtc_set_time函数是RTC核心写接口,先暂停RTC硬件时钟计数,按需将系统标准公历时间转为硬件BCD码和RK特殊日历格式, 批量写入时间寄存器,写入完成后恢复时钟计数,保障时间设置过程不被硬件计时干扰。

rk808_rtc_set_time函数(位于内核源码/drivers/rtc/rtc-rk808.c)
 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
static int rk808_rtc_set_time(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;

    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);

    /* 标记需转换则:标准公历转RK硬件特殊日历 */
    if (rk808_rtc->flag & RTC_NEED_TRANSITIONS)
        gregorian_to_rockchip(tm);

    /* 系统二进制时间转硬件BCD码,填充写入数组 */
    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);

    /* 置位STOP位,暂停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;
    }

    /* 批量写入年月日时分秒星期寄存器 */
    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;
    }

    /* 清除STOP位,恢复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;
}

关键解析:

  • 第8行:打印调试日志,输出待设置的格式化系统时间;

  • 第13行:判断硬件是否需要日历转换,适配RK定制芯片的特殊日历规则;

  • 第14行:调用转换函数,将标准公历转换为RK硬件可识别的特殊日历格式;

  • 第17行:秒数据二进制转BCD码,填充写入数组;

  • 第21行:月份+ 1适配硬件计数规则,硬件月份从1开始,内核从0开始;

  • 第22行:年份减100,将2000基准年份转换为硬件2位年份格式;

  • 第26行:置位RTC停止位,暂停硬件计时,避免写入过程中时间自动更新导致数据错乱;

  • 第35行:通过regmap批量写入秒、分、时、日、月、年、星期所有时间寄存器;

  • 第43行:清除RTC停止位,解除硬件暂停状态,恢复正常计时。

闹钟启停辅助函数

rk808_rtc_stop_alarm/rk808_rtc_start_alarm两个专属辅助函数,专门负责闹钟中断使能开关控制与闹钟状态标志清除,是闹钟配置、中断触发、唤醒复位的核心基础函数。

rk808_rtc_stop_alarm/rk808_rtc_start_alarm函数(位于内核源码/drivers/rtc/rtc-rk808.c)
 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
/* 关闭闹钟中断、清除闹钟触发状态标志 */
static int rk808_rtc_stop_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, 0);

    /* 写入状态寄存器,强制清除闹钟AF触发标志,释放闹钟资源 */
    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_stop_alarm函数:定义闹钟停止函数,关闭中断并清除触发标志,终止闹钟功能

    • 第4行:获取RK808主设备句柄,用于寄存器操作;

    • 第8行:通过regmap清零中断寄存器的闹钟使能位,硬件停止触发闹钟中断;

    • 第12行:写入状态寄存器,强制清除硬件闹钟触发标记(AF位),防止残留中断。

  • rk808_rtc_start_alarm函数:定义闹钟启动函数,开启中断使能,激活闹钟定时唤醒功能

    • 第24行:通过regmap置位中断寄存器的闹钟使能位,允许硬件触发闹钟中断。

读取闹钟配置函数

rk808_rtc_readalarm函数为RTC标准读闹钟接口,批量读取RK808硬件闹钟年月日时分秒寄存器,将硬件BCD码闹钟数据转为系统标准rtc_alarm时间格式,按需做RK非标日历转公历适配,同时读取硬件闹钟开关状态、中断使能状态,向上层内核返回当前已配置的闹钟定时信息。

rk808_rtc_readalarm函数(位于内核源码/drivers/rtc/rtc-rk808.c)
 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
/* RTC标准接口:读取当前硬件中已配置的闹钟时间与闹钟开关状态 */
static int rk808_rtc_readalarm(struct device *dev, struct rtc_wkalrm *alrm)
{
    /* 获取RTC驱动私有数据结构体指针 */
    struct rk808_rtc *rk808_rtc = dev_get_drvdata(dev);
    /* 获取PMIC主设备regmap句柄 */
    struct rk808 *rk808 = rk808_rtc->rk808;
    /* 定义数组,存放批量读取的闹钟寄存器原始BCD数据 */
    u8 alarm_data[NUM_ALARM_REGS];
    /* 定义寄存器返回值变量 */
    int ret;
    /* 定义闹钟中断使能状态临时变量 */
    unsigned int int_val;

    /* 批量一次性读取全部闹钟寄存器:秒/分/时/日/月/年 */
    ret = regmap_bulk_read(rk808->regmap, rk808_rtc->creg->alarm_seconds_reg,
                alarm_data, NUM_ALARM_REGS);
    if (ret) {
        dev_err(dev, "Failed to bulk read alarm reg: %d\n", ret);
        return ret;
    }

    /* 硬件BCD码格式闹钟数据 -> 系统二进制标准时间格式转换 */
    alrm->time.tm_sec = bcd2bin(alarm_data[0] & SECONDS_REG_MSK);
    alrm->time.tm_min = bcd2bin(alarm_data[1] & MINUTES_REG_MAK);
    alrm->time.tm_hour = bcd2bin(alarm_data[2] & HOURS_REG_MSK);
    alrm->time.tm_mday = bcd2bin(alarm_data[3] & DAYS_REG_MSK);
    alrm->time.tm_mon = bcd2bin(alarm_data[4] & MONTHS_REG_MSK) - 1;
    alrm->time.tm_year = bcd2bin(alarm_data[5] & YEARS_REG_MSK) + 100;

    /* 芯片需要日历转换则:硬件非标日历转为系统标准公历 */
    if (rk808_rtc->flag & RTC_NEED_TRANSITIONS)
        rockchip_to_gregorian(&alrm->time);

    /* 读取RTC中断寄存器,获取闹钟中断当前使能开关状态 */
    ret = regmap_read(rk808->regmap, rk808_rtc->creg->int_reg, &int_val);
    if (ret)
        return ret;

    /* 判断闹钟中断使能位是否置1,赋值给内核闹钟启用标记 */
    alrm->enabled = !!(int_val & BIT_RTC_INTERRUPTS_REG_IT_ALARM_M);
    /* 初始化闹钟挂起标记为0,由内核PM子系统统一管理 */
    alrm->pending = 0;

    dev_dbg(dev, "Read alarm time: %4d-%02d-%02d %02d:%02d:%02d enabled=%d\n",
        1900 + alrm->time.tm_year, alrm->time.tm_mon + 1, alrm->time.tm_mday,
        alrm->time.tm_hour, alrm->time.tm_min, alrm->time.tm_sec, alrm->enabled);

    return 0;
}

关键解析:

  • 第16行:批量读取秒、分、时、日、月、年全部闹钟寄存器;

  • 第24行:秒数据BCD转二进制,掩码过滤无效位;

  • 第28行:月份减1,适配内核月份从0开始的计数规则;

  • 第29行:年份加100,将硬件2位年份转换为2000基准年份;

  • 第32行:判断是否需要特殊日历转换,适配RK硬件非标日历;

  • 第33行:将硬件特殊日历转换为系统标准公历;

  • 第36行:读取中断寄存器,获取闹钟中断的使能状态;

  • 第41行:解析中断使能位,赋值给内核闹钟启用标志;

  • 第43行:清空闹钟挂起标记,交由内核电源管理子系统维护;

  • 第45行:打印调试日志,输出读取到的闹钟时间和使能状态。

设置闹钟定时函数

rk808_rtc_setalarm函数接收内核传递的定时闹钟时间,先关闭旧闹钟、清除残留标志释放资源,公历转RK硬件日历、二进制转BCD码,批量写入闹钟寄存器,完成硬件闹钟定时配置。

rk808_rtc_setalarm函数(位于内核源码/drivers/rtc/rtc-rk808.c)
 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
/* RTC标准接口:配置硬件闹钟定时时间,休眠唤醒定时核心底层函数 */
static int rk808_rtc_setalarm(struct device *dev, struct rtc_wkalrm *alrm)
{
    /* 获取RTC驱动私有数据指针 */
    struct rk808_rtc *rk808_rtc = dev_get_drvdata(dev);
    /* 获取PMIC regmap寄存器操作句柄 */
    struct rk808 *rk808 = rk808_rtc->rk808;
    /* 定义闹钟寄存器写入缓存数组 */
    u8 alarm_data[NUM_ALARM_REGS];
    /* 定义函数返回状态变量 */
    int ret;

    dev_dbg(dev, "Set alarm time: %4d-%02d-%02d %02d:%02d:%02d enable=%d\n",
        1900 + alrm->time.tm_year, alrm->time.tm_mon + 1, alrm->time.tm_mday,
        alrm->time.tm_hour, alrm->time.tm_min, alrm->time.tm_sec, alrm->enabled);

    /* 防忙操作:先停止旧闹钟、清除残留标志、释放资源 */
    rk808_rtc_stop_alarm(rk808_rtc);

    /* 需要日历转换:系统标准公历 -> RK硬件特殊非标日历适配 */
    if (rk808_rtc->flag & RTC_NEED_TRANSITIONS)
        gregorian_to_rockchip(&alrm->time);

    /* 系统二进制时间 -> 硬件寄存器识别的BCD码格式转换 */
    alarm_data[0] = bin2bcd(alrm->time.tm_sec);
    alarm_data[1] = bin2bcd(alrm->time.tm_min);
    alarm_data[2] = bin2bcd(alrm->time.tm_hour);
    alarm_data[3] = bin2bcd(alrm->time.tm_mday);
    alarm_data[4] = bin2bcd(alrm->time.tm_mon + 1);
    alarm_data[5] = bin2bcd(alrm->time.tm_year - 100);

    /* 批量写入全部闹钟时间寄存器,一次性生效定时配置 */
    ret = regmap_bulk_write(rk808->regmap, rk808_rtc->creg->alarm_seconds_reg,
                alarm_data, NUM_ALARM_REGS);
    if (ret) {
        dev_err(dev, "Failed to bulk write alarm reg: %d\n", ret);
        return ret;
    }

    /* 根据内核开关标记,决定是否开启闹钟中断唤醒功能 */
    if (alrm->enabled)
        rk808_rtc_start_alarm(rk808_rtc); /* 开启闹钟中断,允许休眠唤醒 */

    return 0;
}

关键解析:

  • 第13行:打印调试日志,输出待设置的闹钟时间和使能状态;

  • 第18行:调用停止闹钟函数,关闭旧闹钟并清除中断标志,防止配置冲突;

  • 第21行:判断硬件是否需要日历转换,适配RK定制芯片的特殊日历规则;

  • 第22行:将系统标准公历转换为RK硬件可识别的特殊日历格式;

  • 第25行:秒数据二进制转BCD码,填充闹钟写入缓存数组;

  • 第29行:月份+ 1适配硬件计数规则,硬件从1开始,内核从0开始;

  • 第30行:年份减100,将2000基准年份转换为硬件2位年份格式;

  • 第33行:通过regmap批量写入秒、分、时、日、月、年全部闹钟寄存器;

  • 第41行:判断内核闹钟使能标记,决定是否启动硬件闹钟;

  • 第42行:调用启动闹钟函数,开启中断使能,支持系统休眠唤醒。

闹钟中断开关控制函数

rk808_rtc_alarm_irq_enable函数是RTC子系统标准回调接口,专门供内核动态开启/关闭闹钟中断,不修改闹钟定时时间,只控制硬件闹钟中断总开关,适配内核休眠前使能唤醒、唤醒后关闭中断的电源管理逻辑。

rk808_rtc_alarm_irq_enable函数(位于内核源码/drivers/rtc/rtc-rk808.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/* RTC标准接口:单独控制闹钟中断全局开关,开启/关闭闹钟唤醒能力 */
static int rk808_rtc_alarm_irq_enable(struct device *dev, unsigned int enabled)
{
    /* 获取RTC驱动私有数据结构体 */
    struct rk808_rtc *rk808_rtc = dev_get_drvdata(dev);

    /* 根据入参enabled标记,判断开启或关闭闹钟中断 */
    if (enabled) {
        /* 开启闹钟中断:激活硬件闹钟唤醒能力 */
        rk808_rtc_start_alarm(rk808_rtc);
    } else {
        /* 关闭闹钟中断:清除标志+释放资源,禁止闹钟触发 */
        rk808_rtc_stop_alarm(rk808_rtc);
    }

    return 0;
}

关键解析:

  • 第8行:根据传入的enabled参数执行分支逻辑;

  • 第10行:参数为1时,调用启动闹钟函数,使能闹钟中断,支持休眠唤醒;

  • 第13行:参数为0时,调用停止闹钟函数,关闭中断并清除触发标志。

中断处理函数

rk808_alarm_irq函数是闹钟中断核心回调函数,休眠唤醒后自动触发,负责清除硬件闹钟状态标志,向Linux RTC子系统上报闹钟中断事件,完成唤醒后置处理,保证中断正常结束、资源正常释放。

rk808_alarm_irq函数(位于内核源码/drivers/rtc/rtc-rk808.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;

    /* 写入状态掩码,清除所有RTC状态标志 */
    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子系统上报闹钟中断事件,通知内核唤醒完成 */
    rtc_update_irq(rk808_rtc->rtc, 1, RTC_IRQF | RTC_AF);
    dev_dbg(&client->dev,
        "%s:irq=%d\n", __func__, irq);
    return IRQ_HANDLED;
}

关键解析:

  • 第9行:写入状态寄存器,清除所有RTC中断标志位,避免中断重复触发;

  • 第18行:调用内核RTC子系统API,上报闹钟中断事件,触发系统休眠唤醒;

  • 第19行:打印中断调试日志,输出当前中断号;

  • 第21行:返回IRQ_HANDLED,告知内核中断已成功处理。

RTC子系统接口配置

通过rtc_class_ops结构体统一绑定时间读写、闹钟配置、中断使能标准接口,对接Linux内核RTC子系统,内核自动生成/dev/rtc设备节点,用户态直接通过标准接口操作时间与闹钟。

rtc_class_ops结构体(位于内核源码/drivers/rtc/rtc-rk808.c)
1
2
3
4
5
6
7
8
/* RTC标准操作接口结构体 */
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, /* 闹钟中断开关接口 */
};

probe函数

probe函数是平台驱动初始化入口,完成多芯片兼容配置、私有资源分配、中断注册、RTC设备注册。

probe函数(位于内核源码/drivers/rtc/rtc-rk808.c)
  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
static int rk808_rtc_probe(struct platform_device *pdev)
{
    /* 从平台设备父设备获取RK808系列PMIC主设备私有数据 */
    struct rk808 *rk808 = dev_get_drvdata(pdev->dev.parent);
    /* 定义RK808 RTC驱动私有数据结构体指针 */
    struct rk808_rtc *rk808_rtc;
    /* 定义设备树节点结构体指针,用于读取rtc设备树配置 */
    struct device_node *np;
    /* 定义函数返回值 */
    int ret;

    /* 根据PMIC芯片型号判断,筛选需要校验rtc设备树状态的芯片型号 */
    switch (rk808->variant) {
    case RK805_ID:
    case RK808_ID:
    case RK809_ID:
    case RK816_ID:
    case RK818_ID:
        /* 从PMIC父设备设备树节点中获取rtc子节点 */
        np = of_get_child_by_name(pdev->dev.parent->of_node, "rtc");
        /* 判断rtc节点存在且设备状态为禁用,则打印日志并返回报错退出初始化 */
        if (np && !of_device_is_available(np)) {
            dev_info(&pdev->dev, "device is disabled\n");
            return -EINVAL;
        }
        break;

    default:
        break;
    }

    /* 为RTC驱动私有数据结构体申请内核托管内存,避免内存泄漏 */
    rk808_rtc = devm_kzalloc(&pdev->dev, sizeof(*rk808_rtc), GFP_KERNEL);
    if (rk808_rtc == NULL)
        return -ENOMEM;

    /* 根据当前PMIC芯片型号,匹配对应的寄存器兼容配置与功能标记 */
    switch (rk808->variant) {
    case RK808_ID:  /* RK808、RK818芯片共用基础寄存器配置,且需要日历格式转换 */
    case RK818_ID:
        rk808_rtc->creg = &rk808_creg;          /* 绑定基础寄存器映射表 */
        rk808_rtc->flag |= RTC_NEED_TRANSITIONS;/* 开启日历双向转换标记 */
        break;
    case RK805_ID:  /* RK805、RK816芯片共用基础寄存器配置,无需日历转换 */
    case RK816_ID:
        rk808_rtc->creg = &rk808_creg;        /* 绑定基础寄存器映射表 */
        break;
    case RK809_ID:  /* RK809、RK817芯片使用专属寄存器映射配置 */
    case RK817_ID:
        rk808_rtc->creg = &rk817_creg;        /* 绑定专属寄存器映射表 */
        break;
    default:        /* 未知芯片型号默认使用基础寄存器配置 */
        rk808_rtc->creg = &rk808_creg;        /* 默认绑定基础寄存器映射表 */
        break;
    }

    /* 将RTC私有数据挂载到平台设备,后续驱动接口可快速获取 */
    platform_set_drvdata(pdev, rk808_rtc);
    /* 关联私有数据与PMIC主设备句柄,用于后续regmap寄存器读写 */
    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;
    }

    /* 初始化写入RTC状态寄存器,清除所有闹钟及RTC状态标志位 */
    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;
    }

    /* 初始化设备唤醒能力,标记当前设备支持RTC闹钟休眠唤醒功能 */
    device_init_wakeup(&pdev->dev, 1);

    /* 向内核RTC子系统申请分配RTC设备结构体资源 */
    rk808_rtc->rtc = devm_rtc_allocate_device(&pdev->dev);
    if (IS_ERR(rk808_rtc->rtc))
        return PTR_ERR(rk808_rtc->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子系统注册RTC设备,完成驱动初始化全流程 */
    return devm_rtc_register_device(rk808_rtc->rtc);
}

关键解析:

  • 第13行:根据PMIC芯片型号,筛选需要检查设备树状态的芯片类型;

  • 第20行:从设备树中获取RTC子节点,判断硬件是否使能;

  • 第22行:RTC节点被禁用时,打印日志并退出驱动初始化;

  • 第33行:为RTC私有数据申请内核托管内存,自动释放无内存泄漏;

  • 第38行:根据芯片型号匹配寄存器配置和功能标记,兼容多型号RK808系列芯片;

  • 第39-42行:为RK808/RK818绑定寄存器映射表,并开启特殊日历转换功能;

  • 第52-53行:为未知型号芯片默认使用基础寄存器配置;

  • 第58行:将RTC私有数据绑定到平台设备,供后续驱动接口调用;

  • 第60行:关联PMIC主设备句柄,用于寄存器读写操作;

  • 第63行:初始化RTC控制寄存器,配置时间读取模式、关闭时钟停止位;

  • 第74行:清除RTC所有状态标志位,初始化硬件中断状态;

  • 第83行:启用设备唤醒功能,标记RTC支持休眠唤醒;

  • 第86行:向内核RTC子系统申请RTC设备资源;

  • 第90行:绑定RTC读写/闹钟等标准操作接口集合;

  • 第93行:从平台资源中获取硬件闹钟中断号;

  • 第98行:注册线程化闹钟中断,绑定中断处理函数rk808_alarm_irq;

  • 第108行:向内核注册RTC设备,完成驱动初始化,用户层可访问RTC设备。

休眠唤醒PM电源管理回调

休眠唤醒PM电源管理回调(位于内核源码/drivers/rtc/rtc-rk808.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 系统进入休眠前,开启中断唤醒能力 */
static int rk808_rtc_suspend(struct device *dev)
{
    struct rk808_rtc *rk808_rtc = dev_get_drvdata(dev);
    /* 设备支持唤醒则使能IRQ唤醒 */
    if (device_may_wakeup(dev))
        enable_irq_wake(rk808_rtc->irq);
    return 0;
}

/* 系统唤醒恢复后,关闭中断唤醒能力 */
static int rk808_rtc_resume(struct device *dev)
{
    struct rk808_rtc *rk808_rtc = dev_get_drvdata(dev);
    /* 唤醒完成后禁用IRQ唤醒,恢复正常中断模式 */
    if (device_may_wakeup(dev))
        disable_irq_wake(rk808_rtc->irq);
    return 0;
}

/* 绑定休眠唤醒操作到平台驱动 */
static SIMPLE_DEV_PM_OPS(rk808_rtc_pm_ops,
    rk808_rtc_suspend, rk808_rtc_resume);

关键解析:

  • rk808_rtc_suspend函数:内核电源管理标准休眠回调函数,系统进入休眠前自动执行

    • 第6行:判断当前设备是否被配置为系统唤醒源;

    • 第7行:使能RTC闹钟中断的唤醒功能,允许闹钟触发系统唤醒。

  • rk808_rtc_resume函数:内核电源管理标准唤醒回调函数,系统从休眠恢复时自动执行

    • 第16行:判断当前设备是否为系统唤醒源;

    • 第17行:关闭中断唤醒功能,将闹钟中断恢复为正常工作模式。

  • 第22行:使用Linux内核标准宏,封装休眠/唤醒函数。

驱动平台注册

基于Linux平台总线注册驱动,绑定probe初始化、PM电源管理回调与驱动名称,适配设备树匹配机制,内核自动匹配PMIC RTC节点并加载驱动。

驱动平台注册(位于内核源码/drivers/rtc/rtc-rk808.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/* 平台驱动核心结构体:绑定初始化、电源管理与驱动名称 */
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);

15.5.1.1. Rockchip PMIC RTC测试

PMIC RTC需要真正使用RK809的RTC功能的板卡才能进行测试,其他板卡参考其他小节测试对应RTC:

  • LubanCat-1(V1、V2)

  • LubanCat-2(V1、V2)

  • LubanCat-2N(V1、V2)

以上板卡从V3版本开始均使用外部低功耗RTC。

测试RTC时间读取和设置

以LubanCat-2NV2板卡为例进行操作,测试需要提前接RTC电池, 参考 快速使用手册RTC章节 进行连接,连接好后再执行以下命令:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#查看系统启动rtc驱动加载信息
dmesg | grep rk808-rtc

#信息打印如下
[    3.171351] rockchip-vop2 fe040000.vop: [drm:vop2_bind] VP2 plane_mask is zero, so ignore register crtc
[    3.173744] rockchip-drm display-subsystem: [drm] Cannot find any crtc or sizes
[    3.173846] rockchip-drm display-subsystem: [drm] Cannot find any crtc or sizes
[    7.252799] rk808-rtc rk808-rtc: registered as rtc0
[    7.271570] rk808-rtc rk808-rtc: setting system clock to 2017-08-04T09:00:08 UTC (1501837208)

#查看rtc0的日期时间
cat /sys/class/rtc/rtc0/date

#信息打印如下
2017-08-04

#查看rtc0的小时、分钟、秒时间
cat /sys/class/rtc/rtc0/time

#信息打印如下
09:12:36

可以看到rk808-rtc被注册为rtc0,并且由于没有同步正确的时间到rk808-rtc,读取到的时间为2017年的错误时间。

设置正确时间到rk808-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
#如果系统有网络,重启ntp服务可网络校准时间
sudo systemctl restart ntp

#如果系统无网络,可通过date命令手动设置当前时间
sudo date -s "2026-04-29 14:35:30"

#通过date命令查询本地时间,CST(中国标准时间)
date

#信息打印如下
Wed Apr 29 14:35:45 CST 2026

#通过date命令查询系统时间(UTC)
date -u

#信息打印如下,作者位于东八区,使用上海时间,因此和UTC时间相差8小时
Wed Apr 29 06:35:48 CST 2026

#系统时间同步到硬件rtc
sudo hwclock -w

#查看rtc0的日期时间
cat /sys/class/rtc/rtc0/date

#信息打印如下
2026-04-29

#查看rtc0的小时、分钟、秒时间
cat /sys/class/rtc/rtc0/time

#信息打印如下
06:36:42

#硬件rtc同步到系统
sudo hwclock -s

以上手动设置时间或者网络同步时间后,-w将系统时间写入到硬件rtc,-s再将rtc时间写回系统,这样每次重启板卡都会自动进行rtc时间同步到系统时间。

断电一段时间再上电查看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
#查看系统启动rtc驱动加载信息
dmesg | grep rk808-rtc

#信息打印如下
[    3.197096] rockchip-vop2 fe040000.vop: [drm:vop2_bind] VP2 plane_mask is zero, so ignore register crtc
[    3.199471] rockchip-drm display-subsystem: [drm] Cannot find any crtc or sizes
[    3.199568] rockchip-drm display-subsystem: [drm] Cannot find any crtc or sizes
[    7.334985] rk808-rtc rk808-rtc: registered as rtc0
[    7.336850] rk808-rtc rk808-rtc: setting system clock to 2026-04-29T06:53:38 UTC (1777445618)

#查看rtc0的日期时间
cat /sys/class/rtc/rtc0/date

#信息打印如下
2026-04-29

#查看rtc0的小时、分钟、秒时间
cat /sys/class/rtc/rtc0/time

#信息打印如下
06:55:43

#通过date命令查询本地时间
date

#信息打印如下
Wed Apr 29 14:55:50 CST 2026

#通过date命令查询系统时间,UTC(世界协调时间)
date -u

#信息打印如下
Wed Apr 29 06:55:53 UTC 2026

可以看到断电时间仍能保持,系统启动后也能自动将RTC时间同步到系统,说明RTC功能正常。

测试RTC定时

通过wakealarm接口可以设置定时时间:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#查看rtc中断号和中断次数
cat /proc/interrupts | grep RTC

#信息打印如下,中断号104,4个0分别对应CPU0~CPU3上的中断次数,均为0次
104:          0          0          0          0     rk817   5 Edge      RTC alarm

#清空定时时间,避免资源繁忙或占用
sudo sh -c "echo 0 > /sys/class/rtc/rtc0/wakealarm"

#设置定时时间,30秒定时
sudo sh -c "echo +30 > /sys/class/rtc/rtc0/wakealarm"

#等待30s再查看rtc中断次数
cat /proc/interrupts | grep RTC

#信息打印如下,在CPU2上产生一次中断
104:          0          0          1          0     rk817   5 Edge      RTC alarm

休眠唤醒测试

可以通过先设置定时时间,然后再进入休眠,待定时时间到后触发中断唤醒系统:

注意

该功能需要使用5.10内核版本及以上的镜像。

 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
#清空定时时间,避免资源繁忙或占用
sudo sh -c "echo 0 > /sys/class/rtc/rtc0/wakealarm"

#设置定时时间,30秒定时
sudo sh -c "echo +30 > /sys/class/rtc/rtc0/wakealarm"

#进入freeze休眠
sudo sh -c "echo freeze > /sys/power/state"

#进入休眠信息打印如下
[  941.552850] PM: suspend entry (s2idle)
[  941.563413] Filesystems sync: 0.010 seconds
[  941.760095] Freezing user space processes ... (elapsed 0.003 seconds) done.
[  941.764232] OOM killer disabled.
[  941.764274] Freezing remaining freezable tasks ... (elapsed 0.002 seconds) done.
[  941.766616] printk: Suspending console(s) (use no_console_suspend to debug)

#唤醒信息打印如下
[  966.826010] PM: pm_system_irq_wakeup: 98 triggered rk817
[  966.835843] rk_gmac-dwmac fe2a0000.ethernet: init for RGMII
[  966.836124] rk_gmac-dwmac fe010000.ethernet: init for RGMII
[  966.967533] rk_gmac-dwmac fe010000.ethernet eth1: configuring for phy/rgmii link mode
[  967.149071] ata1: SATA link down (SStatus 0 SControl 300)
[  968.481365] dwmac4: Master AXI performs any burst length
[  968.481393] rk_gmac-dwmac fe010000.ethernet eth1: No Safety Features support found
[  968.481420] rk_gmac-dwmac fe010000.ethernet eth1: IEEE 1588-2008 Advanced Timestamp supported
[  968.486016] xhci-hcd xhci-hcd.4.auto: xHC error in resume, USBSTS 0x401, Reinit
[  968.486031] usb usb5: root hub lost power or was reset
[  968.486039] usb usb6: root hub lost power or was reset
[  968.607667] OOM killer enabled.
[  968.607684] Restarting tasks ... done.
[  968.614896] Resume caused by misconfigured IRQ 95 debug
[  968.623724] PM: suspend exit

除了freeze休眠,也可以进入mem休眠,关于这两种休眠区别就不作展开,可自行搜索了解,自行测试。

15.5.2. HYM8563 RTC驱动详解

由于大部分使用外部RTC的鲁班猫板卡使用的RTC型号是HYM8563S/BM8563,都可以使用内核自带的rtc-hym8563.c驱动,因此也对该驱动进行详细说明。 hym8563驱动基于I2C子系统、RTC子系统、设备树匹配框架开发,与RK808属于PMIC内置RTC不同,HYM8563是纯独立I2C接口RTC外设芯片, 无需依赖PMIC主控,自带32.768KHz外置晶振计时。

驱动源码位于: 内核源码/drivers/rtc/rtc-hym8563.c

以下以瑞芯微6.1.99内核版本的hym8563驱动进行分析:

核心定义与数据结构

驱动通过宏定义封装HYM8563芯片所有控制寄存器、时间寄存器、闹钟寄存器、定时器寄存器、时钟输出寄存器的物理地址与关键位掩码。 所有寄存器按功能分区管理,涵盖时钟启停、中断开关、时间掩码、闹钟禁用位、定时器分频配置等核心控制标记。

核心定义与数据结构(位于内核源码/drivers/rtc/rtc-hym8563.c)
 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
/* 芯片基础控制寄存器地址定义 */
#define HYM8563_CTL1        0x00        /* 控制寄存器1:时钟启停、测试模式配置 */
#define HYM8563_CTL1_TEST   BIT(7)      /* 测试模式使能位:工厂测试专用,驱动默认关闭 */
#define HYM8563_CTL1_STOP   BIT(5)      /* RTC时钟停止位:置1暂停计数,时间写入必备 */
#define HYM8563_CTL1_TESTC  BIT(3)      /* 测试时钟选择位:正常工作模式无效 */
#define HYM8563_CTL2        0x01        /* 控制寄存器2:中断使能、中断状态标志位 */
#define HYM8563_CTL2_TI_TP  BIT(4)      /* 定时器中断脉冲模式配置位 */
#define HYM8563_CTL2_AF     BIT(3)      /* 日历闹钟中断触发状态标志位 */
#define HYM8563_CTL2_TF     BIT(2)      /* 定时器中断触发状态标志位 */
#define HYM8563_CTL2_AIE    BIT(1)      /* 日历闹钟中断总使能开关 */
#define HYM8563_CTL2_TIE    BIT(0)      /* 定时器中断总使能开关 */

/* 实时时间寄存器地址+有效数据掩码定义 */
#define HYM8563_SEC         0x02        /* 秒数寄存器地址 */
#define HYM8563_SEC_VL      BIT(7)      /* 时间有效性标记位:1=时间无效,0=时间正常 */
#define HYM8563_SEC_MASK    0x7f        /* 秒数BCD码有效位掩码,屏蔽无效高位 */
#define HYM8563_MIN         0x03        /* 分钟寄存器地址 */
#define HYM8563_MIN_MASK    0x7f        /* 分钟BCD码有效位掩码 */
#define HYM8563_HOUR        0x04        /* 小时寄存器地址 */
#define HYM8563_HOUR_MASK   0x3f        /* 小时BCD码有效位掩码(24小时制) */
#define HYM8563_DAY         0x05        /* 日期寄存器地址 */
#define HYM8563_DAY_MASK    0x3f        /* 日期BCD码有效位掩码 */
#define HYM8563_WEEKDAY     0x06        /* 星期寄存器地址 */
#define HYM8563_WEEKDAY_MASK    0x07    /* 星期BCD码有效位掩码 */
#define HYM8563_MONTH       0x07        /* 月份寄存器地址 */
#define HYM8563_MONTH_CENTURY   BIT(7)  /* 世纪标记位:驱动未使用,固定2000年起始 */
#define HYM8563_MONTH_MASK  0x1f        /* 月份BCD码有效位掩码 */
#define HYM8563_YEAR        0x08        /* 年份寄存器地址 */

/* 日历闹钟专用寄存器地址与禁用位定义 */
#define HYM8563_ALM_MIN     0x09        /* 闹钟分钟寄存器 */
#define HYM8563_ALM_HOUR    0x0a        /* 闹钟小时寄存器 */
#define HYM8563_ALM_DAY     0x0b        /* 闹钟日期寄存器 */
#define HYM8563_ALM_WEEK    0x0c        /* 闹钟星期寄存器 */
#define HYM8563_ALM_BIT_DISABLE BIT(7)  /* 闹钟单项禁用位:置1不匹配该时间维度 */

/* 外部时钟输出CLKOUT寄存器定义 */
#define HYM8563_CLKOUT      0x0d        /* 时钟输出控制寄存器 */
#define HYM8563_CLKOUT_ENABLE   BIT(7)  /* 时钟输出总开关使能位 */
#define HYM8563_CLKOUT_32768    0       /* 输出频率:32768Hz */
#define HYM8563_CLKOUT_1024 1           /* 输出频率:1024Hz */
#define HYM8563_CLKOUT_32   2           /* 输出频率:32Hz */
#define HYM8563_CLKOUT_1    3           /* 输出频率:1Hz */
#define HYM8563_CLKOUT_MASK 3           /* 时钟频率配置位掩码 */

/* 硬件倒计时定时器寄存器定义 */
#define HYM8563_TMR_CTL     0x0e        /* 定时器控制寄存器 */
#define HYM8563_TMR_CTL_ENABLE  BIT(7)  /* 倒计时定时器总使能开关 */
#define HYM8563_TMR_CTL_4096    0       /* 定时器分频:4096分频 */
#define HYM8563_TMR_CTL_64  1           /* 定时器分频:64分频 */
#define HYM8563_TMR_CTL_1   2           /* 定时器分频:1:1秒级分频 */
#define HYM8563_TMR_CTL_1_60    3       /* 定时器分频:60秒分频 */
#define HYM8563_TMR_CTL_MASK    3       /* 定时器分频配置掩码 */
#define HYM8563_TMR_CNT     0x0f        /* 定时器倒计时计数值寄存器 */
#define HYM8563_TMR_MAXCNT  0xff        /* 定时器最大计数值:255秒 */
#define HYM8563_TMR_CFG     (HYM8563_TMR_CTL_ENABLE | HYM8563_TMR_CTL_1) /* 定时器默认配置:使能+1秒分频 */

/* HYM8563驱动私有数据结构体,统一管理所有运行资源 */
struct hym8563 {
    struct i2c_client   *client;        /* I2C设备客户端句柄,用于I2C寄存器读写 */
    struct rtc_device   *rtc;           /* 内核RTC子系统设备句柄,对接上层标准接口 */
#ifdef CONFIG_COMMON_CLK
    struct clk_hw       clkout_hw;      /* 时钟输出硬件结构体,适配外部时钟输出功能 */
#endif
    int alarm_or_timer_irq;             /* 双定时模式标记:1=定时器中断,0=日历闹钟中断 */
    int alarm_tm_sec;                   /* 闹钟秒数缓存:日历闹钟仅支持分时时分,秒数软件缓存补齐 */
};

关键解析:

  • 双中断体系:HYM8563自带日历闹钟AF中断和硬件定时器TF中断两套独立中断,。

  • 双定时阈值:定时器最大仅支持255秒短定时,超过255秒自动切换为日历闹钟定时。

读取硬件时间函数

hym8563_rtc_read_time函数通过I2C批量读取HYM8563秒/分/时/日/星期/月/年7个时间寄存器, 将硬件BCD码时间格式转为内核标准rtc_time二进制格式,适配星期、月份内核计数偏移规则,向上层子系统输出标准公历时间。

hym8563_rtc_read_time函数(位于内核源码/drivers/rtc/rtc-hym8563.c)
 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
/* 内核RTC标准接口:读取HYM8563硬件当前实时时间 */
static int hym8563_rtc_read_time(struct device *dev, struct rtc_time *tm)
{
    /* 转换设备为I2C客户端句柄 */
    struct i2c_client *client = to_i2c_client(dev);
    /* 定义批量读取时间寄存器缓存数组 */
    u8 buf[7];
    /* 寄存器操作返回状态值 */
    int ret;

    /* I2C批量读取7个连续时间寄存器,一次性获取完整时间数据 */
    ret = i2c_smbus_read_i2c_block_data(client, HYM8563_SEC, 7, buf);
    if (ret < 0)
        return ret;

    /* 硬件BCD码转换为内核二进制标准时间,按位掩码过滤无效位 */
    tm->tm_sec = bcd2bin(buf[0] & HYM8563_SEC_MASK);     /* 转换秒数 */
    tm->tm_min = bcd2bin(buf[1] & HYM8563_MIN_MASK);     /* 转换分钟 */
    tm->tm_hour = bcd2bin(buf[2] & HYM8563_HOUR_MASK);   /* 转换小时 */
    tm->tm_mday = bcd2bin(buf[3] & HYM8563_DAY_MASK);    /* 转换日期 */
    tm->tm_wday = bcd2bin(buf[4] & HYM8563_WEEKDAY_MASK);/* 转换星期:0代表周日 */
    tm->tm_mon = bcd2bin(buf[5] & HYM8563_MONTH_MASK) - 1;/* 转换月份:内核0代表1月,需减1对齐 */
    tm->tm_year = bcd2bin(buf[6]) + 100;                 /* 转换年份:基准2000年偏移补齐 */

    return 0;
}

关键解析:

  • 第7行:定义7字节数组,存储从硬件读取的秒、分、时、日、周、月、年原始BCD数据;

  • 第12行:通过I2C SMBus协议,从秒寄存器开始批量读取7个连续时间寄存器;

  • 第17行:秒数据BCD转二进制,掩码过滤寄存器无效位;

  • 第18行:分钟数据BCD转二进制,掩码过滤无效位;

  • 第19行:小时数据BCD转二进制,掩码过滤无效位;

  • 第20行:日期数据BCD转二进制,掩码过滤无效位;

  • 第21行:星期数据BCD转二进制,硬件0对应周日;

  • 第22行:月份数据解码后减1,适配Linux内核月份从0开始的计数规则;

  • 第23行:年份数据加100,将硬件2000基准年份对齐内核时间格式。

设置硬件时间函数

hym8563_rtc_set_time函数先做年份合法性校验,将内核标准二进制时间转为硬件BCD码格式,写入前置位时钟停止位暂停RTC计数, 批量写入时间寄存器后恢复时钟运行,避免写入过程时间跳动错乱,保障时间设置精准生效。

hym8563_rtc_set_time函数(位于内核源码/drivers/rtc/rtc-hym8563.c)
 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
/* 内核RTC标准接口:配置写入HYM8563硬件时间 */
static int hym8563_rtc_set_time(struct device *dev, struct rtc_time *tm)
{
    /* 获取I2C客户端句柄 */
    struct i2c_client *client = to_i2c_client(dev);
    /* 时间寄存器写入缓存数组 */
    u8 buf[7];
    /* 操作返回状态变量 */
    int ret;

    /* 年份合法性校验:限制时间范围1900-2100,避免芯片世纪位适配异常 */
    if (tm->tm_year < 100 || tm->tm_year >= 200)
        return -EINVAL;

    /* 内核二进制标准时间转为硬件识别BCD码格式 */
    buf[0] = bin2bcd(tm->tm_sec);       /* 秒数转BCD */
    buf[1] = bin2bcd(tm->tm_min);       /* 分钟转BCD */
    buf[2] = bin2bcd(tm->tm_hour);      /* 小时转BCD */
    buf[3] = bin2bcd(tm->tm_mday);      /* 日期转BCD */
    buf[4] = bin2bcd(tm->tm_wday);      /* 星期转BCD */
    buf[5] = bin2bcd(tm->tm_mon + 1);   /* 月份内核转硬件计数对齐 */
    buf[6] = bin2bcd(tm->tm_year - 100);/* 年份按2000年基准偏移转换 */

    /* 第一步:置位CTL1停止位,暂停RTC硬件计数,防止写入错乱 */
    ret = i2c_smbus_write_byte_data(client, HYM8563_CTL1, HYM8563_CTL1_STOP);
    if (ret < 0)
        return ret;

    /* 第二步:批量写入全部时间寄存器,时间统一生效 */
    ret = i2c_smbus_write_i2c_block_data(client, HYM8563_SEC, 7, buf);
    if (ret < 0)
        return ret;

    /* 第三步:清除停止位,恢复RTC硬件正常计时走时 */
    ret = i2c_smbus_write_byte_data(client, HYM8563_CTL1, 0);
    if (ret < 0)
        return ret;

    return 0;
}

关键解析:

  • 第7行:定义7字节缓存数组,存储转换后的BCD格式时间数据;

  • 第12行:年份合法性校验,限制范围适配硬件,防止时间格式异常;

  • 第16行:秒数据二进制转BCD码,填充写入缓存;

  • 第21行:月份+1对齐硬件计数规则,内核0= 1月,硬件1= 1月;

  • 第22行:年份-100,将内核2000基准年份转为硬件2位年份格式;

  • 第25行:写入控制寄存器,暂停硬件时钟计数,避免写入时时间自动更新;

  • 第30行:通过I2C批量写入秒、分、时、日、周、月、年全部时间寄存器;

  • 第35行:清除时钟停止位,恢复HYM8563硬件正常计时。

读取闹钟配置函数

hym8563_rtc_read_alarm函数用于读取硬件闹钟寄存器配置,转换BCD码为标准时间格式,软件补齐硬件缺失的秒级闹钟数据,同步读取中断使能状态,向上层内核返回当前已配置的闹钟定时信息。

hym8563_rtc_read_alarm函数(位于内核源码/drivers/rtc/rtc-hym8563.c)
 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
static int hym8563_rtc_read_alarm(struct device *dev, struct rtc_wkalrm *alm)
{
    /* I2C设备句柄 */
    struct i2c_client *client = to_i2c_client(dev);
    /* 私有数据指针 */
    struct hym8563 *hym8563 = i2c_get_clientdata(client);
    /* 内核闹钟时间结构体 */
    struct rtc_time *alm_tm = &alm->time;
    /* 闹钟寄存器读取缓存 */
    u8 buf[4];
    /* 操作返回值 */
    int ret;

    /* 批量读取4个闹钟时分日月寄存器数据 */
    ret = i2c_smbus_read_i2c_block_data(client, HYM8563_ALM_MIN, 4, buf);
    if (ret < 0)
        return ret;

    /* 软件缓存补齐闹钟秒数(硬件闹钟无秒寄存器) */
    alm_tm->tm_sec = hym8563->alarm_tm_sec;

    /* 判断闹钟单项禁用位,有效则转换BCD码,无效置为-1标记禁用 */
    alm_tm->tm_min = (buf[0] & HYM8563_ALM_BIT_DISABLE) ? -1 : bcd2bin(buf[0] & HYM8563_MIN_MASK);
    alm_tm->tm_hour = (buf[1] & HYM8563_ALM_BIT_DISABLE) ? -1 : bcd2bin(buf[1] & HYM8563_HOUR_MASK);
    alm_tm->tm_mday = (buf[2] & HYM8563_ALM_BIT_DISABLE) ? -1 : bcd2bin(buf[2] & HYM8563_DAY_MASK);
    alm_tm->tm_wday = (buf[3] & HYM8563_ALM_BIT_DISABLE) ? -1 : bcd2bin(buf[3] & HYM8563_WEEKDAY_MASK);

    /* 读取中断寄存器,判断闹钟/定时器中断是否启用 */
    ret = i2c_smbus_read_byte_data(client, HYM8563_CTL2);
    if (ret < 0)
        return ret;
    alm->enabled = !!(ret & (HYM8563_CTL2_AIE | HYM8563_CTL2_TIE));

    return 0;
}

关键解析:

  • 第10行:定义4字节缓存数组,存储硬件读取的闹钟分、时、日、周原始数据;

  • 第15行:通过I2C批量读取4个连续闹钟寄存器(分/时/日/周);

  • 第20行:硬件闹钟无秒寄存器,从驱动软件缓存中补齐秒数;

  • 第23行:判断分钟禁用位,置1则标记禁用(-1),否则BCD转二进制;

  • 第24行:判断小时禁用位,置1则标记禁用(-1),否则BCD转二进制;

  • 第25行:判断日期禁用位,置1则标记禁用(-1),否则BCD转二进制;

  • 第26行:判断星期禁用位,置1则标记禁用(-1),否则BCD转二进制;

  • 第29行:读取控制寄存器2,获取闹钟/定时器中断使能状态;

  • 第32行:检测中断使能位,赋值闹钟启用标志。

设置闹钟定时函数

hym8563_rtc_set_alarm函数是wakealarm定时核心底层函数:先清空旧定时器与闹钟标志,读取当前时间计算定时间隔,≤255秒短定时自动切硬件倒计时定时器,>255秒长定时切日历闹钟,自动适配双模式,写入闹钟寄存器后开启中断。

hym8563_rtc_set_alarm函数(位于内核源码/drivers/rtc/rtc-hym8563.c)
 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
/* RTC标准接口:配置闹钟定时,自动切换定时器/日历双模式 */
static int hym8563_rtc_set_alarm(struct device *dev, struct rtc_wkalrm *alm)
{
    /* I2C设备句柄 */
    struct i2c_client *client = to_i2c_client(dev);
    /* 驱动私有数据 */
    struct hym8563 *hym8563 = i2c_get_clientdata(client);
    /* 待设置闹钟时间 */
    struct rtc_time *alm_tm = &alm->time;
    /* 当前系统时间缓存 */
    struct rtc_time tm;
    /* 当前时间、闹钟时间、间隔秒数 */
    time64_t now, alarm, interval;
    /* 闹钟寄存器写入缓存 */
    u8 buf[4];
    /* 操作返回状态 */
    int ret;

    /* 第一步:清空旧定时器计数值,复位残留定时数据 */
    ret = i2c_smbus_write_byte_data(client, HYM8563_TMR_CNT, 0);
    if (ret < 0)
        return ret;

    /* 第二步:清空CTL2所有中断使能与状态标志,释放旧闹钟资源 */
    ret = i2c_smbus_write_byte_data(client, HYM8563_CTL2, 0);
    if (ret < 0)
        return ret;

    /* 读取当前硬件实时时间,计算定时间隔秒数 */
    ret = hym8563_rtc_read_time(dev, &tm);
    if (ret < 0)
        return ret;
    alarm = rtc_tm_to_time64(alm_tm);  /* 闹钟时间转时间戳 */
    now = rtc_tm_to_time64(&tm);       /* 当前时间转时间戳 */
    interval = alarm - now;            /* 计算定时剩余秒数 */

    /* 软件缓存闹钟秒数,补齐硬件无秒闹钟短板 */
    hym8563->alarm_tm_sec = alm_tm->tm_sec;

    /* 核心双定时自动判断:短定时用定时器,长定时用日历闹钟 */
    if (interval < HYM8563_TMR_MAXCNT) {
        /* 间隔≤255秒:启用硬件倒计时定时器中断 */
        hym8563->alarm_or_timer_irq = 1;
        i2c_smbus_write_byte_data(client, HYM8563_TMR_CNT, (u8)interval);
    } else {
        /* 间隔>255秒:启用日历闹钟中断,秒数置0靠日期匹配 */
        hym8563->alarm_or_timer_irq = 0;
        alm_tm->tm_sec = 0;
    }

    /* 校验时分日月合法性,非法维度置禁用位,只匹配有效时间 */
    buf[0] = (alm_tm->tm_min < 60 && alm_tm->tm_min >= 0) ? bin2bcd(alm_tm->tm_min) : HYM8563_ALM_BIT_DISABLE;
    buf[1] = (alm_tm->tm_hour < 24 && alm_tm->tm_hour >= 0) ? bin2bcd(alm_tm->tm_hour) : HYM8563_ALM_BIT_DISABLE;
    buf[2] = (alm_tm->tm_mday <= 31 && alm_tm->tm_mday >= 1) ? bin2bcd(alm_tm->tm_mday) : HYM8563_ALM_BIT_DISABLE;
    buf[3] = (alm_tm->tm_wday < 7 && alm_tm->tm_wday >= 0) ? bin2bcd(alm_tm->tm_wday) : HYM8563_ALM_BIT_DISABLE;

    /* 批量写入闹钟配置寄存器 */
    ret = i2c_smbus_write_i2c_block_data(client, HYM8563_ALM_MIN, 4, buf);
    if (ret < 0)
        return ret;

    /* 根据配置开启对应定时中断,完成闹钟设置 */
    return hym8563_rtc_alarm_irq_enable(dev, alm->enabled);
}

关键解析:

  • 第13行:定义64位时间戳变量,计算当前时间与闹钟的间隔秒数;

  • 第15行:定义4字节缓存数组,存储闹钟分/时/日/周的配置数据;

  • 第20行:清空硬件定时器计数寄存器,复位旧的倒计时配置;

  • 第25行:清空控制寄存器2,关闭所有中断、清除历史触发标志;

  • 第30行:读取硬件当前实时时间,为时间间隔计算做准备;

  • 第33行:将闹钟时间转换为64位Unix时间戳;

  • 第34行:将当前系统时间转换为64位Unix时间戳;

  • 第35行:计算闹钟与当前时间的差值,得到定时间隔秒数;

  • 第38行:将闹钟秒数存入软件缓存,弥补硬件闹钟无秒寄存器的缺陷;

  • 第41行:根据间隔秒数自动选择定时模式(硬件定时器/日历闹钟);

  • 第43行:标记当前使用硬件倒计时定时器模式;

  • 第44行:将间隔秒数写入定时器寄存器,启动硬件倒计时;

  • 第47行:标记当前使用日历闹钟模式;

  • 第48行:日历闹钟无秒寄存器,强制秒数置0;

  • 第52行:校验分钟合法性,合法转BCD,非法置禁用位;

  • 第53行:校验小时合法性,合法转BCD,非法置禁用位;

  • 第54行:校验日期合法性,合法转BCD,非法置禁用位;

  • 第55行:校验星期合法性,合法转BCD,非法置禁用位;

  • 第58行:批量写入闹钟分/时/日/周配置寄存器;

  • 第63行:调用中断使能函数,根据配置开启/关闭闹钟唤醒功能。

闹钟中断开关控制函数

hym8563_rtc_alarm_irq_enable函数是RTC子系统标准回调接口,根据双定时模式标记,动态开启/关闭定时器中断或日历闹钟中断, 统一管控中断总开关,休眠唤醒启停复用该函数,保证中断使能时序标准化。

hym8563_rtc_alarm_irq_enable函数(位于内核源码/drivers/rtc/rtc-hym8563.c)
 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
static int hym8563_rtc_alarm_irq_enable(struct device *dev, unsigned int enabled)
{
    /* 获取I2C设备句柄 */
    struct i2c_client *client = to_i2c_client(dev);
    /* 获取驱动私有数据 */
    struct hym8563 *hym8563 = i2c_get_clientdata(client);
    /* 控制寄存器临时缓存变量 */
    int data;

    /* 读取当前CTL2中断配置寄存器原始值 */
    data = i2c_smbus_read_byte_data(client, HYM8563_CTL2);
    if (data < 0)
        return data;

    /* 根据使能标记与定时模式,选择性开启对应中断 */
    if (enabled) {
        if (hym8563->alarm_or_timer_irq)  /* 短定时:开启硬件定时器中断 */
            data |= HYM8563_CTL2_TIE;
        else                              /* 长定时:开启日历闹钟中断 */
            data |= HYM8563_CTL2_AIE;
    } else {                              /* 关闭所有定时中断,清除使能位 */
        data &= ~HYM8563_CTL2_TIE;
        data &= ~HYM8563_CTL2_AIE;
    }

    /* 写入更新中断配置寄存器,生效开关状态 */
    return i2c_smbus_write_byte_data(client, HYM8563_CTL2, data);
};

关键解析:

  • 第11行:读取CTL2控制寄存器,获取当前中断使能状态;

  • 第16行:根据入参enabled判断,执行开启/关闭中断逻辑;

  • 第17行:判断当前为短定时模式;

  • 第18行:置位定时器中断使能位(TIE),开启倒计时中断;

  • 第19行:当前为长定时模式;

  • 第20行:置位闹钟中断使能位(AIE),开启日历闹钟中断;

  • 第21行:入参为0,关闭所有定时中断;

  • 第22行:清除定时器中断使能位;

  • 第23行:清除日历闹钟中断使能位;

  • 第27行:将更新后的配置写入CTL2寄存器,生效中断开关状态。

闹钟中断处理核心函数

hym8563_irq函数是定时中断触发后的核心回调函数,读取中断状态寄存器,清除闹钟AF标志与定时器TF标志,复位定时器计数值,解锁RTC资源,向上层内核返回中断处理完成标记,避免中断重复触发、标志残留问题。

hym8563_irq函数(位于内核源码/drivers/rtc/rtc-hym8563.c)
 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
/* 闹钟/定时器中断触发回调函数,休眠唤醒核心入口 */
static irqreturn_t hym8563_irq(int irq, void *dev_id)
{
    /* 获取私有数据 */
    struct hym8563 *hym8563 = (struct hym8563 *)dev_id;
    /* I2C设备句柄 */
    struct i2c_client *client = hym8563->client;
    /* 寄存器读写缓存 */
    int data, ret;

    /* 上锁保护RTC资源并发访问 */
    rtc_lock(hym8563->rtc);

    /* 读取中断状态寄存器,获取中断触发标记 */
    data = i2c_smbus_read_byte_data(client, HYM8563_CTL2);
    if (data < 0) {
        dev_err(&client->dev, "%s: error reading i2c data %d\n", __func__, data);
        goto out;
    }

    /* 清除日历闹钟中断标志、定时器中断标志,防止重复触发 */
    data &= ~HYM8563_CTL2_AF;
    data &= ~HYM8563_CTL2_TF;
    i2c_smbus_write_byte_data(client, HYM8563_TMR_CNT, 0); /* 复位定时器计数 */

    /* 写入清除后的状态,释放中断标志资源 */
    ret = i2c_smbus_write_byte_data(client, HYM8563_CTL2, data);
    if (ret < 0) {
        dev_err(&client->dev, "%s: error writing i2c data %d\n", __func__, ret);
    }

out:
    rtc_unlock(hym8563->rtc);     /* 解锁RTC资源 */
    return IRQ_HANDLED;           /* 标记中断处理完成 */
}

关键解析:

  • 第12行:对RTC设备加锁,防止多线程并发访问导致数据错乱;

  • 第15行:读取CTL2控制寄存器,获取闹钟/定时器中断触发状态;

  • 第22行:清除闹钟中断标志位(AF),避免中断重复触发;

  • 第23行:清除定时器中断标志位(TF),避免中断重复触发;

  • 第24行:复位硬件定时器计数值,清空倒计时状态;

  • 第27行:将清除中断标志后的配置写回CTL2寄存器,生效中断复位;

  • 第32行:统一退出标签,执行解锁操作;

  • 第33行:释放RTC设备锁,恢复并发访问权限;

  • 第34行:返回IRQ_HANDLED,告知内核中断已处理完成。

芯片上电初始化函数

hym8563_init_device函数是设备上电probe阶段初始化函数,复位控制寄存器、关闭所有中断、清除pending中断标志、配置定时器默认1秒分频,保证芯片上电处于初始状态。

hym8563_init_device函数(位于内核源码/drivers/rtc/rtc-hym8563.c)
 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
/* 芯片上电初始化,复位寄存器、关闭中断、配置默认定时器 */
static int hym8563_init_device(struct i2c_client *client)
{
    int ret;

    /* 读取控制寄存器1,清除时钟停止位,启动RTC计时 */
    ret = i2c_smbus_read_byte_data(client, HYM8563_CTL1);
    if (ret < 0)
        dev_err(&client->dev, "%s: error read i2c data %d\n", __func__, ret);
    ret = i2c_smbus_write_byte_data(client, HYM8563_CTL1, 0);
    if (ret < 0)
        return ret;

    /* 读取中断控制寄存器,关闭闹钟、定时器中断 */
    ret = i2c_smbus_read_byte_data(client, HYM8563_CTL2);
    if (ret < 0)
        return ret;
    ret &= ~HYM8563_CTL2_AIE;   /* 关闭闹钟中断使能 */
    ret &= ~HYM8563_CTL2_TIE;   /* 关闭定时器中断使能 */
    ret &= ~HYM8563_CTL2_TI_TP; /* 关闭脉冲中断模式 */

    /* 清除所有残留中断触发标志 */
    if (ret & HYM8563_CTL2_AF) ret &= ~HYM8563_CTL2_AF;
    if (ret & HYM8563_CTL2_TF) ret &= ~HYM8563_CTL2_TF;

    /* 复位定时器计数,配置定时器默认1秒分频模式 */
    i2c_smbus_write_byte_data(client, HYM8563_TMR_CNT, 0);
    i2c_smbus_write_byte_data(client, HYM8563_TMR_CTL, HYM8563_TMR_CFG);

    /* 写入初始化配置,完成上电复位 */
    return i2c_smbus_write_byte_data(client, HYM8563_CTL2, ret);
}

关键解析:

  • 第7行:读取CTL1控制寄存器原始值,获取当前时钟运行状态;

  • 第10行:向CTL1写入0,清除时钟停止位,启动RTC硬件计时;

  • 第15行:读取CTL2中断控制寄存器,获取中断使能和触发状态;

  • 第18行:清除闹钟中断使能位,关闭日历闹钟中断;

  • 第19行:清除定时器中断使能位,关闭硬件倒计时中断;

  • 第20行:清除中断脉冲模式位,配置为电平中断模式;

  • 第23行:判断并清除闹钟中断触发标志,复位中断状态;

  • 第24行:判断并清除定时器中断触发标志,复位中断状态;

  • 第27行:复位定时器计数寄存器,清空硬件倒计时数值;

  • 第28行:配置定时器控制寄存器,设置默认1秒时钟分频模式;

  • 第31行:将初始化后的CTL2配置写入硬件,完成芯片全流程上电初始化。

RTC子系统标准接口绑定

rtc_class_ops结构体统一绑定所有RTC标准回调接口,对接Linux内核RTC子系统,自动生成/dev/rtc设备节点, 用户态所有date、wakealarm操作都会回调底层驱动对应函数,上层完全屏蔽硬件差异。

rtc_class_ops结构体(位于内核源码/drivers/rtc/rtc-hym8563.c)
1
2
3
4
5
6
7
static const struct rtc_class_ops hym8563_rtc_ops = {
    .read_time      = hym8563_rtc_read_time,    /* 读取时间接口 */
    .set_time       = hym8563_rtc_set_time,     /* 设置时间接口 */
    .alarm_irq_enable   = hym8563_rtc_alarm_irq_enable, /* 中断开关接口 */
    .read_alarm     = hym8563_rtc_read_alarm,   /* 读取闹钟接口 */
    .set_alarm      = hym8563_rtc_set_alarm,    /* 设置闹钟接口 */
};

休眠唤醒PM电源管理函数

适配系统s2idle休眠唤醒流程,休眠前开启中断唤醒能力,唤醒后关闭中断唤醒模式,保证RTC闹钟休眠唤醒正常触发,唤醒后不占用中断资源。

休眠唤醒PM电源管理回调函数(位于内核源码/drivers/rtc/rtc-hym8563.c)
 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
#ifdef CONFIG_PM_SLEEP
/* 系统进入休眠前:开启IRQ唤醒能力 */
static int hym8563_suspend(struct device *dev)
{
    struct i2c_client *client = to_i2c_client(dev);
    int ret;
    /* 设备支持唤醒则使能IRQ唤醒 */
    if (device_may_wakeup(dev)) {
        ret = enable_irq_wake(client->irq);
        if (ret)
            dev_err(dev, "enable_irq_wake failed, %d\n", ret);
        return ret;
    }
    return 0;
}
/* 系统唤醒恢复后:关闭IRQ唤醒能力 */
static int hym8563_resume(struct device *dev)
{
    struct i2c_client *client = to_i2c_client(dev);
    int ret;
    ret = i2c_smbus_read_byte_data(client, HYM8563_CTL1);
    if (ret < 0)
        dev_err(&client->dev, "%s: error read i2c data %d\n", __func__, ret);
    /* 唤醒完成后禁用IRQ唤醒,恢复正常中断模式 */
    if (device_may_wakeup(dev))
        disable_irq_wake(client->irq);
    return 0;
}
#endif

/* 绑定休眠唤醒回调到驱动 */
static SIMPLE_DEV_PM_OPS(hym8563_pm_ops, hym8563_suspend, hym8563_resume);

关键解析:

  • 第1行:内核条件编译,仅开启CONFIG_PM_SLEEP(睡眠电源管理)时编译休眠唤醒逻辑;

  • hym8563_suspend函数:Linux内核PM标准休眠回调函数,系统进入休眠前自动调用

    • 第8行:判断设备是否被配置为系统唤醒源;

    • 第9行:使能RTC中断的休眠唤醒功能,允许闹钟触发系统唤醒。

  • hym8563_resume函数:Linux内核PM标准唤醒回调函数,系统从休眠恢复时自动调用

    • 第21行:读取CTL1控制寄存器,校验并恢复RTC时钟运行状态;

    • 第26行:关闭中断唤醒功能,将RTC中断恢复为正常工作模式。

  • 第32行:使用内核标准宏,封装休眠/唤醒函数为电源管理操作集合。

Probe函数

Probe函数(位于内核源码/drivers/rtc/rtc-hym8563.c)
 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
/* I2C设备匹配成功后驱动初始化入口 */
static int hym8563_probe(struct i2c_client *client)
{
    struct hym8563 *hym8563;
    int ret;
    /* 上电默认初始时间:2021年1月1日12:00:00,时间无效时自动重置 */
    struct rtc_time tm_read, tm = {
        .tm_wday = 0, .tm_year = 121, .tm_mon = 0,
        .tm_mday = 1, .tm_hour = 12, .tm_min = 0, .tm_sec = 0,
    };

    /* 申请驱动私有数据内核内存 */
    hym8563 = devm_kzalloc(&client->dev, sizeof(*hym8563), GFP_KERNEL);
    if (!hym8563)
        return -ENOMEM;

    /* 分配内核RTC设备结构体资源 */
    hym8563->rtc = devm_rtc_allocate_device(&client->dev);
    if (IS_ERR(hym8563->rtc))
        return PTR_ERR(hym8563->rtc);

    /* 关联I2C客户端与私有数据,全局可快速获取 */
    hym8563->client = client;
    i2c_set_clientdata(client, hym8563);

    /* 执行芯片上电硬件初始化复位 */
    ret = hym8563_init_device(client);
    if (ret) {
        dev_err(&client->dev, "could not init device, %d\n", ret);
        return ret;
    }

    /* 注册闹钟线程化中断处理函数 */
    if (client->irq > 0) {
        ret = devm_request_threaded_irq(&client->dev, client->irq, NULL,
                        hym8563_irq, IRQF_TRIGGER_LOW | IRQF_ONESHOT,
                        client->name, hym8563);
        if (ret < 0) {
            dev_err(&client->dev, "irq %d request failed, %d\n", client->irq, ret);
            return ret;
        }
    }

    /* 初始化设备休眠唤醒能力 */
    if (client->irq > 0 || device_property_read_bool(&client->dev, "wakeup-source"))
        device_init_wakeup(&client->dev, true);

    /* 检测RTC时间有效性,掉电失效则自动重置默认时间 */
    ret = i2c_smbus_read_byte_data(client, HYM8563_SEC);
    if (ret < 0)
        return ret;
    hym8563_rtc_read_time(&client->dev, &tm_read);
    if ((ret & HYM8563_SEC_VL) || (tm_read.tm_year < 70) || (tm_read.tm_year > 200) ||
        (tm_read.tm_mon == -1) || (rtc_valid_tm(&tm_read) != 0))
        hym8563_rtc_set_time(&client->dev, &tm);

    /* 绑定RTC标准操作接口,关闭不需要的更新中断 */
    hym8563->rtc->ops = &hym8563_rtc_ops;
    clear_bit(RTC_FEATURE_UPDATE_INTERRUPT, hym8563->rtc->features);

#ifdef CONFIG_COMMON_CLK
    hym8563_clkout_register_clk(hym8563); /* 注册外部时钟输出功能 */
#endif

    /* 向内核注册RTC设备,初始化完成 */
    return devm_rtc_register_device(hym8563-&gt;rtc);
}

关键解析:

  • 第7行:定义默认初始时间结构体,用于RTC掉电时间失效时自动重置;

  • 第8行:初始化默认时间:2021年1月1日12:00:00(tm_year=121=2021-1900);

  • 第13行:为私有数据申请内核托管内存,自动释放无内存泄漏风险;

  • 第18行:向内核RTC子系统申请分配RTC设备结构体资源;

  • 第23行:将I2C客户端句柄绑定到私有数据,方便全局调用;

  • 第24行:将私有数据挂载到I2C设备,后续接口可快速获取;

  • 第27行:调用芯片初始化函数,完成寄存器复位、中断关闭、时钟启动;

  • 第34行:判断硬件存在有效中断号,执行中断注册逻辑;

  • 第35行:注册线程化中断处理函数,绑定闹钟/定时器中断回调;

  • 第36行:配置中断为低电平触发、单次触发模式;

  • 第45行:判断硬件支持中断或设备树配置唤醒源,启用休眠唤醒功能;

  • 第46行:标记设备为系统唤醒源,支持闹钟休眠唤醒;

  • 第49行:读取秒寄存器,检测RTC时间有效标志位(VL);

  • 第52行:读取当前硬件时间,校验时间合法性;

  • 第53-54行:判断时间无效(掉电丢失、年份非法、月份异常),执行时间重置;

  • 第55行:写入默认初始时间,恢复RTC计时功能;

  • 第58行:绑定RTC标准操作接口集;

  • 第59行:关闭RTC更新中断功能,该芯片不支持此特性;

  • 第61-63行:内核条件编译,开启通用时钟时注册时钟输出功能;

  • 第66行:向内核注册RTC设备,用户层可访问/dev/rtcx设备,初始化完成。

I2C驱动注册

设备树compatible匹配表、I2C设备ID表、I2C驱动核心结构体注册,内核上电通过设备树匹配硬件,自动加载hym8563 RTC驱动,完成设备绑定。

I2C驱动注册(位于内核源码/drivers/rtc/rtc-hym8563.c)
 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
/* I2C设备ID匹配表 */
static const struct i2c_device_id hym8563_id[] = {
    { "hym8563", 0 },
    {},
};
MODULE_DEVICE_TABLE(i2c, hym8563_id);

/* 设备树compatible兼容匹配表 */
static const struct of_device_id hym8563_dt_idtable[] = {
    { .compatible = "haoyu,hym8563" },
    {},
};
MODULE_DEVICE_TABLE(of, hym8563_dt_idtable);

/* I2C驱动结构体 */
static struct i2c_driver hym8563_driver = {
    .driver         = {
        .name       = "rtc-hym8563",
        .pm = &hym8563_pm_ops,
        .of_match_table     = hym8563_dt_idtable,
    },
    .probe_new      = hym8563_probe,
    .id_table       = hym8563_id,
};

/* 注册/注销I2C驱动,简化模块入口/出口函数 */
module_i2c_driver(hym8563_driver);

设备树参考节点

设备树参考节点
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
hym8563: hym8563@51 {
    compatible = "haoyu,hym8563";
    reg = <0x51>;
    #clock-cells = <0>;
    clock-frequency = <32768>;
    clock-output-names = "hym8563";
    pinctrl-names = "default";
    pinctrl-0 = <&hym8563_int>;
    interrupt-parent = <&gpio0>;
    interrupts = <RK_PB0 IRQ_TYPE_LEVEL_LOW>;
    wakeup-source;
};

关键解析:

  • compatible = “haoyu,hym8563”严格对应驱动of_device_id匹配表参数;

  • reg = <0x51>配置HYM8563芯片7位I2C从设备物理地址;

  • #clock-cells、clock-frequency、clock-output-names三个属性适配内核公共时钟子系统,固定芯片外接32768Hz标准晶振频率,但驱动实际没有配置寄存器开启CLKOUT引脚时钟输出功能;

  • pinctrl引脚管理绑定默认引脚配置,interrupt-parent指定中断所属GPIO控制器,interrupts配置中断引脚为RK_PB0、低电平触发模式,和驱动中断注册IRQF_TRIGGER_LOW参数对齐;

  • wakeup-source为内核标准唤醒源属性标记,告知内核该设备具备系统深度休眠唤醒能力,驱动probe阶段依据该属性初始化设备唤醒功能,是RTC闹钟休眠唤醒、定时开机必须配置,缺失则休眠后闹钟无法唤醒设备。

15.5.2.1. HYM8563 RTC测试

HYM8563 RTC测试需要使用HYM8563S/BM8563作为外部RTC的板卡进行操作,有以下板卡:

  • LubanCat-1(V3)

  • LubanCat-1h

  • LubanCat-1hs

  • LubanCat-2(V3)

  • LubanCat-2n(V3)

  • LubanCat-3

  • LubanCat-3IO

  • LubanCat-4

  • LubanCat-4IO

  • LubanCat-5

  • LubanCat-5IO

测试RTC时间读取和设置

以LubanCat-2V3板卡为例进行操作,测试需要提前接RTC电池, 参考 快速使用手册RTC章节 进行连接,连接好后再执行以下命令:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#查看系统启动rtc驱动加载信息
dmesg | grep rtc

#信息打印如下
[    2.320013] rockchip-drm display-subsystem: [drm] Cannot find any crtc or sizes
[    2.320132] rockchip-drm display-subsystem: [drm] Cannot find any crtc or sizes
[    6.478939] rk808-rtc rk808-rtc: device is disabled
[    6.479016] rk808-rtc: probe of rk808-rtc failed with error -22
[    6.589060] rtc-hym8563 0-0051: rtc information is valid
[    6.597843] rtc-hym8563 0-0051: registered as rtc0
[    6.598972] rtc-hym8563 0-0051: setting system clock to 2026-04-29T09:16:33 UTC (1777454193)

#查看rtc0的日期时间
cat /sys/class/rtc/rtc0/date

#信息打印如下
2026-04-29

#查看rtc0的小时、分钟、秒时间
cat /sys/class/rtc/rtc0/time

#信息打印如下
09:17:35

可以看到rk808-rtc状态是disabled的,hym8563注册为rtc0,并且由于没有同步正确的时间到hym8563,读取到的时间为错误时间(作者此时时间是2026年04月30日)。

设置正确时间到hym8563:

 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
#如果系统有网络,重启ntp服务可网络校准时间
sudo systemctl restart ntp

#如果系统无网络,可通过date命令手动设置当前时间
sudo date -s "2026-04-30 09:32:30"

#通过date命令查询本地时间
date

#信息打印如下,CST(中国标准时间)
2026年 04月 30日 星期四 09:33:17 CST

#通过date命令查询系统时间,UTC(世界协调时间)
date -u

#信息打印如下,作者位于东八区,使用上海时间,因此和UTC时间相差8小时
2026年 04月 30日 星期四 01:33:20 UTC

#系统时间同步到硬件rtc
sudo hwclock -w

#查看rtc0的日期时间
cat /sys/class/rtc/rtc0/date

#信息打印如下
2026-04-30

#查看rtc0的小时、分钟、秒时间
cat /sys/class/rtc/rtc0/time

#信息打印如下
01:36:52

#硬件rtc同步到系统
sudo hwclock -s

以上手动设置时间或者网络同步时间后,-w将系统时间写入到硬件rtc,-s再将rtc时间写回系统,这样每次重启板卡都会自动进行rtc时间同步到系统时间。

断电一段时间再上电查看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
#查看系统启动rtc驱动加载信息
dmesg | grep rtc

#信息打印如下
[    2.366448] rockchip-drm display-subsystem: [drm] Cannot find any crtc or sizes
[    2.366587] rockchip-drm display-subsystem: [drm] Cannot find any crtc or sizes
[    6.261005] rk808-rtc rk808-rtc: device is disabled
[    6.261069] rk808-rtc: probe of rk808-rtc failed with error -22
[    6.385881] rtc-hym8563 0-0051: rtc information is valid
[    6.405306] rtc-hym8563 0-0051: registered as rtc0
[    6.406973] rtc-hym8563 0-0051: setting system clock to 2026-04-30T01:38:27 UTC (1777513107)

#查看rtc0的日期时间
cat /sys/class/rtc/rtc0/date

#信息打印如下
2026-04-30

#查看rtc0的小时、分钟、秒时间
cat /sys/class/rtc/rtc0/time

#信息打印如下
01:39:16

#通过date命令查询本地时间
date

#信息打印如下
2026年 04月 30日 星期四 09:39:26 CST

#通过date命令查询系统时间(UTC)
date -u

#信息打印如下
2026年 04月 30日 星期四 01:39:28 UTC

可以看到断电时间仍能保持,系统启动后也能自动将RTC时间同步到系统,说明RTC功能正常。

测试RTC定时

通过wakealarm接口可以设置定时时间:

 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
#查看rtc中断号和中断次数
cat /proc/interrupts | grep 8563

#信息打印如下,中断号104,4个0分别对应CPU0~CPU3上的中断次数,均为0次
111:          0          0          0          0  rockchip_gpio_irq  23 Level     hym8563

#清空定时时间,避免资源繁忙或占用
sudo sh -c "echo 0 > /sys/class/rtc/rtc0/wakealarm"

#设置定时时间,30秒定时
sudo sh -c "echo +30 > /sys/class/rtc/rtc0/wakealarm"

#信息打印如下
[  136.835675] rtc-hym8563 0-0051: hym8563_rtc_set_alarm: now:    2026-04-30T01:40:37
[  136.835760] rtc-hym8563 0-0051: hym8563_rtc_set_alarm: expired:2026-04-30T01:41:07
[  136.836661] rtc-hym8563 0-0051: hym8563_rtc_set_alarm: set 0m30s timer, interval=30s

#等待30s后的打印
[  165.914390] rtc-hym8563 0-0051: hym8563_irq: irq stat 0xd

#等待30s后再查看rtc中断次数
cat /proc/interrupts | grep 8563

#信息打印如下,在CPU0上产生一次中断
111:          1          0          0          0  rockchip_gpio_irq  23 Level     hym8563

休眠唤醒测试

可以通过先设置定时时间,然后再进入休眠,待定时时间到后触发中断唤醒系统:

注意

该功能需要使用5.10内核版本及以上的镜像。

 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
#清空定时时间,避免资源繁忙或占用
sudo sh -c "echo 0 > /sys/class/rtc/rtc0/wakealarm"

#设置定时时间,30秒定时
sudo sh -c "echo +30 > /sys/class/rtc/rtc0/wakealarm"

#进入freeze休眠
sudo sh -c "echo freeze > /sys/power/state"

#进入休眠信息打印如下
[  372.762353] PM: suspend entry (s2idle)
[  372.775237] Filesystems sync: 0.012 seconds
[  372.931916] (NULL device *): Direct firmware load for regulatory.db failed with error -2
[  372.932422] (NULL device *): Direct firmware load for regulatory.db.p7s failed with error -2
[  372.932660] Freezing user space processes
[  372.936643] Freezing user space processes completed (elapsed 0.003 seconds)
[  372.936781] OOM killer disabled.
[  372.936804] Freezing remaining freezable tasks
[  373.055417] Freezing remaining freezable tasks completed (elapsed 0.118 seconds)
[  373.055562] printk: Suspending console(s) (use no_console_suspend to debug)

#唤醒信息打印如下
[  372.762353] PM: suspend entry (s2idle)
[  372.775237] Filesystems sync: 0.012 seconds
[  372.931916] (NULL device *): Direct firmware load for regulatory.db failed with error -2
[  372.932422] (NULL device *): Direct firmware load for regulatory.db.p7s failed with error -2
[  372.932660] Freezing user space processes
[  372.936643] Freezing user space processes completed (elapsed 0.003 seconds)
[  372.936781] OOM killer disabled.
[  372.936804] Freezing remaining freezable tasks
[  373.055417] Freezing remaining freezable tasks completed (elapsed 0.118 seconds)
[  373.055562] printk: Suspending console(s) (use no_console_suspend to debug)
[  373.136521] rk_gmac-dwmac fe010000.ethernet eth1: Link is Down
[  399.923850] rtc-hym8563 0-0051: hym8563_irq: irq stat 0xd
[  399.929095] rk_gmac-dwmac fe2a0000.ethernet: init for RGMII
[  400.001963] rk_gmac-dwmac fe2a0000.ethernet eth0: configuring for phy/rgmii link mode
[  400.243253] ata1: SATA link down (SStatus 0 SControl 300)
[  401.555784] dwmac4: Master AXI performs any burst length
[  401.555830] rk_gmac-dwmac fe2a0000.ethernet eth0: No Safety Features support found
[  401.555866] rk_gmac-dwmac fe2a0000.ethernet eth0: IEEE 1588-2008 Advanced Timestamp supported
[  401.556379] rk_gmac-dwmac fe010000.ethernet: init for RGMII
[  401.628606] rk_gmac-dwmac fe010000.ethernet eth1: configuring for phy/rgmii link mode
[  403.182458] dwmac4: Master AXI performs any burst length
[  403.182514] rk_gmac-dwmac fe010000.ethernet eth1: No Safety Features support found
[  403.182551] rk_gmac-dwmac fe010000.ethernet eth1: IEEE 1588-2008 Advanced Timestamp supported
[  403.188743] xhci-hcd xhci-hcd.4.auto: xHC error in resume, USBSTS 0x401, Reinit
[  403.188778] usb usb1: root hub lost power or was reset
[  403.188790] usb usb2: root hub lost power or was reset
[  403.352045] OOM killer enabled.
[  403.352086] Restarting tasks ... done.
[  403.369806] random: crng reseeded on system resumption
[  403.371958] PM: suspend exit

除了freeze休眠,也可以进入mem休眠,关于这两种休眠区别就不作展开,可自行搜索了解,自行测试。

15.6. RTC子系统实验

由于部分板卡不带RTC,部分板卡的RTC在前面小节没有讲解到,因此,增加本实验,并且为了实验统一,使用40pin外挂RTC模块的形式,外挂RTC模块的型号为pcf8563。 本实验驱动基于“I2C子系统 + RTC子系统 + Regmap API”架构开发,完成RTC设备注册、时间读写、闹钟配置、中断服务函数编写。

本章的示例代码目录为: linux_driver/33_rtc_subsystem

15.6.1. PCF8563模块简介

PCF8563是一款低功耗的CMOS实时时钟/日历芯片,支持可编程时钟输出、中断输出和低压检测。 所有地址和数据通过双线双向I2C总线串联传输,最高速率:400 kbps。每次读写数据字节后,寄存器地址自动累加。

具有以下特性和优势:

  • 基于32.768kHz的晶振,提供年、月、日、星期、时、分和秒计时

  • Century flag

  • 时钟工作电压:1.0 - 5.5 V(室温)

  • 低备用电流;典型值为0.25 μA(VDD = 3.0 V,Tamb =25 °C)

  • 400 kHz 双线I2C总线接口(VDD = 1.8 - 5.5 V)

  • 可编程时钟输出(32.768 kHz、1.024 kHz、32 Hz和1Hz)

  • 报警和定时器功能

  • 集成晶振电容器

  • 内部上电复位(POR)

  • I2C总线从机地址:读:A3h;写:A2h

  • 开漏中断管脚

PCF8563芯片英文数据手册: PCF8563产品英文数据手册

PCF8563芯片中文数据手册: PCF8563产品中文数据手册

模块购买参考链接(仅供参考): PCF8563 RTC模块

重要

寄存器说明在芯片数据手册中有十分详细的说明。

截取手册关键说明并结合本实验情况整理得:

寄存器结构

寄存器结构结构如下:

../_images/subsystem_rtc_subsystem_0.jpg ../_images/subsystem_rtc_subsystem_1.jpg

PCF8563包含16个8位寄存器,所有16个寄存器设计成可寻址的8位并行寄存器,但不是所有位都有用。前两个寄存器(内存地址00h和01h)用作控制和状态寄存器。 内存地址02h-08h用做时钟计数器(秒至年计数器)。内存地址09h-0Ch包含报警寄存器,定义报警条件。地址0Dh控制CLKOUT输出频率。0Eh和0Fh是分别是Timer_control和定时器计算器。

控制和状态寄存器1

Control_status_1是PCF8563的核心控制寄存器,负责时钟启停、测试模式控制、复位功能管理,芯片的所有计时、闹钟、定时器功能,都依赖该寄存器的正确配置。

../_images/subsystem_rtc_subsystem_2.jpg

该寄存器保持默认值即可,也即上电复位(POR)覆盖启用,RTC时钟正常运行,正常模式,使用内部32.768kHz晶振计时。

控制和状态寄存器2

Control_status_2是PCF8563的中断控制核心寄存器,负责闹钟/定时器中断的使能、中断标志管理、INT引脚输出模式控制,是驱动实现闹钟唤醒、定时中断功能的关键。

../_images/subsystem_rtc_subsystem_3.jpg ../_images/subsystem_rtc_subsystem_4.jpg

该寄存器读写主要应用于以下4个场景:

  • 初始化:向控制和状态寄存器2直接写入0x00

    • 清零AF(bit3):清除闹钟标志

    • 清零AIE(bit1):关闭闹钟中断

    • 清零bit5~7保留位

    • 清零定时器相关位

  • 闹钟中断模式配置:

    • 配置AIE(bit1):开启/关闭闹钟中断

    • 清AF+保留位:每次配置都清除闹钟标志 + 保留位强制0

  • 闹钟中断处理:

    • 检查AF(bit3)是否置1

  • 读取闹钟配置:

    • AIE(bit1):闹钟中断是否开启

    • AF(bit3):闹钟是否触发

秒寄存器

VL_seconds是PCF8563的秒寄存器,也是读取时间时的第一个校验寄存器。

../_images/subsystem_rtc_subsystem_5.jpg
  • 位号7:VL:低压时钟有效性标志

    • 0 = 电压正常,时钟信息完整,秒值有效

    • 1 = 低压警告(纽扣电池/供电异常),时钟信息可能丢失,秒值无效

  • 位号6~4:秒的十位,BCD编码,范围0~5(因为秒的最大值为59,十位最大为5),对应二进制000~101

  • 位号3~0:秒的个位,BCD编码,范围 0~9,对应二进制0000~1001

分钟寄存器

../_images/subsystem_rtc_subsystem_6.jpg
  • 位号6~4:分钟的十位,BCD编码,范围0~5(分钟最大值为 59,因此十位最大为5,对应二进制000~101)

  • 位号3~0: 分钟的个位,BCD编码,范围0~9

小时寄存器

../_images/subsystem_rtc_subsystem_7.jpg
  • 位号5~4:小时的十位,BCD编码,范围0~2(小时最大值为23,因此十位最大为2,对应二进制00~10)

  • 位号5~4:小时的个位,BCD编码,范围0~9

天数寄存器

../_images/subsystem_rtc_subsystem_8.jpg
  • 位号5~4:日期的十位,BCD编码,范围0~3(日期最大值为 31,因此十位最大为 3,对应二进制00~11)

  • 位号3~0:日期的个位,BCD编码,范围0~9

  • 芯片自带闰年自动补偿:当年份能被4整除(含00年),会自动在2月添加第 29 天,驱动无需额外处理

周天数寄存器

../_images/subsystem_rtc_subsystem_9.jpg
  • 位号2~0:周天数,二进制编码,范围0~6,对应默认映射:0=周日、1=周一、2=周二、3=周三、4=周四、5=周五、6=周六

  • Weekdays寄存器是纯二进制编码,不是BCD编码,直接读取/写入数值即可

世纪标志和月份寄存器

../_images/subsystem_rtc_subsystem_10.jpg ../_images/subsystem_rtc_subsystem_11.jpg
  • 位号7:世纪标志位

    • 0:表示当前世纪为x

    • 1:表示当前世纪为x+1

  • 位号4:月份的BCD编码十位,范围0~1(月份最大值为12,因此十位只能是0或1)

  • 位号3~0:月份的BCD编码个位,范围0~9

年份寄存器

../_images/subsystem_rtc_subsystem_12.jpg
  • 位号7~4:年份的十位,BCD编码,范围0~9(对应十进制 0~9)

  • 位号3~0:年份的个位,BCD编码,范围0~9(对应十进制 0~9)

分报警寄存器

../_images/subsystem_rtc_subsystem_13.jpg
  • 位号7:分钟报警屏蔽位

    • 0:分钟字段参与比较,当前分钟必须和报警分钟完全匹配,闹钟才会触发

    • 1:分钟字段不参与比较(被忽略),不管当前分钟是什么,只要其他报警条件满足,闹钟就会触发

  • 位号6~4:报警分钟的BCD编码十位,范围0~5(分钟最大值为59,因此十位最大为5,对应二进制000~101)

  • 位号3~0:报警分钟的BCD编码个位,范围0~9

其他报警寄存器和分报警寄存器基本一致。

定时器控制寄存器

../_images/subsystem_rtc_subsystem_14.jpg
  • 位号7:定时器使能位

    • 0:定时器禁用,倒计时停止

    • 1:定时器启用,从Timer寄存器加载初值开始倒计数

  • 位号1~0:定时器时钟源选择,4档可选:

    • 00:4.096kHz

    • 01:64Hz

    • 10:1Hz

    • 11:1/60Hz

15.6.2. 设备树插件详解

本实验设备树插件(lubancat-rtc-pcf8563-overlay.dts)用于配置I2C适配器和pcf8563从设备,完整代码如下:

设备树插件(位于linux_driver/33_rtc_subsystem/lubancat-rtc-pcf8563-overlay.dts)
 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
/dts-v1/;
/plugin/;

#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/rockchip.h>
#include <dt-bindings/interrupt-controller/irq.h>

/ {
    fragment@0 {
        target = <&i2c3>;

        __overlay__ {
            clock-frequency = <400000>;
            pinctrl-names = "default";
            pinctrl-0 = <&i2c3m1_xfer>;
            #address-cells = <1>;
            #size-cells = <0>;
            status = "okay";

            pcf8563: pcf8563@51 {
                compatible = "fire,pcf8563";
                reg = <0x51>;
                pinctrl-names = "default";
                pinctrl-0 = <&pcf8563_int>;
                interrupt-parent = <&gpio0>;
                interrupts = <RK_PB0 IRQ_TYPE_LEVEL_LOW>;
                wakeup-source;
            };
        };
    };

    fragment@1 {
        target = <&pinctrl>;

        __overlay__ {
            rtc {
                pcf8563_int: pcf8563-int {
                    rockchip,pins = <0 RK_PB0 RK_FUNC_GPIO &pcfg_pull_up>;
                };
            };
        };
    };

    fragment@2 {
        target = <&rk809>;

        __overlay__ {
            rtc {
                status = "disabled";
            };
        };
    };

    // fragment@3 {
    //     target = <&hym8563>;

    //     __overlay__ {
    //         status = "disabled";
    //     };
    // };
};

关键配置说明:

  • target = &i2c3:指定要修改的I2C适配器为I2C3,需与实际要使用的硬件接口一致;

  • clock-frequency = <400000>:配置I2C通信速率为400KHz,pcf8563支持400KHz通信速率;

  • pinctrl-0 = <&i2c3m1_xfer>:此处使用的具体I2C引脚为i2c3m1;

  • pcf8563@51:从设备节点名称,@后的0x51是pcf8563的7位I2C地址,必须与传感器实际地址一致;

  • compatible = “fire,pcf8563”:驱动匹配的核心标识,需与I2C驱动中of_match_table的属性完全一致,否则驱动无法匹配设备;

  • reg = <0x51>:明确pcf8563的I2C从设备地址,内核通过该属性识别I2C总线上的具体设备;

  • pinctrl:pinctrl配置设置中断引脚为GPIO0_B0,上拉模式,自行选择40pin空闲引脚即可,但需注意要GPIO0组引脚才能支持系统休眠定时唤醒功能;

  • interrupt-parent、interrupts:绑定GPIO0控制器,配置RK_PB0引脚为低电平触发中断,用于闹钟中断信号接收。

  • wakeup-source:标记设备具备休眠唤醒能力,内核允许pcf8563闹钟触发唤醒系统。

  • rk809的rtc子节点disabled:关闭PMIC的RTC功能,避免干扰。

  • hym8563节点disabled:如果板载有其他外部RTC也可以关闭,需查看板卡对应的主设备树确认RTC节点具体名称,不一定都是hym8563。

注意

以上设备树插件是以I2C3_M1为例,需根据板卡实际接口进行修改!

如果不清楚自己板卡有哪些可用的I2C接口,可在板卡执行以下命令确认:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#查看配置文件可用i2c插件
cat /boot/uEnv/uEnv.txt | grep i2c

#以LubanCat2-V3板卡为例,信息打印如下
#dtoverlay=/dtb/overlay/rk356x-lubancat-i2c3-m0-overlay.dtbo
#dtoverlay=/dtb/overlay/rk356x-lubancat-i2c3-m1-overlay.dtbo
#dtoverlay=/dtb/overlay/rk356x-lubancat-i2c5-m0-overlay.dtbo
#dtoverlay=/dtb/overlay/rk356x-lubancat-i2c3-m0-rtc-overlay.dtbo
#dtoverlay=/dtb/overlay/rk356x-lubancat-i2c3-m1-rtc-overlay.dtbo
#dtoverlay=/dtb/overlay/rk356x-lubancat-i2c5-m0-rtc-overlay.dtbo

从可用的设备树插件可以确认,LubanCat2-V3支持i2c3-m0、i2c3-m1、i2c5-m0,从内核源码中找到对应的设备树插件源码如下,以i2c3-m1为例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#查看i2c3-m1设备树插件源码内容
cat kernel/arch/arm64/boot/dts/rockchip/overlay/rk356x-lubancat-i2c3-m1-overlay.dts

#信息打印如下
/dts-v1/;
/plugin/;

/ {
        compatible = "rockchip,rk3568";

        fragment@0 {
                target = <&i2c3>;

                __overlay__ {
                        status = "okay";
                        pinctrl-names = "default";
                        pinctrl-0 = <&i2c3m1_xfer>;
                };
        };
};

可知,如果LubanCat2-V3使用i2c3-m1接口,lubancat-i2c-mpu6050-overlay.dts配置的target和pinctrl-0就是 target = <&i2c3>;pinctrl-0 = <&i2c3m1_xfer>;

结合板卡配套的快速使用手册的40pin引脚对照图章节,可确认i2c3-m1接口对应的物理引脚,如下图:

../_images/subsystem_rtc_subsystem_15.jpg

15.6.3. 驱动代码详解

驱动基于I2C子系统、RTC子系统、Regmap API操作机制开发,核心功能为PCF8563硬件初始化、RTC设备内核注册、时间读写适配、闹钟中断配置与回调处理, 屏蔽底层I2C读写细节,适配内核标准RTC接口规范。

核心定义与数据结构

核心定义与数据结构(位于linux_driver/33_rtc_subsystem/rtc_pcf8563.c)
 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
#define PCF8563_REG_ST1     0x00        /* 控制和状态寄存器1地址,用于控制RTC基础工作状态 */

#define PCF8563_REG_ST2     0x01        /* 控制和状态寄存器2地址,用于中断控制、闹钟标志位管理 */
#define PCF8563_BIT_AIE     (1 << 1)    /* 闹钟中断使能位,置1表示开启闹钟中断功能 */
#define PCF8563_BIT_AF      (1 << 3)    /* 闹钟触发标志位,置1表示闹钟时间已到 */
#define PCF8563_BITS_ST2_N  (7 << 5)    /* 控制和状态寄存器2保留位掩码,用于清零无效位 */

#define PCF8563_REG_SC      0x02        /* 秒寄存器地址,存储当前时间秒值,BCD码格式 */
#define PCF8563_REG_MN      0x03        /* 分寄存器地址,存储当前时间分值,BCD码格式 */
#define PCF8563_REG_HR      0x04        /* 时寄存器地址,存储当前时间时值,BCD码格式 */
#define PCF8563_REG_DM      0x05        /* 日期寄存器地址,存储当前日期,BCD码格式 */
#define PCF8563_REG_DW      0x06        /* 星期寄存器地址,存储当前星期,BCD码格式 */
#define PCF8563_REG_MO      0x07        /* 世纪标志和月份寄存器,存储当前月份,BCD码格式,含世纪位 */
#define PCF8563_REG_YR      0x08        /* 年寄存器地址,存储当前年份低2位,BCD码格式 */
#define PCF8563_REG_AMN     0x09        /* 闹钟分寄存器地址,存储闹钟触发分值,BCD码格式 */
#define PCF8563_REG_TMRC    0x0E        /* 定时器控制寄存器地址,用于配置内部定时器工作模式 */

#define PCF8563_TMRC_1_60   3           /* 定时器分频配置,1/60Hz最低功耗模式 */

#define PCF8563_SC_LV       0x80        /* 低压检测标志位,置1表示RTC供电异常,时间不可靠 */
#define PCF8563_MO_C        0x80        /* 世纪标志位,用于区分19xx/20xx年份 */

/* PCF8563驱动私有数据结构体 */
struct pcf8563_data {
    struct i2c_client *client;  /* I2C客户端句柄,指向I2C从设备实例 */
    struct regmap *regmap;      /* Regmap寄存器映射句柄,用于寄存器读写 */
    struct rtc_device *rtc;     /* RTC设备句柄,注册到内核RTC子系统 */
    int c_polarity;             /* 世纪位极性标记,判断19xx/20xx年份规则 */
    int voltage_low;            /* 低压状态标记,1表示检测到供电异常 */
};

/* Regmap配置参数 */
static const struct regmap_config pcf8563_regmap_config = {
    .reg_bits = 8,                  /* 寄存器地址位宽:8位 */
    .val_bits = 8,                  /* 寄存器数据位宽:8位 */
    .max_register = 0x0F,           /* 最大可访问寄存器地址 */
    .cache_type = REGCACHE_NONE,    /* 关闭寄存器缓存,保证实时性 */
};

关键说明:

  • 私有数据结构体统一管理:I2C、Regmap、RTC设备核心句柄,兼顾世纪位、低压状态标记;

  • Regmap配置:适配PCF8563 8位寄存器规格,关闭缓存避免RTC时间数据读写延迟、数据不一致问题。

读取硬件时间函数

pcf8563_get_time函数是RTC子系统标准回调接口函数,通过regmap批量读取PCF8563控制寄存器+全部时间寄存器数据, 将硬件存储的BCD码格式时间数据转换为内核标准rtc_time二进制时间格式,同步检测低压供电异常状态并做年份世纪偏移适配, 向上层系统输出标准公历时间,适配date命令时间查询与系统时间初始化。

读取硬件时间函数(位于linux_driver/33_rtc_subsystem/rtc_pcf8563.c)
 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
/* 读取RTC当前时间函数:从硬件读取时间,转换为内核标准rtc_time格式 */
static int pcf8563_get_time(struct device *dev, struct rtc_time *tm)
{
    /* 从设备指针转换为I2C客户端指针 */
    struct i2c_client *client = to_i2c_client(dev);
    /* 获取驱动私有数据 */
    struct pcf8563_data *data = i2c_get_clientdata(client);
    /* 批量存储寄存器读取数据 */
    u8 buf[9];
    /* 函数返回值 */
    int ret;

    /* 批量读取前9个寄存器数据 */
    ret = regmap_bulk_read(data->regmap, PCF8563_REG_ST1, buf, 9);
    if (ret)
        return ret;

    /* 判断是否检测到低压标志 */
    if (buf[PCF8563_REG_SC] & PCF8563_SC_LV) {
        data->voltage_low = 1;                          /* 标记低压状态为异常 */
        dev_err(dev, "low voltage, time invalid!\n");   /* 打印时间无效错误日志 */
        return -EINVAL;
    }

    tm->tm_sec  = bcd2bin(buf[PCF8563_REG_SC] & 0x7F);      /* 秒BCD码转二进制,屏蔽低压位 */
    tm->tm_min  = bcd2bin(buf[PCF8563_REG_MN] & 0x7F);      /* 分BCD码转二进制,屏蔽无效位 */
    tm->tm_hour = bcd2bin(buf[PCF8563_REG_HR] & 0x3F);      /* 时BCD码转二进制,屏蔽无效位 */
    tm->tm_mday = bcd2bin(buf[PCF8563_REG_DM] & 0x3F);      /* 日期BCD码转二进制,屏蔽无效位 */
    tm->tm_wday = buf[PCF8563_REG_DW] & 0x07;               /* 获取星期值,屏蔽无效位 */
    tm->tm_mon  = bcd2bin(buf[PCF8563_REG_MO] & 0x1F) - 1;  /* 月BCD码转二进制,系统月份从0开始 */
    tm->tm_year = bcd2bin(buf[PCF8563_REG_YR]);             /* 年BCD码转二进制 */

    if (tm->tm_year < 70)       /* 判断年份是否小于70,对应2070年分界 */
        tm->tm_year += 100;     /* 年份偏移100,适配1970-2069时间范围 */

    /* 根据世纪位设置极性标记,判断19xx/20xx年份,PCF8563_MO_C的值为1000 0000,因此判断的是世纪标志和月份寄存器的位号7:世纪标志位 */
    data->c_polarity = (buf[PCF8563_REG_MO] & PCF8563_MO_C)
        ? (tm->tm_year >= 100)
        : (tm->tm_year < 100);

    return 0;
}

关键解析:

  • 第9行:定义9字节缓存数组,存储批量读取的硬件寄存器原始数据;

  • 第14行:通过regmap批量读取从控制和状态寄存器1开始的9个连续寄存器;

  • 第19行:检测秒寄存器的低压标志位(VL),判断后备电池是否欠压;

  • 第20行:标记驱动私有数据中的低压状态为异常;

  • 第21-22行:打印电池欠压、时间无效的错误日志,返回参数无效错误,终止时间读取;

  • 第25行:秒数据BCD转二进制,掩码屏蔽最高位低压标志位;

  • 第26行:分钟数据BCD转二进制,掩码屏蔽无效位;

  • 第27行:小时数据BCD转二进制,掩码屏蔽无效位;

  • 第28行:日期数据BCD转二进制,掩码屏蔽无效位;

  • 第29行:直接获取星期数据,掩码屏蔽无效位;

  • 第30行:月份数据BCD转二进制后减1,适配内核月份从0开始的计数规则;

  • 第31行:年份数据BCD转二进制,硬件仅存储2位年份;

  • 第33行:判断年份小于70,区分1970-1999和2000-2069范围;

  • 第34行:年份加100,将硬件2位年份对齐内核1970基准时间格式;

  • 第37行:读取月份寄存器的世纪标志位(C 位),判断年份属于19xx还是20xx;

  • 第38行:世纪位置1时,判断年份是否≥100,标记世纪极性;

  • 第39行:世纪位清0时,判断年份是否<100,标记世纪极性。

设置硬件时间函数

pcf8563_set_time函数将内核标准tm时间格式转换为PCF8563硬件所需BCD编码格式,批量写入时间寄存器, 根据世纪位极性自动配置世纪标志和月份寄存器的的世纪标志位,实现跨世纪年份同步。

设置硬件时间函数(位于linux_driver/33_rtc_subsystem/rtc_pcf8563.c)
 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
/* 设置RTC硬件时间函数:将内核标准时间写入硬件,同步RTC时钟 */
static int pcf8563_set_time(struct device *dev, struct rtc_time *tm)
{
    /* 从设备指针转换为I2C客户端指针 */
    struct i2c_client *client = to_i2c_client(dev);
    /* 获取驱动私有数据 */
    struct pcf8563_data *data = i2c_get_clientdata(client);
    /* 存储时间寄存器写入数据 */
    u8 buf[7];

    buf[0] = bin2bcd(tm->tm_sec);        /* 秒二进制转BCD码,准备写入 */
    buf[1] = bin2bcd(tm->tm_min);        /* 分二进制转BCD码,准备写入 */
    buf[2] = bin2bcd(tm->tm_hour);       /* 时二进制转BCD码,准备写入 */
    buf[3] = bin2bcd(tm->tm_mday);       /* 日期二进制转BCD码,准备写入 */
    buf[4] = tm->tm_wday & 0x07;         /* 星期纯二进制,直接写入 */
    buf[5] = bin2bcd(tm->tm_mon + 1);    /* 月份+1后转BCD码,硬件月份从1开始 */
    buf[6] = bin2bcd(tm->tm_year % 100); /* 年份取低2位转BCD码 */

    /* 根据世纪位极性回填世纪标志位 */
    if (data->c_polarity ? (tm->tm_year >= 100) : (tm->tm_year < 100))
        buf[5] |= PCF8563_MO_C; /* 置位世纪标志位,
                                 * buf[5]是世纪标志和月份寄存器,PCF8563_MO_C的值为1000 0000
                                 * 因此设置的是世纪标志和月份寄存器的位号7:世纪标志位 */

    /* 批量写入时间寄存器并返回结果 */
    return regmap_bulk_write(data->regmap, PCF8563_REG_SC, buf, 7);
}

关键解析:

  • 第9行:定义7字节缓存数组,存储秒、分、时、日、周、月、年的写入数据;

  • 第11行:秒数据二进制转BCD码,适配硬件寄存器格式;

  • 第12行:分钟数据二进制转BCD码,适配硬件寄存器格式;

  • 第13行:小时数据二进制转BCD码,适配硬件寄存器格式;

  • 第14行:日期数据二进制转BCD码,适配硬件寄存器格式;

  • 第15行:星期数据直接赋值,掩码屏蔽无效位;

  • 第16行:内核月份从0开始,+1对齐硬件从1开始的计数规则,再转BCD码;

  • 第17行:硬件仅存储2位年份,取内核年份低两位转换为BCD码;

  • 第20行:根据读取时间时记录的世纪位极性,判断是否需要置位世纪标志;

  • 第21行:条件满足时,置位月份寄存器的世纪标志位;

  • 第26行:通过regmap批量写入7个时间寄存器,完成硬件时钟同步。

读取闹钟配置函数

pcf8563_read_alarm函数批量读取PCF8563闹钟分、时、日、周四个报警寄存器, 逐个判断每个寄存器最高位AE闹钟屏蔽位:AE=1代表该字段不参与闹钟匹配,内核对应时间字段赋值为-1标记无效; AE=0代表字段参与匹配,将BCD编码闹钟时间转为十进制赋值给内核alarm结构体。 星期字段依旧遵循纯二进制规则,无需BCD转换。

pcf8563_read_alarm函数(位于linux_driver/33_rtc_subsystem/rtc_pcf8563.c)
 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
/* 读取闹钟配置函数:从硬件读取闹钟时间与使能状态 */
static int pcf8563_read_alarm(struct device *dev, struct rtc_wkalrm *alm)
{
    /* 从设备指针转换为I2C客户端指针 */
    struct i2c_client *client = to_i2c_client(dev);
    /* 获取驱动私有数据 */
    struct pcf8563_data *data = i2c_get_clientdata(client);
    /* 批量存储闹钟寄存器读取数据 */
    u8 buf[4];
    /* 存储状态寄存器2的值 */
    unsigned int st2;

    /* 批量读取4个闹钟寄存器 */
    if (regmap_bulk_read(data->regmap, PCF8563_REG_AMN, buf, 4))
        return -EIO;

    alm->time.tm_sec  = 0;                          /* PCF8563不支持秒级闹钟,固定为0 */
    alm->time.tm_min  = bcd2bin(buf[0] & 0x7F);     /* 闹钟分BCD码转二进制 */
    alm->time.tm_hour = bcd2bin(buf[1] & 0x3F);     /* 闹钟时BCD码转二进制 */
    alm->time.tm_mday = bcd2bin(buf[2] & 0x3F);     /* 闹钟日期BCD码转二进制 */
    alm->time.tm_wday = buf[3] & 0x07;             /* 闹钟星期是二进制,不需要转换 */

    /* 读取状态寄存器2 */
    if (regmap_read(data->regmap, PCF8563_REG_ST2, &st2))
        return -EIO;

    alm->enabled = !!(st2 & PCF8563_BIT_AIE);   /* 获取闹钟中断使能状态 */
    alm->pending = !!(st2 & PCF8563_BIT_AF);    /* 获取闹钟触发标志状态 */

    return 0;
}

关键说明:

  • 第9行:定义4字节缓存数组,存储闹钟分、时、日、周寄存器的原始硬件数据;

  • 第11行:定义变量存储状态寄存器2的读取值,用于解析中断使能和触发标志;

  • 第14行:通过regmap批量读取4个连续闹钟寄存器(分钟/小时/日期/星期);

  • 第17行:PCF8563硬件无秒级闹钟寄存器,闹钟秒数固定赋值为0;

  • 第18行:闹钟分钟数据BCD码转二进制,掩码屏蔽寄存器无效位;

  • 第19行:闹钟小时数据BCD码转二进制,掩码屏蔽寄存器无效位;

  • 第20行:闹钟日期数据BCD码转二进制,掩码屏蔽寄存器无效位;

  • 第21行:闹钟星期为纯二进制存储,直接取值并屏蔽无效位;

  • 第24行:读取PCF8563状态寄存器2,获取闹钟中断配置和触发状态;

  • 第27行:解析闹钟中断使能位(AIE),赋值给内核闹钟启用标志;

  • 第28行:解析闹钟触发标志位(AF),赋值给内核闹钟挂起标志。

设置闹钟配置函数

pcf8563_set_alarm函数用于接收应用层下发的闹钟时间参数,逐个判断分、时、日、周字段是否有效:内核字段为-1则置位对应AE屏蔽位,忽略该闹钟匹配条件;字段有有效值则清零AE屏蔽位,分/时/日十进制转BCD编码写入寄存器。

pcf8563_set_alarm函数(位于linux_driver/33_rtc_subsystem/rtc_pcf8563.c)
 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
/* 设置闹钟配置函数:将闹钟时间写入硬件,配置闹钟触发条件 */
static int pcf8563_set_alarm(struct device *dev, struct rtc_wkalrm *alm)
{
    /* 从设备指针转换为I2C客户端指针 */
    struct i2c_client *client = to_i2c_client(dev);
    /* 获取驱动私有数据 */
    struct pcf8563_data *data = i2c_get_clientdata(client);
    /* 存储闹钟寄存器写入数据 */
    u8 buf[4];

    /* 判断是否设置秒级闹钟 */
    if (alm->time.tm_sec) {
        time64_t t = rtc_tm_to_time64(&alm->time);  /* 转换闹钟时间为时间戳 */
        t += 60 - alm->time.tm_sec;                 /* 向上取整到最近一分钟 */
        rtc_time64_to_tm(t, &alm->time);            /* 转换回rtc_time格式 */
    }

    buf[0] = bin2bcd(alm->time.tm_min);     /* 闹钟分二进制转BCD码 */
    buf[1] = bin2bcd(alm->time.tm_hour);    /* 闹钟时二进制转BCD码 */
    buf[2] = bin2bcd(alm->time.tm_mday);    /* 闹钟日期二进制转BCD码 */
    buf[3] = alm->time.tm_wday & 0x07;      /* 闹钟星期值,屏蔽无效位 */

    /* 批量写入闹钟寄存器 */
    if (regmap_bulk_write(data->regmap, PCF8563_REG_AMN, buf, 4))
        return -EIO;

    /* 配置闹钟中断使能状态并返回结果 */
    return pcf8563_set_alarm_mode(data, alm->enabled);
}

关键解析:

  • 第9行:定义4字节缓存数组,存储闹钟分、时、日、周的写入数据;

  • 第12行:判断闹钟秒数非0,PCF8563硬件不支持秒级闹钟,需做兼容处理;

  • 第13行:将闹钟时间转换为64位Unix时间戳,方便时间计算;

  • 第14行:时间戳加上差值,将秒数向上取整到下一个整分钟;

  • 第15行:将修正后的时间戳转换回内核rtc_time格式;

  • 第18行:闹钟分钟数据二进制转BCD码,适配硬件寄存器格式;

  • 第19行:闹钟小时数据二进制转BCD码,适配硬件寄存器格式;

  • 第20行:闹钟日期数据二进制转BCD码,适配硬件寄存器格式;

  • 第21行:闹钟星期数据赋值,掩码屏蔽无效位;

  • 第24行:通过regmap批量写入4个连续闹钟寄存器;

  • 第28行:调用底层函数,配置闹钟中断使能/禁用状态,返回闹钟中断配置结果。

闹钟中断模式配置函数

pcf8563_set_alarm_mode函数是属于驱动内部底层私有接口,不直接对接内核RTC子系统回调,专门为上层pcf8563_alarm_irq_enable、闹钟设置、中断重置等功能提供统一寄存器操作入口。 核心作用是统一读取修改PCF8563状态寄存器2,专门负责闹钟中断全局开关控制、闹钟触发标志位软件清零、寄存器保留位清零规整,所有闹钟中断硬件配置全部收敛在此函数,避免多处代码直接操作寄存器,减少重复代码。

pcf8563_set_alarm_mode函数(位于linux_driver/33_rtc_subsystem/rtc_pcf8563.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* 闹钟中断模式配置函数:设置闹钟中断使能/禁用,并清除闹钟标志位 */
static int pcf8563_set_alarm_mode(struct pcf8563_data *data, bool on)
{
    /* 临时存储寄存器读取值 */
    unsigned int val;
    /* 函数返回值 */
    int ret;

    /* 读取状态寄存器2的值 */
    ret = regmap_read(data->regmap, PCF8563_REG_ST2, &val);
    if (ret)
        return ret;

    /* 判断是否需要开启闹钟中断 */
    if (on)
        val |= PCF8563_BIT_AIE;     /* 开启闹钟中断使能位 */
    else
        val &= ~PCF8563_BIT_AIE;    /* 关闭闹钟中断使能位 */

    val &= ~(PCF8563_BIT_AF | PCF8563_BITS_ST2_N);      /* 清除闹钟标志位与保留位 */

    /* 写入配置后的寄存器值并返回结果 */
    return regmap_write(data->regmap, PCF8563_REG_ST2, val);
}

关键解析:

  • 第10行:通过regmap读取PCF8563状态寄存器2,获取当前中断配置;

  • 第15行:根据入参on判断,执行开启/关闭闹钟中断逻辑;

  • 第16行:参数为true时,置位闹钟中断使能位(AIE),允许闹钟触发中断;

  • 第18行:参数为false时,清除闹钟中断使能位(AIE),禁止闹钟触发中断;

  • 第20行:清除闹钟触发标志位(AF),避免中断重复触发;同时清除寄存器保留位;

  • 第23行:将修改后的配置写回状态寄存器2,生效中断设置,返回寄存器写入操作结果,完成中断模式配置。

闹钟中断使能独立控制函数

pcf8563_alarm_irq_enable函数是RTC子系统标准回调接口,内核或应用层开启/关闭闹钟时,内核会自动调用该函数。 核心作用是统一入口控制PCF8563硬件闹钟中断总开关,不直接操作寄存器,而是调用底层封装的pcf8563_set_alarm_mode函数统一配置。

pcf8563_alarm_irq_enable函数(位于linux_driver/33_rtc_subsystem/rtc_pcf8563.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/* 闹钟中断使能/禁用控制:统一控制闹钟中断的开启与关闭 */
static int pcf8563_alarm_irq_enable(struct device *dev, unsigned int enable)
{
    /* 从设备指针转换为I2C客户端指针 */
    struct i2c_client *client = to_i2c_client(dev);
    /* 获取驱动私有数据 */
    struct pcf8563_data *data = i2c_get_clientdata(client);

    /* 调用闹钟模式配置函数,设置中断使能 */
    return pcf8563_set_alarm_mode(data, !!enable);
}

关键解析:

  • 第10行:调用底层闹钟模式配置函数,将enable参数强转为布尔值,实现中断使能/禁用,返回函数执行结果,完成闹钟中断的使能配置。

闹钟中断服务处理函数

pcf8563_irq函数是硬件中断触发后的回调函数,PCF8563闹钟时间匹配成功、INT引脚拉低触发中断后,内核线程化中断服务会自动进入该函数。核心作用是判断中断是否为闹钟中断、向上层RTC子系统上报闹钟事件、重新使能闹钟中断,保证闹钟可以重复触发。

pcf8563_irq函数(位于linux_driver/33_rtc_subsystem/rtc_pcf8563.c)
 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
/* 闹钟中断处理函数:响应闹钟触发事件 */
static irqreturn_t pcf8563_irq(int irq, void *dev_id)
{
    /* 获取I2C客户端设备实例 */
    struct i2c_client *client = dev_id;
    /* 获取驱动私有数据 */
    struct pcf8563_data *data = i2c_get_clientdata(client);
    /* 存储状态寄存器2的值 */
    unsigned int st2;

    /* 读取状态寄存器2,判断是否成功 */
    if (regmap_read(data->regmap, PCF8563_REG_ST2, &st2))
        return IRQ_NONE;

    /* 判断闹钟标志位是否未触发 */
    if (!(st2 & PCF8563_BIT_AF))
        return IRQ_NONE;

    /* 向RTC子系统上报闹钟中断事件 */
    rtc_update_irq(data->rtc, 1, RTC_IRQF | RTC_AF);

    /* 重新使能闹钟中断,等待下一次触发 */
    pcf8563_set_alarm_mode(data, 1);

    /* 中断处理完成,返回已处理 */
    return IRQ_HANDLED;
}
  • 第12行:读取PCF8563状态寄存器2,获取中断标志和使能状态;

  • 第16行:检测闹钟触发标志位(AF),判断是否为闹钟中断;

  • 第20行:调用内核RTC子系统API,上报1次闹钟中断事件;

  • 第23行:调用底层函数,重新使能闹钟中断并清除中断标志,准备下一次触发;

  • 第26行:返回IRQ_HANDLED,告知内核中断已成功处理完毕。

probe函数

probe函数是设备树与驱动compatible匹配成功后自动执行,完成私有数据内存分配、Regmap初始化、定时器低功耗配置、中断标志清零、RTC设备内核注册、闹钟中断引脚申请、休眠唤醒功能配置,是驱动硬件初始化核心入口。

probe函数(位于linux_driver/33_rtc_subsystem/rtc_pcf8563.c)
 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
/* probe函数:I2C设备匹配成功后执行,初始化硬件与驱动资源 */
static int pcf8563_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    /* 驱动私有数据指针 */
    struct pcf8563_data *data;
    /* 函数返回值 */
    int ret;

    /* 动态分配私有数据内存,自动释放 */
    data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;

    /* 绑定I2C客户端实例到私有数据 */
    data->client = client;
    /* 将私有数据挂载到I2C客户端 */
    i2c_set_clientdata(client, data);

    /* 初始化Regmap寄存器映射 */
    data->regmap = devm_regmap_init_i2c(client, &pcf8563_regmap_config);
    if (IS_ERR(data->regmap)) {
        dev_err(&client->dev, "regmap init failed\n");
        return PTR_ERR(data->regmap);
    }

    /* 配置定时器为最低功耗模式 */
    ret = regmap_write(data->regmap, PCF8563_REG_TMRC, PCF8563_TMRC_1_60);
    if (ret)
        return ret;

    /* 清除所有中断标志位,禁用中断 */
    ret = regmap_write(data->regmap, PCF8563_REG_ST2, 0x00);
    if (ret)
        return ret;

    /* 向内核RTC子系统注册RTC设备 */
    data->rtc = devm_rtc_device_register(&client->dev, "pcf8563", &pcf8563_rtc_ops, THIS_MODULE);
    if (IS_ERR(data->rtc))
        return PTR_ERR(data->rtc);

    /* 判断设备树是否配置中断引脚 */
    if (client->irq > 0) {
        /* 申请线程化中断,绑定闹钟中断服务函数 */
        ret = devm_request_threaded_irq(&client->dev, client->irq,
                        NULL, pcf8563_irq,
                        IRQF_TRIGGER_LOW | IRQF_ONESHOT,
                        "pcf8563", client);
        if (ret) {
            dev_err(&client->dev, "irq request failed\n");
            return ret;
        }
    }

    /* 设置设备支持休眠唤醒功能 */
    device_set_wakeup_capable(&client->dev, true);

    dev_info(&client->dev, "PCF8563 RTC probed successfully\n");
    return 0;
}
  • 第10行:为私有数据申请内核托管内存,自动释放无内存泄漏风险;

  • 第15行:将I2C客户端句柄绑定到私有数据,方便后续函数调用;

  • 第17行:将私有数据挂载到I2C设备,全局接口可快速获取驱动上下文;

  • 第20行:初始化regmap寄存器映射,简化I2C寄存器读写操作;

  • 第27行:配置定时器控制寄存器,设置为1/60Hz最低功耗模式,降低芯片功耗;

  • 第32行:向状态寄存器2写入0,清除所有中断标志、禁用所有中断,初始化硬件中断状态;

  • 第37行:向内核RTC子系统注册RTC设备,绑定设备名、标准操作接口集,生成/dev/rtcX设备节点;

  • 第42行:判断硬件存在有效中断号,执行中断注册逻辑;

  • 第44-47行:申请线程化中断,绑定闹钟中断处理函数pcf8563_irq,配置中断为低电平触发、单次触发模式;

  • 第55行:标记设备支持系统休眠唤醒功能,允许闹钟中断唤醒整机;

  • 第57行:打印RTC驱动初始化成功日志。

驱动设备匹配与注册

定义设备树匹配表,绑定设备树compatible属性与驱动内核驱动结构体,使用module_i2c_driver简化驱动注册注销逻辑,无需手动编写模块入口出口函数,内核自动管理驱动加载与卸载。

驱动设备匹配与注册(位于linux_driver/33_rtc_subsystem/rtc_pcf8563.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/* 定义设备树匹配表 */
static const struct of_device_id pcf8563_of_match[] = {
    { .compatible = "fire,pcf8563" },
    { }
};

/* 声明设备树匹配表,供内核识别 */
MODULE_DEVICE_TABLE(of, pcf8563_of_match);


/* 定义i2c总线设备结构体 */
static struct i2c_driver pcf8563_driver = {
    .probe = pcf8563_probe,
    .driver = {
        .name = "rtc-pcf8563",
        .of_match_table = pcf8563_of_match,
    },
};

/* 注册/注销I2C驱动,简化模块入口/出口函数 */
module_i2c_driver(pcf8563_driver);

15.6.4. Makefile说明

本节实验使用的Makefile如下所示,编写该Makefile时,只需要根据实际情况修改变量KERNEL_DIR、CROSS_COMPILE和obj-m即可。

注意

如果使用6.1内核编译失败,可使用sdk自带的交叉编译工具链,参考以下CROSS_COMPILE的绝对路径或“驱动章节实验环境搭建”章节的获取并配置编译工具的环境变量部分。

Makefile(位于linux_driver/33_rtc_subsystem/Makefile)
 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
#指定内核路径,可以是相对路径或绝对路径
KERNEL_DIR=../../kernel/
# KERNEL_DIR=/home/guest/LubanCat_Linux_Generic_Full_SDK/kernel-6.1
# KERNEL_DIR=/home/guest/LubanCat_Linux_rk3588_SDK/kernel

#指定目标架构为arm64
ARCH=arm64

#指定交叉编译工具链的前缀
CROSS_COMPILE=aarch64-linux-gnu-
# CROSS_COMPILE=/home/guest/LubanCat_Linux_Generic_Full_SDK/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-

#导出为环境变量
export  ARCH  CROSS_COMPILE

#指定要编译的内核模块目标文件
obj-m := rtc_pcf8563.o

#all :默认目标,执行时会编译驱动模块
#$(MAKE) :调用make工具
#-C $(KERNEL_DIR) :指定的内核源码目录
#M=$(CURDIR) :模块的源码位于当前目录
#modules :编译模块
all:
    $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules

.PHONE:clean

#清理编译生成的文件
clean:
    $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean

15.6.5. 编译设备树和驱动

15.6.5.1. 编译设备树

修改内核目录/arch/arm64/boot/dts/rockchip/overlays下的Makefile文件,添加我们编辑好的设备树插件,并把设备树插件文件放在和Makefile文件同级目录下,以进行设备树插件的编译。

../_images/subsystem_rtc_subsystem_16.jpg

然后在内核源码顶层目录执行以下命令编译设备树插件:

1
2
3
#这里以rk356x系列6.1.99内核配置文件为例
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- lubancat_linux_rk356x_defconfig
make ARCH=arm64 -j4 CROSS_COMPILE=aarch64-linux-gnu- dtbs

提示

其余系列板卡参考 使用内核的构建脚本编译设备树插件 章节进行编译。

15.6.5.2. 加载设备树

编译出来的设备树插件位于 内核源码/arch/arm64/boot/dts/rockchip/overlay/lubancat-rtc-pcf8563-overlay.dtbo, 将设备树插件先传到板卡,再拷贝到板卡的 /boot/dtb/overlay/ 目录下。

1
2
3
4
#先传输到板卡

#再拷贝到板卡的/boot/dtb/overlay/目录下
sudo cp -f lubancat-rtc-pcf8563-overlay.dtbo /boot/dtb/overlay/

然后在 /boot/uEnv/uEnv.txt 按照格式添加我们的设备树插件,需要在#overlay_start和#overlay_end之间添加,然后重启开发板,那么系统就会加载我们编译的设备树插件。

../_images/subsystem_rtc_subsystem_17.jpg

15.6.5.3. 编译驱动

在实验目录下输入 make 即可编译驱动和应用程序,编译得到内核模块rtc_pcf8563.ko。

15.6.6. 模块接线说明

通过杜邦线连接PCF8563模块和板卡,接线如下:

1
2
3
4
5
6
7
8
PCF8563模块         板卡
   VCC   --------   3.3V
   GND   --------   GND
   SCL   --------   SCL
   SDA   --------   SDA
   INT   --------   设备树配置的自定义引脚

// 其余引脚不接

15.6.7. 程序运行结果

如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。

15.6.7.1. 实验操作

测试RTC时间读取和设置

使用以下命令加载驱动,加载驱动前需确保PCF8563模块已经连接到板卡:

 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
#查看系统启动rtc驱动加载信息
dmesg | grep rtc

#信息打印如下,内部rtc被禁用并且无其他板载外部RTC加载
[    2.379727] rockchip-drm display-subsystem: [drm] Cannot find any crtc or sizes
[    2.379848] rockchip-drm display-subsystem: [drm] Cannot find any crtc or sizes
[    6.289144] rk808-rtc rk808-rtc: device is disabled
[    6.289234] rk808-rtc: probe of rk808-rtc failed with error -22

#加载驱动前先确认设备地址存在,使用i2cdetect命令,其中3表示是i2c3,需要根据实际使用的i2c修改
sudo i2cdetect -a -y 3

#信息打印如下,可以看到51地址
    0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- 51 -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

#加载驱动
sudo insmod rtc_pcf8563.ko

#信息输出如下
[  131.148145] rtc-pcf8563 3-0051: registered as rtc0
[  131.148693] rtc-pcf8563 3-0051: setting system clock to 2000-01-01T03:30:26 UTC (946697426)
[  131.149160] rtc-pcf8563 3-0051: PCF8563 RTC probed successfully
[  131.151273] systemd-journald[309]: Time jumped backwards, rotating.

#查看rtc0的日期时间
cat /sys/class/rtc/rtc0/date

#信息打印如下
2000-01-01

#查看rtc0的小时、分钟、秒时间
cat /sys/class/rtc/rtc0/time

#信息打印如下
03:31:18

可以看到PCF8563被注册为rtc0,并且由于没有同步正确的时间到PCF8563,读取到的时间为2000年的错误时间。

设置正确时间到PCF8563:

 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
#如果系统有网络,重启ntp服务可网络校准时间
sudo systemctl restart ntp

#如果系统无网络,可通过date命令手动设置当前时间
sudo date -s "2026-05-06 14:30:30"

#通过date命令查询本地时间,CST(中国标准时间)
date

#信息打印如下
2026年 05月 06日 星期三 14:31:04 CST

#通过date命令查询系统时间(UTC)
date -u

#信息打印如下,作者位于东八区,使用上海时间,因此和UTC时间相差8小时
2026年 05月 06日 星期三 06:31:20 UTC

#系统时间同步到硬件rtc
sudo hwclock -w

#查看rtc0的日期时间
cat /sys/class/rtc/rtc0/date

#信息打印如下
2026-05-06

#查看rtc0的小时、分钟、秒时间
cat /sys/class/rtc/rtc0/time

#信息打印如下
06:31:43

#硬件rtc同步到系统
sudo hwclock -s

以上手动设置时间或者网络同步时间后,-w将系统时间写入到硬件rtc,-s再将rtc时间写回系统,这样每次重启板卡都会自动进行rtc时间同步到系统时间。

断电一段时间再上电查看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
#加载驱动
sudo insmod rtc_pcf8563.ko

#信息输出如下
[   38.297596] rtc-pcf8563 3-0051: registered as rtc0
[   38.298545] rtc-pcf8563 3-0051: setting system clock to 2026-05-06T06:34:20 UTC (1778049260)
[   38.299485] rtc-pcf8563 3-0051: PCF8563 RTC probed successfully

#查看rtc0的日期时间
cat /sys/class/rtc/rtc0/date

#信息打印如下
2026-05-06

#查看rtc0的小时、分钟、秒时间
cat /sys/class/rtc/rtc0/time

#信息打印如下
06:34:51

#通过date命令查询本地时间
date

#信息打印如下
2026年 05月 06日 星期三 14:34:55 CST

#通过date命令查询系统时间,UTC(世界协调时间)
date -u

#信息打印如下
2026年 05月 06日 星期三 06:34:57 UTC

可以看到断电时间仍能保持,系统启动并加载驱动后也能自动将RTC时间同步到系统,说明RTC功能正常。

测试RTC定时

通过wakealarm接口可以设置定时时间:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#查看rtc中断号和中断次数
cat /proc/interrupts | grep pcf8563

#信息打印如下,中断号104,4个0分别对应CPU0~CPU3上的中断次数,均为0次
111:          0          0          0          0  rockchip_gpio_irq   8 Level     pcf8563

#清空定时时间,避免资源繁忙或占用
sudo sh -c "echo 0 > /sys/class/rtc/rtc0/wakealarm"

#设置定时时间,1分钟定时,pcf8563不支持秒报警
sudo sh -c "echo +60 > /sys/class/rtc/rtc0/wakealarm"

#等待1分钟再查看rtc中断次数
cat /proc/interrupts | grep RTC

#信息打印如下,在CPU0上产生一次中断
111:          1          0          0          0  rockchip_gpio_irq   8 Level     pcf8563

休眠唤醒测试

可以通过先设置定时时间,然后再进入休眠,待定时时间到后触发中断唤醒系统:

注意

该功能需要使用5.10内核版本及以上的镜像,并且INT引脚使用的40pin GPIO需为GPIO0组,因为只有GPIO0组引脚在系统休眠时不掉电,使用其他组引脚在系统休眠时掉电无法检测到外部触发中断,导致无法唤醒系统。

 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
#清空定时时间,避免资源繁忙或占用
sudo sh -c "echo 0 > /sys/class/rtc/rtc0/wakealarm"

#设置定时时间,1分钟定时
sudo sh -c "echo +60 > /sys/class/rtc/rtc0/wakealarm"

#进入freeze休眠
sudo sh -c "echo freeze > /sys/power/state"

#进入休眠信息打印如下
[  750.882472] PM: suspend entry (s2idle)
[  750.926367] Filesystems sync: 0.043 seconds
[  751.081677] (NULL device *): Direct firmware load for regulatory.db failed with error -2
[  751.081712] (NULL device *): Direct firmware load for regulatory.db.p7s failed with error -2
[  751.081880] Freezing user space processes
[  751.085054] Freezing user space processes completed (elapsed 0.003 seconds)
[  751.085122] OOM killer disabled.
[  751.085139] Freezing remaining freezable tasks
[  751.261127] Freezing remaining freezable tasks completed (elapsed 0.175 seconds)
[  751.261245] printk: Suspending console(s) (use no_console_suspend to debug)

#唤醒信息打印如下
[  830.308115] rk_gmac-dwmac fe2a0000.ethernet: init for RGMII
[  830.380186] rk_gmac-dwmac fe2a0000.ethernet eth0: configuring for phy/rgmii link mode
[  830.621572] ata1: SATA link down (SStatus 0 SControl 300)
[  831.917392] dwmac4: Master AXI performs any burst length
[  831.917429] rk_gmac-dwmac fe2a0000.ethernet eth0: No Safety Features support found
[  831.917459] rk_gmac-dwmac fe2a0000.ethernet eth0: IEEE 1588-2008 Advanced Timestamp supported
[  831.917933] rk_gmac-dwmac fe010000.ethernet: init for RGMII
[  831.990180] rk_gmac-dwmac fe010000.ethernet eth1: configuring for phy/rgmii link mode
[  833.544038] dwmac4: Master AXI performs any burst length
[  833.544064] rk_gmac-dwmac fe010000.ethernet eth1: No Safety Features support found
[  833.544088] rk_gmac-dwmac fe010000.ethernet eth1: IEEE 1588-2008 Advanced Timestamp supported
[  833.550136] xhci-hcd xhci-hcd.4.auto: xHC error in resume, USBSTS 0x401, Reinit
[  833.550158] usb usb1: root hub lost power or was reset
[  833.550167] usb usb2: root hub lost power or was reset
[  833.712585] OOM killer enabled.
[  833.712617] Restarting tasks ... done.
[  833.722868] random: crng reseeded on system resumption
[  833.726959] PM: suspend exit

除了freeze休眠,也可以进入mem休眠,关于这两种休眠区别就不作展开,可自行搜索了解,自行测试。

15.6.8. 实验注意事项

  • I2C设备地址:PCF8563固定I2C地址为0x51,设备树reg属性必须匹配,地址错误会导致驱动匹配失败、设备无法识别;

  • 寄存器编码格式区分:严格区分BCD编码寄存器与星期纯二进制寄存器,星期读写禁止使用BCD转换函数,否则星期数据解析异常;

  • 世纪位配置:禁止手动修改硬件世纪位翻转逻辑,依赖PCF8563硬件自动跨世纪处理,驱动仅需读写解析即可;

  • 闹钟屏蔽位正确配置:AE屏蔽位1为忽略对应字段、0为匹配对应字段,禁止所有屏蔽位全部置1,否则闹钟持续触发造成中断死循环;

  • 低压状态检测:驱动自带低压供电检测,后备电池电量不足会提示时间无效,需及时更换电池保证计时准确;

  • 中断引脚极性匹配:设备树中断配置为低电平触发,硬件引脚必须配置为上拉,否则闹钟中断无法正常触发;

  • 定时时间设置:pcf8563没有秒寄存器,设置少于1分钟的部分会向上取整到最近一分钟;

  • 休眠唤醒要求:使用定时唤醒系统功能,需要INT引脚使用GPIO0组引脚,因为只有GPIO0组引脚在系统休眠时不掉电,使用其他组引脚在系统休眠时掉电无法检测到外部触发中断,导致无法唤醒系统。