实时时钟 ( RTC )是用于跟踪非易失性存储器中绝对时间的设备,非易失性存储器可以位于处理器内部,也可以通过 I2C 或 SPI 总线连接到外部。
可以使用 RTC 来执行以下操作:
- 读取和设置绝对时钟,并在时钟更新期间产生中断
- 产生周期性中断
- 设置警报
RTC 和系统时钟有不同的用途。前者是以非易失方式维护绝对时间和日期的硬件时钟,而后者是由内核维护的软件时钟,用于实现gettimeofday(2)
和time(2)
系统调用,以及在文件上设置时间戳等等。系统时钟报告从起点开始的秒和微秒,起点定义为 POSIX 纪元:1970-01-01 00:00:00 +0000 (UTC)
。
在本章中,我们将涵盖以下主题:
- 介绍 RTC 框架 API
- 描述这种驱动的体系结构,以及一个虚拟驱动示例
- 处理警报
- 通过 sysfs 接口或使用 hwclock 工具,从用户空间管理 RTC 设备
在 Linux 系统上,RTC 框架使用三种主要的数据结构。它们是strcut rtc_time
、struct rtc_device
和struct rtc_class_ops
结构。前者是表示给定日期和时间的不透明结构;第二种结构表示物理 RTC 设备;最后一个表示由驱动公开并由 RTC 核心用来读取/更新设备的日期/时间/警报的一组操作。
从驱动中提取 RTC 功能所需的唯一标题是:
#include <linux/rtc.h>
同一个文件包含前面部分列举的所有三种结构:
struct rtc_time {
int tm_sec; /* seconds after the minute */
int tm_min; /* minutes after the hour - [0, 59] */
int tm_hour; /* hours since midnight - [0, 23] */
int tm_mday; /* day of the month - [1, 31] */
int tm_mon; /* months since January - [0, 11] */
int tm_year; /* years since 1900 */
int tm_wday; /* days since Sunday - [0, 6] */
int tm_yday; /* days since January 1 - [0, 365] */
int tm_isdst; /* Daylight saving time flag */
};
这个结构类似于<time.h>
中的struct tm
,用来打发时间。下一个结构是struct rtc_device,
,代表内核中的芯片:
struct rtc_device {
struct device dev;
struct module *owner;
int id;
char name[RTC_DEVICE_NAME_SIZE];
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 rtc_task *irq_task;
spinlock_t irq_task_lock;
int irq_freq;
int max_user_freq;
struct work_struct irqwork;
};
以下是结构元素的含义:
dev
:这是器件结构。owner
:这是拥有这个 RTC 设备的模块。使用THIS_MODULE
就够了。id
:这是内核/dev/rtc<id>
给 RTC 设备的全局索引。name
:这是给 RTC 设备起的名字。ops
:这是这个 RTC 设备暴露给核心管理或者来自用户空间的一组操作(比如读取/设置时间/告警)。ops_lock
:这是内核内部用来保护 ops 函数调用的互斥体。cdev
:这是与该 RTC 关联的充电设备,/dev/rtc<id>
。
下一个重要的结构是struct rtc_class_ops
,它是一组作为回调来执行标准的功能,并限制在 RTC 设备上。它是顶层和底层 RTC 驱动之间的通信接口:
struct rtc_class_ops {
int (*open)(struct device *);
void (*release)(struct device *);
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 (*read_callback)(struct device *, int data);
int (*alarm_irq_enable)(struct device *, unsigned int enabled);
};
前面代码中的所有钩子都给定了一个struct device
结构作为参数,这与嵌入在struct rtc_device
结构中的钩子相同。这意味着在这些钩子中,用户可以在任何给定时间使用to_rtc_device()
宏访问 RTC 设备本身,该宏构建在container_of()
宏之上。
#define to_rtc_device(d) container_of(d, struct rtc_device, dev)
当从用户空间调用设备上的open()
、close()
或read()
功能时,内核内部调用open()
、release()
和read_callback()
钩子。
read_time()
是从设备读取时间并填充struct rtc_time
输出参数的驱动函数。该函数应在成功时返回0
,否则返回负错误代码。
set_time()
是驱动功能,根据作为输入参数给出的struct rtc_time
结构更新设备的时间。返回参数的备注与read_time
功能相同。
如果您的设备支持报警功能,驾驶员应提供read_alarm()
和set_alarm()
来读取/设置设备上的报警。struct rtc_wkalrm
将在本章后面描述。alarm_irq_enable()
也应提供,以启用报警。
RTC 设备在内核中表示为struct rtc_device
结构的一个实例。与其他内核框架设备注册(其中设备作为注册函数的参数给出)不同,RTC 设备由内核构建,并在rtc_device
结构返回给驱动之前首先注册。使用rtc_device_register()
功能在内核中构建和注册设备:
struct rtc_device *rtc_device_register(const char *name,
struct device *dev,
const struct rtc_class_ops *ops,
struct module *owner)
可以看到函数的每个参数的含义,如下所示:
name
:这是你的 RTC 设备名称。它可能是芯片的名称,例如:ds1343。dev
:这是父设备,用于设备模型目的。例如,对于位于 I2C 或 SPI 总线上的芯片,dev
可以设置为spi_device.dev
或i2c_client.dev
。ops
:这是你的 RTC 操作,根据 RTC 拥有的功能或者你的驱动可以支持的功能来填充。owner
:这是这个 RTC 设备所属的模块。大多数情况下THIS_MODULE
就够了。
注册应该在probe
功能中进行,显然可以使用这个功能的资源管理版本:
struct rtc_device *devm_rtc_device_register(struct device *dev,
const char *name,
const struct rtc_class_ops *ops,
struct module *owner)
这两个函数在成功时返回内核构建的struct rtc_device
结构上的指针,或者返回应该使用IS_ERR
和PTR_ERR
宏的指针错误。
相关反向操作为rtc_device_unregister()
和devm_ rtc_device_unregister()
:
void rtc_device_unregister(struct rtc_device *rtc)
void devm_rtc_device_unregister(struct device *dev,
struct rtc_device *rtc)
驱动负责提供读取和设置设备时间的功能。这些是 RTC 驱动至少能提供的。当涉及到读取时,读取回调函数被赋予一个指向已分配/清零的struct rtc_time
结构的指针,驱动必须填充该结构。因此,RTC 几乎总是以二进制编码十进制 ( BCD )存储/恢复时间,其中每个四位组(4 位系列)代表 0 到 9 之间的数字(而不是 0 到 15 之间的数字)。内核提供了bcd2bin()
和bin2bcd()
两个宏,分别将 BCD 编码转换为十进制,或者将十进制转换为 BCD。接下来要注意的是一些rtc_time
字段,有一些边界要求,需要做一些翻译的地方。数据以 BCD 形式从设备中读取,应使用bcd2bin()
进行转换。
由于struct rtc_time
结构复杂,内核提供了rtc_valid_tm()
帮助器,以验证给定的rtc_time
结构,并在成功时返回0
,这意味着该结构表示有效的日期/时间:
int rtc_valid_tm(struct rtc_time *tm);
以下示例描述了 RTC 读取操作回调:
static int foo_rtc_read_time(struct device *dev, struct rtc_time *tm)
{
struct foo_regs regs;
int error;
error = foo_device_read(dev, ®s, 0, sizeof(regs));
if (error)
return error;
tm->tm_sec = bcd2bin(regs.seconds);
tm->tm_min = bcd2bin(regs.minutes);
tm->tm_hour = bcd2bin(regs.cent_hours);
tm->tm_mday = bcd2bin(regs.date);
/*
* This device returns weekdays from 1 to 7
* But rtc_time.wday expect days from 0 to 6\.
* So we need to substract 1 to the value returned by the chip
*/
tm->tm_wday = bcd2bin(regs.day) - 1;
/*
* This device returns months from 1 to 12
* But rtc_time.tm_month expect a months 0 to 11\.
* So we need to substract 1 to the value returned by the chip
*/
tm->tm_mon = bcd2bin(regs.month) - 1;
/*
* This device's Epoch is 2000\.
* But rtc_time.tm_year expect years from Epoch 1900\.
* So we need to add 100 to the value returned by the chip
*/
tm->tm_year = bcd2bin(regs.years) + 100;
return rtc_valid_tm(tm);
}
在使用 BCD 转换函数之前,以下标题是必需的:
#include <linux/bcd.h>
当涉及到set_time
功能时,会给出一个指向struct rtc_time
的指针作为输入参数。该参数已经填充了要存储在 RTC 芯片中的值。不幸的是,这些是十进制编码的,应该在发送到芯片之前转换成 BCD。bin2bcd
做转换。struct rtc_time
结构的某些领域也应受到同样的关注。以下是描述通用set_time
函数的伪代码:
static int foo_rtc_set_time(struct device *dev, struct rtc_time *tm)
{
regs.seconds = bin2bcd(tm->tm_sec);
regs.minutes = bin2bcd(tm->tm_min);
regs.cent_hours = bin2bcd(tm->tm_hour);
/*
* This device expects week days from 1 to 7
* But rtc_time.wday contains week days from 0 to 6\.
* So we need to add 1 to the value given by rtc_time.wday
*/
regs.day = bin2bcd(tm->tm_wday + 1);
regs.date = bin2bcd(tm->tm_mday);
/*
* This device expects months from 1 to 12
* But rtc_time.tm_mon contains months from 0 to 11\.
* So we need to add 1 to the value given by rtc_time.tm_mon
*/
regs.month = bin2bcd(tm->tm_mon + 1);
/*
* This device expects year since Epoch 2000
* But rtc_time.tm_year contains year since Epoch 1900\.
* We can just extract the year of the century with the
* rest of the division by 100\.
*/
regs.cent_hours |= BQ32K_CENT;
regs.years = bin2bcd(tm->tm_year % 100);
return write_into_device(dev, ®s, 0, sizeof(regs));
}
RTC's epoch differs from the POSIX epoch, which is only used for the system clock. If the year according to the RTC's epoch and the year register is less than 1970, it is assumed to be 100 years later, that is, between 2000 and 2069.
我们可以用一个简单而虚假的驱动来总结前面的概念,这个驱动只是在系统上注册一个 RTC 设备:
#include <linux/platform_device.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/time.h>
#include <linux/err.h>
#include <linux/rtc.h>
#include <linux/of.h>
static int fake_rtc_read_time(struct device *dev, struct rtc_time *tm)
{
/*
* One can update "tm" with fake values and then call
*/
return rtc_valid_tm(tm);
}
static int fake_rtc_set_time(struct device *dev, struct rtc_time *tm)
{
return 0;
}
static const struct rtc_class_ops fake_rtc_ops = {
.read_time = fake_rtc_read_time,
.set_time = fake_rtc_set_time
};
static const struct of_device_id rtc_dt_ids[] = {
{ .compatible = "packt,rtc-fake", },
{ /* sentinel */ }
};
static int fake_rtc_probe(struct platform_device *pdev)
{
struct rtc_device *rtc;
rtc = rtc_device_register(pdev->name, &pdev->dev,
&fake_rtc_ops, THIS_MODULE);
if (IS_ERR(rtc))
return PTR_ERR(rtc);
platform_set_drvdata(pdev, rtc);
pr_info("Fake RTC module loaded\n");
return 0;
}
static int fake_rtc_remove(struct platform_device *pdev)
{
rtc_device_unregister(platform_get_drvdata(pdev));
return 0;
}
static struct platform_driver fake_rtc_drv = {
.probe = fake_rtc_probe,
.remove = fake_rtc_remove,
.driver = {
.name = KBUILD_MODNAME,
.owner = THIS_MODULE,
.of_match_table = of_match_ptr(rtc_dt_ids),
},
};
module_platform_driver(fake_rtc_drv);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("John Madieu <[email protected]>");
MODULE_DESCRIPTION("Fake RTC driver description");
RTC 报警是由设备在给定时间触发的可编程事件。实时温度控制报警被表示为struct rtc_wkalarm
结构的一个实例:
struct rtc_wkalrm {
unsigned char enabled; /* 0 = alarm disabled, 1 = enabled */
unsigned char pending; /* 0 = alarm not pending, 1 = pending */
struct rtc_time time; /* time the alarm is set to */
};
驾驶员应提供set_alarm()
和read_alarm()
操作,以设置和读取报警发生的时间,以及alarm_irq_enable()
,这是用于启用/禁用报警的功能。当set_alarm()
功能被调用时,它作为一个输入参数给出,一个指向struct rtc_wkalrm
的指针,其.time
字段包含报警必须被设置的时间。驱动有责任以正确的方式提取每个值(如有必要,使用bin2dcb()
,并将其写入设备的适当寄存器中。rtc_wkalrm.enabled
告知警报是否应在设置后立即启用。如果为真,驱动必须启用芯片中的报警。给read_alarm()
一个指向struct rtc_wkalrm
的指针也是如此,但这次是作为输出参数。驱动必须用从设备读取的数据填充结构。
{read | set}_alarm()
and {read | set}_time()
functions behave the same way, except that each pair of functions reads/stores data from/into different sets of registers in the device.
在向系统报告报警事件之前,必须将实时时钟芯片连接到系统芯片的内部时钟线路。当报警发生时,它依赖于驱动为低电平的实时时钟的 INT 线。根据制造商的不同,线路保持低电平,直到状态寄存器被读取,或者一个特殊位被清零:
在这一点上,我们可以使用通用的 IRQ 应用编程接口,如request_threaded_irq()
,来注册警报 IRQ 的处理程序。在 IRQ 处理程序中,使用rtc_update_irq()
功能通知内核关于 RTC IRQ 事件是很重要的:
void rtc_update_irq(struct rtc_device *rtc,
unsigned long num, unsigned long events)
rtc
:这是引发 IRQ 的 rtc 设备num
:显示报告了多少个内部评级(通常是一个)events
:这是RTC_IRQF
的面具,有RTC_PF
、RTC_AF
、RTC_UF
中的一个或多个
/* RTC interrupt flags */
#define RTC_IRQF 0x80 /* Any of the following is active */
#define RTC_PF 0x40 /* Periodic interrupt */
#define RTC_AF 0x20 /* Alarm interrupt */
#define RTC_UF 0x10 /* Update interrupt for 1Hz RTC */
该函数可以从任何上下文中调用,无论是原子的还是非原子的。IRQ 处理程序可能如下所示:
static irqreturn_t foo_rtc_alarm_irq(int irq, void *data)
{
struct foo_rtc_struct * foo_device = data;
dev_info(foo_device ->dev, "%s:irq(%d)\n", __func__, irq);
rtc_update_irq(foo_device ->rtc_dev, 1, RTC_IRQF | RTC_AF);
return IRQ_HANDLED;
}
请记住,具有报警功能的 RTC 设备可以用作唤醒源。也就是说,只要警报触发,系统就可以从暂停模式中唤醒。该特性依赖于 RTC 设备产生的中断。使用device_init_wakeup()
功能将设备声明为唤醒源。实际唤醒系统的 IRQ 也必须向电源管理核心注册,使用dev_pm_set_wake_irq()
功能:
int device_init_wakeup(struct device *dev, bool enable)
int dev_pm_set_wake_irq(struct device *dev, int irq)
我们不会在本书中详细讨论电源管理。这个想法只是给你一个概述,如何 RTC 设备可以改善你的系统。驱动drivers/rtc/rtc-ds1343.c
可以帮助实现这样的功能。让我们通过为一个 SPI foo RTC 设备编写一个假的probe
函数来把所有的事情联系起来:
static const struct rtc_class_ops foo_rtc_ops = {
.read_time = foo_rtc_read_time,
.set_time = foo_rtc_set_time,
.read_alarm = foo_rtc_read_alarm,
.set_alarm = foo_rtc_set_alarm,
.alarm_irq_enable = foo_rtc_alarm_irq_enable,
.ioctl = foo_rtc_ioctl,
};
static int foo_spi_probe(struct spi_device *spi)
{
int ret;
/* initialise and configure the RTC chip */
[...]
foo_rtc->rtc_dev =
devm_rtc_device_register(&spi->dev, "foo-rtc",
&foo_rtc_ops, THIS_MODULE);
if (IS_ERR(foo_rtc->rtc_dev)) {
dev_err(&spi->dev, "unable to register foo rtc\n");
return PTR_ERR(priv->rtc);
}
foo_rtc->irq = spi->irq;
if (foo_rtc->irq >= 0) {
ret = devm_request_threaded_irq(&spi->dev, spi->irq,
NULL, foo_rtc_alarm_irq,
IRQF_ONESHOT, "foo-rtc", priv);
if (ret) {
foo_rtc->irq = -1;
dev_err(&spi->dev,
"unable to request irq for rtc foo-rtc\n");
} else {
device_init_wakeup(&spi->dev, true);
dev_pm_set_wake_irq(&spi->dev, spi->irq);
}
}
return 0;
}
在 Linux 系统上,有两个内核选项需要注意,以便从用户空间正确管理 RTC。这些是CONFIG_RTC_HCTOSYS
和CONFIG_RTC_HCTOSYS_DEVICE
。
CONFIG_RTC_HCTOSYS
包括内核构建过程中的代码文件drivers/rtc/hctosys.c
,从启动和恢复时的 RTC 开始设置系统时间。启用此选项后,系统时间将使用从指定的实时时钟设备读取的值进行设置。RTC 装置应在CONFIG_RTC_HCTOSYS_DEVICE
中规定:
CONFIG_RTC_HCTOSYS=y
CONFIG_RTC_HCTOSYS_DEVICE="rtc0"
在前面的例子中,我们告诉内核从 RTC 设置系统时间,我们指定使用的 RTC 为rtc0
。
负责在 sysfs 中实例化 RTC 属性的内核代码在drivers/rtc/rtc-sysfs.c
中定义,在内核源代码树中。一旦注册,RTC 设备将在/sys/class/rtc
下创建一个rtc<id>
目录。该目录包含一组只读属性,其中最重要的是:
date
:该文件打印 RTC 接口的当前日期:
$ cat /sys/class/rtc/rtc0/date
2017-08-28
time
:打印该 RTC 的当前时间:
$ cat /sys/class/rtc/rtc0/time
14:54:20
hctosys
:该属性表示 RTC 设备是否为CONFIG_RTC_HCTOSYS_DEVICE
中指定的设备,表示该 RTC 用于设置系统启动和恢复的时间。将1
解读为真,将0
解读为假:
$ cat /sys/class/rtc/rtc0/hctosys
1
dev
:该属性显示设备的大调和小调。主修:副修:
$ cat /sys/class/rtc/rtc0/dev
251:0
since_epoch
:此属性将打印自 UNIX 纪元(自 1970 年 1 月 1 日起)以来经过的秒数:
$ cat /sys/class/rtc/rtc0/since_epoch
1503931738
硬件时钟 ( hwclock )是用于访问 RTC 设备的工具。man hwclock
命令可能比本节讨论的任何内容都更有意义。也就是说,让我们编写一些命令,从系统时钟设置 hwclock RTC:
$ sudo ntpd -q # make sure system clock is set from network time
$ sudo hwclock --systohc # set rtc from the system clock
$ sudo hwclock --show # check rtc was set
Sat May 17 17:36:50 2017 -0.671045 seconds
前面的示例假设主机有一个网络连接,可以通过该网络连接访问 NTP 服务器。也可以手动设置系统时间:
$ sudo date -s '2017-08-28 17:14:00' '+%s' #set system clock manually
$ sudo hwclock --systohc #synchronize rtc chip on system time
如果不作为参数给出,hwclock
假设 RTC 设备文件是/dev/rtc
,这实际上是到真实 RTC 设备的符号链接:
$ ls -l /dev/rtc
lrwxrwxrwx 1 root root 4 août 27 17:50 /dev/rtc -> rtc0
本章向您介绍了 RTC 框架及其应用编程接口。其精简的功能和数据结构集使其成为最轻量级的框架,并且易于掌握。使用本章中描述的技能,您将能够为大多数现有的 RTC 芯片开发驱动,甚至更进一步,从用户空间处理此类设备,轻松设置日期和时间以及警报。下一章,脉宽调制驱动,与这一章没有什么共同之处,但却是嵌入式工程师的必读。