Linux驱动开发九.内核定时器使用——1.定时器底层驱动

定时器和中断是我们最常用到的功能,在前面裸机开发的时候我们通过EPIT、GPT等定时器实现定时功能,那种算硬件定时器。今天我们来看下如何在Linux内核里实现软件定时功能的。

Linux内核时间管理

Linux系统在运行的时候有大量的函数需要时间管理,比如调度程序、延时程序等。并且我们在驱动开发的时候很要用各种定时器。硬件系统在系统运行时会周期性产生中断,系统使用定时中断来提供时钟源。时钟源的频率可以设置,设置好以后就会产生定时中断,系统通过这个定时中断来定时。中断周期产生的频率叫做系统频率(节拍率tick rate)。系统频率在编译内核的时候可以根据需求设置(设置路径为Kernel Features--->Timer frequency)

 

默认情况下系统节拍率是100HZ。如果选择较高的节拍率会提高系统时间精度,但是会导致中断产生的频繁,会产生不必要的开销。所以我们需要根据我们的实际需求,选择合适的节拍率。节拍率在内核中提供了个宏供我们使用:HZ

 

系统节拍数

Linux内核中有个全局变量jiffies,用来记录系统自启动以来的运行的节拍数,系统在启动时会将jiffies初始化为0,变量会根据节拍率自增。jiffies的定义在内核文件include/linux/jiffies.h中

extern u64 __jiffy_data jiffies_64;
extern unsigned long volatile __jiffy_data jiffies;

可以从数据类型看出来,其中第一个是一个64位的整形变量,第二个是个32为的长整型变量。所以第一个是供64位系统使用的,并且为了兼容不同的硬件,jiffies是jiffies_64的低32位。数据结构是这样的

 

 

 当我们访问变量jiffies的时候,其实是访问了jiffies_64的低32位。所以不管是32位的系统还是64位的系统,我们都可以使用jiffies来获取值。

jiffies绕回

我们使用了一个全局变量来描述系统运行的时间,既然这个变量是个整形数据,那么必然存在一个溢出的可能性,如果系统频率是最大值1000Hz,那么每一秒jiffies就会增加1000。32位的unsigned数据最大值为0xFFFFFFFF=4294967295,那么48.7天这个jiffies就溢出了。这个现象叫绕回。但是对于64位系统来说,这个值就足够我们使用了(粗略算一下有584942417355年,快6亿年怎么都够用了),当绕回现象发生后,jiffies的值会重新回到0。

Linux内核给我们提供了一组函数来做时间比较,比如一个任务的执行周期是10s,那么就可以把任务开始的jiffies和10s对应的的jiffies值相加和当前的jiffies比较,如果发生绕回,那么当前的jiffies由于回到0重新计算值会远小于计算出的值。这时候做相应的处理就行了。内核给我们提供的API函数如下

函数 描述
time_after(unkown, known) 两个参数都为jiffies值,一般情况下
unkown 通常为当前系统jiffies,known 通常是需要对比的值。
time_before(unkown, known)
time_after_eq(unkown, known)
time_before_eq(unkown, known)

比如第一个time_after,如果unknown的值超过known,返回值久违真,否则为假,内核中给了对应的函数说明

 

 

 并且上面几个函数可以用来判定是否超过指定时长,用来判定是否超时。在计算known的值时应该是指定的时间*系统节拍率。比如我们需要程序在10S内结束,就可以这样使用

unsigned long time_out = jiffies+10*HZ
if(time_after(jiffies,timeout)){
    /*超时发生*/
}
else{
    /*超时未发生*/
}  

为了方便我们使用,Linux内核还为我们提供了一组函数用来将jiffies和ms、us和ns之间相互转化

函数 描述
int jiffies_to_msecs(const unsigned long j) 将jiffies值换算为对应的毫秒值
int jiffies_to_usecs(const unsigned long j) 将jiffies值换算为对应的微秒值
u64 jiffies_to_nsecs(const unsigned long j) 将jiffies值换算为对应的纳秒值
long msecs_to_jiffies(const unsigned int m) 将指定的毫秒值换算为jiffies值
long usecs_to_jiffies(const unsigned int u) 将指定的微秒值换算为jiffies值
unsigned long nsecs_to_jiffies(u64 n) 将指定的纳秒值换算为jiffies值

