i.MX6ULL驱动开发 | 19 - Linux内核定时器的编程方法与使用示例

Posted Mculover666

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了i.MX6ULL驱动开发 | 19 - Linux内核定时器的编程方法与使用示例相关的知识,希望对你有一定的参考价值。

本系列文章所编写的驱动源码仓库,欢迎Star~
https://github.com/Mculover666/linux_driver_study

一、Linux内核定时器编程

Linux内核中,在时钟中断发生后会唤醒 TIMER_SOFTIRQ 软中断,运行当前处理器上到期的所有定时器。

在linux设备驱动编程中,可以利用Linux内核中提供的一组函数或数据结构来完成定时器触发工作或者完成周期性的事务,并且不用关心具体的软件定时器究竟对应怎样的内核和硬件行为

1. timer_list

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
;

当定时器到期后,function成员将被执行,data成员是传入回调函数的参数,expires成员表示定时器超时时间,单位是节拍数。

2. 初始化定时器

#define init_timer(timer)						\\
	__init_timer((timer), 0)

该函数会初始化 timer_list 的entry的next为NULL,并给base指针赋值。

3. 增加定时器

/**
 * add_timer - start a timer
 * @timer: the timer to be added
 *
 * The kernel will do a ->function(->data) callback from the
 * timer interrupt at the ->expires point in the future. The
 * current time is 'jiffies'.
 *
 * The timer's ->expires, ->function (and if the handler uses it, ->data)
 * fields must be set prior calling this function.
 *
 * Timers with an ->expires field in the past will be executed in the next
 * timer tick.
 */
void add_timer(struct timer_list *timer);

该函数用于向Linux内核注册定时器,将定时器加入到内核动态定时器链表中。注册定时器之后,定时器就会开始运行。

4. 删除定时器

/**
 * del_timer - deactive a timer.
 * @timer: the timer to be deactivated
 *
 * del_timer() deactivates a timer - this works on both active and inactive
 * timers.
 *
 * The function returns whether it has deactivated a pending timer or not.
 * (ie. del_timer() of an inactive timer returns 0, del_timer() of an
 * active timer returns 1.)
 */
int del_timer(struct timer_list *timer);

该函数用于删除一个定时器。

除此之外,还有一个函数也用来删除一个定时器,但会同步等待其被处理完,所以该API的调用不能发生在中断上下文中。

5. 修改定时器的超时时间

/**
 * mod_timer - modify a timer's timeout
 * @timer: the timer to be modified
 * @expires: new timeout in jiffies
 *
 * mod_timer() is a more efficient way to update the expire field of an
 * active timer (if the timer is inactive it will be activated)
 *
 * mod_timer(timer, expires) is equivalent to:
 *
 *     del_timer(timer); timer->expires = expires; add_timer(timer);
 *
 * Note that if there are multiple unserialized concurrent users of the
 * same timer, then mod_timer() is the only safe way to modify the timeout,
 * since add_timer() cannot modify an already running timer.
 *
 * The function returns whether it has modified a pending timer or not.
 * (ie. mod_timer() of an inactive timer returns 0, mod_timer() of an
 * active timer returns 1.)
 */
int mod_timer(struct timer_list *timer, unsigned long expires);

6. 关于定时器的超时时间

定时器的超时时间往往是在目前 jiffies 的基础上添加一个时延,若为HZ,则表示延迟1s。

xxx_timer.expires = jiffies+delay

在定时器处理函数中,在完成相应的工作后,往往会延后 expires 并将定时器再次添加到内核定时器链表中,以便定时器能再次被触发。

二、使用示例——秒字符设备

编写一个second字符设备,在被打开的时候初始化一个定时器并将其添加到内核中,每秒输出一次当前的jiffies。

1. 编写基本模块

#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>

static int __init second_module_init(void)

    return 0;


static void __exit second_module_exit(void)




module_init(second_module_init);
module_exit(second_module_exit);

MODULE_AUTHOR("Mculover666");
MODULE_LICENSE("GPL");

2. 编写字符设备驱动框架

引入头文件:

#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>

封装全局变量:

struct second_dev 
    dev_t dev;                  /*!< 设备号             */
    struct cdev *cdev;          /*!< cdev对象           */
    struct class *class;        /*!< 设备类             */
    struct device *device0;     /*!< 设备节点           */
;

static struct second_dev second;

编写字符设备驱动框架:

static int second_open(struct inode *inode, struct file *fp)

    return 0;


static int second_read(struct file *fp, char __user *buf, size_t size, loff_t *off)

    return 0;


static int second_write(struct file *fp, const char __user *buf, size_t size, loff_t *off)

    return 0;


static int second_release(struct inode *inode, struct file *fp)

    return 0;


static struct file_operations second_fops = 
    .owner = THIS_MODULE,
    .open = second_open,
    .read = second_read,
    .write = second_write,
    .release = second_release,
;

