linux驱动移植-RTC驱动

----------------------------------------------------------------------------------------------------------------------------

内核版本: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
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Graceful_scenery

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值