实时时钟(RTC)是用于跟踪非易失性存储器中的绝对时间的设备,RTC设备可以位于处理器内部,也可以通过I2C或SPI总线连接到外部。
你可以使用RTC进行以下操作:
- 读取和设置绝对时钟,并在时钟更新期间产生中断
- 生成周期性中断
- 设置时钟(alarms)
RTC和系统时钟有不同的用途。前者是硬件时钟,以稳定的方式维护绝对时间和日期,而后者是由内核维护的软件时钟,用于实现gettimeofday(2)和time(2)个系统调用,设置文件上的时间戳,等等。系统时钟从起点(定义为POSIX epoch)报告秒和微秒:
1970-01-01 00:00:00 +0000 (UTC)。
在本博中,我们将讨论以下主题:
- 介绍RTC框架API
- 描述驱动程序的架构,以及一个虚拟驱动程序示例
- 处理时钟(alarms)
- 通过sysfs接口或使用hwclock工具,从用户空间管理RTC设备
RTC框架数据结构
Linux系统中RTC框架使用的主要数据结构有三种。它们是struct rtc_time、struct rtc_device和struct rtc_class_ops结构体。第一个是表示给定日期和时间的不透明结构;第二个结构代表物理RTC器件;最后一个代表一组由驱动程序公开的操作,并被RTC核心用于读取/更新设备的data/time/alarm。
从驱动程序中提取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,它表示内核中的chip:
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设备公开的一组操作(如read/set time/alarm),由核心管理或从用户空间管理。
- ops_lock: 这是内核内部使用的互斥量,用于保护ops函数调用。
- cdev: 这是与这个RTC相关联的字符设备,/dev/rtc<id>。
下一个重要的结构体是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结构体中嵌入的参数相同。这意味着在这些回调中,你可以在任何给定的时间访问RTC设备本身,使用to_rtc_device()宏,它构建在container_of()宏之上:
#define to_rtc_device(d) container_of(d, struct rtc_device, dev)
当从用户空间在设备上调用open()、close()或read()函数时,内核内部会调用rtc_class_ops结构体的open()、release()和read_callback()。
read_time()是一个驱动程序函数,从设备读取时间并填充struct rtc_time输出参数。如果函数成功,则返回0,否则返回负的错误代码。
set_time()是一个驱动程序函数,根据作为输入参数的struct rtc_time结构来更新设备的时间。返回值类似于read_time函数。
如果设备支持alarm功能,驱动程序应该提供read_alarm()和set_alarm()来读取/设置设备上的alarm。struct rtc_wkalrm将在本文章后面描述。还应该提供alarm_irq_enable()来启用alarm。
RTC API
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驱动程序所能提供的最起码的功能。当读取时,read回调函数被赋予一个指向已分配/归零struct rtc_time结构体的指针,驱动程序必须填充该结构体。因此,RTC几乎总是以BCD码存储/恢复时间,其中每个四位(4位序列)表示0到9之间的数字(而不是0到15之间)。内核提供了两个宏bcd2bin()和bin2bcd(),分别用于从BCD编码转换为十进制或从十进制转换为BCD。您应该注意的下一件事是rtc_time字段,它有一些边界要求,并且必须在其中进行一些转换。数据以BCD方式从设备读取,然后应该使用bcd2bin()进行转换。
由于结构体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 subtract 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 subtract 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纪元不同于POSIX纪元,POSIX纪元仅用于系统时钟。如果年份(根据RTC的纪元和year寄存器)小于1970年,则假定是100年后;也就是在2000年到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_DESCRIPTION("Fake RTC driver description");
具体的rtc驱动程序参见内核源码的drivers/rtc目录,例如rtc-bq32k.c、rtc-ds1343.c等等。
玩转alarms
RTC alarms是可编程事件,由设备在给定时间触发。RTC alarm表示为struct rtc_wkalrm结构体的实例:
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应该发生的时间,以及alarm_irq_enable(),这是一个用于启用/禁用alarm的函数。当set_alarm()函数被调用时,它被作为一个输入参数给出,一个指向struct rtc_wkalrm的指针,该结构体的.time字段包含了alarm必须设置的时间。这取决于驱动程序以正确的方式提取每个值(如果需要,使用bin2dcb()),并将其写入适当的寄存器中。rtc_wkalrm.enabled表示alarm设置完成后是否立即启用。如果为true,驱动器必须启用芯片中的alarm。read_alarm()也是如此,它提供了一个指向struct rtc_wkalrm的指针,但这次是作为输出参数。驱动程序必须用从设备读取数据填充此结构。
{read | set}_alarm()和{read | set}_time()函数的行为相同,只是每对函数从设备中的不同寄存器集读取/存储数据。
在向系统上报alarm事件之前,RTC芯片必须连接到SoC的IRQ线。alarm发生时依靠RTC的INT线驱动为低。根据制造商的不同,在状态寄存器被读取或特殊位被清除之前,该中断线一直保持为低:
此时,我们可以使用通用的IRQ API,例如request_threaded_irq(),以便注册alarm IRQ的处理程序。在IRQ处理程序中,使用rtc_update_irq()函数将RTC IRQ事件通知内核是很重要的:
void rtc_update_irq(struct rtc_device *rtc, unsigned long num, unsigned long events)
- rtc: 产生中断的RTC设备
- num: 表示报告了多少个irq(通常是一个)
- 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 */
该函数(rtc_update_irq())可以从任何上下文(原子的或非原子的)调用。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; }
请记住,具有alarm功能的RTC设备可以用作唤醒源。也就是说,当alarm触发时,系统可以从suspend模式中被唤醒。此功能依赖于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设备如何改善你的系统的概述。驱动程序/rtc/rtcds1343.c可以帮助实现这些功能。让我们通过为SPI foo RTC设备编写一个伪探测函数来把所有东西放在一起:
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;
/* initialize 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; }
RTC和用户空间
在Linux系统上,为了从用户空间正确地管理RTC,需要考虑两个内核选项。它们是CONFIG_RTC_HCTOSYS和CONFIG_RTC_HCTOSYS_DEVICE。
CONFIG_RTC_HCTOSYS包含内核构建过程中的drivers/rtc/hctosys.c代码文件,用于设置启动和恢复时从rtc开始的系统时间。一旦该选项被启用,系统时间将使用从指定的RTC设备读取的值来设置。RTC设备应该在CONFIG_RTC_HCTOSYS_DEVICE中指定:
CONFIG_RTC_HCTOSYS=y CONFIG_RTC_HCTOSYS_DEVICE="rtc0"
在上面的示例中,我们告诉内核从RTC设置系统时间,并指定要使用的RTC设备为rtc0。
sysfs接口
负责在sysfs中实例化RTC属性的内核代码定义在内核源代码树的drivers/rtc/rtc-sysfs.c中。一旦注册,RTC设备将在/sys/class/rtc目录下创建一个rtc<id>目录。该目录包含一组只读属性,其中最重要的是:
- date: 该文件打印RTC接口的当前日期:
$ cat /sys/class/rtc/rtc0/date 2023-02-15
- time: 打印RTC的当前时间:
$ cat /sys/class/rtc/rtc0/time 19:10:20
- hctosys: 该属性表示该RTC设备是否是CONFIG_RTC_HCTOSYS_DEVICE中指定的RTC设备,这意味着该RTC用于设置系统启动和系统恢复时的系统时间。读出的值,1为真,0为假:
$ cat /sys/class/rtc/rtc0/hctosys 1
- dev: 这个属性显示了设备的主设备号和次设备号。读作major:minor:
$ cat /sys/class/rtc/rtc0/dev 251:0
- since_epoch:该属性将打印自UNIX纪元(自1970年1月1日以来)经过的秒数:
$ cat /sys/class/rtc/rtc0/since_epoch 1583171645
hwclock实用程序
硬件时钟(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 Wed Feb 15 11:23:40 2023 0.000000 seconds
假设主机具有可以访问NTP服务器的网络连接。也可以手动设置系统时间:
$ sudo date -s '2023-07-27 12:28: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 19:20 /dev/rtc -> rtc0
以上介绍了RTC框架及其API。RTC框架减少了函数和数据结构集,使其成为最轻量级的框架,易于掌握。使用以上描述的技能,你将能够为大多数现有的RTC芯片开发一个驱动程序。你还可以从用户空间处理此类设备,轻松设置日期和时间以及alarms。
标签:RTC,rtc,tm,time,device,驱动,struct From: https://www.cnblogs.com/wanglouxiaozi/p/17124237.html