java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Linux内核定时器

Linux内核定时器使用及说明

作者:卿相诉七弦

文章详细介绍了Linux内核定时器的特性、核心数据结构、时间相关转换函数以及操作API,通过示例展示了如何编写和使用定时器,包括按键消抖的应用

1.Linux内核定时器特征

  1. 依赖Linux系统心跳节拍发生器的定时器;
  2. 基于未来的时间点的一种定时方式;
  3. 支持多段定时,互不干扰:一段定时还没有完成,可以再启动新一段定时;
  4. 内核定时器支持的是单次定时方式,不是循环定时;
  5. 内核定时器定时精度不高,如果需要高精度定时需要使用硬件定时器或使用内核高精度定时器;
  6. 定时器的超时处理函数只会在注册它的CPU上运行,不会存在并发问题。

2.Linux内核定时器核心数据结构

内核使用struct timer_list表示一个内核定时器,定义如下:

struct timer_list {
       struct hlist_node entry;                                    //实现定时器结构链表化管理
       unsigned long             expires;                          //到期时间点      不是时长
       void                     (*function)(unsigned long);        //超时处理函数
       unsigned long             data;
       u32               flags;
       int                 slack;
};

重要成员:

到期时间,基于未来时间点的时钟节拍数。等于定时时刻当前的时钟节拍计数(存储在系统的全局expires变量jiffies)+定时时长对应的时钟节拍数量。

示例:从现在开始定时1秒:expires = jiffies + 1*Hz

Hz:表示一秒对应的时钟节拍数

3.Linux内核时间相关转换函数

定时使用的不是时长,是基于未来时间点,单位是时钟节拍。

HZ宏:1秒对应时钟节拍。

unsigned long msecs_to_jiffies(const unsigned int m)             //把ms转换为时钟节拍
unsigned long usecs_to_jiffies(const unsigned int u)             //把us转换为时钟节拍

示例:定时2s+200ms+500us

到期时间:jiffies + 2*HZ+msecs_to_jiffies(200)+msecs_to_jiffies(500)

4.Linux内核定时器操作相关API

4.1 静态定义结构体变量并且初始化(宏):DEFINE_TIMER

头文件

#include <linux/timer.h>

原型

#define DEFINE_TIMER(_name, _function, _expires, _data)             \

struct timer_list _name = TIMER_INITIALIZER(_function, _expires, _data)

参数

_name变量名

_function超时处理函数

_expires到期时间,一般在启动定时前需要重新初始化。

_data传递给超时处理函数的参数

功能

定义一个定时器结构并且初始化function, expires, data成员。

4.2 动态初始化定时器(宏):setup_timer

头文件

#include <linux/timer.h>

原型

#define setup_timer(timer, fn, data)      __setup_timer((timer), (fn), (data), 0)

参数

timer定时器结构指针

n超时处理函数

data超时处理函数参数

功能

运行时初始化定时器

struct timer_list btn_timer;
setup_timer(&btn_timer, fn, data)
btn_timer.expires = jiffies+定时时长对应的时钟节拍数;
启动定时器。。。

4.3 注册定时器到内核:add_timer

头文件

#include <linux/timer.h>

原型

void add_timer(struct timer_list *timer)

返回值

功能

向内核注册一个定时器,同步启动定时。本质上把定时器结构添加到定时链表

注意:同一个结构变量在启动定时后,没有超时前重复注册没有用的。

struct timer_list timer,
……
add_timer(&timer);
add_timer(&timer);
……

4.4 注销内核定时器:del_timer,del_timer_sync

头文件

#include <linux/timer.h>

原型

int del_timer(struct timer_list *timer)

int del_timer_sync(struct timer_list *timer)

功能

从内核中注销一个定时器。(本质上把定时器结构从定时链表取走)

区别:  del_timer 单处理器系统安全删除定时器函数,del_timer_sync SMP系统安全删除定时器的函数。

4.5 修改定时器定时时间:mod_timer

头文件

#include <linux/timer.h>

原型

int mod_timer(struct timer_list *timer, unsigned long expires)

参数

timer定时器指针;

expires未来的到期时钟节拍。

功能

修改定时器时间以及启动定时。

5.Linux内核定时器示例

5.1 内核定时器编程步骤

5.2 定时2s

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

static void timer_func(unsigned long data);

