linux笔记—rtc子系统

  • 正式开始让人崩溃的linux系列,希望自己能写完。
    先拿rtc开刀。

  • 这里我尽可能记录下思维的细节,而不是仅局限于代码,希望自己能领会Linux内核开发者的想法。

  • 内核版本:linux4.1

  • 需要了解:简略了解字符驱动 和 Linux设备驱动模型


RTC

  • rtc即real time clock,实时时钟。
  • rtc一般负责系统关机后计时,面对繁多的Linux RTC设备,内核干脆提供了一个rtc子系统,来支持所有的rtc设备。
  • 参考资料

rtc子系统

  • rtc设备本质上是一个字符设备,rtc子系统在字符设备的基础上抽象与硬件无关的部分,并在这个基础上拓展sysfs和proc文件系统下的访问。
  • 分析时候始终记住两点:
    • rtc子系统是为了让rtc设备驱动编写更为简单,与硬件无关部分已被抽离。
    • rtc子系统是基于字符设备而来的。

文件框架

  • rtc子系统的源码在 /drivers/rtc
    1.jpg
    删减了很多rtc-xxx.c的驱动,只留下了ds1307作为示例,这里看到实际上代码并不多。

  • 具体文件分析

    • rtc.h:定义与RTC有关的数据结构。

    • class.c:向内核注册RTC类,为底层驱动提供rtc_device_register与rtc_device_unregister接口用于RTC设备的注册/注销。初始化RTC设备结构、sysfs、proc。

    • Interface.c:提供用户程序与RTC的接口函数,其中包括ioctl命令。

    • rtc-dev.c:将RTC设备抽象为通用的字符设备,提供文件操作函数集。

    • rtc-sysfs.c:管理RTC设备的sysfs属性,获取RTC设备名、日期、时间等。

    • rtc-proc.c:管理RTC设备的procfs属性,提供中断状态和标志查询。

    • rtc-lib.c:提供RTC、Data和Time之间的转换函数。

    • rtc-xxx,c:RTC设备的实际驱动,此处以rtc-ds1307为例。

    • hctosys.c:开机时获取RTC时间。

  • 整个文件系统框架

  • RTC子系统具体可分为3层:

    • 用户层:RTC子系统向上层提供了接口,用户通过虚拟文件系统,间接调用RTC设备,具体有3种方式。
      • /dev/rtc RTC设备抽象而来的字符设备,常规文件操作集合。
      • /sys/class/rtc/rtcx 通过sysfs文件系统进行RTC操作,也是最常用的方式。
      • /proc/driver/rtc 通过proc文件系统获取RTC相关信息。
    • RTC核心层:与硬件无关,用于管理RTC设备注册/注销、提供上层文件操作的接口等。
    • RTC驱动:特定RTC设备的驱动程序,实现RTC核心层的回掉函数。编写RTC驱动需要按照RTC子系统的接口填写对应函数并建立映射即可。RTC核心层函数实现的过程和数量与特定硬件紧密相关。
  • 初看linux系统的人来说,这个图够头晕的了,但是呢,实际上没那么麻烦。由浅入深,一点一点来分析。

rtc子系统分析

rtc-dev

  • rtc子系统基于字符设备,字符设备对应的肯定是rtc-dev.c了,我们的分析由rtc-dev起步。

  • rtc-dev.c
    rtc-dev.c

  • 典型的字符设备,模块的初始化/卸载自然是 rtc_dev_init(void) 和 rtc_dev_exit(void)。
    设备接入,添加/删除设备 rtc_dev_add_device(struct rtc_device rtc) 和 e) void rtc_dev_del_device(struct rtc_devicertc)
    还有ioctl和open等函数,熟悉字符设备驱动的不用多说。

  • 追踪一下这两组函数在哪里调用的。

    • rtc_dev_init(void)对应在 class.c的rtc_init(void)函数
    • rtc_dev_add_device对应在class.c的 rtc_device_register()函数。
  • rtc_dev_init(void)对应实在系统初始化时使用,对应rtc_init(void)也是在系统初始化之后调用。

  • rtc_dev_add_device()是在驱动匹配后调用,rtc_device_register()也是在驱动匹配后调用。打开rtc-ds1307.c>-prob函数,能找到rtc_device_register()也就证实了这个思路。

  • 接下来转入class.c

