定时器和中断

一、定时器

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);

(还包括释放设备号等)

  • 25
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值