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:秒值转换
如十进制秒数35:数值十位是3,个位是5
bin2bcd(35):二进制0011 0101,对应十六进制0x35,RTC寄存器实际存储值为0x35;
bcd2bin(0x35):把高4位0x3作为十位、低4位0x5作为个位,计算3x10+5 = 十进制35。
示例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驱动读写时间必须通过该结构体交互,是内核与驱动时间数据的统一标准。
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 触发状态,用于闹钟读取、闹钟设置、中断状态管理。
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设备后由内核自动分配管理。
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核心层通过该结构体调用驱动底层硬件读写逻辑。
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.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寄存器差异, 私有数据结构体统一管理驱动运行时硬件资源、中断号与兼容标记,实现多芯片硬件适配。
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天不符,驱动实现双向日历转换函数, 完成硬件特殊日历与系统标准公历的互相适配,保证时间读写前后日期精准一致。
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格式, 触发硬件时间快照锁存确保读取时间一致性,按需调用日历转换函数修正为标准公历,向上层子系统输出精准时间数据。
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特殊日历格式, 批量写入时间寄存器,写入完成后恢复时钟计数,保障时间设置过程不被硬件计时干扰。
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两个专属辅助函数,专门负责闹钟中断使能开关控制与闹钟状态标志清除,是闹钟配置、中断触发、唤醒复位的核心基础函数。
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非标日历转公历适配,同时读取硬件闹钟开关状态、中断使能状态,向上层内核返回当前已配置的闹钟定时信息。
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码,批量写入闹钟寄存器,完成硬件闹钟定时配置。
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子系统标准回调接口,专门供内核动态开启/关闭闹钟中断,不修改闹钟定时时间,只控制硬件闹钟中断总开关,适配内核休眠前使能唤醒、唤醒后关闭中断的电源管理逻辑。
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子系统上报闹钟中断事件,完成唤醒后置处理,保证中断正常结束、资源正常释放。
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设备节点,用户态直接通过标准接口操作时间与闹钟。
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设备注册。
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电源管理回调
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节点并加载驱动。
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芯片所有控制寄存器、时间寄存器、闹钟寄存器、定时器寄存器、时钟输出寄存器的物理地址与关键位掩码。 所有寄存器按功能分区管理,涵盖时钟启停、中断开关、时间掩码、闹钟禁用位、定时器分频配置等核心控制标记。
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二进制格式,适配星期、月份内核计数偏移规则,向上层子系统输出标准公历时间。
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计数, 批量写入时间寄存器后恢复时钟运行,避免写入过程时间跳动错乱,保障时间设置精准生效。
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码为标准时间格式,软件补齐硬件缺失的秒级闹钟数据,同步读取中断使能状态,向上层内核返回当前已配置的闹钟定时信息。
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秒长定时切日历闹钟,自动适配双模式,写入闹钟寄存器后开启中断。
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子系统标准回调接口,根据双定时模式标记,动态开启/关闭定时器中断或日历闹钟中断, 统一管控中断总开关,休眠唤醒启停复用该函数,保证中断使能时序标准化。
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资源,向上层内核返回中断处理完成标记,避免中断重复触发、标志残留问题。
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秒分频,保证芯片上电处于初始状态。
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操作都会回调底层驱动对应函数,上层完全屏蔽硬件差异。
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闹钟休眠唤醒正常触发,唤醒后不占用中断资源。
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函数
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->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驱动,完成设备绑定。
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模块
重要
寄存器说明在芯片数据手册中有十分详细的说明。
截取手册关键说明并结合本实验情况整理得:
寄存器结构
寄存器结构结构如下:
PCF8563包含16个8位寄存器,所有16个寄存器设计成可寻址的8位并行寄存器,但不是所有位都有用。前两个寄存器(内存地址00h和01h)用作控制和状态寄存器。 内存地址02h-08h用做时钟计数器(秒至年计数器)。内存地址09h-0Ch包含报警寄存器,定义报警条件。地址0Dh控制CLKOUT输出频率。0Eh和0Fh是分别是Timer_control和定时器计算器。
控制和状态寄存器1
Control_status_1是PCF8563的核心控制寄存器,负责时钟启停、测试模式控制、复位功能管理,芯片的所有计时、闹钟、定时器功能,都依赖该寄存器的正确配置。
该寄存器保持默认值即可,也即上电复位(POR)覆盖启用,RTC时钟正常运行,正常模式,使用内部32.768kHz晶振计时。
控制和状态寄存器2
Control_status_2是PCF8563的中断控制核心寄存器,负责闹钟/定时器中断的使能、中断标志管理、INT引脚输出模式控制,是驱动实现闹钟唤醒、定时中断功能的关键。
该寄存器读写主要应用于以下4个场景:
初始化:向控制和状态寄存器2直接写入0x00
清零AF(bit3):清除闹钟标志
清零AIE(bit1):关闭闹钟中断
清零bit5~7保留位
清零定时器相关位
闹钟中断模式配置:
配置AIE(bit1):开启/关闭闹钟中断
清AF+保留位:每次配置都清除闹钟标志 + 保留位强制0
闹钟中断处理:
检查AF(bit3)是否置1
读取闹钟配置:
AIE(bit1):闹钟中断是否开启
AF(bit3):闹钟是否触发
秒寄存器
VL_seconds是PCF8563的秒寄存器,也是读取时间时的第一个校验寄存器。
位号7:VL:低压时钟有效性标志
0 = 电压正常,时钟信息完整,秒值有效
1 = 低压警告(纽扣电池/供电异常),时钟信息可能丢失,秒值无效
位号6~4:秒的十位,BCD编码,范围0~5(因为秒的最大值为59,十位最大为5),对应二进制000~101
位号3~0:秒的个位,BCD编码,范围 0~9,对应二进制0000~1001
分钟寄存器
位号6~4:分钟的十位,BCD编码,范围0~5(分钟最大值为 59,因此十位最大为5,对应二进制000~101)
位号3~0: 分钟的个位,BCD编码,范围0~9
小时寄存器
位号5~4:小时的十位,BCD编码,范围0~2(小时最大值为23,因此十位最大为2,对应二进制00~10)
位号5~4:小时的个位,BCD编码,范围0~9
天数寄存器
位号5~4:日期的十位,BCD编码,范围0~3(日期最大值为 31,因此十位最大为 3,对应二进制00~11)
位号3~0:日期的个位,BCD编码,范围0~9
芯片自带闰年自动补偿:当年份能被4整除(含00年),会自动在2月添加第 29 天,驱动无需额外处理
周天数寄存器
位号2~0:周天数,二进制编码,范围0~6,对应默认映射:0=周日、1=周一、2=周二、3=周三、4=周四、5=周五、6=周六
Weekdays寄存器是纯二进制编码,不是BCD编码,直接读取/写入数值即可
世纪标志和月份寄存器
位号7:世纪标志位
0:表示当前世纪为x
1:表示当前世纪为x+1
位号4:月份的BCD编码十位,范围0~1(月份最大值为12,因此十位只能是0或1)
位号3~0:月份的BCD编码个位,范围0~9
年份寄存器
位号7~4:年份的十位,BCD编码,范围0~9(对应十进制 0~9)
位号3~0:年份的个位,BCD编码,范围0~9(对应十进制 0~9)
分报警寄存器
位号7:分钟报警屏蔽位
0:分钟字段参与比较,当前分钟必须和报警分钟完全匹配,闹钟才会触发
1:分钟字段不参与比较(被忽略),不管当前分钟是什么,只要其他报警条件满足,闹钟就会触发
位号6~4:报警分钟的BCD编码十位,范围0~5(分钟最大值为59,因此十位最大为5,对应二进制000~101)
位号3~0:报警分钟的BCD编码个位,范围0~9
其他报警寄存器和分报警寄存器基本一致。
定时器控制寄存器
位号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从设备,完整代码如下:
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接口对应的物理引脚,如下图:
15.6.3. 驱动代码详解¶
驱动基于I2C子系统、RTC子系统、Regmap API操作机制开发,核心功能为PCF8563硬件初始化、RTC设备内核注册、时间读写适配、闹钟中断配置与回调处理, 屏蔽底层I2C读写细节,适配内核标准RTC接口规范。
核心定义与数据结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | #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命令时间查询与系统时间初始化。
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编码格式,批量写入时间寄存器, 根据世纪位极性自动配置世纪标志和月份寄存器的的世纪标志位,实现跨世纪年份同步。
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转换。
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编码写入寄存器。
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,专门负责闹钟中断全局开关控制、闹钟触发标志位软件清零、寄存器保留位清零规整,所有闹钟中断硬件配置全部收敛在此函数,避免多处代码直接操作寄存器,减少重复代码。
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函数统一配置。
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子系统上报闹钟事件、重新使能闹钟中断,保证闹钟可以重复触发。
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设备内核注册、闹钟中断引脚申请、休眠唤醒功能配置,是驱动硬件初始化核心入口。
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简化驱动注册注销逻辑,无需手动编写模块入口出口函数,内核自动管理驱动加载与卸载。
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的绝对路径或“驱动章节实验环境搭建”章节的获取并配置编译工具的环境变量部分。
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文件同级目录下,以进行设备树插件的编译。
然后在内核源码顶层目录执行以下命令编译设备树插件:
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之间添加,然后重启开发板,那么系统就会加载我们编译的设备树插件。
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组引脚在系统休眠时不掉电,使用其他组引脚在系统休眠时掉电无法检测到外部触发中断,导致无法唤醒系统。