----------------------------------------------------------------------------------------------------------------------------
内核版本:linux 5.2.8根文件系统:busybox 1.25.0u-boot:2016.05----------------------------------------------------------------------------------------------------------------------------
一、RTC框架
1.1 RTC概述
RTC,英文全称Real Time Clock,中文就是实时时钟,是一个可以为系统提供精确的时间基准的元器件,主要是用来计时,产生闹钟等。
RTC一般可以使用备份电池供电,所以即使设备关机掉电,RTC也能在备份电池的供电下继续正常计时,这样在每次系统开机上电时就可以从RTC设备中读取到准确的时间。
RTC时间在每次系统启动的时候会使用,在需要的时候也可以由系统将要设置的时间写入到RTC设备。
在linux系统中,RTC可以使用周期性的中断来产生闹钟,也可以在系统suspend的时候作为系统的唤醒源使用。
1.2 RTC框架
在Mini2440裸机开发之RTC小节中我们已经介绍了RTC裸机程序的编写。而在linux中,内核设计了一套RTC框架,如果想增加某一种新RTC硬件的驱动,只需要去编写芯片相关的代码,然后调用内核提供函数注册到RTC核心层即可。
RTC框架主要由以下部分组成:
- hardware:提供时间设置,通过一定的接口(比如通过I2C外接hym8563芯片,或者SoC片内集成了RTC)和RTC驱动进行通信;
- driver:完成RTC硬件的访问功能,提供访问接口,以驱动的方式注册到内核;
- class.c:这个文件向linux设备模型核心注册了一个类RTC,然后向驱动程序提供了注册/注销接口;
- interface.c:屏蔽硬件相关的细节,提供了用户程序与RTC驱动的接口函数,用户程序一般通过ioctl与RTC驱动交互,这里定义了每个ioctl命令需要调用的函数;
- rtc-lib.c:提供通用的时间操作函数,如rtc_time_to_tm,rtc_valid_tm;
- rtc-dev.c:创建字符设备节点,即在/dev/目录下创建设备节点供应用层访问,如open、read、ioctl等,访问方式填充到file_operations结构体中;
- hctosys.c:将硬件时钟写给 wall time;
- rtc-sysfs.c:与sysfs有关;
- rtc-proc.c:与proc文件系统有关;
1.3 目录结构
linux内核将RTC驱动相关的代码放在drivers/rtc目录下,这下面的文件还是比较多的,我们大概了解一下即可。
除了我们上面介绍的那些文件外,以rtc-xxx命名的大部分都是各个平台的RTC驱动,比如rtc-s3c.c、rtc-hym8563.c。
二、RTC核心数据结构
学习RTC驱动,首先要了解驱动框架涉及到的数据结构,知道每个数据结构以及成员的含义之后,再去看源码就容易了。
2.1 struct rtc_device
linux内核使用struct rtc_device数据结构来描述一个rtc设备,rtc_device包含了字符设备,rtc设备操作集,中断等信息,定义在include/linux/rtc.h:
struct rtc_device {
struct device dev;
struct module *owner;
int id;
const struct rtc_class_ops *ops;
struct mutex ops_lock;
struct cdev char_dev;
unsigned long flags;
unsigned long irq_data;
spinlock_t irq_lock;
wait_queue_head_t irq_queue; // 等待队列头
struct fasync_struct *async_queue;
int irq_freq;
int max_user_freq;
struct timerqueue_head timerqueue;
struct rtc_timer aie_timer;
struct rtc_timer uie_rtctimer;
struct hrtimer pie_timer; /* sub second exp, so needs hrtimer */
int pie_enabled;
struct work_struct irqwork;
/* Some hardware can't support UIE mode */
int uie_unsupported;
/* Number of nsec it takes to set the RTC clock. This influences when
* the set ops are called. An offset:
* - of 0.5 s will call RTC set for wall clock time 10.0 s at 9.5 s
* - of 1.5 s will call RTC set for wall clock time 10.0 s at 8.5 s
* - of -0.5 s will call RTC set for wall clock time 10.0 s at 10.5 s
*/
long set_offset_nsec;
bool registered;
/* Old ABI support */
bool nvram_old_abi;
struct bin_attribute *nvram;
time64_t range_min;
timeu64_t range_max;
time64_t start_secs;
time64_t offset_secs;
bool set_start_time;
#ifdef CONFIG_RTC_INTF_DEV_UIE_EMUL
struct work_struct uie_task;
struct timer_list uie_timer;
/* Those fields are protected by rtc->irq_lock */
unsigned int oldsecs;
unsigned int uie_irq_active:1;
unsigned int stop_uie_polling:1;
unsigned int uie_task_active:1;
unsigned int uie_timer_active:1;
#endif
};
其中部分参数含义如下:
- dev:设备驱动模型中的device,可以将rtc_device看做其子类;
- owner:模块的拥有者;
- id:rtc设备编号;
- ops:rtc设备操作函数集;
- ops_lock:互斥锁,解决对rtc设备并发访问问题;
- char_dev:rtc字符设备;
- flags:rtc的状态标志,比如RTC_DEV_BUSY;
- irq_data:irq中断数据;
- irq_lock:自旋锁,互斥访问irq中断数据;
- irq_queue:数据查询时用到的rtc等待队列头;头节点为struct wait_queue_head_t类型,等待队列中的元素为struct wait_queue_t类型;
- async_queue:异步队列;
- irq_freq:rtc中断频率,实际上就是时钟节拍中断的频率;
- max_user_freq:rtc时钟节拍中断最大频率;
- irqwork:该工作会在rtc中断函数中执行,工作函数会被设置为rtc_timer_do_work;
- timerqueue:定时器队列,使用最小堆算法实现,根节点保存的是最先触发的定时器;struct timerqueue_head类型;
- aie_timer:闹钟定时器,保存闹钟时间,闹钟时间到时执行定时器超时处理函数rtc_aie_update_irq;struct rtc_timer类型;
- uie_rtctimer:更新定时器,定时器超时处理函数被设置为rtc_uie_update_irq;struct rtc_timer类型;
- pie_timer:周期高精度定时器,定时器超时处理函数设置为rtc_pie_update_irq;struct hrtimer类型;
- pie_enabled:周期中断使能标志;
- range_int:最小秒数;用来描述rtc设备支持的最小时间;
- range_max:最大秒数;用来描述rtc设备支持的最大时间;
- start_secs:开始秒数;rtc设备设置的起始时间;
- offset_secs:时间偏移秒数;RTC读取到的时间+偏移秒数就是真实的时间;
- uie_task:该工作会在uie_timer定时器超时函数rtc_uie_timer中执行,工作函数会被设置为rtc_uie_task;
- uie_timer:更新定时器,定时器超时函数会被设置为rtc_uie_timer;struct timer_list类型;
range_int、range_max、start_secs、offset_secs这几个参数是做时间映射使用的,比如我们之前介绍的S3C2440中的RTC能够表示的年数范围是00~99,这种情况下我们想将时间映射到2000~2099年的范围,就需要做一个时间区域的映射,比如加上2000年的偏移。
我们从rtc设备读取到最小时间为00-01-01 00:00:00,而我们想表示2000-01-01 00:00:00~2100-01-01 00:00:00这段时间,那么我们就可以将:
- start_secs:设置为时间00-01-01 00:00:00对应的秒数;
- range_int:设置为时间2000-01-01 00:00:00对应的秒数;
- range_max:设置为时间2100-01-01 00:00:00对应的秒数;
- offset_secs:时间区域映射的偏移量;
当然,对于有些rtc设备,它自身能够表示2000~2999年的范围,就没必要做这个应映射了。
rtc_device结构体屏蔽了不同RTC硬件之间的差异,通过rtc_class_ops结构体为上层提供了访问硬件设备的统一接口,该结构体中包含了对硬件操作的相关函数。
2.2 struct rtc_class_ops
考虑到RTC物理设备可能采用不同的接线方式,比如I2C、平台、SPI等,linux内核使用struct rtc_class_ops数据结构来描述如何对rtc设备进行操作,定义在include/linux/rtc.h:
/*
* For these RTC methods the device parameter is the physical device
* on whatever bus holds the hardware (I2C, Platform, SPI, etc), which
* was passed to rtc_device_register(). Its driver_data normally holds
* device state, including the rtc_device pointer for the RTC.
*
* Most of these methods are called with rtc_device.ops_lock held,
* through the rtc_*(struct rtc_device *, ...) calls.
*
* The (current) exceptions are mostly filesystem hooks:
* - the proc() hook for procfs
* - non-ioctl() chardev hooks: open(), release()
*
* REVISIT those periodic irq calls *do* have ops_lock when they're
* issued through ioctl() ...
*/
struct rtc_class_ops {
int (*ioctl)(struct device *, unsigned int, unsigned long);
int (*read_time)(struct device *, struct rtc_time *);
int (*set_time)(struct device *, struct rtc_time *);
int (*read_alarm)(struct device *, struct rtc_wkalrm *);
int (*set_alarm)(struct device *, struct rtc_wkalrm *);
int (*proc)(struct device *, struct seq_file *);
int (*alarm_irq_enable)(struct device *, unsigned int enabled);
int (*read_offset)(struct device *, long *offset);
int (*set_offset)(struct device *, long offset);
};
其中部分参数含义如下:
- ioctl:io控制函数,用来实现各种控制命令;
- read_time:读取时间;
- set_time:设置时间;
- read_alarm:读取闹钟;
- set_alarm:设置闹钟;
- proc:procfs接口;
- alarm_irq_enable:闹钟中断使能;
- read_offset:
- set_offset:
2.3 struct rtc_time
struct rtc_time用于表示时间,定义在include/uapi/linux/rtc.h:
/*
* The struct used to pass data via the following ioctl. Similar to the
* struct tm in <time.h>, but it needs to be here so that the kernel
* source is self contained, allowing cross-compiles, etc. etc.
*/
struct rtc_time {
int tm_sec;
int tm_min;
int tm_hour;
int tm_mday;
int tm_mon;
int tm_year;
int tm_wday;
int tm_yday;
int tm_isdst;
};
其中参数含义如下:
- tm_sec:秒数;
- tm_min:分钟数;
- hour:小时数;
- mday:天;
- mon:月份;
- year:年;
- tm_wday:一周中的某一天;
- tm_year:以1900年为基准,偏移年份;
- tm_yday:一年中的某一天;
2.4 struct rtc_wkalrm
struct rtc_wkalrm用于表示唤醒闹钟,定义在include/uapi/linux/rtc.h:
/*
* This data structure is inspired by the EFI (v0.92) wakeup
* alarm API.
*/
struct rtc_wkalrm {
unsigned char enabled; /* 0 = alarm disabled, 1 = alarm enabled */
unsigned char pending; /* 0 = alarm not pending, 1 = alarm pending */
struct rtc_time time; /* time the alarm is set to */
};
重点需要关注的是enabled,只有为1时才表示开启闹钟,闹钟开启才会触发INT_RTC闹钟中断,否则不会触发INT_RTC闹钟中断。
2.5 struct rtc_timer
struct rtc_timer定义在include/linux/rtc.h:
struct rtc_timer {
struct timerqueue_node node;
ktime_t period; // 定时器触发周期,多久触发一次,比如1小时
void (*func)(struct rtc_device *rtc); // 定时器超时处理函数
struct rtc_device *rtc;
int enabled; // 定时器开关使能
};
这里有一个重要的成员node,其类型为struct timerqueue_node,RTC框架中通过struct timerqueue_node和struct timerqueue实现了最小堆定时器。
关于最小堆的概念这里就不详细介绍了,其本质就是一个完全二叉树,只不过满足以下要求:
- 其中任意非叶子节点数值均不大于其左子节点和右子节点的值;
也就是说最小堆的根节点保存着整个完全二叉树的最小元素。因此我们可以通过堆排序算法来对一组数字进行排序。时间复杂度为$O(nlogn)$。
2.5.1 struct timerqueue_node
struct timerqueue_node定义在include/linux/timerqueue.h文件,用于表示定时器队列中的节点,expires保存着绝对时间:
struct timerqueue_node {
struct rb_node node;
ktime_t expires;
};
还是内核一贯的风格, struct timerqueue_node 还是作为一个成员在需要排序的对象中,然后使用 container_of计算被浸入的对象的指针。
2.5.2 struct timerqueue_head
struct timerqueue_head定义在include/linux/timerqueue.h文件,用于表示定时器队列头:
struct timerqueue_head {
struct rb_root head;
struct timerqueue_node *next;
};
其next字段指向最小堆的第一个元素,每次插入新的struct timerqueue_node可能被更新(每次新插入一个节点,都会重新构造最小堆)。
2.5.3 相关API
通过timerqueue_init_head初始化定时器队列头:
static inline void timerqueue_init_head(struct timerqueue_head *head)
{
head->head = RB_ROOT;
head->next = NULL;
}
通过timerqueue_add向最小堆中插入一个元素,返回true表示插入成功;每新增一个节点,都会重新更新最小堆,将插入的元素调整到正确的位置:
bool timerqueue_add(struct timerqueue_head *head, struct timerqueue_node *node)
通过timerqueue_del从最小堆中移除指定的元素,返回true表示移除成功;每移除一个节点,都会重新更新最小堆;
bool timerqueue_del(struct timerqueue_head *head,
struct timerqueue_node *node);
通过timerqueue_getnext获取最小堆的根节点,也就是定时器过期时间最早的节点:
/**
* timerqueue_getnext - Returns the timer with the earliest expiration time
*
* @head: head of timerqueue
*
* Returns a pointer to the timer node that has the
* earliest expiration time.
*/
static inline
struct timerqueue_node *timerqueue_getnext(struct timerqueue_head *head)
{
return head->next;
}
三、RTC驱动API
linux内核提供了一组函数用于操作rtc_device结构体。
3.1 注册rtc设备(rtc_register_device)
rtc_register_device用于注册RTC设备,定义在include/linux/rtc.h:
#define rtc_register_device(device) \
__rtc_register_device(THIS_MODULE, device)
其入参device一般为平台设备的dev成员。该函数内部调用了__rtc_register_device,位于drivers/rtc/class.c:
int __rtc_register_device(struct module *owner, struct rtc_device *rtc)
{
struct rtc_wkalrm alrm;
int err;
if (!rtc->ops)
return -EINVAL;
rtc->owner = owner;
rtc_device_get_offset(rtc);
/* Check to see if there is an ALARM already set in hw */
err = __rtc_read_alarm(rtc, &alrm); // 该函数主要是调用rtc->ops->read_alarm(即s3c_rtc_getalarm)获取闹钟时间并保存在alarm参数中,同时会读取RTCALM寄存器的全局闹钟使能位的值并赋值给alarm->enabled ;
if (!err && !rtc_valid_tm(&alrm.time)) // 如果闹钟时间有效,进入该分支
rtc_initialize_alarm(rtc, &alrm);
rtc_dev_prepare(rtc); // 字符设备初始化
err = cdev_device_add(&rtc->char_dev, &rtc->dev);
if (err)
dev_warn(rtc->dev.parent, "failed to add char device %d:%d\n",
MAJOR(rtc->dev.devt), rtc->id);
else
dev_dbg(rtc->dev.parent, "char device (%d:%d)\n",
MAJOR(rtc->dev.devt), rtc->id);
rtc_proc_add_device(rtc);
rtc->registered = true;
dev_info(rtc->dev.parent, "registered as %s\n",
dev_name(&rtc->dev));
return 0;
}
函数主要流程如下:
- 调用rtc_device_get_offset根据start_secs、range_min、range_max去计算偏移秒数offset_secs:
- 调用__rtc_read_alarm读取rtc闹钟寄存器,获取闹钟时间;
- 调用rtc_initialize_alarm函数,如果闹钟开启(即alarm->enabled为1),则会执行timerqueue_add(&rtc->timerqueue, &rtc->aie_timer.node),这里是将rtc->aie_timer.node节点添加到rtc->timerqueue队列中;实际上在注册RTC设备时,我们还没有开启闹钟(即alarm->enabled为0),因此并将aie_timer定时器添加到定时器队列中;
- 调用rtc_dev_prepare初始化字符设备,设置主设备号为MAJOR(rtc_devt),次设备号为rtc->id,设置字符设备的文件操作集为rtc_dev_fops;
- 调用cdev_device_add注册字符设备;
- 其内部先是调用了cdev_add(cdev, dev->devt, 1)将1个字符设备添加到内核;
- 然后调用device_add(dev)注册dev设备,这样在模块加载的时候,udev daemon就会自动为我们创建设备节点文件/dev/rtc(n);
- 调用rtc_proc_add_device创建proc文件系统接口;
3.1.1 rtc_device_get_offset
rtc_device_get_offset定义在include/linux/rtc.h,函数主要是根据start_secs、range_min、range_max去计算偏移秒数offset_secs;
static void rtc_device_get_offset(struct rtc_device *rtc)
{
time64_t range_secs;
u32 start_year;
int ret;
/*
* If RTC driver did not implement the range of RTC hardware device,
* then we can not expand the RTC range by adding or subtracting one
* offset.
*/
if (rtc->range_min == rtc->range_max)
return;
ret = device_property_read_u32(rtc->dev.parent, "start-year", // 获取基址年份
&start_year);
if (!ret) {
rtc->start_secs = mktime64(start_year, 1, 1, 0, 0, 0);
rtc->set_start_time = true;
}
/*
* If user did not implement the start time for RTC driver, then no
* need to expand the RTC range.
*/
if (!rtc->set_start_time)
return;
range_secs = rtc->range_max - rtc->range_min + 1;
/*
* If the start_secs is larger than the maximum seconds (rtc->range_max)
* supported by RTC hardware or the maximum seconds of new expanded
* range (start_secs + rtc->range_max - rtc->range_min) is less than
* rtc->range_min, which means the minimum seconds (rtc->range_min) of
* RTC hardware will be mapped to start_secs by adding one offset, so
* the offset seconds calculation formula should be:
* rtc->offset_secs = rtc->start_secs - rtc->range_min;
*
* If the start_secs is larger than the minimum seconds (rtc->range_min)
* supported by RTC hardware, then there is one region is overlapped
* between the original RTC hardware range and the new expanded range,
* and this overlapped region do not need to be mapped into the new
* expanded range due to it is valid for RTC device. So the minimum
* seconds of RTC hardware (rtc->range_min) should be mapped to
* rtc->range_max + 1, then the offset seconds formula should be:
* rtc->offset_secs = rtc->range_max - rtc->range_min + 1;
*
* If the start_secs is less than the minimum seconds (rtc->range_min),
* which is similar to case 2. So the start_secs should be mapped to
* start_secs + rtc->range_max - rtc->range_min + 1, then the
* offset seconds formula should be:
* rtc->offset_secs = -(rtc->range_max - rtc->range_min + 1);
*
* Otherwise the offset seconds should be 0.
*/
if (rtc->start_secs > rtc->range_max ||
rtc->start_secs + range_secs - 1 < rtc->range_min)
rtc->offset_secs = rtc->start_secs - rtc->range_min;
else if (rtc->start_secs > rtc->range_min)
rtc->offset_secs = range_secs;
else if (rtc->start_secs < rtc->range_min)
rtc->offset_secs = -range_secs;
else
rtc->offset_secs = 0;
}
3.1.2 __rtc_read_alarm
__rtc_read_alarm定义在drivers/rtc/interface.c,该函数主要是调用rtc->ops->read_alarm(即s3c_rtc_getalarm)获取闹钟时间并保存在alarm参数中,同时会读取RTCALM寄存器的全局闹钟使能位的值并赋值给alarm->enabled ;
如果获取到的闹钟时间中的时、分、秒、年、月、日存在错误,还会进行一个校正工作;
int __rtc_read_alarm(struct rtc_device *rtc, struct rtc_wkalrm *alarm)
{
int err;
struct rtc_time before, now;
int first_time = 1;
time64_t t_now, t_alm;
enum { none, day, month, year } missing = none;
unsigned int days;
/* The lower level RTC driver may return -1 in some fields,
* creating invalid alarm->time values, for reasons like:
*
* - The hardware may not be capable of filling them in;
* many alarms match only on time-of-day fields, not