Linux异常处理体系结构
嵌入式Linux设备驱动开发之Linux异常处理体系结构
《嵌入式Linux应用完全开发手册》第4篇第20章总结归纳
本章目标
- 了解Linux异常处理体系结构
- 掌握Linux中断处理体系结构,了解几种重要的数据结构
- 学习中断处理函数的注册、处理、卸载流程
- 掌握在驱动程序中使用中断的方法
Linux异常处理体系结构概述
Linux异常处理的层次结构
内核的中断处理结构有很好的扩充性,并适当屏蔽了一些实现细节。但是开发人员一个深入“黑盒子”了解其中的实现原理。
异常的作用
异常,就是可以打断CPU正常运行流程的一些事情,比如外部中断、未定义的指令、视图修改只读的数据、执行swi(软中断)指令等。当这些事情发生时,CPU暂停当前的程序,先处理异常事件,然后再继续执行被中断地程序。操作系统中经常通过异常来完成一些特定地功能。
- 当CPU执行未定义的机器指令将触发“未定义指令异常”,操作系统可以利用这个特点使用一些自定义的机器指令,它们在异常处理函数中实现。
- 可以将一块数据设为只读的,然后提供给多个进程使用,这样可以节省内存。当某个进程视图修改其中的数据时,将触发“数据访问中止异常”,在异常处理函数中将这块数据复制出一份可写的副本,提供给这个进程使用。
- 当用户程序试图读写的数据或执行的指令不在内存中,也会触发一个“数据访问中止异常”或“指令预取中止异常”,在异常处理函数中将这些数据或指令读入内存(内存不足时还可以将不使用的数据、指令换出内存),然后重新执行被中断的程序。这样可以节省内存,还使得操作系统可以运行这类程序:它们使用的内存远大于实际的物理内存。
- 当程序使用不对齐的地址访问内存时,也会触发“数据访问中止异常”,在异常处理程序中先使用多个对齐的地址读出数据;对于读操作,从中选取数据组合好后返回给被中断的程序;对于写操作,修改其中的部分数据后再写入内存。这使得程序(特别是应用程序)不用考虑地址对齐的问题。
- 应用程序可以通过“swi”指令触发“swi异常”,操作系统在swi异常处理函数中实现各种系统调用。
Linux内核对异常的设置
内核在start_kernel
函数中调用trap_init
、init_IRQ
两个函数来设置异常的处理函数。
trap_init函数分析
trap_init
函数(arch/arm/kernel/trap.c
)被用来设置各种异常的处理向量,包括中断向量。所谓向量,就是一些被安放在固定位置的代码,当发生异常时,CPU会自动执行这些固定位置上的指令。ARM架构的CPU的异常向量基址可以是0x00000000,也可以是0xffff0000,Linux内核使用后者。trap_init
函数将异常向量复制到0xffff0000处,部分代码如下:
1 | void __init trap_init(void) |
第4行中,vectors等于0xffff0000。地址__vectors_start-_vectors_end之间的代码就是异常向量,在arch/arm/kernel/entry-armv.S
中定义,它们被复制到地址0xffff0000处。
异常向量的代码很简单,它们只是一些跳转指令。发生异常时,CPU自动执行这些指令,跳转去执行更复杂的代码,比如保存被中断程序的执行环境,调用异常处理函数,恢复被中断程序的执行环境并重新运行。这些“更复杂的代码”在地址__stubs_start-__stubs_end之间,它们在arch/arm/kernel/entry-armv.S
中定义。第5行将它们复制到地址0xffff0000+0x200处。
异常向量跳去执行的代码都是使用汇编写的,为给读者一个形象概念,下面讲解部分代码,他们在arch/zarm/kernel/entry-armv.S
中。
异常向量的代码如下,其中的“stubs_offset”用来重新定位跳转的位置(向量被复制到地址0xffff0000处,跳转的目的代码被复制到地址0xffff0000+0x200处)。
1 | .equ stubs_offset, __vectors_start + 0x200 - __stubs_start |
其中的vector_und、vector_pabt等表示要跳转去执行的代码。以vector_und为例,它仍在arch/arm/kernel/entry-armv.S
中,通过vector_stub宏来定义,代码如下:
1 | vector_stub und, UND_MODE |
第1行的vector_stub是一个宏,它根据后面的参数“und,UND_MODE”定义了以“vector_und”为标号的一段代码。vector_stub宏的功能为:计算处理完异常后的返回地址、保存一些寄存器(比如r0、lr、spsr),然后进入管理模式,最后根据被中断的工作模式调用第3-18行中的某个跳转分支。当发生异常时,CPU会根据异常的类型进入某个工作模式,但是很快vector_stub宏又会强制CPU进入管理模式,在管理模式下进行后续处理,这种方法简化了程序设计,使得异常发生前的工作模式要么是用户模式,要么是管理模式。
第3-18行中的代码表示在各个工作模式中执行未定义指令时,发生的异常的处理分支。比如第3行的__und_usr
表示在用户模式下执行未定义指令时,所发生的未定义异常将由它来处理;第6行的__und_svc
表示在管理模式下执行未定义指令,所发生的异常将由它来处理。在其他工作模式下不可能发生未定义指令异常,否则使用__und_invalid
来处理错误。ARM架构CPU中使用4位数据表示工作模式(目前只有7种工作模式),所有共有16个跳转分支。
不同的跳转分支(比如__und_usr
、__und_svc
)只是在它们的入口处(比如保存被中断程序的寄存器)稍有差别,后续的处理大体相同,都是调用相应的C函数。比如未定义指令异常发生时,最终会调用C函数do_undefinstr
函数进行处理。各种的异常C处理函数可以分为5类,它们分布在不同的文件种。
- 在
arch/arm/kernel/traps.c
中
未定义指令异常的C处理函数在这个文件中定义,总入口函数为do_undefinstr
。 - 在
arch/arm/mm/fault.c
中
与内存访问相关的异常的C处理函数在这个文件中定义,比如数据访问中止异常、指令预取中止异常。总入口函数为do_DataAbort
、do_PrefetchAbort
。 - 在
arch/arm/mm/irq.c
中
中断处理函数的这个文件中定义,总入口函数为asm_do_IRQ
,它调用其他文件注册的中断处理函数。 - 在
arch/arm/kernel/calls.S
中
在这个文件中,swi异常的处理函数被组织成一个表格;swi指令机器码的位[23:0]被用来作为索引。这样,通过不同的“swi index”指令就可以调用不同的swi异常处理函数,它们被称为系统调用,比如sys_open、sys_read、sys_write
。 - 没有使用的异常
在Linux 2.6.22.6中没有使用FIQ异常。trap_init
函数搭建了各类异常的处理框架。当发生异常时,各种C处理函数会被调用。这些C函数还要进一步细分异常发生的情况,分别调用更具体的处理函数。比如未定义指令异常的C处理函数总入口为do_undefinstr
,这个函数里还要根据具体的未定义指令调用它的模拟函数。
除了中断外,内核已经为各类异常准备了细致而完备的处理函数,比如swi异常处理函数为每一种系统调用都准备了一个sys_
开头函数,数据访问中止异常的处理函数为对齐错误、页权限错误、段翻译错误等具体异常都准备了相应的处理函数。这些异常的处理函数与开发板的配置无关,基本不用修改。
init_IRQ 函数分析
中断也是一种异常,之所以把它单独提出来,是因为中断的处理与具体开发板密切相关,除一些必须、共用的中断(系统时钟中断、片内外设UART中断)外,必须由驱动开发者提供处理函数。内核提炼出中断处理的共性,搭建了一个非常容易扩充的中断处理体系。
init_IRQ函数(代码在arch/arm/kernel/irq.c
中)被用来初始化中断的处理框架,设置各种中断的默认处理函数。当发生中断时,中断总入口函数asm_do_IRQ
就可以调用这些函数作进一步处理。
下图为异常处理体结构:
常见的异常
ARM架构Linux内核中,只用到了5种异常,在它们的处理函数中进一步细分发生这些异常的原因。
异常总类 | 异常细分 |
---|---|
未定义指令异常 | ARM 指令break Thumb 指令break ARM 指令mrc |
指令预取中止异常 | 取值时地址翻译错误(translation fault),系统中还没有为这个指令建立映射关系 |
数据访问中止异常 | 访问数据时段地址翻译错误(section translation fault) 访问数据时页地址翻译错误(page translation fault) 地址对齐错误 段权限错误(section permission fault) 页权限错误(page permission fault) … |
中断异常 | GPIO中断、WDT中断、定时器中断、USB中断、UART中断等 |
swi异常 | 各类下图调用 sys_open、sys_read、sys_write 等 |
Linux 中断处理体系结构
中断处理体系结构的初始化
中断处理体系结构
Linux内核将所有的中断统一编号,使用一个irq_desc结构数组来描述这些中断:每个数组项对应一个中断(也有可能是一组中断,它们共用相同的中断号),里面记录了中断的名称、中断状态、中断标记(比如中断类型、是否共享中断等),并提供了中断的底层硬件访问函数(清除、屏蔽、使能中断),提供了这个中断的处理函数入口,通过它可以调用用户注册的中断处理函数。
通过irq_desc结构数组就可以了解中断处理体系结构,irq_desc结构的数组类型在include/linux/irq.h
中定义,如下所示:
1 | struct irq_desc { |
第2行的handle_irq
是这个或这组中断的处理函数入口。发生中断时,总入口函数asm_do_IRQ
将根据中断号调用相应irq_desc
数组项中的handle_irq
。handle_irq
使用chip结构中的函数来清除、屏蔽或者重新使能中断,还一一调用用户在actions链表中注册的中断处理函数。
第3行的irq_chip结构类型也是在include/linux/irq.h
中定义,其中的成员大多用于操作底层硬件,比如设置寄存器以屏蔽中断、使能中断、清除中断等。这个结构的部分成员如下:
1 | struct irq_chip { |
irq_desc结构中第5行的irqaction结构类型在include/linux/interrupt.h
中定义。用户注册的每个中断处理函数用一个irqaction结构来表示,一个中断(比如共享中断)可以有多个处理函数,它们的irqaction结构链接成一个链表,以action为表头。irq_desc结构定义如下:
1 | struct irqaction { |
irq_desc结构数组、它的成员struct irq_chip *chip
、struct irqaction *action
,这3种数据结构成了中断处理体系的框架。这3者的关系如图所示:
中断的处理流程如下:
- 发生中断时,CPU执行异常向量vector_irq的代码
- 在vector_irq里面,最终会调用中断处理的总入口函数的代码
asm_do_IRQ
根据中断号调用irq_desc数组项中的handle_irq
handle_irq
会使用chip成员中的函数来设置硬件,比如清除中断、禁止中断、重新使能中断。handle_irq
逐个调用用户在action链表中注册的处理函数
可见,中断体系结构的初始化就是构造这些数据结构,比如irq_desc数组项中的handle_irq
、chip等成员;用户注册中断时就是构造action链表;用户卸载中断时就是从action链表中去除不需要的项。
中断处理体系结构的初始化
init_IRQ
函数被用来初始化中断处理体系结构,代码在arch/arm/kernel/irq.c
中。
1 | void __init init_IRQ(void) |
第5-6行初始化irq_desc结构数组中每一项的中断状态。
第8行调用架构相关的中断初始化函数。对于S3C2410、S3C2440开发板,这个函数就是s3c24xx_init_irq,移植Linux内核时讲述的machine_desc结构中的init_irq成员就是指向这个函数。
s3c24xx_init_irq函数在arch/arm/plat-s3c24xx/irq.c
中定义,它为所有的中断设置了芯片相关的数据结构(irq_desc[irq].chip
),设置了处理函数入口(irq_desc[irq].handle_irq
)。以外部中断EINT4-EINT23为例,用来设置它们的代码如下:
1 | for(irqno = IRQ_EINT4; irqno <= IRQ_EINT23; irqno++) { |
set_irq_chip
的作用就是irq_desc[irqno].chip = &s3c_irqext_chip
。以后就可以通过irq_desc[irqno].chip
结构中的函数指针设置这些外部中断的触发方式(电平触发、边沿触发等)、使能中断、禁止中断等。
第4行设置这些中断的处理函数入口为handle_edge_irq
,即irq_desc[irqno].handle_irq = handle_edge_irq
。发生中断时,handle_edge_irq
函数会调用用户注册的具体处理函数。
第5行设置中断标志为“IRQF_VALID”,表示可以使用它们。
用户注册中断处理函数的过程
用户(即驱动程序)通过request_irq
函数向内核注册中断处理函数,request_irq
函数根据中断号找到irq_desc数组项,然后在它的action链表中添加一个表项。request_irq
函数在kernel/irq/manage.c
中定义,函数原型如下:
1 | int request_irq(unsigned int irq, irq_handle_t handler, unsigned long irqflags, const char *devname, void *dev_id) |
request_irq
函数首先使用这4个参数构造一个irqaction结构,然后调用setup_irq
函数将它链入链表中,代码如下:
1 | action = kmalloc(sizeof(struct irqaction), GFP_ATOMIC); |
setup_irq
函数也是在kernel/irq/manage.c
中定义,它完成如下3个功能
- 将新建的irqaction结构链入irq_desc[irq]结构的action链表中,有两种可能。
①如果action链表为空,则直接链入。
②否则先判断新建的irqaction结构和链表中的irqaction结构所表示的中断类型是否一致,即是否都声明为“可共享的”(IRQF_SHARED)、是否都使用相同的触发方式(电平、边沿、极性),如果一致,则将新建的irqaction结构链入。 - 设置irq_desc[irq]结构中chip成员的还没设置的指针,让它们指向一些默认函数
这通过irq_chip_set_defaults
函数来完成,它在kernel/irq/chip.c
中定义。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15void irq_chip_set_defaults(struct irq_chip *chip)
{
if (!chip->enable)
chip->enable = default_enable; //它调用chip->unmask
if (!chip->disable)
chip->disable = default_disable; //此函数为空
if (!chip->startup)
chip->startup = default_startup; //它调用chip->enable
if (!chip->shutdown)
chip->shutdown = chip->disable;
if (!chip->name)
chip->name = chip->typename;
if (!chip->end)
chip->end = dummy_irq_chip.end;
} - 设置中断的触发方式
如果request_irq
函数中传入的irqflags参数表示中断的触发方式为高电平触发、低电平触发、上升沿触发或下降沿触发,则调用irq_desc[irq]结构中的chip->set_type成员函数来进行设置:设置引脚功能为外部中断,设置中断触发方式。 - 启动中断
如果irq_desc[irq]结构中status成员没有指明为IRQ_NOAUTOEN(表示注册中断时不要使能中断),还要调用chip->startup或chip->enable来启动中断。所谓启动中断通常就是使能中断。
一般来说,只有那些“可以自动使能的”中断对应irq_desc[irq].status才会被指明为IRQ_NOAUTOEN。所以,无论哪种情况,执行request_irq注册中断之后,这个中断就已经被使能了,在编写驱动程序时要注意这点。
总结一下使用request_irq
函数注册中断后的“成果”。
①irq_desc[irq]结构中的action链表中已经链入了用户注册的中断处理函数
②中断的触发方式已经被设置好
③中断已经被使能
总之,执行irq_request
函数之后,中断就可以发生并能够被处理了。
中断的处理过程
asm_do_IRQ
是中断的C语言总入口函数,它在arch/arm/kernel/irq.c
中定义,部分代码如下:
1 | asmlinkage void __exception asm_do_IRQ(unsigned int irq, struct pt_regs *regs) |
第6行的desc_handle_irq
函数直接调用desc结构中的handle_irq成员函数,它就是irq_desc[irq].handle_irq。
需要说明的是,asm_do_IRQ函数中参数irq的取值范围为IRQ_EINT0-(IRQ_EINT0 + 31),只有32个取值。它可能是一个实际中断的中断号,也可能是一组中断的中断号。这是由S3C2410、S3C2440的芯片特性决定的:发生中断时INTPND寄存器的某一位被置1,INTOFFSET寄存器中记录了是哪一位(0-31),中断向量调用asm_do_IRQ
之前根据INTOFFSET寄存器的值确定irq参数。每一个实际的中断在irq_desc数组中都有一项与它对应,它们的数目不止32。当asm_do_IRQ
函数中参数irq表示的是一组中断时,irq_desc[irqno].handle_irq来进一步处理。
以外部中断EINT8-EINT32为例,它们通常是边沿触发。
- 它们被触发时,INTOFFSET寄存器中的值都是5,
asm_do_IRQ
函数中参数irq的值为IRQ_EINT0 + 5
,即IRQ_EINT8t23
。上面代码中第6行将调用irq_desc[IRQ_EINT8t23].handle_irq
来进行处理。 irq_desc[IRQ_EINT8t23].handle_irq
在前面init_IRQ
函数初始化中断体系结构的时候被设为s3c_irq_demux_extint8
。s3c_irq_demux_extint8
函数的代码在arch/arm/plat-s3c24xx/irq.c
中,它首先读取EINTPND、EINTMASK寄存器,确定发生了哪些中断,重新计算它们的中断号,然后调用irq_desc数组项中的handle_irq成员函数。
代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20static void
s3c_irq_demux_extint8(unsigned int irq,
struct irq_desc *desc)
{
unsigned long eintpnd = __raw_readl(S3C24XX_EINTPEND); //EINT8-EINT23发生时,相应位被置1
unsigned long eintmsk = __raw_readl(S3C24XX_EINTMASK); //屏蔽寄存器
eintpnd &= ~eintmsk; //清除被屏蔽的位
eintpnd &= ~0xff; //清除低8位(EINT8对应位8,...)
/* 循环处理所有的子中断 */
while (eintpnd) {
irq = __ffs(eintpnd); //确定eintpnd中为1的最高位
eintpnd &= ~(1<<irq); //将此位清0
irq += (IRQ_EINT4 - 4); //重新计算中断号,前面计算出irq等于8时,中断号为IRQ_EINT8
desc_handle_irq(irq, irq_desc + irq); //调用这个中断的真正的处理函数入口
}
}- IRQ_EINT8-IRQ_EINT23这几个中断的处理函数入口,在
init_IRQ
函数初始化中断体系结构的时候已经被设置为handle_edge_irq
函数。上面第185行的代码就是调用这个函数,它在kernel/irq/chip.c
中定义。从它的名字可以知道,它用来处理边沿触发的中断(处理电平触发的中断为handle_level_irq
)。以下的讲解中,只关心一般的的情形,忽略有关中断嵌套的代码,部分代码如下:第5行用来统计中断发生的次数。1
2
3
4
5
6
7
8
9
10
11
12void fastcall
handle_edge_irq(unsigned int irq, struct irq_desc *desc)
{
...
kstat_cpu(cpu).irqs[irq]++;
/* Start handling the irq*/
desc->chip->ack(irq);
...
action_ret = handle_IRQ_event(irq, event);
...
}
第8行响应中断,通常是清除当前中断使得可以接收下一个中断。对于IRQ_EINT8-IRQ_EINT23这几个中断,desc->chip在前面init_IRQ
函数初始化中断体系结构的时候被设为s3c_irqext_chip。desc->chip->ack就是s3c_extirq_ack
函数,它用来清除中断。
第10行通过handle_IRQ_event
函数来逐个执行action链表中用户注册的中断处理函数,它在kernel/irq/handle.c
中定义,关键代码如下:从第2行可以知道,用户注册的中断处理函数的参数为中断号irq、action->dev_id。后一个参数是通过request_irq函数注册中断时传入的dev_id参数。它由用户自己指定、自己使用,可以为空,当这个中断是“共享中断”除外。1
2
3
4
5
6
7
8do {
ret = action->handler(irq, action->dev_id); //执行用户注册的中断处理函数
if(ret == IRQ_HANDLED)
statue |= action->flags;
retval |= ret;
action = action->next; //下一个
}while(action);
对于电平触发的中断,它们的irq_desc[irq].handle_irq通常是handle_level_irq
函数。它也是在kernel/irq/chip.c
中定义,其功能与上述handle_edge_irq
函数相似,关键代码如下:第5行用来屏蔽和响应中断,响应中断通常就是清除中断,使得可以接收下一个中断。1
2
3
4
5
6
7
8
9
10
11
12
13void fastcall
handle_level_irq(unsigned int irq, struct irq_desc *desc)
{
...
mask_ack_irq(desc, irq);
...
kstat_cpu(cpu).irqs[irq]++;
...
action_ret = handle_IRQ_event(irq, action);
...
desc->chip->unmask(irq);
...
}
第7行用来统计中断发生的次数。
第9行通过handle_IRQ_event
函数来逐个执action链表中用户注册的中断处理函数。
第11行开启中断,与第5行对应。
在handle_edge_irq
、handle_level_irq
函数的开头都清除了中断。所以一般来说,在用户注册的中断函数中就不用再次清除中断了。但是对于电平触发的中断也有例外:虽然handle_level_irq
函数已经清除了中断,但是它只限于清除SoC内部的信号;如果外设输入到SoC的中断信号仍然有效,这就会导致当前中断处理完毕后,会误认为再次发生了中断。对于这种情况,需要在用户注册的最大处理函数中清除中断:先清除外设的中断,然后再清除SoC内部的中断信号。
忽略上述的中断号重新计算过程,中断的处理流程可以总结如下:
①中断向量调用总入口函数asm_do_IRQ
,传入中断号irq。
②asm_do_IRQ
函数根据中断号irq调用irq_desc[irq].handle_irq,它是这个中断的处理函数入口。对于电平触发的中断,这个入口通常为handle_level_irq
;对于边沿触发的中断,这个入口通常为handle_edge_irq
。
③入口函数首先清除中断,入口函数是handle_level_irq
时还要屏蔽中断。
④逐个调用用户在irq_desc[irq].action链表中注册的中断处理函数。
⑤入口函数是handle_level_irq
时还要重新开启中断。
卸载中断处理函数
中断是一种很稀缺的资源,当不再使用一个设备时,应该释放它占据的中断。这通过free_irq
函数来实现,它与request_irq
一样,也是在kernel/irq/manage.c
中定义。它的函数原型如下:
1 | void free_irq(unsigned int irq, void *dev_id); |
它需要用到两个参数:irq和dev_id,它们与通过request_irq
注册中断函数时使用的参数一样。使用中断号irq定位actions链表,再使用dev_id在action链表中找到要卸载的表项。所以,同一个中断的不同中断处理函数必须使用不同的dev_id来区分,这就要求在注册共享中断时参数dev_id必须唯一。free_irq
函数的处理过程与request_irq
函数相反。
- 根据中断号irq、dev_id从action链表中找到表项,将它移除。
- 如果它是唯一的表项,还要调用irq_desc[irqno].chip->shutdown或irq_desc[irq].chip->disable来关闭中断。
使用中断的驱动程序示例
按键驱动程序源码分析
开发板上有4个按键,它们的连线如图所示:
模块的初始化函数和卸载函数
1 | /* |
与LED驱动相似,执行“insmod s3c24xx_buttons.ko”命令加载驱动时就会调用这个驱动初始化函数s3c24xx_buttons_init
;执行“rmmod s3c24xx_buttons.ko”命令卸载驱动时就会调用卸载函数s3c24xx_buttons_exit
。前者调用register_chrdev
函数向内核注册驱动程序,后者调用unregister_chrdev
卸载这个驱动程序。
驱动程序的核心是s3c24xx_buttons_fops结构,定义如下:
1 | static struct file_operations s3c24xx_buttons_fops = { |
s3c24xx_buttons_open 函数
在应用程序执行open("/dev/buttons",...)
系统调用时,s3c24xx_buttons_open
函数将被调用。它用来注册4个按键的中断处理程序,代码如下:
1 | /* |
request_irq
函数的作用前面已经讲解过。注册成功后,这4个按键所用GPIO引脚的功能被设为外部中断,触发方式为下降沿触发,中断处理函数为buttons_interrupt
。最后一个参数(void *)&press_cnt[i]
将在buttons_interrupt
函数中用到,它用来存储按键按下的次数。free_irq
用来卸载已经注册的中断。
参数button_irqs
定义如下,表示了4个按键的中断号、中断触发方式、中断名称(名称供执行“cat /proc/interrupts”时显示用)。
1 | struct button_irq_desc { |
s3c24xx_buttons_close 函数
s3c24xx_buttons_close
函数的作用与s3c24xx_buttons_open
函数相反,它用来卸载4个按键的中断处理函数,代码如下:
1 | /* |
s3c24xx_buttons_read 函数
中断处理函数会在press_cnt数组中记录按键按下的次数。s3c24xx_buttons_read
函数首先判断是否有按键按下,如果没有则休眠等待;否则读取press_cnt数组的数据,代码如下:
1 | /* |
第20行的wait_event_interruptible
首先会判断ev_press是否为0,如果为0才会令当前进程进入休眠;否则向下继续执行。它的第一个参数button_waitq是一个等待队列,在前面第6行中定义;第二个参数ev_press用来表示中断是否已经发生,中断服务程序将它置1,s3c24xx_buttons_read
将它清0。如果ev_press为0,则当前进程会进入休眠,中断发生时,中断处理函数buttons_interrupts
会把它唤醒。
第23行将ev_press清0。
第26行将press_cnt数组的内容复制到用户空间。buff参数表示的缓冲区位于用户空间,使用copy_to_user
向它赋值。
第27行将press_cnt数组清0。
中断处理函数 buttons_interrupt
这4个按键的中断处理函数都是buttons_interrupt,代码如下:
1 | static irqreturn_t buttons_interrupt(int irq, void *dev_id) |
buttons_interrupt
函数第一个参数irq表示发生的中断号,第二个参数dev_id就是request_irq
注册中断时传入的“&press_cnt[i]”。
第5行将按键计数加1。
第6-7行将ev_press设为1,唤醒休眠的进程。
将s3c24xx_buttons.c放到内核源码目录drivers/char
下,在drivers/char/Makefile
中增加如下一行:
1 | obj-m += s3c24xx_buttons.o |
在内核根目录下执行“make modules”命令即可在drivers/char
目录下生成可加载模块s3c24xx_buttons.ko,把它放到开发板根文件系统的/lib/modules/2.6.22.6/
目录下,就可以使用“insmod s3c24xx_buttons.ko”、“rmmod s3c24xx_buttons.ko”命令进行加载、卸载了。
测试程序情景分析
加载模块
执行“insmod s3c24xx_buttons.ko”即可加载模块,这时在控制台执行“cat /proc/devices”命令可以看到内核中已经有了buttons设备,可以看到如下字样:
1 | Character devices |
这表明按键设备属于字符设备,主设备号为232。
测试程序打开设备
运行测试程序button_test后,/dev/buttons
设备就被打开了,可以使用“cat /proc/interrupts”命令看到注册了4个中断。
1 | # cat /proc/interrupts |
测试程序button_test中打开设备的代码如下:
1 |
|
测试程序读取数据
读取数据的代码如下:
1 | //这是一个无限循环,进程有可能在read函数中休眠,当有按键被按下时,它才返回 |