class.c

  • linux驱动模型中class.c对应类的意思,是rtc类。
    每个class对应都有自己核心数据结果,对应rtc类就是rtc-device

  • rtc_device
    rtc_device代表 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
       struct rtc_device  {
      struct device dev;
      struct module *owner;
      int id; //RTC设备的次设备号
      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 fasync_struct *async_queue;
      struct rtc_task *irq_task;
      spinlock_t irq_task_lock;
      int irq_freq;
      int max_user_freq;
      #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
      };

    • 很长?很😵对不对?只要关注一点就行_

      1
      2
      3
      4
      5
      6
      7
      8
      9

      int id; //代表是那个rtc设备
      char name[RTC_DEVICE_NAME_SIZE]; //代表rtc设备的名称
      const struct rtc_class_ops *ops; //rtc操作函数集,需要驱动实现
      struct mutex ops_lock; //操作函数集的互斥锁

      struct cdev char_dev; //代表rtc字符设备,因为rtc就是个字符设备
      unsigned long flags; //rtc的状态标志,例如RTC_DEV_BUSY

    • 上文书说到,驱动程序的prob函数里面调用了rtc_device_register()这货的类型就是rtc_device。参加驱动程序怎样调用rtc_device_register(),与其他核心的基本结构不同的是,驱动程序以不是以rtc-device为参数注册设备到子系统,而是注册函数会返回一个rtc_deivce的结构给驱动。

  • rtc_class_ops
    这个是rtc_device的一部分。_

    • 数据结构

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      struct rtc_class_ops {
      int (*open)(struct device *); //打开设备时的回调函数,这个函数应该初始化硬件并申请资源
      void (*release)(struct device *); //这个函数是设备关闭时被调用的,应该注销申请的资源
      int (*ioctl)(struct device *, unsigned int, unsigned long); //ioctl函数,对想让RTC自己实现的命令应返回ENOIOCTLCMD
      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 *); //procfs接口
      int (*set_mmss)(struct device *, unsigned long secs); //将传入的参数secs转换为struct rtc_time然后调用set_time函数。程序员可以不实现这个函数,但 前提是定义好了read_time/set_time,因为RTC框架需要用这两个函数来实现这个功能。
      int (*irq_set_state)(struct device *, int enabled); //周期采样中断的开关,根据enabled的值来设置
      int (*irq_set_freq)(struct device *, int freq); //设置周期中断的频率
      int (*read_callback)(struct device *, int data); ///用户空间获得数据后会传入读取的数据,并用这个函数返回的数据更新数据。
      int (*alarm_irq_enable)(struct device *, unsigned int enabled); //alarm中断使能开关,根据enabled的值来设置
      int (*update_irq_enable)(struct device*, unsigned int enabled); //更新中断使能开关,根据enabled的值来设置
      };
    • 该结构体中函数大多数都是和rtc芯片的操作有关,需要驱动程序实现。
      所有RTC驱动都必须实现read_time、set_time函数,其他函数可选。

  • 参考其他的资料,class的分析如下
    class

  • static int __init rtc_init(void)

    • 调用class_create创建RTC类,创建/sys/class/rtc目录,初始化RTC类相关成员,向用户空间提供设备信息。
    • 调用rtc-dev.c实现的rtc_dev_init();动态分配RTC字符设备的设备号。
    • 调用rtc_sysfs_init(rtc_class);创建/sys/class/rtc下属性文件_
  • static void _exit rtc_exit(void)

    • 调用rtc-dev.c实现的rtc_dev_exit();注销设备号。
    • 调用class_destroy(rtc_class);注销/sys/class下的rtc目录
  • struct rtc_device *rtc_device_register(const char *name, struct device *dev,const struct rtc_class_ops *ops,struct module *owner)_

    • 申请一个idr整数ID管理机制结构体,并且初始化相关成员
    • 将设备dev关联sysfs下的rtc类
    • 初始化rtc结构体的信号量
    • 初始化rtc结构体中的中断
    • 设置rtc的名字
    • 初始化rtc字符设备的设备号,然后注册rtc设备,自动创建/dev/rtc(n)设备节点文件
    • 注册字符设备
    • 在/sys/rtc/目录下创建一个闹钟属性文件
    • 创建/proc/driver/rtc目录
  • void rtc_device_unregister(struct rtc_device *rtc)

    • 删除sysfs中的rtc设备,即删除/sys/class/rtc目录
    • 删除dev下的/dev/rtc(n)设备节点文件
    • 删除虚拟文件系统接口,即删除/proc/driver/rtc目录
    • 卸载字符设备
    • 清空rtc的操作函数指针rtc->ops
    • 释放rtc的device结构体_
  • static void rtc_device_release(struct device dev)

    • 卸载idr数字管理机制结构体
    • 释放rtc结构体的内存