内核定时器

 知道了Linux的内核时间管理,我们就可以使用超时来实现定时功能了。当然这样操作起来比较麻烦,所以Linux为我们提供了一个软件定时器。和裸机的PIT等硬件定时器不同的是,软件定时器不用设置一堆寄存器然后初始化,只要提供一个超时时间还有定时器需要运行的函数就可以了。

Linux在内核中通过一个结构体timer_list来描述一个内核定时器。这个定时器在include/linux/timer.h中

struct timer_list {
    /*
     * All fields that change during normal runtime grouped to the
     * same cacheline
     */
    struct list_head entry;
    unsigned long expires;
    struct tvec_base *base;

    void (*function)(unsigned long);
    unsigned long data;

    int slack;

#ifdef CONFIG_TIMER_STATS
    int start_pid;
    void *start_site;
    char start_comm[16];
#endif
#ifdef CONFIG_LOCKDEP
    struct lockdep_map lockdep_map;
#endif
};

我们主要用到的成员就是加粗的几个

expiers,就是超时时间,但注意这个参数的单位是节拍数。需要在使用的过程中将指定的时间换算成对应的节拍数。

function是定时器的回调函数。注意是个指针,

data,function绑定函数的参数

常用API

timer.h文件中还给我们定义了一些API来对定时器进行初始化或设置

extern void add_timer_on(struct timer_list *timer, int cpu);
extern int del_timer(struct timer_list * timer);
extern int mod_timer(struct timer_list *timer, unsigned long expires);
extern int mod_timer_pending(struct timer_list *timer, unsigned long expires);
extern int mod_timer_pinned(struct timer_list *timer, unsigned long expires);

下面一个个看下

init_timer

定时器初始化,当我们声明了一个定时器timer_list变量后要通过该函数初始化

struct timer_list timer;
init_timer(&timer);

函数无返回值,直接调用即可

add_timer

add_timer用于向Linux内核中注册一个新的定时器,该定时器一旦被注册,定时器就会开始运行。从这个函数范例开始,里面的timer都是在上一个过程声明的定时器变量。

add_timer(&timer);

函数无返回值。

del_timer

del_timer用于删除指定的定时器,不管该定时器是否被激活,都可以被删除。所以在调用该函数是要等待定时器处理的函数执行完毕,特别是多核处理器。

ret = del_timer(&timer);

函数返回值为int,1时表示定时器已经激活,0时表示定时器还没被激活

del_timer_sync

del_timer_sync是del_timer的同步版,会等待所有处理器将定时器处理的函数都是用完成后再删除,要注意的是该函数不能用中断上下文中。返回值也和del_timer一样。

mod_timer

用于修改定时器的定时值,如果定时器还未被激活,该函数会激活定时器

unsigned long time_out;
ret = mod_timer(&timer,time_out);

参数time_out为修改后的定时时间,返回值为1时表示在调用该函数前定时器未被激活,1时表示调用前被激活。由于Linux内核定时器只运行一次而不是周期运行的,所以当定时器需要周期运行时该函数会经常被使用。

下面结合代码来演示下这个定时器是怎么使用的

驱动构成

下面结合部分代码看一下内核定时器是怎么使用的,定时器对蜂鸣器进行操作,实现定时通断效果

设备结构体

首先,要对设备结构体进行修改

 1 struct new_dev
 2 {
 3     dev_t dev_id;
 4     int major;
 5     int minor;
 6     struct class *class;
 7     struct device *device;
 8     struct cdev cdev;
 9     struct device_node *dev_nd;
10     int gpio;
11     struct timer_list timer;    //定时器
12     atomic_t timer_per;         //定时器周期
13 };

第11行的timer就是定义的定时器结构体

第12行的timer_per是定时器的周期,注意这里用了个原子整形变量,因为考虑到可能存在数据读写冲突,这里一定要把数据保护起来防止发生竞争现象。

定时器初始化

定时器初始化在设备注册绑定的函数内(设备初始化)。

1 init_timer(&new_dev.timer);                                     //定时器初始化
2 atomic_set(&new_dev.timer_per,value);                           //读取原子变量获取定时周期值(单位:ms)
3 new_dev.timer.expires = jiffies + msecs_to_jiffies(value);      //设置定时周期
4 new_dev.timer.function = timer_func;                            //绑定定时回调函数
5 new_dev.timer.data = (unsigned long)&new_dev;                   //回调函数参数
6 add_timer(&new_dev.timer);                                      //添加并启动定时器