//静态定义:定义并且初始化
DEFINE_TIMER(my_timer,timer_func,0,(unsigned long)"LIU");

//定时器超时函数
static void timer_func(unsigned long data)
{
	printk("data:%s\r\n",(char *)data);
	//启动新一次定时,实现循环定时
	mod_timer(&my_timer,jiffies + 2*HZ);
}

static int __init timer_list_init(void)
{
	//重新初始化到期时间
	my_timer.expires = jiffies + 2*HZ;	//2s
	
	//马上启动定时器
	printk("-----begin------\r\n");
	add_timer(&my_timer);
	printk("------end-------\r\n");
    return 0; 
}

static void __exit timer_list_exit(void)
{
	//删除定时器
	del_timer_sync(&my_timer);
}

module_init(timer_list_init);			// 指定模块初始化函数
module_exit(timer_list_exit);			// 指定模块清理函数
MODULE_LICENSE("GPL");					// 声明模块许可证(GPL v2) 

5.3 循环定时

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

static void timer_func(unsigned long data);

//静态定义:定义并且初始化
DEFINE_TIMER(my_timer,timer_func,0,(unsigned long)"Time out");

//定时器超时函数
static void timer_func(unsigned long data)
{
	printk("data:%s\r\n",(char *)data);
	//启动新一次定时,实现循环定时
	mod_timer(&my_timer,jiffies + 2*HZ);
}

static int __init timer_list_init(void)
{
	//重新初始化到期时间
	my_timer.expires = jiffies + 2*HZ;	//2s
	
	//马上启动定时器
	printk("-----begin------\r\n");
	add_timer(&my_timer);
	printk("------end-------\r\n");
    return 0; 
}

static void __exit timer_list_exit(void)
{
	//删除定时器
	del_timer_sync(&my_timer);
}

module_init(timer_list_init);			// 指定模块初始化函数
module_exit(timer_list_exit);			// 指定模块清理函数
MODULE_LICENSE("GPL");					// 声明模块许可证(GPL v2) 

5.4 全局驱动数据

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

//全局驱动数据
struct drv_data{
	struct timer_list timer;
	char *data;
	u64 expires;
};
static struct drv_data gvar;

//定时器超时处理函数
static void timer_func(unsigned long data)
{
	struct drv_data *pdata = (struct drv_data *)data;
	printk("data:%s\r\n",pdata->data);
	//启动新一次定时,实现循环定时
	mod_timer(&pdata->timer,jiffies + pdata->expires);
}

static int __init timer_list_init(void)
{
	//普通成员初始化
	gvar.data    = "Time out";
	gvar.expires = 2*HZ;
	
	//初始化定时器
	setup_timer(&gvar.timer,timer_func, (unsigned long)&gvar);
	
	//重新初始化到期时间
	gvar.timer.expires = jiffies + gvar.expires;	//2s
	
	//马上启动定时器
	printk("-----begin------\r\n");
	add_timer(&gvar.timer);
	printk("------end-------\r\n");
    return 0; 
}

static void __exit timer_list_exit(void)
{
	//删除定时器
	del_timer_sync(&gvar.timer);
}
module_init(timer_list_init);			// 指定模块初始化函数
module_exit(timer_list_exit);			// 指定模块清理函数
MODULE_LICENSE("GPL");					// 声明模块许可证(GPL v2) 

5.5 Linux内核定时器应用:实现按键消抖

5.5.1 按键的抖动分析

裸机按键:检测,   延时,  再检测,确认按键

传统的防抖方法用户体验较差,因为延时时间难以精确设定,还会占用较多的CPU资源。

早期的按键驱动程序设计:当按键中断触发时,直接读取按键状态来判断按键动作,这种方法容易受到机械抖动的影响。

引入内核定时器机制后,可以在按键中断触发时启动一个定时任务。待定时器超时时,在其回调函数中读取按键的状态值。利用这一机制,可以每次在未超时时重新设置定时器,从而确保每次从当前时刻重新开始计算延时,有效避免了由于抖动带来的误判。这种方式不仅提高了检测的准确性,还优化了系统资源的使用效率。