Rtc子系统初始化

  • 上图
    初始化

  • 使用rtc子系统首先需要在内核编译选项中启用RTC子系统支持。

    • 必须启用Real Time Clock
    • 使用/dev下的设备文件对应开启CONFIG_RTC_INTF_DEV
    • 使用/proc下的接口对应开启CONFIG_RTC_INTF_PROC
    • 使用/sysfs下的接口对应开启CONFIG_RTC_INTF_SYSFS
  • _系统启动后,如配置启用rtc子系统,则会首先执行rtc_init_函数,创建rtc类、初始化相关成员、分配设备号等

  • 创建rtc类后,之后调用rtc_dev_init()动态分配rtc字符设备的设备号。之后调用rtc_sysfs_init()初始化/sys/class/rtc目录中的属性文件

Rtc设备注册

  • Rtc设备本质上属于字符设备,依附于系统内总线。一般来说cpu内部rtc依附于platform总线,外置rtc芯片则依附于通信方式对应总线。其过程与通用字符设备相似,rtc子系统在设备注册过程中附加了prob和sysfs相关的注册和初始化操作。

  • 上图
    Rtc设备注册

  • Rtc设备挂载后,相应总线会搜索匹配的驱动程序,驱动程序成功match后,进入驱动实现的probe函数,执行设备注册等操作。

  • 完成总线设备注册后,probe会跳转到rtc_device_register()函数,将设备注册到rtc子系统。

  • Rtc设备本质属于字符设备,会调用rtc_dev_prepare()函数,初始化字符设备,设置rtc相关的file operation函数集合。

  • 之后依次调用rtc_dev_add_device(rtc)、rtc_sysfs_add_device(rtc)、rtc_proc_add_device(rtc) ,进行注册字符设备、在/sys/rtc/目录下创建一个闹钟属性文件、创建/proc/driver/rtc目录等操作。

  • rtc_device_register()会将驱动实现的rtc_class_ops结构体与具体设备联系起来。

