分类: 嵌入式
2016-10-22 12:01:09
原文地址:vxworks驱动开发原理——中断处理程序(2) 作者:哭泣的土地
函数库主要为上层应用程序提供了一组与外部硬件无关的中断控制函数。其中比较常用的就是函数intConnect、intEnable、intDisable。
1. STATUS intConnect
(
VOIDFUNCPTR *vector,
VOIDFUNCPTR routine,
int parameter
)
该函数的作用是将一个C函数与一个硬件中断连接起来,当发生了硬件中断时,自动执行该C函数。
它主要完成了三件事情,一是调用函数(* intEoiGet)(......)获取该中断的ISR处理前后需要执行的函数routineBoi、routineEoi及其参数;
二是调用函数intHandlerCreateI86填充数组intConnectCode,主要填充routineBoi、routine、routineEoi,组成完整的中断执行代码;
三是调用函数intVecSet将数组intConnectCode存放到参数vector指定的位置。
注意:代码中调用intHandlerCreate的情况属较老的vxWorks版本,对于新版本已经被intHandlerCreateI86函数所代替,因此不再过多分析。
2. intEnable()
允许一个特定的中断,这个函数由全局函数指针sysIntLvlEnableRtn定义,不过目前是没有定义,如果用户需要的话可以自己定义sysIntLvlEnableRtn。
3. intDisable()
关闭一个特定的中断,这个函数由全局函数指针sysIntLvlDisableRtn定义,不过目前是没有定义,如果用户需要的话可以自己定义sysIntLvlDisableRtn。
4. intHandlerCreate()
这是是早期的toanado版本。这个函数的作用是将intConnectCode复制到代码段并填充代码中的intEnt函数、C函数及参数、intExit函数,如有必要(定义了intEOI),则同样要填充_routineEoi函数,如果不需要填充则直接将相应的位置填NOP指令代替,最终得到一个完整的中断处理函数。不过需要注意的是这个函数并没有填充intConnectCode代码中的routineBoi函数及参数而是直接用NOP代替了。
注意,routineEoi函数的参数没有填充。此外还要注意routineBoi函数及参数的位置,用NOP填充了,因此就少了一个pushl指令,后面的addl $12, %esp指令也要做相应修正。
5. intHandlerCreateI86()
这个是目前的toanado版本实现。这个函数的功能和intHandlerCreate是一样的,不过已经进行了一些改进。主要在一下几个方面:
如果有必要(参数routineBoi有效)则填充函数_routineBoi函数及其参数;否则用NOP填充。
如有必要(参数routineEoi有效)中则填充_routineEoi函数及其参数;否则用NOP只填充_routineEoi函数而不填充其参数。
参考这段代码的注释:
LOCAL UCHAR intConnectCode [] = /* intConnect stub */
{
/*
* 00 e8 kk kk kk kk call _intEnt * tell kernel
* 05 50 pushl %eax * save regs
* 06 52 pushl %edx
* 07 51 pushl %ecx
* 08 68 pp pp pp pp pushl $_parameterBoi * push BOI param
* 13 e8 rr rr rr rr call _routineBoi * call BOI routine
* 18 68 pp pp pp pp pushl $_parameter * push param
* 23 e8 rr rr rr rr call _routine * call C routine
* 28 68 pp pp pp pp pushl $_parameterEoi * push EOI param
* 33 e8 rr rr rr rr call _routineEoi * call EOI routine
* 38 83 c4 0c addl $12, %esp * pop param
* 41 59 popl %ecx * restore regs
* 42 5a popl %edx
* 43 58 popl %eax
* 44 e9 kk kk kk kk jmp _intExit * exit via kernel
*/
intConnectCode十分难以理解,主要在于它本身是一段机器代码,为了程序的高效运行,软件直接对机器代码进行修改并将其保存到相应的中断处理地址。这段程序一共需要执行5个函数调用,这个5个函数分别是:
l call _intEnt。调用intEnt()进入中断,无调用参数,必须调用执行。
l routineBoi(......)。有参数,参数为一个4字节的整数,可以调用也可以不调用。
l C函数,有参数,参数为一个4字节的整数,核心的中断处理程序,必须调用。
l routineEoi(......),有参数,参数为一个4字节的整数,可以调用也可以不调用。
l jmp _intExit。跳转进入函数intExit函数以退出中断,无参数,必须执行,其实它和call _intEnt的执行模式是类似的,都是一个相对跳转指令。
指令call _intEnt如果翻译成机器语言的话,call指向的是一个相对的地址,具体来说是相对于call指令的下一条指定pushl $_parameter的开始地址,因此有*(int *)&pCode[ICC_INT_ENT] = (int)intEnt -(int)&pCode[ICC_INT_ENT + 4]。
对于call函数的运行,首先进行跳转,跳转到相应的子程序,相应的子程序在运行前要根据相反的顺序读出堆栈里面保存的参数,因此可以看出对有参数函数的call调用,必须在调用前将其参数压入堆栈。不过这里需要注意的是,子程序从堆栈里面读取参数并不是采用popl的办法,而是利用堆栈指针进行访问,也就是说当子程序可以直接读取堆栈esp指针指向的内容,这样并没有改变堆栈中保存数据的数量,因此可以看出在将寄存器ecx压入堆栈之后,先后将routinBoi参数、C函数参数以及routineEoi参数压入堆栈,待这些函数退出之后原来保存ecx的之上又有了三个参数了,因此需要执行$12, %esp以修改堆栈指针,然后再调用popl %ecx指令恢复的才是原来保存在ecx里面的内容。因此如果三个函数routinBoi、C函数以及routineEoi如果有一个没有执行,也就不会把相应的参数压入堆栈,那么在后面的popl执行之前调整esp的数值的时候也就需要考虑相应的修改。在本函数中,routineBoi和routineEoi的判断不太一样,如果routineBoi不存在就将压入routineBoi参数及调用routineBoi函数的操作用nop指令代替,因此后续的esp就需要做出相应修正;而对于routineEoi函数,如果不存在就将call _routineEoi指令修改为nop,原来压入routineEoi参数的指令并没有改变,也就是不管routineEoi参数是否有效都要压入堆栈,因此后面的esp无需对此进行调整了。
6. intLockLevelSet(newLevel)
设置变量intLockMask的数值为newLevel。
函数库intArchLib中有个全局变量intLockMask。汇编代码库intALib.s中的intLock()及intUnlock()函数使用该变量通过操作CPU的EFLAGS 寄存器进行中断屏蔽或者解除中断屏蔽。调用intLock()时将屏蔽中断,同时将调用前的EFLAGS的屏蔽标识位保存到变量intLockMask中;调用intUnlock()解除中断屏蔽时,函数根据intLockMask的状态决定是否需要解除,如果确实需要解除,则才进行中断屏蔽解除操作。
注意,intLock()函数可以从中断级或任务级程序中调用。因此仅仅修改EFLAGS 寄存器的屏蔽位并不能立即生效,直至调用函数intUnlock()。
7. intLockLevelGet()
返回intLockMask的数值。参见intLockLevelSet。
8. intVecBaseSet(baseAddr)
设置中断向量起始地址,包括设置变量intVecBase和CPU中IDT寄存器的数值。
在Pentium的保护模式下,IDT寄存器共有48bit,其中高32bit用于保存中断向量表的起始地址,低16位描述的是中断描述符表的最大容量,这里设置为0x7ff,即限制为32KB。
intVBRSet函数baseAddr数值写入到写入到idt寄存器中。参见汇编程序文件src\arch\i86\intALib.s。
9. intVecBaseGet()
获取intVecBase。
10. intVecSet()
(
FUNCPTR *vector,
FUNCPTR function
)
在中断向量表中填写入中断向量vector(相对于中断向量表的偏移量)对应的中断处理函数。一般来说,中断向量表从内存的0地址开始,不过也可以用函数intVecBaseSet来设定中断向量表开始的位置intVecBase。如果设定了中断向量表开始的位置,那么vector(相对于中断向量表的偏移量)在内存中实际存放的地址=中断向量+基地址。如图3.3。
图3.3 中断向量的地址偏移
上图描述是一个主要的原理性的示意图,因为中断是一个原始的CPU指令,它和CPU的结构紧密相关,图3.4说明了pentium处理器实模式下中断向量的存放格式。
图3.4 Pentium处理器实模式下对应的中断向量保存格式
11. intVecSet2()
和函数intVecSet类似,区别在于保护模式下中断向量保存的格式。如图3.5。
图3.5 Pentium处理器在保护模式下对应的中断向量格式
12. intVecGet()
返回的是中断处理代码的地址。
13. intVecGet2()
返回的是中断处理代码的地址,同时返回idtSelector和idtGate的值。
14. intVecTableWriteProtect()
这个函数是为了对中断向量表进行写保护。这涉及到内存管理的内容,以后再做讨论。
15. intRegsLock()
数据结构REG_SET中屏蔽中断位,并非对实际硬件的操作。
16. intRegsUnlock()
数据结构REG_SET中解除中断位,并非对实际硬件的操作。