中断和时钟技术可以提升驱动程序的效率
中断
中断在Linux中的实现
通常情况下,一个驱动程序只需要申请中断,并添加中断处理函数就可以了,中断的到达和中断函数的调用都是内核实现框架完成的。所以程序员只要保证申请了正确的中断号及编写了正确的中断处理函数即可。
中断的宏观分类
1.硬中断
由系统硬件产生的中断。系统硬件通常引起外部事件。外部事件事件具有随机性和突发性,因此硬件中断也具有随机性和突发性。
2.软中断
软中断是执行中断指令时产生的。软中断不用外设施加中断请求信号,因此软中断的发生不是随机的是程序安排好的。汇编程序中软中断指令int n,n必须是中断向量。
处理器接收软中断有两个来源:(1)处理器执行到错误的指令代码;(2)软件产生中断,如进程的调度就是使用的软中断。
中断产生的位置分类
1.外部中断
外部中断一般是由计算机外设发出的中断请求,如键盘中断、定时器中断等。外部中断是可以通过编程方式给予屏蔽的。
2.内部中断
内部中断指因硬件出错(如突然断电、奇偶校验错等)或运算出错(除数为0、运算溢出、单步中断等)所引起的中断。内部中断是不可屏蔽的中断。通常情况下,大多数内部中断由Linux内核进行了处理,所以驱动程序员不需要关心这些问题。
同步和异步中断
1.同步中断
同步中断是指令执行的过程中由CPU控制的,CPU在执行完一条指令后才发出中断。也就是说,在指令执行的过程中,即时有中断的到来,只要指令还没执行完,CPU就不会去执行该中断。同步中断一般是因为程序错误所引起的,例如内存管理中的缺页中断,被0除出错等。当CPU决定处理同步中断时,会调用异常处理函数,使系统从错误的状态恢复过来。当错误不可恢复时,就会死机会蓝屏。
2.异步中断
异步中断是由硬件设备随机产生的,产生中断时并不考虑与处理器时钟同步问题,该类型的中断时可以随时产生。例如在网卡驱动程序中,当网卡接收到数据包后,会向CPU发送一个异步中断事件,表示数据到来,CPU并不知道何时将接收该事件。异步中断的中断处理函数与内核的执行顺序是异步执行的,两者没有必然的联系,也不会互相影响。
中断是否可屏蔽
1. 可屏蔽中断
2. 不可屏蔽中断
中断入口跳转方法
1. 向量中断
采用向量中断的CPU 通常为不同的中断分配不同的中断号,当检测到某中断号的中断到来后,就自动跳转到与该中断号对应的地址执行。不同中断号的中断有不同的入口地址。
2. 非向量中断
非向量中断的多个中断共享一个入口地址,进入该入口地址后再通过软件判断中断标志来识别具体是哪个中断。
也就是说,向量中断由硬件提供中断服务程序入口地址,非向量中断由软件提供中断服务程序入口地址。
非向量中断服务程序典型结构
irq_handler()
{
...
int int_src = read_int_status(); /*读硬件的中断相关寄存器*/
switch (int_src) {/*判断中断源*/
case DEV_A:
dev_a_handler();
break;
case DEV_B:
dev_b_handler();
break;
...
default:
break;
}
...
}
定时器在硬件上也依赖中断来实现,图10.1 所示为典型的嵌入式微处理内可编程间隔定时器(PIT)的工作原理,它接收一个时钟输入,当时钟脉冲到来时,将目前计数值增1 并与预先设置的计数值(计数目标)比较,若相等,证明计数周期满,产生定时器中断并复位目前计数值。
嵌入式系统以及X86 PC 中大多包含可编程中断控制器(PIC),许多MCU 内部就集成了PIC。如在80386 中,PIC 是两片i8259A 芯片的级联。通过读写PIC 的寄存器,程序员可以屏蔽/使能某中断及获得中断状态,前者一般通过中断MASK 寄存器完成,后者一般通过中断PEND 寄存器完成。
中断的实现过程
中断的实现过程较为复杂,涉及中断信号线、中断控制器等概念。
中断处理程序架构
Linux 将中断处理程序分解为两个半部:顶半部(top half)和底半部(bottom half)。
顶半部完成尽可能少的比较紧急的功能,它往往只是简单地读取寄存器中的中断状态并清除中断标志后就进行“登记中断”的工作。
底半部来完成中断事件的绝大多数任务。底半部几乎做了中断处理程序所有的事情,而且可以被新的中断打断,这也是底半部和顶半部的最大不同,因为顶半部往往被设计成不可中断。底半部则相对来说并不是非常紧急的,而且相对比较耗时,不在硬件中断服务程序中执行。
如果中断要处理的工作本身很少,则完全可以直接在顶半部全部完成。
查看/proc/interrupts文件可以获得系统中中断的统计信息。在单处理器的系统中,第1列是中断号,第2列是向CPU0产生该中断的次数,之后的是对于中断的描述。
1.中断信号线(IRQ)
中断信号线是对中断输入线和中断输出线的统称。中断输入线是指接收中断信息的引脚。中断输出线是指发送中断信息的引脚。每一个能够产生中断的外设都有一条或者多条中断输出线(简称IRQ),用来通知处理器产生中断。
2.中断控制器
中断控制器位于ARM处理器核心和中断源之间。外部中断源将中断发送到中断控制器。中断控制器根据优先级进行判断,然后通过引脚将中断请求发送给ARM处理器核心。
当外部中断同时产生中断时,中断优先级产生逻辑会判断哪一个中断将被执行。中断屏蔽寄存器:当 屏蔽位为0表示对应的中断可以正常执行,否则对应的中断被禁止。
中断处理过程
产生中断->跳转到中断向量表入口地址->asm_do_IRQ()中断处理公共段
中断编程
中断的安装与释放
当设备需要中断功能时,应该安装中断。如果驱动程序员没有通过安装中断的方式通知Linux内核需要使用中断,那么内核只会简单的应答并且忽略该中断。
1.申请中断线
申请中断线可以使内核指导外设应该使用哪一个中断号,哪一个中断处理函数。
函数实现:kernel/irq/Manage.c
request_irq(unsigned int irq, irq_handler_t handler, unsigned long irqflags,
const char *name, void *dev_id)
{
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}
*********************************************************************
irq 是要申请的硬件中断号。
handler 是向系统登记的中断处理函数(顶半部),是一个回调函数,中断发生时,系统调用这个函数,dev_id 参数将被传递给它。
irqflags 是中断处理的属性,可以指定中断的触发方式以及处理方式。
在触发方式方面,可以是IRQF_TRIGGER_RISING、IRQF_TRIGGER_FALLING、IRQF_TRIGGER_HIGH、IRQF_TRIGGER_LOW 等。
在处理方式方面,若设置了IRQF_DISABLED,表明中断处理程序是快速处理程序,快速处理程序被调用时屏蔽所有中断,慢速处理程序则不会屏蔽其他设备的驱动;若设置了IRQF_SHARED,则表示多个设备共享中断,
name 使用字符串表示中断的属主
dev_id 在中断共享时会用到,一般设置为这个设备的设备结构体或者NULL(若未用到中断共享)。
关于这几个参数更详细的信息参考Linux驱动设计——中断与时钟的后续章节
**********************************************************************
request_irq()返回0 表示成功,返回-EINVAL 表示中断号无效或处理函数指针为NULL,返回-EBUSY 表示中断已经被占用且不能共享。
顶半部handler 的类型irq_handler_t 定义为:
typedef irqreturn_t (*irq_handler_t)(int, void *);
typedef int irqreturn_t;
2.释放中断线 kernel/irq/Manage.c
void free_irq(unsigned int irq, void *dev_id)
{
kfree(__free_irq(irq, dev_id));
}
使能和屏蔽中断
屏蔽一个中断源
void disable_irq(int irq); //等待当前的中断处理完成,不能用在中断处理的的顶半部(因为disable会等待中断顶半部处理完成才会退出),否则会造成系统死锁。
void disable_irq_nosync(int irq); //立即返回
void enable_irq(int irq);
屏蔽本CPU 内的所有中断:
#define local_irq_save(flags) ... //会将目前中断状态存在flags(unsigned long类型)中,直接传递而不是通过指针。
void local_irq_disable(void); //只禁用中断不存储状态
底半部机制
实现底半部的机制主要有tasklet、工作队列、和软中断。
1. tasklet
使用时只需要定义tasklet 及其处理函数并将两者关联。
void my_tasklet_func(unsigned long); /*定义一个处理函数*/
DECLARE_TASKLET(my_tasklet, my_tasklet_func, data); /*定义一个tasklet 结构my_tasklet, 与my_tasklet_func(data)函数相关联*/
代码DECLARE_TASKLET(my_tasklet,my_tasklet_func,data)实现了定义名称为my_tasklet 的tasklet 并将其与my_tasklet_func()这个函数绑定,而传入这个函数的参数为data。
在需要调度tasklet 的时候引用一个tasklet_schedule()函数就能使系统在适当的时候进行调度运行:
tasklet_schedule(&my_tasklet);
tasklet使用模板
顶半部通过tasklet_schedule()【此处为&xxx_tasklet】指定tasklet结构[xxx_tasklet],然后DECLARE_TASKLET(xxx_tasklet,xxx_do_tasklet,0)将此tasklet结构与其底半部函数关联起来。
1 /*定义tasklet 和底半部函数并关联*/
2 void xxx_do_tasklet(unsigned long);
3 DECLARE_TASKLET(xxx_tasklet, xxx_do_tasklet, 0);
4
5 /*中断处理底半部*/
6 void xxx_do_tasklet(unsigned long)
7 {
8 ...
9 }
10
11 /*中断处理顶半部*/
12 irqreturn_t xxx_interrupt(int irq, void *dev_id) //中断处理函数
13 {
14 ...
15 tasklet_schedule(&xxx_tasklet);
16 ...
17 }
18
19 /*设备驱动模块加载函数*/
20 int _ _init xxx_init(void)
21 {
22 ...
23 /*申请中断*/
24 result = request_irq(xxx_irq, xxx_interrupt,
25 IRQF_DISABLED, "xxx", NULL);
26 ...
27 return IRQ_HANDLED;
28 }
29
30 /*设备驱动模块卸载函数*/
31 void _ _exit xxx_exit(void)
32 {
33 ...
34 /*释放中断*/
35 free_irq(xxx_irq, xxx_interrupt);
36 ...
37 }
2. 工作队列(使用方法与tasklet非常相似)
定义一个工作队列和一个底半部执行函数:
struct work_struct my_wq; /*定义一个工作队列*/
void my_wq_func(unsigned long); /*定义一个处理函数*/
通过INIT_WORK()可以初始化这个工作队列并将工作队列与处理函数绑定:
INIT_WORK(&my_wq, (void (*)(void *)) my_wq_func, NULL);
/*初始化工作队列并将其与处理函数绑定*/
与tasklet_schedule()对应的用于调度工作队列执行的函数为schedule_work(),如:
schedule_work(&my_wq);/*调度工作队列执行*/
工作队列使用模板
类似tasklet,首先定义一个[struct work_struct xxx;]工作队列,及其对应的处理函数,然后使用INIT_WORK()将工作队列与其处理函数绑定,然后在中断的顶半部执行schedule_work()即可。
1 /*定义工作队列和关联函数*/
2 struct work_struct xxx_wq; //1
3 void xxx_do_work(unsigned long); //2
4
5 /*中断处理底半部*/
6 void xxx_do_work(unsigned long)
7 {
8 ...
9 }
10
11 /*中断处理顶半部*/
12 irqreturn_t xxx_interrupt(int irq, void *dev_id, struct pt_regs *regs)
13 {
14 ...
15 schedule_work(&xxx_wq); //4
16 ...
17 return IRQ_HANDLED;
18 }
19
20 /*设备驱动模块加载函数*/
21 int xxx_init(void)
22 {
23 ...
24 /*申请中断*/
25 result = request_irq(xxx_irq, xxx_interrupt,
26 IRQF_DISABLED, "xxx", NULL);
27 ...
28 /*初始化工作队列*/
29 INIT_WORK(&xxx_wq, (void (*)(void *)) xxx_do_work, NULL); //3
30 ...
31 }
32
33 /*设备驱动模块卸载函数*/
34 void xxx_exit(void)
35 {
36 ...
37 /*释放中断*/
38 free_irq(xxx_irq, xxx_interrupt);
39 ...
40 }
与tasklet代码模板不同的是,上述程序在设计驱动模块加载函数中增加了初始化工作队列的代码(第29 行)。
注意:
尽管Linux 社区多建议在设备第一次打开时才申请设备的中断并在最后一次关闭时释放中断以尽量减少中断被这个设备占用的时间,但是,许多情况下,驱动工程师还是将中断申请和释放的工作放在了设备驱动的模块加载和卸载函数中。why?
3. 软中断
软中断(softirq)也是一种传统的底半部处理机制,它的执行时机通常是顶半部返回的时候,tasklet 是基于软中断实现的,因此也运行于软中断上下文。
softirq_action 结构体表征一个软中断,这个结构体中包含软中断处理函数指针和传递给该函数的参数。使用open_softirq()函数可以注册软中断对应的处理函数,而raise_softirq()函数可以触发一个软中断。
软中断和tasklet 运行于软中断上下文,仍然属于原子上下文的一种,而工作队列则运行于进程上下文。因此,软中断和tasklet 处理函数中不能睡眠,而工作队列处理函数中允许睡眠。
local_bh_disable()和local_bh_enable()是内核中用于禁止和使能软中断和tasklet 底半部机制的函数。
内核中采用softirq 的地方包括HI_SOFTIRQ、TIMER_SOFTIRQ、NET_TX_SOFTIRQ、NET_RX_SOFTIRQ、SCSI_SOFTIRQ、TASKLET_SOFTIRQ 等,一般来说,驱动的编写者不会也不宜直接使用softirq。
硬中断、软中断和信号的区别:
硬中断是外部设备对CPU 的中断,软中断是中断底半部的一种处理机制,而信号则是由内核(或其他进程)对某个进程的中断。在论及系统调用的场合,人们也常说通过软中断(例如ARM 为swi)陷入内核,此时软中断的概念是指由软件指令引发的中断,和我们这个地方说的softirq 是两个完全不同的概念。
S3C6410实时时钟中断实例
S3C6410 处理器内部集成了实时钟(RTC)模块,该模块能够在系统断电的情况下由后备电池供电继续工作,其主要功能相对于一个时钟,记录年、月、日、时、分、秒等。S3C6410 的RTC可产生两种中断:周期节拍(tick)中断和报警(alarm)中断,前者相当于一个周期性的定时器,后者相当于一个“闹钟”,它在预先设定的时间到来时产生中断。
/*open函数*/
static int s3c_rtc_open(struct device *dev)
{
struct platform_device *pdev = to_platform_device(dev);
struct rtc_device *rtc_dev = platform_get_drvdata(pdev);
int ret;
/*申请alarm 中断*/
ret = request_irq(s3c_rtc_alarmno, s3c_rtc_alarmirq,
IRQF_DISABLED, "s3c2410-rtc alarm", rtc_dev); if (ret) {
dev_err(dev, "IRQ%d error %d\n", s3c_rtc_alarmno, ret);
return ret;
} /*申请tick 中断*/
ret = request_irq(s3c_rtc_tickno, s3c_rtc_tickirq,
IRQF_DISABLED, "s3c2410-rtc tick", rtc_dev); if (ret) {
dev_err(dev, "IRQ%d error %d\n", s3c_rtc_tickno, ret);
goto tick_err;
} return ret; tick_err:
free_irq(s3c_rtc_alarmno, rtc_dev);
return ret;
} /*release函数*/
static void s3c_rtc_release(struct device *dev)
{
struct platform_device *pdev = to_platform_device(dev);
struct rtc_device *rtc_dev = platform_get_drvdata(pdev); s3c_rtc_setpie(dev, );
/*释放中断*/
free_irq(s3c_rtc_alarmno, rtc_dev);
free_irq(s3c_rtc_tickno, rtc_dev);
} /*中断处理*/
static irqreturn_t s3c_rtc_alarmirq(int irq, void *id)
{
struct rtc_device *rdev = id; rtc_update_irq(rdev, , RTC_AF | RTC_IRQF); s3c_rtc_set_bit_byte(s3c_rtc_base,S3C2410_INTP,S3C2410_INTP_ALM); return IRQ_HANDLED;
} static irqreturn_t s3c_rtc_tickirq(int irq, void *id)
{
struct rtc_device *rdev = id; rtc_update_irq(rdev, , RTC_PF | RTC_IRQF); s3c_rtc_set_bit_byte(s3c_rtc_base,S3C2410_INTP,S3C2410_INTP_TIC); return IRQ_HANDLED;
} /*实时时钟更新rtc_update_irq()*/
void rtc_update_irq(struct rtc_device *rtc,
unsigned long num, unsigned long events)
{
spin_lock(&rtc->irq_lock);
rtc->irq_data = (rtc->irq_data + (num << )) | events;
spin_unlock(&rtc->irq_lock); spin_lock(&rtc->irq_task_lock);
if (rtc->irq_task)
rtc->irq_task->func(rtc->irq_task->private_data);
spin_unlock(&rtc->irq_task_lock); wake_up_interruptible(&rtc->irq_queue);
kill_fasync(&rtc->async_queue, SIGIO, POLL_IN);
}
S3C6410 实时钟驱动的中断处理比较简单,没有明确地分为上下两个半部,而只存在顶半部。
rtc_update_irq()函数定义于drivers/rtc/interface.c 文件中,被各种实时钟驱动共享。
上述中断处理程序并没有底半部(或者说没有严格意义上的tasklet、工作队列或软中断底半部),实际上,它只是唤醒一个等待队列rtc->irq_queue 并发出一个SIGIO 信号,而这个等待队列的唤醒也将导致一个阻塞的进程被执行(这个阻塞的进程可看作底半部)。现在我们看到,等待队列可以作为中断处理程序顶半部和进程同步的一种良好机制。但是,任何情况下,都不能在顶半部等待一个等待队列,而只能唤醒。
中断共享
多个设备共享一根硬件中断线。
中断共享的使用方法:
(1)共享中断的多个设备在申请中断时,都应该使用IRQF_SHARED 标志,而且一个设备以IRQF_SHARED 申请某中断成功的前提是该中断未被申请,或该中断虽然被申请了,但是之前申请该中断的所有设备也都以IRQF_SHARED 标志申请该中断。
(2)尽管内核模块可访问的全局地址都可以作为request_irq(…,void *dev_id)的最后一个参数dev_id,但是设备结构体指针显然是可传入的最佳参数。
(3)在中断到来时,会遍历执行共享此中断的所有中断处理程序,直到某一个函数返回IRQ_HANDLED。在中断处理程序顶半部中,应迅速地根据硬件寄存器中的信息比照传入的dev_id 参数判断是否是本设备的中断,若不是,应迅速返回IRQ_NONE,如图所示。
共享中断的编程模板
1 /*中断处理顶半部*/
2 irqreturn_t xxx_interrupt(int irq, void *dev_id, struct pt_regs *regs)
3 {
4 ...
5 int status = read_int_status();/*获知中断源*/
6 if(!is_myint(dev_id,status)) /*判断是否是本设备中断*/
7 return IRQ_NONE; /*不是本设备中断,立即返回*/
8
9 /* 是本设备中断,进行处理 */
10 ...
11 return IRQ_HANDLED; /* 返回IRQ_HANDLED 表明中断已被处理 */
12 }
13
14 /*设备驱动模块加载函数*/
15 int xxx_init(void)
16 {
17 ...
18 /*申请共享中断*/
19 result = request_irq(sh_irq, xxx_interrupt,
20 IRQF_SHARED, "xxx", xxx_dev);
21 ...
22 }
23
24 /*设备驱动模块卸载函数*/
25 void xxx_exit(void)
26 {
27 ...
28 /*释放中断*/
29 free_irq(xxx_irq, xxx_interrupt);
30 ...
31 }
按键中断实例
按键设备原理图
与按键相连的端口寄存器
GPGCON
GPGDAT
GPGUP(端口上拉寄存器)
各寄存器的设置
按键中断实例程序分析
初始化函数 s3c2440_buttons_init()主要负责模块的初始化工作。模块初始化包括设置中断触发方式,注册中断号等。
static int __init s3c2440_buttons_init(void)
{
int ret;
/*设置按键K1为下降沿中断*/
set_irq_type(K1_IRQ1, IRQ_TYPE_EDGE_FALLING);
/*注册中断处理函数*/
ret=request_irq(K1_IRQ1, isr_button,SA_INTERRUPT,DEVICE_NAME,NULL);
if(ret)
{
printk("K1_IRQ1: could not register interrupt\n");
return ret;
}
printk(DEVICE_NAME "initialized\n");
return ;
}
view code
int set_irq_type(unsigned int irq, unsigned int type);
参数irq表示中断号,type表示中断触发类型(低电平触发、高电平触发、下降沿触发、上升沿触发、边沿触发)。
中断触发类型详见 include/linux/irq.h
#define IRQ_TYPE_NONE 0x00000000 /* Default, unspecified type */
#define IRQ_TYPE_EDGE_RISING 0x00000001 /* Edge rising type */
#define IRQ_TYPE_EDGE_FALLING 0x00000002 /* Edge falling type */
#define IRQ_TYPE_EDGE_BOTH (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING)
#define IRQ_TYPE_LEVEL_HIGH 0x00000004 /* Level high type */
#define IRQ_TYPE_LEVEL_LOW 0x00000008 /* Level low type */
#define IRQ_TYPE_SENSE_MASK 0x0000000f /* Mask of the above */
#define IRQ_TYPE_PROBE 0x00000010 /* Probing in progress */
ret=request_irq(K1_IRQ1,...);为按键1申请中断。K1_IRQ1是要申请的中断号。参数isr_button是中断回调函数;然后是位掩码选项SA_INTERRUPT/SA_SHIRQ/SA_SAMPLE_RANDOM;中断的拥有者;共享的中断信号线(不使用时置为NULL)。
中断处理函数 isr_button()
当按键按下时,中断会触发,然后触发中断处理函数。该函数主要是判断按键K1是否按下。
static irqreturn_t isr_button(int irq, void *dev_id, struct pt_regs *regs)
{
unsigned long GPGDAT;
GPGDAT=(unsigned long)ioremap(0x56000064,); //映射内核地址
if(irq==K1_IRQ1)
{
if((*(volatile unsigned long *)GPGDAT) & ==) //K1是否按下
{
printk("K1 is pressed\n");
}
}
return ;
}
退出函数s3c2440_buttons_exit()主要功能是释放中断线
static void __exit s3c2440_buttons_exit(void)
{
free_irq(K1_IRQ,NULL); //释放中断线
printk(DEVICE_NAME "exit\n");
}
时钟机制
Linux驱动程序中经常会使用一些时钟机制,主要是用来延时一段时间。在这段时间内硬件设备可以完成相应的工作。
时间度量
一个与时钟中断相关的全局变量HZ。时钟中断是由系统定时硬件以周期性的间隔产生,这个周期性的值由HZ来表示。
HZ一般被定义为1000,表示一秒钟时钟中断发生1000次。每当时钟中断发生时,内核内部计数器的值就会加上1。内部计数器由jiffies变量表示,当系统初始化时,这个变量被设置为0。
时间延时
C语言中,经常使用sleep()函数将程序延时一段时间,这个函数能够实现毫秒级的延时。在设备驱动程序中,很多对设备的操作也需要延时一段时间,使设备完成某些特定的任务。
Linux内核中,两种常见的延时技术
1.短时延时
当设备驱动程序需要等待硬件处理的完成时,会主动地延时一段时间。这个时间一般是几十毫秒,甚至更短的时间。例如,驱动程序向设备的某个寄存器写入数据时,由于寄存器的写入速度较慢,所以需要驱动程序等待一定的时间,然后继续执行下面的工作。
static inline void ndelay(unsigned long x);
static inline void udelay(unsigned long usecs);
static inline void msleep(unsigned int msecs);
2.长时延时
长时延时实现一般是比较当前jiffies和目标jiffies的值。长时延时可以使用忙等待来实现。
延时3秒钟的实例:
unsigned long timeout = jiffies + 3*HZ;
while(time_before(jiffies, timeout));
time_before宏简单的比较两个时间值得大小。如果参数1<参数2,返回true。
To be continue...