interface.c

  • 在rtc-proc.c、rtc_sysfs和ioctl命令中,所有的操作调用的都是interface.c提供的接口,这里以ioctl的一个例子说明整个调用的过程

  • 上图
    interface.c

  • 以icotl命令RTC_RD_TIME为例,说明命令调用的流程。

    • RTC_RD_TIME对应的是/dev下ioctl命令,调用被转发至/rtc-dev.c

    • rtc-dev.c->rtc_dev_ioctl(struct file *file,unsigned int cmd, unsigned long arg)函数中。RTC_RD_TIME对应的代码为err = rtc_read_time(rtc, &tm); rtc_read_time是interface.c文件提供的接口之一。

    • interface.c->rtc_read_time(struct rtc_device *rtc, struct rtc_time *tm)函数中对应rtc_class_ops转发代码为err = rtc->ops->read_time(rtc->dev.parent, tm);将操作转发至匹配的rtc设备。

    • 设备驱动这里以rtc-ds1307为例,但设备注册时通过mcp794xx_rtc_ops结构体将rtc_class_ops对应函数与驱动程序实现的函数绑定

      1
      2
      3
      4
      5
      6
      7
      static const struct rtc_class_ops mcp794xx_rtc_ops = {
      .read_time = ds1307_get_time,
      .set_time = ds1307_set_time,
      .read_alarm = mcp794xx_read_alarm,
      .set_alarm = mcp794xx_set_alarm,
      .alarm_irq_enable = mcp794xx_alarm_irq_enable,
      };
    • 最终执行转入ds1307.c-> ds1307_get_time函数,执行与硬件相关的操作。

rtc-sysfs.c

  • 由前半部分可知,/sys/class/rtc/是在rtc-init调用rtc_sysfs_init后生成。

    1
    2
    3
    4
    5

    void __init rtc_sysfs_init(struct class *rtc_class) {
    rtc_class->dev_groups = rtc_groups;
    }

  • 这里的rtc_groups是rtc-sysfs.c中定义了这样一个attribute函数指针数组:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    static struct attribute *rtc_attrs[] = {
    &dev_attr_name.attr,
    &dev_attr_date.attr,
    &dev_attr_time.attr,
    &dev_attr_since_epoch.attr,
    &dev_attr_max_user_freq.attr,
    &dev_attr_hctosys.attr,
    NULL,
    };
    ATTRIBUTE_GROUPS(rtc);
  • _在rtc_sysfs_init函数调用后绑定了sysfs节点操作函数的集合,使得设备匹配驱动程序后而生成对应的rtcn文件夹。

  • dev_attr_name和dev_attr_data由宏DEVICE_ATTR_RO和DEVICE_ATTR_RW生成,他们分别定义了只读的和可读可写的attribute节点。每个属性函数下都有DEVICE_ATTR_XX()宏声明,绑定到对应attribute节点。

rtc-proc.c

  • proc文件系统是软件创建的文件系统,内核通过他向外界导出信息,下面的每一个文件都绑定一个函数,当用户读取这个文件的时候,这个函数会向文件写入信息。

  • rtc-proc.c中初始化了file_operations结构体:

    1
    2
    3
    4
    5
    6
    static const struct file_operations rtc_proc_fops = {
    .open = rtc_proc_open,
    .read = seq_read,
    .llseek = seq_lseek,
    .release = rtc_proc_release,
    };
  • _RTC驱动在向RTC核心注册自己的时候,由注册函数rtc_device_resgister调用rtc_proc_add_device来实现proc接口的初始化,这个函数如下定义:

    1
    2
    3
    4
    5
    6

    void rtc_proc_add_device(struct rtc_device *rtc)
    {
    if (rtc->id == 0)
    proc_create_data("driver/rtc", 0, NULL, &rtc_proc_fops, rtc);
    }

    主要是完成创建文件节点,并将文件的操作函数与节点联系起来。调用这个函数后,在/proc/driver目录下就会有一个文件rtc

rtc设备访问

  • rtc子系统最多可以支持16个rtc设备,多个rtc设备会在/dev/和 /sys/class/rtc/下生成rtc0、rtc1…等不同节点(下文以rtcn代称)。而系统启动时会选择一个rtc设备读取计时,在/dev下有rtc文件,rtc文件指向系统选择的rtc设备对应的rtcn(一般为rtc0)。
  • 用户层访问rtc设备有3种途径:
    • /dev/rtcn open等字符设备操作和ioctl命令。
    • /sys/class/rtc/rtcn sysfs 属性,一些属性是只读。
    • /proc/driver/rtc 第一个rtc会占用procfs接口,在procfs下暴露更多信息。