这里要注意一点,代码第6行后定时器会直接运行,因为我们回调函数是实现外设的控制,这句代码一定要放到外设初始化后面(对这个过程来说就是GPIO子系统初始化)。

其次就是我们将定时器周期设置的单位为毫秒,在整个设备初始化的过程开始将原子变量timer_per初始化设置为500,在这里需要重新读取一下值并保存到变量value中。然后计算实际周期对应的jiffies(这里我有个疑问,就是设置expires和绑定function之间要不要不要放太多代码,要不是占用周期过长可能会影响定时周期精度)。还有要注意的地方是这个周期不是外设运行的周期(高电平翻转总时长),而是定时器的溢出周期,设置为500ms时意思是到500ms后执行回调函数,如果只是翻转输出电平的话实际输出周期就成1s了。

在第5行我们传递参数时,由于要求传递的参数形式为长整形,所以使用了强制类型转换,在回调函数中使用的时候要将其恢复回来。

回调函数

回调函数使用时要注意传递参数的形式转换

1 timer_func(unsigned long arg){
2     static int stat = 1;
3     int value = 0;
4     struct new_dev *dev = (struct new_dev*)arg;
5     stat = !stat;
6     gpio_set_value(dev->gpio,stat);
7     value = atomic_read(&dev->timer_per);
8     mod_timer(&dev->timer,jiffies + msecs_to_jiffies(value));
9 }

在上面初始化定时器的代码的第5行,我们将参数强制转换为long整形数据,所以这里使用时要再恢复成new_dev结构体。

还有就是第8行,因为在前面说过,Linux内核中定时器只运行1次,所以如果需要重复循环定时的话需要使用mod_timer将定时器重新激活。其他就没什么说的了。

最后就是放出来最终代码

/**
 * @file timer.c
 * @author your name (you@domain.com)
 * @brief 定时器驱动测试程序
 * @version 0.1
 * @date 2022-07-14
 * 
 * @copyright Copyright (c) 2022
 * 
 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/types.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_irq.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>

#include <linux/ioctl.h>
#define DEVICE_CNT      1
#define DEVICE_NAME    "time"


#define CMD_CLOSE       _IO(0xEF,1)
#define CMD_OPEN        _IO(0xEF,2)
#define CMD_PERIOD      _IOW(0xEF,3,int)  


struct new_dev
{
    dev_t dev_id;
    int major;
    int minor;
    struct class *class;
    struct device *device;
    struct cdev cdev;
    struct device_node *dev_nd;
    int gpio;
    struct timer_list timer;    //定时器
    atomic_t timer_per;         //定时器周期
};

struct new_dev new_dev;

static int new_dev_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &new_dev;     /* 设置私有数据 */
    return 0;
}



/**
 * @brief 文件操作集合
 * 
 */
static const struct file_operations key_fops = {
    .owner = THIS_MODULE,
    .open =  new_dev_open,
};



/**
 * @brief 外设设备GPIO系统初始化
 * 
 * @param dev 设备结构体
 * @return int 
 */
int beep_init(struct new_dev *dev)
{
    int ret = 0 ;

    //从设备树搜索设备节点
    dev->dev_nd = of_find_node_by_path("/beep");
    if(dev->dev_nd == NULL){
        printk("no device found\r\n");
        ret = -EINVAL;
        goto fail_nd;
    }

    //获取beep对应GPIO
    dev->gpio = of_get_named_gpio(dev->dev_nd,"beep-gpios",0);
    printk("beep_gpio=%d\r\n",dev->gpio);
    if(dev->gpio < 0){
        printk("no GPIO found!\r\n");
        ret = -EINVAL;     //errno-base.h中定义的异常数值到34,这里从100开始使用防止冲突
        goto fail_gpio;
    }

    //请求GPIO
    ret = gpio_request(dev->gpio,"beep-gpio");
    if(ret){
        printk("gpio request err\r\n");
        ret = -EBUSY;
        goto fail_request;}
    

    ret = gpio_direction_output(dev->gpio,1);
    if(ret < 0){
        ret = -EINVAL;
        goto fail_gpioset;
    }

    return 0;
    fail_gpioset:
        gpio_free(dev->gpio);
    fail_request:
    fail_gpio:
    fail_nd:
        return ret;
}

/**
 * @brief Construct a new timer func object
 * 定时器回调函数
 * @param arg 
 */
