本文主要是介绍Linux内核定时器使用及说明,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
《Linux内核定时器使用及说明》文章详细介绍了Linux内核定时器的特性、核心数据结构、时间相关转换函数以及操作API,通过示例展示了如何编写和使用定时器,包括按键消抖的应用...
1.Linux内核定时器特征
- 依赖Linux系统心跳节拍发生器的定时器;
- 基于未来的时间点的一种定时方式;
- 支持多段定时,互不干扰:一段定时还没有完成,可以再启动新一段定时;
- 内核定时器支持的是单次定时方式,不是循环定时;
- 内核定时器定时精度不高,如果需要高精度定时需要使用硬件定时器或使用内核高精度定时器;
- 定时器的超时处理函数只会在注册它的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:表示一秒对应的时钟节拍数
- function超时处理函数,并不是硬件中断服务程序。
- data传递给超时处理函数的参数,可以把一个变量的地址转换成unsigned long
3.Linux内核时间相关转换函数
定时使用的不是时长,是基于未来时间点,单位是时钟节拍。
HZ宏:1秒对应时钟节拍。
unsigned long msecs_to_jiffies(const unsigned int m) //把ms转换为时钟节拍 unsigned long usecs_to_phpjiffies(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+定时时长对应的时钟节拍数; 启动定时器。。。
4http://www.chinasem.cn.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 内核定时器编程步骤
- 编写一个定时器超时处理函数。
- 定义struct timer_list结构变量。
- 初始化上一步定义的结构变量。
- 重新初始化到期时间,马上启动定时器(注册定时器)。
- 如果不需要定时器,可以删除定时器(注销定时器),这一步是可选。
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,中断程序返回
- 当出现第一个下降沿,进入中断,启动10ms定时,退出中断
- 定时10ms过程中,出现抖动,上升沿出现,进入中断,启动10ms定时,退出中断
- 定时10ms过程中,出现抖动,下降沿出现,进入中断,启动10ms定时,退出中断
- 重复2,3步骤……直到最后一次抖动边沿,10ms后就有机会超时。
- 在超时函数读取按键状态
松开检测方法相同。
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-js1", }, }; 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_t编程o_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;
}
现象

总结
这篇关于Linux内核定时器使用及说明的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!