一、定时器
1、jiffies
Linux 内核使用全局变量 jiffies 来记录系统从启动以来的系统节拍数,系统启动的时候会将 jiffies 初始化为 0。
HZ 表示每秒的节拍数,jiffies 表示系统运行的 jiffies 节拍数,所以 jiffies/HZ 就是系统运行时间,单位为秒。
防止溢出,需要使用如下几个API函数:
如果 unkown 超过 known 的话,time_after 函数返回真,表示未超时;否则返回假,超时了。
如果 unkown 没有超过 known 的话 time_before 函数返回真,表示未超时;否则返回假,超时了。
time_after_eq 函数和 time_after 函数类似,只是多了判断等于这个条件。同理,time_before_eq 函数和 time_before 函数也类似。
可以用以下代码进行进行判断:
unsigned long timeout;
timeout = jiffies + (2 * HZ);
/* 判断有没有超时 */
if(time_before(jiffies, timeout)) {
/* 超时未发生 */
} else {
/* 超时发生 */
}
jiffies 和 ms、us、ns 之间的转换函数
2、内核定时器API函数
Linux 内核定时器使用很简单,只需要提供超时时间(相当于定时值)和定时处理函数即可,当超时时间到了以后设置的定时处理函数就会执行,但是内核定时器并不是周期性运行的,超时以后就会自动关闭,因此如果想要实现周期性定时,那么就需要在定时处理函数中重新开启定时器。
要使用内核定时器首先要先定义一个 timer_list 变量,表示定时器
struct timer_list {
struct list_head entry;
unsigned long expires;
struct tvec_base *base; /* 定时器超时时间,单位是节拍数 */
void (*function)(unsigned long); /* 定时处理函数 */
unsigned long data; /* 要传递给 function 函数的参数 */
int slack;
};
expires 成员变量表示超时时间,单位为节拍数。比如我们现在需要定义一个周期为 2 秒的定时
器,那么这个定时器的超时时间就是 jiffies+(2*HZ),因此 expires=jiffies+(2*HZ)。
function 就是定时器超时以后的定时处理函数,我们要做的工作就放到这个函数里面,需要我们编写这个定时处理函数。
init_timer 函数
init_timer 函数负责初始化 timer_list 类型变量,当我们定义了一个 timer_list 变量以后一定
要先用 init_timer 初始化一下。init_timer 函数原型如下:
void init_timer(struct timer_list *timer)
timer:要初始化的定时器。
add_timer 函数
add_timer 函数用于向 Linux 内核注册定时器,使用 add_timer 函数向内核注册定时器以后,
定时器就会开始运行,函数原型如下:
void add_timer(struct timer_list *timer)
timer:要注册的定时器。
返回值:没有返回值。
del_timer 函数
del_timer 函数用于删除一个定时器,不管定时器有没有被激活,都可以使用此函数删除。
在多处理器系统上,定时器可能会在其他的处理器上运行,因此在调用 del_timer 函数删除定时
器之前要先等待其他处理器的定时处理器函数退出。del_timer 函数原型如下:
int del_timer(struct timer_list * timer)
timer:要删除的定时器。
返回值:0,定时器还没被激活;1,定时器已经激活。
del_timer_sync 函数
del_timer_sync 函数是 del_timer 函数的同步版,会等待其他处理器使用完定时器再删除,
del_timer_sync 不能使用在中断上下文中。del_timer_sync 函数原型如下所示:
int del_timer_sync(struct timer_list *timer)
timer:要删除的定时器。
返回值:0,定时器还没被激活;1,定时器已经激活。
mod_timer 函数
mod_timer 函数用于修改定时值,如果定时器还没有激活的话,mod_timer 函数会激活定时
器!函数原型如下:
int mod_timer(struct timer_list *timer, unsigned long expires)
timer:要修改超时时间(定时值)的定时器。
expires:修改后的超时时间。
返回值:0,调用 mod_timer 函数前定时器未被激活;1,调用 mod_timer 函数前定时器已
被激活。
3、定时器使用流程
(1)定义定时器
创建 timer_list结构体变量,可以在设备结构体中进行定义:struct timer_list timer;
struct timer_dev{
dev_t devid;
struct cdev cdev; /* 设备号*/
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
struct device_node *nd; /* 设备节点 */
int led_gpio; /* key 所使用的 GPIO 编号 */
struct timer_list timer; /* 定义一个定时器*/
};
(2)初始化并启动定时器
void init(void)
{
init_timer(&timer); /* 初始化定时器*/
timer.function = xxx_function; /* 设置定时处理函数*/
timer.expires=jffies + msecs_to_jiffies(2000); /* 超时时间 2 秒 */
timer.data = (unsigned long)&timer_dev; /* 将设备结构体作为参数 */
add_timer(&timer); /* 启动定时器*/
}
(3)定时器回调函数
void xxx_function(unsigned long arg){
//定时器处理代码mod_timer(&dev->timertest, jiffies + msecs_to_jiffies(2000));//重新启动定时器
}
(4)删除定时器
del_timer(&timer);
/* 或者使用 */
del_timer_sync(&timer);
补充: unlocked_ioctl 函数
unlocked_ioctl 函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应。
驱动开发程序中在设备操作函数file_operations中定义
/* 设备操作函数 */
static struct file_operations xxx_fops = {
.owner = THIS_MODULE,
.open = timer_open,
.unlocked_ioctl = xxx_unlocked_ioctl,
};
驱动开发程序中对应的ioctl函数为:
static long xxx_unlocked_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct xxx_dev *dev = (struct xxx_dev *)filp->private_data;
return 0;
}
参数 filp : 要打开的设备文件(文件描述符)
cmd : 应用程序发送过来的命令,命令是自己定义的,但是需要符合定义规则:
#define A_CMD _IO(0XEF, 0x1) //没有参数的命令
#define B_CMD _IOR(0XEF, 0x2,int) //从驱动读取int数据
#define C_CMD _IOW(0XEF, 0x3, int) //向驱动写入int数据#define D_CMD _IOW(0XEF, 0x4, int) //双向数据传输
arg : 命令所带的参数
可以在ioctl函数中通过switch (cmd)选择不同命令。
应用程序中
添加头文件和宏(与驱动程序一致)
#include "linux/ioctl.h"
#define A_CMD _IO(0XEF, 0x1) //没有参数的命令
#define B_CMD _IOR(0XEF, 0x2,int) //从驱动读取int数据
#define C_CMD _IOW(0XEF, 0x3, int) //向驱动写入int数据#define D_CMD _IOW(0XEF, 0x4, int) //双向数据传输
通过ioctl()函数发送给驱动
ioctl(fd, cmd, arg);
fd是open()函数打开的返回值——文件描述符:fd = open(filename, O_RDWR);
cmd是命令宏定义
arg是参数
二、中断处理
每个中断都有一个中断号,通过中断号即可区分不同的中断,在 Linux 内核中使用一个 int 变量表示中断号
1、中断API函数
request_irq 函数
在 Linux 内核中使用中断是需要request_irq 函数进行申请, request_irq函数可能会导致睡眠,因此不能在中断上下文或者其他禁止睡眠的代码段中使用 request_irq 函数。request_irq 函数原型
如下:
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev)
irq:要申请中断的中断号。
handler:中断处理函数,当中断发生以后就会执行此中断处理函数。
flags:中断标志,可以通过“|”来实现多种组合
name:中断名字,设置以后可以在/proc/interrupts 文件中看到对应的中断名字。
dev:如果将 flags 设置为 IRQF_SHARED 的话,dev 用来区分不同的中断,一般情况下将dev 设置为设备结构体,dev 会传递给中断处理函数 irq_handler_t 的第二个参数。
返回值:0 中断申请成功,其他负值 中断申请失败,如果返回-EBUSY 的话表示中断已经
被申请了。
free_irq 函数
free_irq 函数用于使用完成后释放掉相应的中断。如果中断不是共享的,那么 free_irq 会删除中断处理函数并且禁止中断。 free_irq函数原型如下所示:
void free_irq(unsigned int irq,
void *dev)
irq:要释放的中断的中断号。
dev:如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。共享中断
只有在释放最后中断处理函数的时候才会被禁止掉。
返回值:无。
中断处理函数
使用 request_irq 函数申请中断的时候需要设置中断处理函数,中断处理函数格式如下所示:
irqreturn_t (*irq_handler_t) (int, void *)
第一个参数是要中断处理函数要相应的中断号。
第二个参数是一个指向 void 的指针,也就是个通用指针,需要与 request_irq 函数的 dev 参数保持一致。用于区分共享中断的不同设备,dev 也可以指向设备数据结构。
中断使能与禁止函数
void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)
enable_irq 和 disable_irq 用于使能和禁止指定的中断,irq 就是要禁止的中断号。
disable_irq函数要等到当前正在执行的中断处理函数执行完才返回,因此使用者需要保证不会产生新的中断,并且确保所有已经开始执行的中断处理程序已经全部退出。
void disable_irq_nosync(unsigned int irq)
isable_irq_nosync 函数调用以后立即返回,不会等待当前中断处理程序执行完毕。
local_irq_enable()
local_irq_disable()
local_irq_enable 用于使能当前处理器中断系统,local_irq_disable 用于禁止当前处理器中断
系统。
local_irq_save(flags)
local_irq_restore(flags)
local_irq_save 函数用于禁止中断,并且将中断状态保存在 flags 中。
local_irq_restore 用于恢复中断,将中断到 flags 状态。
2、上半部和下半部
上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可
以放在上半部完成。
下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部
去执行,这样中断处理函数就会快进快出。
上半部直接通过编写中断处理函数进行处理,下半部机制主要有软中断、tasklet和工作队列等,主要使用tasklet
tasklet
Linux 内核使用 tasklet_struct 结构体来表示 tasklet
struct tasklet_struct{
struct tasklet_struct *next; /* 下一个 tasklet*/
unsigned long state; /* tasklet 状态*/
atomic_t count; /* 计数器,记录对 tasklet 的引用数 */
void (*func)(unsigned long); /* tasklet 执行的函数*/
unsigned long data; /* 函数 func 的参数*/};
其中func 函数就是 tasklet 要执行的处理函数,用户定义函数内容,相当于中断处理函数。
首先要定义一个tasklet
struct tasklet_struct testtasklet;
然后可以在驱动入口函数中对其进行初始化
tasklet_init(&testtasklet, testtasklet_func, data);
初始化函数原型是
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long),
unsigned long data);
t:要初始化的 tasklet
func:tasklet 的处理函数。
data:要传递给 func 函数的参数
然后在中断回调函数中,调度tasklet
void testtasklet_func(unsigned long data){
/* tasklet 具体处理内容 */
}
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id){
......
tasklet_schedule(&testtasklet);/* 调度 tasklet*/
......
}
tasklet_schedule 函数原型如下:
void tasklet_schedule(struct tasklet_struct *t)
t:要调度的 tasklet,
3、设备树中断信息节点
如果使用设备树的话就需要在设备树中设置好中断属性信息,Linux 内核通过读取设备树
中的中断属性信息来配置中断。
假设以按键作为设备输入使用中断
在按键节点下添加 interrupt-parent和 interrupts 属性
key {
#address-cells = <1>;
#size-cells = <1>;
compatible = "atkalpha-key";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_key>;
key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>; /* KEY0 */
interrupt-parent = <&gpio1>;
interrupts = <18 IRQ_TYPE_EDGE_BOTH>; /* FALLING RISING */
status = "okay";};
IRQ_TYPE_EDGE_BOTH 表示上升沿和下降沿同时有效
IRQ_TYPE_EDGE_BOTH =RQ_TYPE_EDGE_RISING(上升沿)+IRQ_TYPE_EDGE_FALLING(下降沿)
4、获取中断号
irq_of_parse_and_map 函数
中断信息已经写到了设备树里面,通过 irq_of_parse_and_map 函数从 interupts 属性中提取到对应的设备号,函数原型如下:
unsigned int irq_of_parse_and_map(struct device_node *dev,
int index)
dev:设备节点。
index:索引号,interrupts 属性可能包含多条中断信息,通过 index 指定要获取的信息。
返回值:中断号。
gpio_to_irq 函数
如果使用 GPIO 的话,可以使用 gpio_to_irq 函数来获取 gpio 对应的中断号,函数原型如
下:
int gpio_to_irq(unsigned int gpio)
gpio:要获取的 GPIO 编号。
返回值:GPIO 对应的中断号。
5、中断使用流程
(1) 修改设备树节点
interrupt-parent和 interrupts 属性
(2)定义中断 IO 描述结构体 ,用于申请中断
struct irq_keydesc {
int gpio; /* gpio */
int irqnum; /* 中断号 */
unsigned char value; /* 按键对应的键值 */
char name[10]; /* 名字 */
irqreturn_t (*handler)(int, void *);
};
(3)在设备结构体中添加中断IO描述结构体
struct imx6uirq_dev{
dev_t devid;
struct cdev cdev;.........
struct irq_keydesc irqkeydesc[KEY_NUM];
.......
};
这里使用了结构体数组因为可能不止一个中断,KEY_NUM需要宏定义
(4)设备树获取属性
imx6uirq.nd = of_find_node_by_path("/key");//获取节点
imx6uirq.irqkeydesc[i].gpio = of_get_named_gpio(imx6uirq.nd,"key-gpio", 0);//获取gpio
gpio_request(imx6uirq.irqkeydesc[i].gpio,imx6uirq.irqkeydesc[i].name);//申请IO
gpio_direction_input(imx6uirq.irqkeydesc[i].gpio);//设置为输入
imx6uirq.irqkeydesc[i].irqnum = irq_of_parse_and_map(imx6uirq.nd, i);//获取中断号
imx6uirq.irqkeydesc[0].handler = key0_handler;//指定中断函数
ret = request_irq(imx6uirq.irqkeydesc[i].irqnum,
imx6uirq.irqkeydesc[i].handler,
IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING,
imx6uirq.irqkeydesc[i].name, &imx6uirq);//申请中断
(5)编写中断服务函数
static irqreturn_t key0_handler(int irq, void *dev_id){}
(6)驱动出口函数释放中断
free_irq(imx6uirq.irqkeydesc[i].irqnum, &imx6uirq);
gpio_free(imx6uirq.irqkeydesc[i].gpio);
(还包括释放设备号等)