timer_func(unsigned long arg){
    static int stat = 1;
    int value = 0;
    struct new_dev *dev = (struct new_dev*)arg;
    stat = !stat;
    gpio_set_value(dev->gpio,stat);
    value = atomic_read(&dev->timer_per);
    mod_timer(&dev->timer,jiffies + msecs_to_jiffies(value));
}

/**
 * @brief 设备初始化
 * 
 * @return int 
 */
static int __init timer_init(void){
    int ret = 0; 
    unsigned int value = 500;

    //申请设备号
    new_dev.major = 0;
    if(new_dev.major){
        //手动指定设备号,使用指定的设备号
        new_dev.dev_id = MKDEV(new_dev.major,0);
        ret = register_chrdev_region(new_dev.dev_id,DEVICE_CNT,DEVICE_NAME);
    }
    else{
        //设备号未指定,申请设备号
        ret = alloc_chrdev_region(&new_dev.dev_id,0,DEVICE_CNT,DEVICE_NAME);
        new_dev.major = MAJOR(new_dev.dev_id);
        new_dev.minor = MINOR(new_dev.dev_id);
    }
    printk("dev id geted!\r\n");

    if(ret<0){
        //设备号申请异常,跳转至异常处理
        goto faile_devid;
    }

    //字符设备cdev初始化
    new_dev.cdev.owner = THIS_MODULE;

    cdev_init(&new_dev.cdev,&key_fops);                 //文件操作集合映射

    ret = cdev_add(&new_dev.cdev,new_dev.dev_id,DEVICE_CNT);
    if(ret<0){
        //cdev初始化异常,跳转至异常处理
        goto fail_cdev;
    }

    printk("chr dev inited!\r\n");


    //自动创建设备节点
    new_dev.class = class_create(THIS_MODULE,DEVICE_NAME);
    if(IS_ERR(new_dev.class)){
        //class创建异常处理
        printk("class err!\r\n");
        ret = PTR_ERR(new_dev.class);
        goto fail_class;
    }
    printk("dev class created\r\n");
    new_dev.device = device_create(new_dev.class,NULL,new_dev.dev_id,NULL,DEVICE_NAME);
    if(IS_ERR(new_dev.device)){
        //设备创建异常处理
        printk("device err!\r\n");
        ret = PTR_ERR(new_dev.device);
        goto fail_device;
    }
    printk("device created!\r\n");


    //gpio外设初始化
    ret = beep_init(&new_dev);
    if(ret<0){
        printk("gpio init err\r\n");
        goto fail_gpioinit;
    }


    //定时器初始化
    init_timer(&new_dev.timer);
    atomic_set(&new_dev.timer_per,value);
    new_dev.timer.expires = jiffies + msecs_to_jiffies(value);
    new_dev.timer.function = timer_func;
    new_dev.timer.data = (unsigned long)&new_dev;
    add_timer(&new_dev.timer);

    return ret;

fail_gpioinit:

fail_device:
    //device创建失败,意味着class创建成功,应该将class销毁
    printk("device create err,class destroyed\r\n");
    class_destroy(new_dev.class);
fail_class:
    //类创建失败,意味着设备应该已经创建成功,此刻应将其释放掉
    printk("class create err,cdev del\r\n");
    cdev_del(&new_dev.cdev);
fail_cdev:
    //cdev初始化异常,意味着设备号已经申请完成,应将其释放
    printk("cdev init err,chrdev register\r\n");
    unregister_chrdev_region(new_dev.dev_id,DEVICE_CNT);
faile_devid:
    //设备号申请异常,由于是第一步操作,不需要进行其他处理
    printk("dev id err\r\n");
    return ret;
}


static void __exit timer_exit(void)
{

    gpio_set_value(new_dev.gpio,1);
    del_timer(&new_dev.timer);

    cdev_del(&new_dev.cdev);
    unregister_chrdev_region(new_dev.dev_id,DEVICE_CNT);

    device_destroy(new_dev.class,new_dev.dev_id);
    class_destroy(new_dev.class);

    gpio_free(new_dev.gpio);

}
module_init(timer_init);
module_exit(timer_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ZeqiZ");
View Code

编译完成后加载ko模块,可以发现挂载完成后蜂鸣器开始定时工作。

现在只是有了最基础的驱动,后面一张我们来做一个APP程序来调用这个驱动。

posted @ 2022-07-14 20:39  银色的音色  阅读(1341)  评论(0编辑  收藏  举报