中断程序启动定时器,每次从当前时间开始定时10ms,中断程序返回

  1. 当出现第一个下降沿,进入中断,启动10ms定时,退出中断
  2. 定时10ms过程中,出现抖动,上升沿出现,进入中断,启动10ms定时,退出中断
  3. 定时10ms过程中,出现抖动,下降沿出现,进入中断,启动10ms定时,退出中断
  4. 重复2,3步骤……直到最后一次抖动边沿,10ms后就有机会超时。
  5. 在超时函数读取按键状态

松开检测方法相同。

5.5.2 按键驱动程序改进

drv-btn.c

//增加内核定时器功能
#include<linux/module.h>
#include<linux/fs.h>
#include<linux/miscdevice.h>
#include<asm/uaccess.h>
#include<linux/interrupt.h>
#include<linux/irq.h>
#include<linux/gpio.h>
#include<linux/slab.h>

#define LEDS_MINOR	255
#define DEVICE_NAME  "my-buttons" // 设备节点名称,将在/dev目录下创建 

#ifndef ARRAYSIZE
#define ARRAYSIZE(a)	(sizeof(a)/sizeof(a[0]))
#endif

//按键数量,在模块初始化函数中进行计算
static int key_size;
//按键缓冲区,一个元素存放一个按键值,'1'表示按下,'0'表示松开
//在模块的初始化函数中分配缓冲区空间
static char *keys_buf;	

//使用面向对象思想设计按键,把一个按键信息进行封装
struct key_info{
	int id;					//按键编号
	int gpio;				//统一的GPIO编号
	unsigned long flags;	//触发方式
	char *name;				//按键名
	int irq;				//中断编号
	struct timer_list timer;//增加一个定时器作消抖
};

//实例化对象
static struct key_info keys[]={
	[0]={
		.id 	= 0,
		.gpio 	= 5,
		.flags 	= IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
		.name 	= "key-0",
	},
	[1]={
		.id 	= 0,
		.gpio 	= 54,
		.flags 	= IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
		.name 	= "key-1",
	},	
};

static int key_open (struct inode *pinode, struct file *pfile)
{
    printk("line:%d,%s is call\n",__LINE__,__FUNCTION__);
    return 0;    
}

loff_t key_llseek (struct file *pfile, loff_t offset, int whence)
{
    return 0;    
}

static ssize_t key_read (struct file *pfile, char __user *buf, size_t count, loff_t *offset)    
{
    int ret;
	if(count > key_size)	count = key_size;
    if(count == 0)			return 0;
	
	//准备数据,但是按键数据在中断中实时更新,不需要在这里读取
	//复制数据到用户空间
	ret = copy_to_user(buf, keys_buf, count);
	if(ret){
		printk("error:copy_to_user\r\n");
		ret = -EFAULT;
		goto errot_copy_to_user;
	}
	return count;  // 返回成功读取的字节数  
errot_copy_to_user:
	return ret;
}

int key_release (struct inode *pinode, struct file *pfile)
{
    return 0;
}

// 文件操作结构体(关联实现的操作函数) 
static const struct file_operations dev_fops = {
    .open = key_open,      // 打开设备 
    .read = key_read,      // 读取设备状态 
	.owner = THIS_MODULE,
};

// 杂项设备定义 
static struct miscdevice key_device = {
    .name = DEVICE_NAME,	// 设备名称(出现在/dev目录下) 
    .minor = LEDS_MINOR, 	// 次设备号(建议使用动态分配) 
    .fops = &dev_fops,		// 关联文件操作函数集 
};

//超时处理函数
void btn_timer(unsigned long data)
{

	int s;
	struct key_info *pdata = (struct key_info *)data;

	//检测当前的电平状态
	s = !gpio_get_value(pdata->gpio);
	keys_buf[pdata->id]='0'+s;	//保存状态
}

//按键中断函数
//设置了双边触发,按下和松开都会进入这个函数
irqreturn_t btns_irq_handler(int irq,void *devid)
{
	struct key_info *pdata = (struct key_info *)devid;

	//启动新一次定时
	mod_timer(&pdata->timer,jiffies + msecs_to_jiffies(20));
	return IRQ_HANDLED;
}

