RTC驱动

实时时钟(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, &regs, 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, &regs, 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必须设置的时间。这取决于驱动程序以正确的方式提取每个值(如果需要,使用bin2bcd()),并将其写入适当的寄存器中。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。

posted @ 2023-02-15 19:44  闹闹爸爸  阅读(146)  评论(0编辑  收藏  举报