Linux驱动中断和定时器

目录

中断

顶半部/底半部机制

软中断:

Tasklet:

工作队列:

定时器


中断

中断是正在执行的程序被另一个程序打断,去执行另一个程序的处理函数,当执行完再返回执行被打断的程序。分为内中断(异常)和外中断(硬件中断)。

当cpu收到一个中断会去中断向量表中查找该中断的处理函数(中断上下文)和地址,然后根据地址进入处理函数。注意中断过程中不允许阻塞睡眠和进程切换,且执行时间越快越好。

驱动中使用中断相当于使用系统资源,需要申请和释放。

需要使用的头文件为:

#include

#include

 申请函数为:

int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev_id);

参数:

        irq:表示中断号(中断线),/proc/interrupts文件来查看系统已经使用的中断号

        handler:中断处理函数

        flags:中断标志可以为 0,也可能是下列一个或多个标志的位掩码

        name:系统中记录中断名称

        dev_id:用于共享中断,传递给中断处理函数的参数,非共享类型的中断,直接设置成为 NULL

返回0表示成功,非0表示失败

flags标志:

IRQF_DISABLED:此标志表明给定的中断处理程序是一个快速中断处理程序,除了时钟中断外,绝大多数中断都不使用标志

IRQF_SAMPLE_RANDOM:此标志表明这个设备产生的中断对内核熵池有贡献

IRQF_SHARED:此标志表明可以在多个中断处理程序之间共享中断线(中断号),在同一个中断线上注册的每个处理程序必须指定这个标志。其意思就是说没有这个标志那一条中断线上只能有一个处理函数,有这个标志可以多个中断可以共享同一条中断线

上面第5个参数dev_id就是用来区分不同的中断使用同一个中断号的标志,也可以给处理函数传参

释放函数为:

void free_irq(unsigned int irq, void *dev_id)

参数:

        irq:中断号

        dev_id:用于共享中断,传递给中断处理函数的参数,非共享类型的中断,直接设置成为 NULL

下面以按键为例使用混杂设备驱动框架,实现GPIO硬件中断。不了解混杂设备或者想知道更多实现字符设备的驱动框架请看上一篇文章(多种字符设备驱动实现方式)。

//通过GPIO引脚号,获取中断号

int gpio_to_irq(unsigned gpio)

 参数:

        gpio:GPIO引脚号

返回值为中断号

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 


struct btn_res{
	int gpio;//端口号
	char *name;//名称
};

struct btn_res btn_info[] = {
	[0] = {
		.gpio = PAD_GPIO_B+9,
		.name = "Key0",
	},
	[1] = {
		.gpio = PAD_GPIO_A+28,
		.name = "Key1",
	},
	[2] = {
		.gpio = PAD_GPIO_B+30,
		.name = "Key2",
	},
	[3] = {
		.gpio = PAD_GPIO_B+31,
		.name = "Key3",
	}
};



int btn_open(struct inode *inode, struct file *filp)
{
	printk("btn_open!\n");
 
	return 0;
}


int btn_release(struct inode *inode, struct file *filp)
{
	printk("btn_release!\n");
 
	return 0;
}


//声明操作函数集合
struct file_operations btn_fops = {
	.owner = THIS_MODULE,
	.open = btn_open,
	.release = btn_release,//对应用户close接口
};


//分配初始化miscdevice
struct miscdevice btn_dev = {
	.minor = MISC_DYNAMIC_MINOR,//系统分配次设备号
	.name = "btn",//设备文件名
	.fops = &btn_fops,//操作函数集合
};


//中断处理函数
//返回IRQ_HANDLED表示成功,IRQ_NONE表示失败
irqreturn_t btn_handler(int irq, void *dev_id)
{
	int state;//引脚状态
	struct btn_res *pdata = (struct btn_res *)dev_id;//引脚数据
	
	//区分哪个按键
	//区分按下松开
	state = gpio_get_value(pdata->gpio);

	printk("key %s %s!\n",pdata->name,state?"released":"pressed");

	return IRQ_HANDLED;//处理成功
}