static int __init xyd_btn_init(void)
{
    int ret,i;

	key_size = ARRAY_SIZE(keys);	//计算按键数量

	//分配按键缓冲区
	keys_buf = kzalloc(key_size,GFP_KERNEL);
	if(keys_buf == NULL)
		return -EFAULT;
	
	//循环注册中断
	for(i = 0;i < key_size;i++){
		keys[i].irq = gpio_to_irq(keys[i].gpio);
		if(keys[i].irq < 0)
		{
			printk("error:gpio_to_irq\r\n");
			goto error_gpio_to_irq;  
		}
		printk("irq:%d\r\n",keys[i].irq);
		
		//传递每个按键结构变量地址,发生中断时可以通过参数取得
		ret = request_irq(keys[i].irq, btns_irq_handler, keys[i].flags, keys[i].name, (void *)&keys[i]);
		if(ret < 0)
		{
			printk("error:request_irq\r\n");
			goto error_request_irq;  
		}

		//初始化按键的定时器,参数是按键自身的结构内存地址
		setup_timer(&keys[i].timer,btn_timer, (unsigned long)&keys[i]);
	}
    ret = misc_register(&key_device);
    if(ret < 0){
        printk("error:misc_register\r\n");
        goto error_misc_register;  
    }
    return 0;  								// 初始化成功 

error_misc_register:
error_request_irq:
	while(--i >= 0){
		free_irq(keys[i].irq, (void *)&keys[i]);	//注销中断
	}
	
error_gpio_to_irq:
	kfree(keys_buf);		//释放按键缓冲区空间
    return ret; 
}

static void __exit key_btn_exit(void)
{
	int i = key_size;
	while(--i >= 0){
		free_irq(keys[i].irq, (void *)&keys[i]);	//注销中断
	}
	misc_deregister(&key_device);	//注销杂项设备
	kfree(keys_buf);				//释放按键缓冲区空间
}

module_init(xyd_btn_init);		  								// 指定模块初始化函数 
module_exit(key_btn_exit);  									// 指定模块清理函数 
MODULE_LICENSE("GPL");											// 声明模块许可证(GPL v2) 
MODULE_AUTHOR("LIU");    										// 模块作者 
MODULE_DESCRIPTION("RK3399 GPIO KEY Control Driver"); 			// 模块描述 

app.c

#include<stdio.h>              // 标准输入输出库(printf, perror等)
#include<stdlib.h> 
#include<string.h> 
#include<sys/types.h>          // 系统数据类型定义(如dev_t)
#include<sys/stat.h>           // 文件状态信息(文件模式等)
#include<fcntl.h>              // 文件控制选项(open等)
#include<unistd.h>             // 系统调用封装(lseek, read, write, sleep等)
#include<sys/ioctl.h>          // I/O控制操作(ioctl)
#include<errno.h> 

#define BTN_SIZE	1					// 按键数量
#define DEV_NAME 	"/dev/my-buttons"  	// 默认设备名

int main(int argc, char **argv)
{
    int fd,ret, i;                
    const char *devname; 				// 设备路径指针(初始化为默认路径)
    unsigned char pre_buf[BTN_SIZE+1],recv_buf[BTN_SIZE+1];

	memset(pre_buf,'0',BTN_SIZE);
	memset(recv_buf,'0',BTN_SIZE);
	
    if(argc == 1) 
        devname = DEV_NAME;
    else if(argc == 2)
        devname= argv[1];
    else {
        printf("Usage:%s [/dev/devname]\r\n", argv[0]);
        return 0;
    }

    fd = open(devname, O_RDWR);  			// O_RDWR:以读写模式打开
    if(fd < 0) {
        perror("open");   					// 打印系统错误信息
        printf("fd=%d\r\n", fd);
        return -1;            				// 打开失败退出程序
    }
    printf("fd=%d\r\n", fd);  				// 成功打开后输出fd值

    while(1) {
		ret = read(fd,recv_buf,BTN_SIZE);	// 读取按键数据
		if(ret < 0){
			if(errno != EAGAIN){
				perror("read");
				exit(-1);
			}else	continue;
		}

		//只在状态发生变化时候才输出
		for(i = 0;i < BTN_SIZE;i++){
			//分别判断每一个按键状态是否发生变化
			if(recv_buf[i] != pre_buf[i]){
				//更新当前状态为上一次状态
				pre_buf[i] = recv_buf[i];

				//判断这次变化是按下还是松开
				if(pre_buf[i] == '1')	printf("KEY%d is press!\r\n",i+1);
				else					printf("KEY%d is up!\r\n",i+1);
			}
		}
    }    
    return 0;
}

现象

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文