/dev

  • 在/dev下用户可以通过两种方式访问rtc设备,第一个是通过字符设备定义的open、read等函数(需要驱动程序实现)、另一个是通过定义的ioctl命令。第一种方式是直接打开rtc-dev.c定义的open等函数,在open等中直接调用驱动程序实现的函数。通过ioctl命令访问则是将操作转发到了interface.c定义的接口,间接调用驱动程序实现的函数。

  • ioctl()函数访问/dev下的设备。以下是典型函数:_

    • ioctl(fd,RTC_ALM_SET, &rtc_tm);
      设置alarm中断的触发时刻,不超过24小时。第三个参数为structrtc_time结构体,读取时会忽略年月日信息。alarm中断与wakeupalarm中断只能同时使用1个,以最后一次设定为准。

    • ioctl(fd,RTC_ALM_READ, &rtc_tm)
      读取alarm中断的触发时刻。

    • ioctl(fd,RTC_WKALM_SET, &alarm);
      设置wakeupalarm中断的触发时刻, wakeupalarm中断的触发时刻可以在未来的任意时刻。alarm中断与wakeupalarm中断只能同时使用1个,以最后一次设定为准。

    • ioctl(fd,RTC_WKALM_RD, &alarm);
      读取wakeupalarm中断的触发时刻。

    • ioctl(fd,RTC_IRQP_SET, tmp);
      设置周期中断的频率,tmp的值必须是2的幂,非Root用户无法使用64HZ以上的周期中断。

    • ioctl(fd,RTC_IRQP_READ, &tmp);
      读取周期中断的频率。

    • ioctl(fd,RTC_SET_TIME, &rtc_tm)
      更新RTC芯片的当前时间。

    • ioctl(fd,RTC_RD_TIME, &rtc_tm);
      读取RTC硬件中的当前时间。

  • 以open操作为例,在用户层对/dev下设备执行open会被转发至rtc_dev_open(struct inode *inode, struct file *file)函数,通过err= ops->open ? ops->open(rtc->dev.parent) : 0;判断驱动程序是否通过连接的rtc_class_ops结构体实现了open函数,驱动程序实现了open函数,则将open操作转发至驱动程序。

/sys

  • /sys/class/rtc/rtcn下面的sysfs接口提供了操作rtc属性的方法,所有的日期时间都是墙上时间,而不是系统时间。
    • date: RTC提供的日期
    • hctosys: 如果在内核配置选项中配置了CONFIG_RTC_HCTOSYS,RTC会在系统启动的时候提供系统时间,这种情况下这个位就是1,否则为0
    • max_user_freq: 非特权用户可以从RTC得到的最大中断频
    • name: RTC的名字,与sysfs目录相关
    • since_epoch: 从纪元开始所经历的秒数
    • time: RTC提供的时间
    • wakealarm: 唤醒时间的时间事件。 这是一种单次的唤醒事件,所以如果还需要唤醒,在唤醒发生后必须复位。这个域的数据结构或者是从纪元开始经历的妙数,或者是相对的秒数

/proc

  • /proc/driver/rtc下只对应第一个rtc设备,与sysfs下相比,该设备暴露更多信息
  • 对应截图
    /proc

RTC子系统测试

Hwclock命令或使用测试文件。

  • Hwclock命令可以执行最简单的RTC测试。常用命令示例如下
    • hwclock #查看RTC时间
    • hwclock -set -date=”07/07/17 10:10” #设置硬件RTC时间(月/日/年 时:分:秒)
    • hwclock -w #系统时间同步至RTC
    • hwclock -s #同步RTC到系统时间
  • Linux内核提供了RTC子系统的测试示例文件,位于tools/testing/selftests/timers/rtctest.c,包含了基于ioctl命令的完整测试。