static int __init second_module_init(void)

    int ret;

    //分配cdev设备号
    ret = alloc_chrdev_region(&second.dev, 0, 1, "second");
    if (ret != 0) 
        printk("alloc_chrdev_region fail!");
        return -1;
    

    //初始化cdev
    second.cdev = cdev_alloc();
    if (!second.cdev) 
        printk("cdev_alloc fail!");
        return -1;
    

    //设置fop操作函数
    second.cdev->owner = THIS_MODULE;
    second.cdev->ops = &second_fops;

    //注册cdev
    cdev_add(second.cdev, second.dev, 1);

    // 创建设备类
    second.class = class_create(THIS_MODULE, "second_class");
    if (!second.class) 
        printk("class_create fail!");
        return -1;
    

    //创建设备节点
    second.device0 = device_create(second.class, NULL, second.dev, NULL, "second0");
    if (IS_ERR(second.device0)) 
        printk("device_create device0 fail!");
        return -1;
    

    return 0;


static void __exit second_module_exit(void)

    // 将设备从内核删除
    cdev_del(second.cdev);

    // 释放设备号
    unregister_chrdev_region(second.dev, 1);

    // 删除设备节点
    device_destroy(second.class, second.dev);

    // 删除设备类
    class_destroy(second.class);

3. 编写定时器程序

引入头文件:

#include <linux/timer.h>

添加全局变量:

struct second_dev 
    dev_t dev;                  /*!< 设备号             */
    struct cdev *cdev;          /*!< cdev对象           */
    struct class *class;        /*!< 设备类             */
    struct device *device0;     /*!< 设备节点           */

    struct timer_list timer;    /*!< 软件定时器         */
    atomic_t counter;           /*!< 秒计数             */
;

编写定时器中断函数:

static void second_timer_handler(unsigned long arg)

    // 重启定时器
    mod_timer(&second.timer, jiffies + HZ);

    // 秒计数
    atomic_inc(&second.counter);

    printk(KERN_INFO "current jiffies is %ld\\n", jiffies);

在open函数中,初始化定时器并注册到内核中,开始运行:

static int second_open(struct inode *inode, struct file *fp)

    // 初始化定时器,超时时间1s
    init_timer(&second.timer);
    second.timer.expires = jiffies + HZ;
    second.timer.function = second_timer_handler;
    

    // 初始化秒计数值
    atomic_set(&second.counter, 0);

    // 注册定时器到内核,开始运行
    add_timer(&second.timer);

    return 0;

在release函数中,从定时器删除定时器:

static int second_release(struct inode *inode, struct file *fp)

    // 将定时器从内核删除
    del_timer(&second.timer);
    return 0;

此时,编译驱动模块,加载到内核中,可以看到对应的设备节点:

4. 编写测试应用程序

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdint.h>

int main(int argc, char *argv[])

    int fd;

    // 检查参数
    if (argc != 2) 
        printf("usage: ./test_second [device]\\n");
        return -1;
    

    fd = open(argv[1], O_RDWR);

    while (1) 

    

编译:

arm-linux-gnueabihf-gcc test_second.c -o test_second

运行测试:

5. 驱动向应用传递秒计数值

引入头文件:

#include <linux/uaccess.h>

优化open函数:

static int second_open(struct inode *inode, struct file *fp)

    // 初始化定时器,超时时间1s
    init_timer(&second.timer);
    second.timer.expires = jiffies + HZ;
    second.timer.function = second_timer_handler;
    
    // 初始化秒计数值
    atomic_set(&second.counter, 0);

    // 注册定时器到内核,开始运行
    add_timer(&second.timer);

    fp->private_data = &second;

    return 0;

编写read函数:

static int second_read(struct file *fp, char __user *buf, size_t size, loff_t *off)

    int ret;
    int sec;
    struct second_dev *dev = (struct second_dev *)fp->private_data;

    sec = atomic_read(&dev->counter);
    ret = copy_to_user(buf, &sec, sizeof(sec));

    return 0;

重新编译驱动模块,加载到系统中。

6. 应用读取秒计数值

优化应用程序,读取按键值:

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdint.h>

int main(int argc, char *argv[])

    int fd;
    int ret;
    int sec;

    // 检查参数
    if (argc != 2) 
        printf("usage: ./test_second [device]\\n");
        return -1;
    

    fd = open(argv[1], O_RDWR);

    while (1) 
        ret = read(fd, &sec, sizeof(sec));
        if (ret < 0) 
            printf("read sec fail\\n");
        
        printf("sec is %d\\n", sec);
        sleep(1);
    

重新编译,运行,测试结果如下:

以上是关于i.MX6ULL驱动开发 | 19 - Linux内核定时器的编程方法与使用示例的主要内容,如果未能解决你的问题,请参考以下文章

i.MX6ULL驱动开发1——字符设备开发模板

i.MX6ULL驱动开发 | 15 - Linux UART 驱动框架

i.MX6ULL驱动开发 | 09 -基于Linux自带的LED驱动点亮LED

i.MX6ULL驱动开发 | 08 -基于pinctrl子系统和gpio子系统点亮LED

i.MX6ULL驱动开发 | 08 -基于pinctrl子系统和gpio子系统点亮LED

i.MX6ULL驱动开发 | 13 - Linux SPI 驱动框架