目录
1.Linu内核定时器
1 Linux 内核定时器概述
2 Linux 内核定时器核心数据结构
3 Linux 内核时间相关转换函数
4 Linux 内核定时器操作相关 API
1. 静态定义结构体变量并且初始化(宏)
2. 定时器初始化(宏)
3.设置定时器(宏)
4. 注册定时器到内核
5.从内核注销定时器
6. 修改定时器定时时间值, 并且重新注册
5 Linux 内核定时器编程步骤
0. 编写定时器超时函数
7 循环定时器使用示例
2.定时器
3.定时器有两个概念:
4.定时器消抖原理
5,在内核中比较 add_timer(&timer)的用法:
6.在自已的驱动程序中仿照
7.驱动源码
8.测试源码
1.Linu内核定时器
1 Linux 内核定时器概述
Linux 内核定时器是内核用来控制在未来某个时间点(基于 jiffies) 执行某个函数的一种机制, 相关源码在linux/timer.h 和 kernel/timer.c 文件中。
Linux 内核定时器的超时函数运行过一次后就不会再被运行了(相当于单次定时效果),但可以通过在超时函数中重新注册定时器来实现循环定时效果。 在 SMP 芯片的 CPU 上,定时函数总是在注册它的同一 CPU 上运行。
2 Linux 内核定时器核心数据结构
内核使用一个 timer_list 结构来描述一个内核定时器。
以上结构用来表示一个定时器, 你想定时一段时间就必须定义这个结构, 实现 expires, function, data 成员。
如果想同时定时多段时间, 就定义多个定时器, 实现必须的成员, 然后向内核注册。
必须成员: expires, function, data
定时要素: 定时时间和超时函数, 这两个就是上面结构体的 expires 和 function 成员。
function 是指向超时函数的指针, 原型: void xxx_function(unsigned long); 当超时后内核会执行 function 指针指向的函数, 这个函数执行需要传递实际参数, 传入的实际参数就是结构体中的 data 成员。
expires: 未来的时间, 基本 jiffies 变量的时间, 单位是时钟节拍, jiffies 是一个内核全局变量, 每个时钟节拍会自加 1, 表示了系统开机到现在过去了多少个时钟节拍。
定时器时间: jiffies + 你要定时的时间对应的时钟节拍数。
示例: 定时 2 秒, 到期时间就是 jiffies + 2 * HZ Hz 表示 1 秒对应的时钟节拍数
如果想定时 ms 级的, 怎么处理? HZ/10 , --- 100ms
3 Linux 内核时间相关转换函数
unsigned long usecs_to_jiffies(const unsigned int u)
功能: 把微秒转换成时钟节拍
参数: u 时间微秒
返回: 对应的时钟节拍数量
unsigned long msecs_to_jiffies(const unsigned int m)
功能: 把毫秒转换成时钟节拍
参数: u 时间毫秒
返回: 对应的时钟节拍数量
示例: 要定时从现在开始, 3 毫秒执行一个函数
expires 设置为 jiffies+ msecs_to_jiffies(3)
4 Linux 内核定时器操作相关 API
1. 静态定义结构体变量并且初始化(宏)
DEFINE_TIMER(_name, _function, _expires, _data)
功能: 定义一个名字为_name 的 struct timer_list 结构的变量, 并且初始化它的 function, expires, data 成员
参数:
_name: struct timer_list 结构的变量名
_function, _expires, _data: 用来分别填充 _name 变量的 function, expires, data 成员。
2. 定时器初始化(宏)
init_timer(timer)
功能: 只是对 struct timer_list 结构成员进行一些基础初始化操作, function, expires, data 成员还需要用户自己填充。
参数: timer 应该是一个 struct timer_list 结构变量地址
3.设置定时器(宏)
setup_timer(timer, fn, data)
功能: 设置定时器中的 function, data 和一些基础成员, expires 并没有初始化, 需要用户自己进行初始化
参数:
timer : 是一个 struct timer_list 结构变量地址
fn, data: 分别填充 timer 变量中的 function, data 成员
4. 注册定时器到内核
void add_timer(struct timer_list *timer)
功能: 向内核注册一个定时器, 注册后会马上开始计时。
参数: 是一个 struct timer_list 结构变量地址, 并且要求这个结构变量是已经初始化好必须成员
5.从内核注销定时器
int del_timer(struct timer_list * timer);
功能: 从内核定时链表上删除指定的定时器, 删除后就不会再执行绑定的函数
参数: 是一个 struct timer_list 结构变量地址
6. 修改定时器定时时间值, 并且重新注册
int mod_timer(struct timer_list *timer, unsigned long expire0.s);
功能: 修改定时器定时时间值, 并且重新注册, 不管这个定时的超时函数是否执行过。 执行完成后会马上启动定时。 功能有点像 add_timer。
参数:
timer 要修改的定时器结构变量地址, 要求这个结构变量的 function, data 已经初始化好的。
expires: 有来填充结构中的 expires 成员, 也就是未来的时间点。
5 Linux 内核定时器编程步骤
0. 编写定时器超时函数
void time_function(unsigned long){ … }
1. 定义一个核心结构 timer_list 变量
struct timer_list timelist; //单纯定时, 还需要自己去初始化
2. 初始化上一步定义的变量
init_timer(&timelist) ;
setup_timer(&timelist, time_function, 123)
timelist. expires = jiffies + HZ*2; //2 秒
如果使用静态方式: 可以把上面两步一次完成
DEFINE_TIMER(timelist , time_function, jiffies + HZ*2, 123)
3.在要启动定时地方使用 add_timer
注册定时器或使用 mod_timer 修改定时器定时时间
add_timer(&timelist);或mod_timer(&timelist, jiffies + HZ*2);
4. 如果要取消定时调用 del_timer 取消一个定时器
del_timer(&timelist);
6 单次定时器使用示例
/* hello.c */
#include <linux/module.h>
#include <linux/init.h>
#include <linux/delay.h> //添加上延时头文件 ,为了验证超时函数不能休眠
//1)添加头文件
#include <linux/timer.h>
//3)定义一个 timer_list 结构变量
struct timer_list timerlist;
//2)实现一个超时函数:注意超时函数不能写任何可能引导休眠的函数
void timerlist_func(long data)
{
printk("%s is call! data:%d\r\n",__FUNCTION__,data);
}
static int __init timerlist_init(void)
{
//4)对 timer_list 结构变量进行初始
init_timer(&timerlist);
setup_timer(&timerlist, timerlist_func, 123);
timerlist.expires = jiffies + HZ*2;
//5)注册定时器,启动定时
add_timer(&timerlist); //启动定时器
printk("%s is call!\r\n",__FUNCTION__);
return 0;
}
static void __exit timerlist_exit(void) //Module exit function specified by module_exit()
{
//6)注销定时器
//del_timer(&timerlist); //单核处理器使用这个版本
del_timer_sync(&timerlist);//多核处理器使用这个版本,单核心 处理器上使用这个版本也没有问题
}
module_init(timerlist_init);
module_exit(timerlist_exit);
MODULE_ALIAS("alternate_name");
MODULE_VERSION("version_string");
MODULE_DESCRIPTION("TMIER_LIST_DEMO");
MODULE_AUTHOR("XYD");
7 循环定时器使用示例
/* hello.c */
#include <linux/module.h>
#include <linux/init.h>
#include <linux/delay.h> //添加上延时头文件 ,为了验证超时函数不能休眠
//1)添加头文件
#include <linux/timer.h>
//3)定义一个 timer_list 结构变量
struct timer_list timerlist;
//2)实现一个超时函数:注意超时函数不能写任何可能引导休眠的函数
void timerlist_func(long data)
{
printk("%s is call! data:%d\r\n",__FUNCTION__,data);
mod_timer(&timerlist, jiffies + HZ*2); //再次修改本定时器超时时间 为当前时间 后面 2 秒
//msleep(500); //故意 休眠,测试内核是否会异常
}
static int __init timerlist_init(void)
{
//4)对 timer_list 结构变量进行初始
init_timer(&timerlist);
setup_timer(&timerlist, timerlist_func, 123);
timerlist.expires = jiffies + HZ*2;
//5)注册定时器,启动定时
add_timer(&timerlist); //启动定时器
printk("%s is call!\r\n",__FUNCTION__);
return 0;
}
static void __exit timerlist_exit(void) //Module exit function specified by module_exit()
{
//6)注销定时器
//del_timer(&timerlist); //单核处理器使用这个版本
del_timer_sync(&timerlist);//多核处理器使用这个版本,单核心 处理器上使用这个版本也没有问题
}
module_init(timerlist_init);
module_exit(timerlist_exit);
MODULE_ALIAS("alternate_name");
MODULE_VERSION("version_string");
MODULE_DESCRIPTION("TMIER_LIST_DEMO");
MODULE_AUTHOR("XYD");
加了睡眠内核异常
这里产生了“抖动”,按键是机械开关,按下松开时里面的金属弹片可能抖动了好几次。这
种抖动产生了多次“脉冲”导致多次中断。
2.定时器
引入这个概念将“抖动”去掉。
3.定时器有两个概念:
1,超时时间:
2,时间到了之后的“处理函数”。
可以在中断处理中,如定时 10ms 后处理确定按键值上报。
4.定时器消抖原理
产生中断
在中断中加定时器,当遇到 A 中断时加一个 10ms 的定时器,过了 10ms 后就去执行“处理函数”(确定按键值上报)。因为机械的抖动会非常快,没等到 10ms 后的处理,这时因为抖动又来了一个中断 B,这时中断 B 把之前的那个定时器修改了。所以 A 中断的定时器就取消了。最后又来了一个中断 C,同样会修改掉 B 中断的定时器。上图中是假设抖动时产生了 3 个中断,所以对于同一个“定时器”,最终中断 C 的定时器没有被修改,所以 10ms 后由中断 C 的处理函数上报了按键值。最后这个 10ms 是从抖动 C 处开始。
这样 3 个抖动的中断只会导致最后处理一个“上报按键值”(定时器过后的处理函数只会执行一次)。以上便是用定时器消除抖动的原理。
因为是修改同一个定时器,所以前面的定时又取消,相当于把“闹钟”时间往后调整时,最终只要响一次闹铃。
5,在内核中比较 add_timer(&timer)的用法:
定义一个 timer_list 结构体变量“timer”。
6.在自已的驱动程序中仿照
“定义-->初始化-->使用”定时器的定义和触发时间:
jiffies 相关概念。
在入口函数中使用: static int sixth_drv_init(void):
初始化定时器: buttons_timer_function()。
设备定时器处理函数: buttons_timer.function = buttons_timer_function;
将定时器加到内核: add_timer(&buttons_timer);
当按下按键后--->到中断处理函数“buttons_irq()”中去。
中断处理函数以前是确实热键值,唤醒应用程序或者发信号等等操作。现在并没先做这些事情,而是为了防止按键抖动。加了定时器,让唤醒 APP 或发信号让定时器到达时间时的“定时器处理函数”中完成。而这里“中断处理函数”中先是修改定时器的超时时间为 10ms:Mod_timer(&buttons_timer, jiffies+HZ/100);
这里产生中断到到这个中断处理函数时,会来一个抖动就把时间基于当前时间值推后 10ms。
(看上图),这样就把多个中断合并成了一个定时器处理。
接着“中断处理函数 buttons_irq()”就不再操作,返回: return IRQ_RETVAL(IRQ_HANDLED);
上面最后的处理函数我们自已定义的是“buttons_timer_function”,可以查看 timer_list 结构中 function 的定义原型:
根据定时器处理函数原型写一个定时器处理函数:
定时器有两要素:超时时间 和 定时器处理函数。上面时间还没有写,处理函数框架写出来
了。用“add_timer()”去使用这个时间,它是把“定时器”告诉内核,当定时器中的超时时间到了后,定时器处理函数“buttons_timer_function()”就会被调用。
在中断处理程序中启动修改定时器的超时时间,下面是以前的中断程序:
这个中断程序中只需要修改超时时间。 Mod_timer();超时是指“闹钟”什么时间闹铃。基于
“jiffles”这个值,这是一个全局变量,系统每隔 10ms,这个值就会产生一个系统时钟中断。
超时时间可以设置成“当前值”加上某个值。如 1 秒就是 HZ,从定义中可以看到 HZ 是 100.
这里的意思是 1 秒钟里这个当前的 jiffies 值会增加 100.
add_timer(&buttons_timer,jiffies+HZ);是指当前 jiffies 时间过了 100 个系统时钟中断(系统滴答)后,这个定时器的超时时间“buttons_timer”就到达了。 HZ 是 1 秒,那么定时器在 10ms时启动的话,就是 HZ/100 即 10ms。
假设现在, jiffies 的值为“50”, HZ 为“100”,那么这个定时器 buttons_timer 的超时时间为:
Buttons_times.expires = 50+100/100(jieeies+HZ/100) = 51.
系统是每隔 10ms 产生一个系统时钟中断,系统时钟中断中这个 jiffies 的值会累加。这里假设 jiffies 为 50,则下一个系统时钟时, jiffies 就变成 51 了, 51 一到,在这个系统时钟中断处理函数里面会从这个定时器链表里面把这里的定时器找出来,看看哪个定时器的时间已经到了(buttons_timer 这个定时器就在这个链表中)。
若这里的 jiffies 已经大于等于这个“buttons_timer”的定时器超时时间“expires”时
(jiffies>=buttons_timer.expirs) ,就去调用与这个定时器相关的定时器处理函数。这里要是
buttons_timer.expirs 已经超时,就会调用上面自已定义的“buttons_timer_function()”这个定时器处理函数。
当定时器超时时,定时器处理函数的工作:
首先“dev_id”要记录下来:
要定义一个结构体, static struct pid_desc *irq_pd;发生中断时的引脚描述。
7.驱动源码
//1.首先添加头文件
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>
#include <linux/miscdevice.h>
#include <linux/device.h>
#include <linux/irq.h>
#define THIRD_DRV_NAME "third_drv_name"
struct class *third_class;
struct class_device *third_device_class;
/*2.操作key*/
/*定义配置寄存器和数据寄存器指针*/
/*2.1查看原理图和芯片数据手册*/
/*
S2---->EINT0---->GPF0----->[1:0]
S3---->EINT2---->GPF2----->[5:4]
S4---->EINT11---->GPG3----->[7:6]
S5---->EINT19---->GPF11----->[23:22]
EINT0---->IRQ_EINT0----->/proc/irq/16/S2
EINT2---->IRQ_EINT2----->/proc/irq/18/S3
EINT11---->IRQ_EINT11----->/proc/irq/55/S4
EINT19---->IRQ_EINT19----->/proc/irq/63/S5
GPFCON---->0x56000050------->00 = intput
GPFDAT---->0x56000054------->0为高有效
GPGCON---->0x56000060------->00 = intput
GPGDAT---->0x56000064------->0为高有效
*/
volatile unsigned long *gpfcon = NULL;
volatile unsigned long *gpfdat = NULL;
volatile unsigned long *gpgcon = NULL;
volatile unsigned long *gpgdat = NULL;
/*
*3.2.引脚描述结构体
*/
struct pin_desc
{
unsigned int irq;
unsigned int pin;
unsigned int key_val;
const char *keyname;//按键名字
struct timer_list timer;//消除抖动定时器
};
/*
*3.3.key初始状态(没有按下): 0x01,0x02,0x03,0x04
*key状态(按下): 0x81,0x82,0x83,0x84
*/
struct pin_desc pins_desc[4] =
{
{IRQ_EINT0, S3C2410_GPF0, 0x01, "S2"},
{IRQ_EINT2, S3C2410_GPF2, 0x02, "S3"},
{IRQ_EINT11, S3C2410_GPG3, 0x03, "S4"},
{IRQ_EINT19, S3C2410_GPG11, 0x04, "S5"},
};
/*
* 4.4定义全局变量key_val,保存key状态
*/
static unsigned char key_val;
//5.1声明一个新的等待队列类型的中断
//button_waitq:就是中断名字,被用来后面的唤醒中断和等待中断
static DECLARE_WAIT_QUEUE_HEAD(button_waitq);
//5.1定义全局变量even _press,用于中断事件标志
/*
* 定义中断事件标志
* 0:进入等待队列 1:退出等待队列
*/
static volatile int ev_press = 0;
//4.3request_irq函数的中断服务函数是buttons_irq
//irq:中断号, void *:表示支持所有类型
// 启动定时器
static irqreturn_t buttons_irq(int irq, void *dev_id)
{
struct pin_desc *pinsdesc = (struct pin_desc *)dev_id;
//修改定时器超时时间是以当前时间起 50ms 后超时
mod_timer(&pinsdesc->timer, jiffies + msecs_to_jiffies(50));
return IRQ_RETVAL(IRQ_HANDLED);
}
/** 3.5超时函数:只有 IO 电平稳定了才有机会执行超时函数,
所以稳定后可以直接读取 IO 电平* 当按下松开都会产生中断,
都执行这个函数。* 程序要把取得按键的状态,保存起来,
让用户通过用户编程 API 接口读取按键状态* 所以,
函数中完成:
1)判断按键还是松开;
2)把按键状态存储起来*/
void timerlist_func(long data)
{
struct pin_desc *pinsdesc = (struct pin_desc *)data;
unsigned int pin_val;
pin_val = s3c2410_gpio_getpin(pinsdesc->pin);
if(pin_val)
{
key_val = 0x80 | pinsdesc->key_val;
}
else
{
key_val = pinsdesc->key_val;
}
ev_press = 1; /* 表示中断发生了 */
//2.2qname:指向声明的等待队列类型中断名字
//唤醒一个中断,会将这个中断重新添加到runqueue队列(将中断置为TASK_RUNNING状态)
//在中断服务函数里,发生中断时, 就将even _press置1,并唤醒中断button_wait(.read函数里就会发送数据给用户层):
wake_up_interruptible(&button_waitq);
}
static int third_drv_open(struct inode *pinode, struct file *pfile)
{
//request_irq(IRQ_EINT0, buttons_irq, IRQT_BOTHEDGE, "S2", &pins_desc[0]);
//request_irq(IRQ_EINT2, buttons_irq, IRQT_BOTHEDGE, "S3", &pins_desc[1]);
//request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "S4", &pins_desc[2]);
//request_irq(IRQ_EINT19, buttons_irq, IRQT_BOTHEDGE, "S5", &pins_desc[3]);
printk("<0>""%s is call!\n", __FUNCTION__);
return 0;
}
static ssize_t third_drv_read(struct file *pfile, char __user *buf, size_t size, loff_t *poff)
{
if(size != 1)
{
return -EINVAL;
}
/*
等待事件中断函数,用来将中断放回等待队列,
前提是condition要为0,然后将这个中断从runqueue队列中删除(将中断置为TASK_INTERRUPTIBLE状态),然后会在函数里一直for(; ;)判断condition为真才退出
注意:此时的中断属于僵尸进程(既不在等待队列,也不在运行队列),当需要这个进程时,需要使用wake_up_interruptible(*qname)来唤醒中断
qname: (wait queue):为声明的等待队列的中断名字
condition:状态,等于0时就是中断进入休眠, 1:退出休眠
*/
//当even _press为真,表示有按键按下,退出等待队列
wait_event_interruptible(button_waitq,ev_press);
//even _press为真,有数据了,发送给用户层
copy_to_user(buf, &key_val, 1);
//数据发完后,立马设为休眠状态,避免误操作
ev_press = 0;
return 0;
}
//4.5然后写.release成员函数,释放中断:
static int third_drv_close(struct inode *pinode, struct file *pfile)
{
int i = 0;
for(i = 0; i < 4; i++)
{
free_irq(pins_desc[i].irq, &pins_desc[i]);
}
return 0;
}
static const struct file_operations third__fops =
{
.owner = THIS_MODULE,
.open = third_drv_open,
.read = third_drv_read,
//1.2添加free_irq函数,来释放中断服务函数
.release = third_drv_close,
};
/*1.首先写好驱动摸版*/
/*注:
编译成模块时module_init和module_exit
编译到内核时: __init和__exit起作用
*/
int major = 0;
static int __init third_drv_init(void)
{
int i = 0;
int ret = -1;
/*1.1注册杂项设备*/
//早期经典字符设备,需手动创设备节点,一个主设备号只可以注册一次
/*
主设备号,当 major 传递 0 时候表示由内核自动分配一个可用的主设备号.
设备名,不需要和/dev 下对应节点名相同
早期经典字符设备需要手动创建设备
如果应用使用的是的/dev/chrdev_name
mknod /dev/chrdev_name c 252 0
或mknod /dev/chrdev_name c 252 1
或mknod /dev/chrdev_name c 252 x
但是mknod /dev/chrdev_name c 250 x
一旦主设备号不是252,即是节点名相同都是open fail
结论:应用程序寻找程序程序不是通过设备名,而是通过设备号
*/
/*
注:这里的节点名不一定要和/dev下一致,这里注册是/proc/devices下的名字
一旦注册成功改主设备下的次设备号全部用完
*/
major = register_chrdev(0, THIRD_DRV_NAME, &third__fops);
/*
1.2自动创建设备文件结点
类的所有者, 固定是 THIS_MODULE
类名,随便,能有含义最好, 不是 /dev/ 下设备的名字。这个名字决定了/sys/class/name
*/
third_class = class_create(THIS_MODULE, "third");
/* /dev/xyz */
third_device_class = class_device_create(third_class, NULL, MKDEV(major, 0), NULL, "third_device");
/*2.2将物理地址和虚拟地址进行映射*/
//用ioremap(开始地址 结束大小)
/*
先映射gpfcon为虚拟地址,F寄存器:gpfcon:0x56000050 gpfcon:0x56000054
gpfcon:0x56000058 Reserved:0x5600005c,2440为32位CPU,所以4个4字节为16细字节,
故这里映射16字节
*/
gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
//这里指针+1是以上面unsigned long为单位的
gpfdat = gpfcon + 1;
gpgcon = (volatile unsigned long *)ioremap(0x56000060, 16);
gpgdat = gpgcon + 1;
for(i = 0; i < 4; i++)
{
//3.2添加定时初始化
init_timer(&pins_desc[i].timer);
setup_timer(&pins_desc[i].timer, timerlist_func, (long)&pins_desc[i]);
//这里并没有启动定时器
//3.4注册定时器,启动定时
//add_timer(&pins_desc[i].timer);
//启动定时器
/*4.1申请4个中断*/
//.在third_drv_open函数中,申请4个中断:
/* IRQ_EINT0:中断号, 定义在 asm/arch/irqs.h,被linux/irq.h调用
buttons_irq :中断服务函数,
IRQT_ BOTHEDGE:双边沿中断, 定义在 asm/irq.h,被linux/irq.h调用
“S1”:保存文件到/proc/interrupt/S1,
1:dev_id,中断函数的参数, 被用来释放中断服务函数,中断时并会传入中断服务函数
*/
ret = request_irq(pins_desc[i].irq, buttons_irq, IRQT_BOTHEDGE, pins_desc[i].keyname, &pins_desc[i]);
if(ret < 0)
{
break;
}
}
//如果前面注册有失败的,则全部释放中断:注意只释放已经注册成功的中断。
if(ret < 0)
{
for(--i; i > 0 ; i--)
{
//得到 Kx 中断编号
free_irq(pins_desc[i].irq, &pins_desc[i]);
//释放中断
}
}
return 0;
}
static void __exit third_drv_exit(void)
{
unsigned int i = 0;
/*1.3注销早期经典字符设备*/
unregister_chrdev(major, THIRD_DRV_NAME);
class_device_unregister(third_device_class);
class_destroy(third_class);
iounmap(gpfcon);
iounmap(gpgcon);
for(i = 0; i < 4; i++)
{
del_timer_sync(&pins_desc[i].timer); //注销定时器,可选
}
}
module_init(third_drv_init);
module_exit(third_drv_exit);
MODULE_LICENSE("GPL");
8.测试源码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#define THIRD_DEVICE_NAME "/dev/third_device"
int main(int argc, char **argv){
int fd;
unsigned char key_val;
fd = open(THIRD_DEVICE_NAME, O_RDWR);
if(fd < 0){
printf("open fail\n");
return -1;
}
while(1){
read(fd, &key_val, 1);//读取一个值,(当在等待队列时,本进程就会进入休眠状态)
printf("key_val: 0x%x\n",key_val);
//sleep(5);
}
return 0;
}
测试基本稳定,未出现多次打印的情况