//加载函数
int btnirq_init(void)
{
	int ret,i,j,irq;
	//注册miscdevice
	ret = misc_register(&btn_dev);
	if(ret<0){
		printk("misc_register failed!\n");
		goto failure_misc_register;
	}

	/*ARRAY_SIZE求数组元素个数*/
	for(i=0;i

顶半部/底半部机制

我们知道中断处理函数的要求是越快越好,但是有一些场合做不到,它们需要使用中断,又不能很快的处理完。为了解决这个问题Linux将中断分为了两个部分topbottom

顶半部(top half):用来处理紧急,耗时比较短的事务,且顶半部不可被打断,底半部需要在顶半部中调度。

底半部(bottom half):用来处理不紧急,耗时较长的事务,且允许稍后完成。

底半部机制的实现有三种方式:软中断,Tasklet,工作队列。(本文重点讲解Tasklet和工作队列)

  • 软中断:

它是tasklet实现的基础(tasklet实际上只是在软中断的基础上添加了一定的机制)。软中断一般是“可延迟函数”的总称,可以并发运行在多个CPU上(即使同一类型的也可以)。所以软中断必须设计为可重入的函数(允许多个CPU同时操作),因此也需要使用自旋锁来保护其数据结构。Linux内核使用结构体softirq_action表示软中断,定义在include/linux/interrupt.h文件中。

/* 用于描述一个软中断 */
struct softirq_action
{
    /* 软中断的处理函数 */        
    void    (*action)(struct softirq_action *);
};

//软中断描述符有10个
enum
 
{
   HI_SOFTIRQ=0,
 
   TIMER_SOFTIRQ,
 
   NET_TX_SOFTIRQ,
 
   NET_RX_SOFTIRQ,
 
   BLOCK_SOFTIRQ,
 
   BLOCK_IOPOLL_SOFTIRQ,
 
   TASKLET_SOFTIRQ,
 
   SCHED_SOFTIRQ,
 
   HRTIMER_SOFTIRQ,
 
   RCU_SOFTIRQ,  /* Preferable RCU should always be the last softirq */
 
   NR_SOFTIRQS
 
};
  • Tasklet:

tasklet是利用软中断来实现的另外一种底半部机制,一个使用tasklet的中断程序首先会通过执行中断处理程序来快速完成顶半部的工作,接着通过调用tasklet使得底半部的工作得以完成,Linux内核使用结构体tasklet_struct来定义。

struct tasklet_struct
{
    struct tasklet_struct *next; //下一个tasklet
    unsigned long state;    //tasklet状态
    atomic_t count;   //计数器,记录对 tasklet 的引用数
    void (*func)(unsigned long);//tasklet的处理函数(底半部)
    unsigned long data;//传递给tasklet处理函数的参数
};

使用流程为:

//定义初始化
    struct tasklet_struct myTasklet;   //定义
    //myTaskletFunc是处理函数,data是处理函数的参数
    tasklet_init(myTasklet,myTaskletFunc,data);   //初始化
//或者
    DECLARE_TASKLET(myTasklet,myTaskletFunc,data);  //定义初始化一步到位
    
//调度tasklet
    //需要调度tasklet的时候引用一个tasklet_schedule()函数就能使系统在适当的时候进行调度
    tasklet_schedule(&myTasklet);    

还是以按键为例使用混杂设备驱动框架,实现Tasklet。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 


struct btn_res{
	int gpio;//端口号
	char *name;//名称
};

struct btn_res btn_info[] = {
	[0] = {
		.gpio = PAD_GPIO_B+9,
		.name = "Key0",
	},
	[1] = {
		.gpio = PAD_GPIO_A+28,
		.name = "Key1",
	},
	[2] = {
		.gpio = PAD_GPIO_B+30,
		.name = "Key2",
	},
	[3] = {
		.gpio = PAD_GPIO_B+31,
		.name = "Key3",
	}
};

int btn_open(struct inode *inode, struct file *filp)
{
	printk("btn_open!\n");
 
	return 0;
}


int btn_release(struct inode *inode, struct file *filp)
{
	printk("btn_release!\n");
 
	return 0;
}

//声明操作函数集合
struct file_operations btn_fops = {
	.owner = THIS_MODULE,
	.open = btn_open,
	.release = btn_release,//对应用户close接口
};


//分配初始化miscdevice
struct miscdevice btn_dev = {
	.minor = MISC_DYNAMIC_MINOR,//系统分配次设备号
	.name = "btn",//设备文件名
	.fops = &btn_fops,//操作函数集合
};

//tasklet处理函数(底半部)
void btn_tasklet_func(unsigned long data)
{
	int state;//引脚状态
	struct btn_res *pdata = (struct btn_res *)data;//引脚数据
	
	printk("btn_tasklet_func\n");
	
	//区分哪个按键
	//区分按下松开
	state = gpio_get_value(pdata->gpio);

	printk("key %s %s!\n",pdata->name,state?"released":"pressed");
}

//分配初始化tasklet
DECLARE_TASKLET(btn_tasklet, btn_tasklet_func, 0);


//中断处理函数(顶半部)
//返回IRQ_HANDLED表示成功,IRQ_NONE表示失败
irqreturn_t btn_handler(int irq, void *dev_id)
{
	btn_tasklet.data = (unsigned long)dev_id;
	//调度tasklet
	tasklet_schedule(&btn_tasklet);

	printk("left btn_handler\n");
	return IRQ_HANDLED;//处理成功
}

//加载函数
int btnirq_init(void)
{
	int ret,i,j,irq;
	//注册miscdevice
	ret = misc_register(&btn_dev);
	if(ret<0){
		printk("misc_register failed!\n");
		goto failure_misc_register;
	}

	/*ARRAY_SIZE求数组元素个数*/
	for(i=0;i

tasklet本身运行在中断上下文,处理函数不能阻塞/睡眠。

  • 工作队列:

工作队列是另外一种底半部执行方式,工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,工作队列包括工作和延时工作两种,工作队列在Linux内核中是一个结构体。

需要的头文件为:

#include  

//1、工作队列
struct work_struct {
    atomic_long_t data;
    struct list_head entry;//链表指针 把每个工作连接在一个链表上组成一个双向链表
    work_func_t func;//工作的处理函数
#ifdef CONFIG_LOCKDEP
    struct lockdep_map lockdep_map;
#endif
};

//2、延时工作队列:延时队列是在调度时,需要等待指定时间才会调用工作函数
struct delayed_work {
    struct work_struct work;//工作
    struct timer_list timer;//内核定时器,在下文会有描述
};

 一般流程为:

工作队列:
//定义初始化
    //动态定义初始化
    struct work_struct mywork;    
    INIT_WORK(&mywork,mywork_func);
    //静态定义初始化
    DECLARE_WORK(mywork,mywork_func); 
    
//调度工作队列
    schedule_work(&mywork);//空闲时调用

延时队列:
//定义初始化
    //动态定义初始化
    struct delayed_work mydelaywork; 
    INIT_DELAYED_WORK(&mydelaywork,mywork_func);
    //静态定义初始化
    DECLARE_DELAYED_WORK(mydelaywork, mywork_func); 
    
//调度延时工作队列
    schedule_delayed_work(&mydelaywork,3*HZ);//延时指定时间调用,HZ==10ms

 还是以按键为例使用混杂设备驱动框架,实现延时工作队列(实现工作队列只要把函数换一换)。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

struct btn_res{
	int gpio;//端口号
	char *name;//名称
};

struct btn_res btn_info[] = {
	[0] = {
		.gpio = PAD_GPIO_B+9,
		.name = "Key0",
	},
	[1] = {
		.gpio = PAD_GPIO_A+28,
		.name = "Key1",
	},
	[2] = {
		.gpio = PAD_GPIO_B+30,
		.name = "Key2",
	},
	[3] = {
		.gpio = PAD_GPIO_B+31,
		.name = "Key3",
	}
};

struct delayed_work mydelaywork; 
struct btn_res *pdata = NULL;

int btn_open(struct inode *inode, struct file *filp)
{
	printk("btn_open!\n");
 
	return 0;
}


int btn_release(struct inode *inode, struct file *filp)
{
	printk("btn_release!\n");
 
	return 0;
}

//声明操作函数集合
struct file_operations btn_fops = {
	.owner = THIS_MODULE,
	.open = btn_open,
	.release = btn_release,//对应用户close接口
};


//分配初始化miscdevice
struct miscdevice btn_dev = {
	.minor = MISC_DYNAMIC_MINOR,//系统分配次设备号
	.name = "btn",//设备文件名
	.fops = &btn_fops,//操作函数集合
};

//工作处理函数(底半部)
void btn_work_func(struct work_struct *work)
{
	int state;//引脚状态

	//printk("enter btn_work_func!\n");
	//区分哪个按键
	//区分按下松开
	state = gpio_get_value(pdata->gpio);

	printk("key %s %s!\n",pdata->name,state?"released":"pressed");
}

//中断处理函数(顶半部)
//返回IRQ_HANDLED表示成功,IRQ_NONE表示失败
irqreturn_t btn_handler(int irq, void *dev_id)
{
	pdata = (struct btn_res *)dev_id;//引脚数据

	//登记延时工作
	schedule_delayed_work(&mydelaywork, 3*HZ);//延时30ms处理

	printk("left btn_handler\n");
	return IRQ_HANDLED;//处理成功
}

//加载函数
int btnirq_init(void)
{
	int ret,i,j,irq;
	//注册miscdevice
	ret = misc_register(&btn_dev);
	if(ret<0){
		printk("misc_register failed!\n");
		goto failure_misc_register;
	}

	/*ARRAY_SIZE求数组元素个数*/
	for(i=0;i

工作队列和延时工作队列工作于进程上下文,使用内核线程来执行,参与任务调度,可以睡眠 。

以上我们使用了三种方式分别介绍了Linux中的中断,小伙伴需要想清楚他之间的区别和联系,接下来介绍一下定时器。

定时器

先来了解一下tick,HZ,jiffies。

  • tick:内核心跳时钟,周期性产生时钟中断,每一次时钟中断中完成系统相关的工作,HZ是tick的倒数(心跳时钟的频率)。
  • HZ:系统硬件定时器的工作频率,ARM中HZ一般都等于100,所以频率100Hz,一般有100,250,500,1000。
  •  jiffies:内核中用来表示时间的32位(unsigned long)全局变量,记录了开机以来产生了多少次时钟中断。

详细了解请看(对linux内核中jiffies+Hz表示一秒钟的理解)。

与单片机的定时器不同,Linux定时器是在当前时间上加上需要定时的时间,它会在未来到达设置的时间时产生中断。例如现在17:47,我想定时半个小时,那就把定时时间调到18:17,他会在设置好的中断,就很像闹钟。内核定时器的精度不高,不能作为高精度定时器使用,其内核定时器不是周期性运行的,超时以后就会自动关闭,因此要想实现周期性的定时,就需要在定时处理函数中重新开启定时器。

那怎么实现呢?因为jiffies记录了开机以来产生了多少次时钟中断,每次中断都是固定时间,所以相当于jiffies就是当前时间,只需要在当前时间上加上想定时的时间就可以了。如果确切的知道当前Linux系统的tick,例如HZ为100,是10ms,那可以使用jiffies+HZ表示定时1秒钟,jiffies加一表示过了10ms,加一百就是过了1秒钟,这就是前面延时工作队列中说HZ表示1秒钟。不过不推荐这样使用,有函数用来转换jiffies与时间。

//jiffies转ms
unsigned int jiffies_to_msecs(const unsigned long j);
//jiffies转us
unsigned int jiffies_to_usecs(const unsigned long j);

//ms转jiffies
unsigned long msecs_to_jiffies(const unsigned int m);
//us转jiffies
unsigned long usecs_to_jiffies(const unsigned int u);

 使用定时器需要的头文件为:

#include

定时器在Linux内核中用一个结构体来表示。

struct timer_list {
/*
* All fields that change during normal runtime grouped to the
* same cacheline
*/
    struct list_head entry;
    unsigned long expires;//定时时间,定时时间点的jiffies值
    struct tvec_base *base;

    void (*function)(unsigned long);//超时处理函数
    unsigned long data;//传递给定时处理函数的参数
}

初始化定时器

//动态初始化
struct timer_list mytimer;  
init_timer(&mytimer);
//重要的三个成员初始化
mytimer.expires = jiffies+msecs_to_jiffies(2000); //jiffies+定时时间
mytimer.function = mytimer_function;              //超时处理函数
mytimer.data = data;                              //定时处理函数参数


//静态初始化
DEFINE_TIMER(_name,_function,_expires,_data);
//_name:定时器结构体名称
//_function:定时处理函数
//_expires:定时时间
//_data:定时处理函数参数

向内核添加一个定时器。

void add_timer(struct timer_list *timer);

向内核删除一个定时器。 

int del_timer(struct timer_list * timer);

//返回0表明定时器没有被激活,返回1表示定时器已经激活。

修改定时器时间。

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

//用于修改定时值,如果定时器还没有被激活,该函数可以激活定时器。

//返回0表明调用mod_timer函数前定时器没有被激活,返回1表明调用mod_timer函数前定时器已经激活。

//mod_timer = del_timer + 修改expires + add_timer

下面以LED2秒为周期闪烁为例使用混杂设备驱动框架,实现定时器。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 


struct led_dest{
	int gpio;//gpio端口号
	char *name;//名称
};

//定义led的硬件信息
struct led_dest led_info[] = {
	[0] = {
		.gpio = PAD_GPIO_E+13,
		.name = "LED0",
	},
	[1] = {
		.gpio = PAD_GPIO_C+17,
		.name = "LED1",
	},
	[2] = {
		.gpio = PAD_GPIO_C+8,
		.name = "LED2",
	},
	[3] = {
		.gpio = PAD_GPIO_C+7,
		.name = "LED3",
	}
};

//分配内核定时器
struct timer_list mytimer;

int led_open(struct inode *inode, struct file *filp)
{
	printk("led_open!\n");
 
	return 0;
}


int led_release(struct inode *inode, struct file *filp)
{
	printk("led_release!\n");
 
	return 0;
}


//声明操作函数集合
struct file_operations led_fops = {
	.owner = THIS_MODULE,
	.open = led_open,
	.release = led_release,//对应用户close接口
};


//分配初始化miscdevice
struct miscdevice led_dev = {
	.minor = MISC_DYNAMIC_MINOR,//系统分配次设备号
	.name = "led",//设备文件名
	.fops = &led_fops,//操作函数集合
};

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

	//将GPIO电平取反
	gpio_set_value(led_info[0].gpio,!gpio_get_value(led_info[0].gpio));
	
	mod_timer(&mytimer, jiffies+2*HZ);//重置定时器
}

//加载函数
int mytimer_init(void)
{
	int ret,i;
	
	//注册miscdevice
	ret = misc_register(&led_dev);
	if(ret<0){
		printk("misc_register failed!\n");
		goto failure_misc_register;
	}
	//申请gpio资源并初始化
	for(i=0;i

好了,以上就是Linux中断和定时器的内容了,有什么疑问和建议欢迎在评论区提出来喔。 

 

你可能感兴趣的:(Linux驱动,Linux,嵌入式硬件,驱动开发,linux)