作者:Walzer
日期:2005.3.11
我被INTEL的Application Engineer特地纠正了“中断向量表”的说法,应该叫Exception Vector.按我理解,应该只有IRQ和FIQ算做interrupt吧,像reset和几个abort叫做exception的确更合适些。
一般而言, 硬件的异常产生后,CPU将跳转到0x00000000地址访问中断向量表(normal exception vectors), 但ARM920T / ARM9 / ARM10 系列的CPU支持把中断向量表放到高地址0xFFFF0000(high exception vectors). 该跳转地址的决定因素为协处理器的CP15:BI13. 即CP15:BIT13 = 0时, 跳转到低地址; CP15:BIT13 = 1时, 跳转到高地址. 根据INTEL应用工程师的回答,该地址在MMU使能之前为physical address, 使能之后为virtual address, 该点有待确认.
由于WinCE在编译nk.nb0或xboot.nb0时, 和其他application使用了同一个Linker的缘故, 从0x00000000~0x00001000这段是reserved的,留作给application的PE头(Portable Executable header, 什么用的我还没搞懂, MSDN上有详细说明). 而我们的代码从0x00001000这行开始放起.但在0x00000000有句跳转指令"b 1000". 其实这中间还有几行代码, 但不知道干什么用的,反正被跳过去不执行就是了.
因为上述缘故, WinCE里就不能把exception vectors放在normal的低地址了, 只能选用高地址放置.
在WINCE500\PRIVATE\WINCEOS\COREOS\NK\KERNEL\ARM\armtrap.s中, 下面这段VectorInsturctions代码将被复制到0xFFFF0000位置, 占据8*4=32bit的长度. 我们最经常使用的IRQ中断在0xFFFF0018的位置. 其中0x14是保留给以后扩展的,目前并没有用到.按照ARM体系, 应该有7种中断. 而实际上只有IRQ和FIQ会跳到OEMInterruptHandler里面(后者是OEMInterruptHandlerFIQ).
VectorInstructions
ldr pc, [pc, #0x3E0-8] ; reset
ldr pc, [pc, #0x3E0-8] ; undefined instruction
ldr pc, [pc, #0x3E0-8] ; SVC
ldr pc, [pc, #0x3E0-8] ; Prefetch abort
ldr pc, [pc, #0x3E0-8] ; data abort
ldr pc, [pc, #0x3E0-8] ; unused vector location
ldr pc, [pc, #0x3E0-8] ; IRQ
ldr pc, [pc, #0x3E0-8] ; FIQ
这只是第一次跳转, 跳转到0xFFFF03E0后,这里才是中断向量表(vector table). 这个vector table并不在armtrap.s里,而是在同目录的exvector.s里单独定义, 这里就是各Interrupt Service Routine的入口地址了.
VectorTable
DCD -1 ; reset
DCD UndefException ; undefined instruction
DCD SWIHandler ; SVC
DCD PrefetchAbort ; Prefetch abort
DCD DataAbortHandler ; data abort
DCD -1 ; unused vector
DCD IRQHandler ; IRQ
DCD FIQHandler ; FIQ
实际上不论是ExceptionVectors或者VectorTable, 一开始都是放置在ROM里面的,这就需要在系统startup的时候把它们搬运到指定的位置,下面这段代码应该就是干的这事.
; Setup the vector area.
;
; (r8) = ptr to exception vectors
add r7, pc, #VectorInstructions - (.+8)
ldmia r7!, {r0-r3} ; load 4 instructions
stmia r8!, {r0-r3} ; store the 4 vector instructions
ldmia r7!, {r0-r3} ; load 4 instructions
stmia r8!, {r0-r3} ; store the 4 vector instructions
; convert VectorTable to Physical Address
ldr r0, =VectorTable ; (r0) = VA of VectorTable
mov r1, r11 ; (r1) = &OEMAddressTable[0]
bl PaFromVa
mov r7, r0 ; (r7) = PA of VectorTable
add r8, r8, #0x3E0-(8*4) ; (r8) = target location of the vector table
ldmia r7!, {r0-r3}
stmia r8!, {r0-r3}
ldmia r7!, {r0-r3}
stmia r8!, {r0-r3}
OK了,运行到这步后,各种不同的exception就要分别进入其ISR了. 这些ISR仍然是在armtrap.s里面的. 以IRQHandler为例. 从0xFFFF03F8跳转到ISR入口处: NESTED_ENTRY IRQHandler, 后面好长一段汇编, 算了一下145行, 晕倒, 看不懂. 这些都是WINCE做好的不需要OEM再去修改。其实我们暂时先看懂一个标识符就OK了: CALL
第一次CALL了这个函数CeLogInterrupt, 它位于wince500\private\winceos\coreos\nk\kernel\logger.c里面, 实质性工作就是一个interrupt counter,把一个计算中断数量的全局变量加一.
接下来CALL了我们亲切的"OEMInterruptHandler". 在INTEL的BSP里提供了intr.c, 里头实现了这个OEMInterruptHandler函数. 这个函数都做些什么呢? 首先通过OEMAddressTalbe把CPU上的Interrupt Controller Registers影射到virtual address, 其实我们只关心这里面的一个地方: ICHP[IRQ], 这里指示了IRQ Highest Priority Field,所以我们只要把这5个BIT取出来判断就可以了. 当然从这里取得的只是硬件的physical IRQ号(有的地方翻译成设备中断号), 得通过OALIntrTranslateIrq
转换成能够在系统里使用的logical SysIntr号(调度中断号).
IRQ号可以在BSP里找到定义(Bxxxxx_intr.h), 而SysIntr则分为三类: WINCE规定好的 / OEM自行分配 / DRIVER运行时动态获取. WINCE在public\common\oak\inc\nkintr.h中规定一些较为核心的SYSINTR,如SYSINTR_NOP, RESCHED, BREAK, CHAIN, 还有TIMEING, RTC_ALARM等, 而在BSP里bsp_cfg.h则定义了外围设备的SYSINTR, 如OHCI, UART, KEYPAD等. 前面这两种情况的SYSINTR, 在注册表platform.reg里也必须写值, 并且得和程序里定义的一样. 第三种是运行时动态获取的SYSINTR, 我还没碰到过, 不是很清楚.
从OEMInterruptHandler中返回了一个该IRQ所对应的SysIntr号, 放置在R0里.
接下来, call了这个函数指针"pfnOEMIntrOccurs", 到private里追踪, 该指针在schedule.c里指向了函数FakedOEMIntrOccurs, 而这个函数就在该赋值语句的上面几行,具体操作为:return dwSysIntr. 哈被耍了, 人家函数名就叫Fake, 自己要去追的, 活该.
再往下走, "cmp r0, #SYSINTR_RESCHED", 轮询的时间片已经用完了, 只好进行调度了,PendEvents, 等下个时间片再来解决了.具体做法十分严谨,不过我还不太看得懂这堆ARM汇编, 望洋兴叹了.
这样汇编里的ISR就走完了。
现在从最高层面上的device driver往下走.
device drvier在initialize过程中,肯定有个初始化中断的过程. 典型的做法是首先调用GetISRInfo函数从注册表里把IRQ和SysIntr读出来(当然同时还读了InterfaceType, BusNumber等其他东东), 然后前面会声明个m_hISTEvent的handler, 在这里就CreatEvent, 建立IST事件. 如果EVENT建立成功, 下面就InterruptInitialize(SysIntr, m_hISTEvent),把这个调度中断号和ISTEVENT关联起来. 具体在WINCE HELP里面可以查到InterruptInitialize这个API的用法.
关于这个InterruptInitialize, 可以在wince500\private\winceos\coreos\core\dll\Coredll.def里面Ln1355找到这么一行:
InterruptInitialize=xxx_InterruptInitialize @627
也就是InterruptInitialize实际上属于Coredll.dll的,并且被调用时实际上是去运行xxx_InterruptInitialize函数 ( 前三个字估计是MS程序员写什么政治敏感词汇, 结果提交代码时被系统屏蔽成xxx了,哈哈) 这个xxx函数是看不到源代码的,不过我们可以跟踪抓出它的汇编
xxx_InterruptInitialize:
03F70744 mov r12, sp
03F70748 stmdb sp!, {r0 - r3}
03F7074C stmdb sp!, {r4, r12, lr}
03F70750 sub sp, sp, #0x14
$M20120:
03F70754 mov r3, #0
03F70758 sub r3, r3, #0xE, 22
03F7075C ldr r3, [r3]
03F70760 sub r3, r3, #0x14
03F70764 ldr r3, [r3]
03F70768 tst r3, #1
03F7076C beq |$M20120+44h (03f70798)|
03F70770 ldr r3, [pc, #0x74]
03F70774 ldr r3, [r3]
03F70778 cmp r3, #0
03F7077C beq |$M20120+44h (03f70798)|
03F70780 ldr r3, [pc, #0x64]
03F70784 ldr r3, [r3]
03F70788 add r3, r3, #0x69, 30
03F7078C ldr r3, [r3]
03F70790 str r3, [sp, #0xC]
03F70794 b |$M20120+4Ch (03f707a0)|
03F70798 ldr r3, [pc, #0x48]
03F7079C str r3, [sp, #0xC]
03F707A0 ldr r3, cbData
03F707A4 ldr r2, pvData
03F707A8 ldr r1, hEvent
03F707AC ldr r0, idInt
03F707B0 ldr r4, [sp, #0xC]
03F707B4 mov lr, pc
03F707B8 bx r4
03F707BC str r0, [sp, #0x10]
03F707C0 ldr r3, [sp, #0x10]
03F707C4 str r3, [sp, #4]
03F707C8 add r0, sp, #0
03F707CC bl |KillThreadIfNeeded_t::~KillThreadIfNeeded_t (03f60928)|
03F707D0 ldr r3, [sp, #4]
03F707D4 str r3, [sp, #8]
03F707D8 ldr r0, [sp, #8]
03F707DC add sp, sp, #0x14
03F707E0 ldmia sp, {r4, sp, lr}
03F707E4 bx lr
@_@ 看不懂. 期待高人指点
另外wince500\public\common\oak\inc\Mkfuncs.h里面Ln579看到
BOOL xxx_InterruptInitialize(DWORD idInt, HANDLE hEvent, LPVOID pvData, DWORD cbData);
#define InterruptInitialize xxx_InterruptInitialize
这个是头文件的include路径是 mkfuncs.h -> kfuncs.h -> winbase.h -> windows.h. 可能还有其他支路,但一般要调用的话,include <windows.h>就行了
然后简单说一下主要的调用层次关系 xxx_InteeruptInitialize -> SC_InterruptInitialize (wince500\private\winceos\coreos\nk\kernel\intrapi.c) -> DoInterruptEnable (同上intrapi.c) -> OEMInterruptEnable (wince500\platform\common\intr\common\oem.c) -> OALIntrEnableIrqs (wince500\platform\common\intr\pxa27x\intr.c)
这么长的三段主要就是为了说明,中断并不是在整个系统boot起来后就一口气全部ENABLE的, 而是根据当前加载的模块, 在调用InterruptInitialize的时候才进行Enable, 这可以是去对INTR MASK REGISTER里面对应的BIT进行UNMASK, 或者是把GPIO EDGE DETECT ENABLE, 取决于中断实现的硬件途径.
接上面话题,驱动中在InterruptInitialize后,最后在CeSetPriority把THREAD的优先级设一下就OK了. 现在跳到driver的IST去看看,开始的地方一般就是个大的WHILE,里面有个WaitForSingleObject(m_hISTEvent, INFINITE),
OK,那么在设备初始化完成后,Interrupt Service Thread就保持一个SUSPEND状态,保持WaitForSingleObject. 中断发生后,在OEMInterruptHandler中会把这个IRQ转换成SYSINTR并通知系统( 这个就是想当然的了,我没有看到具体怎样通知系统的, 估计这段代码MS没有公开), 则由于前面已经将SYSINTR和ISTEVENT相关联, 所以系统收到这个SYSINTR后,就把SET相关的EVENT. 从而IST从WaitForSingleObject里跳出往下继续执行,进而读取自己模块的Interrupt Status Register进行判断,操作其他register控制硬件, 完成后调用InterruptDone函数告知系统,然后又回到WaitForSinigleObject里去傻等了.
现在整个中断处理过程中,只剩下一块的代码没有看到/找到。那就是转换成SysIntr后,如何告知系统,并且把相关的模块的中断处理事件SetEvent. 我估计是在private里面。如果要深究的话,可以从上面提到的xxx_InterruptXXXXXX系列函数入手。
-------------------------------------------------------
下面举个实际的例子,我在已有的代码中开启一个STUART的中断让APPLICATION使用(intel pxa270平台),步骤如下
1.首先在platform\windowtv\src\common\pxa27x\inc\bulverde_intr.h中检查是否定义了IRQ_STUART, 所定义的中断号是否和pxa27x manual上面的一样. 如果没有就自己添加进去。
2.在 \platform\windowtv\src\common\intr\pxa27x\intr.c 中的g_InPriorties数组里, 加入IRQ_STUART. 该数组在OALIntrInit()中初始化Interrupt Priority Registers的时候会用到
3.在\platform\windowtv\src\inc\bsp_cfg,h中添加系统中断号SYSINTR_STUART. 这里要用define也行.
4.\platform\windowtv\src\kernel\oal\intr.c中, 函数BSPIntrInit()里添加软硬件中断号的关联 OALIntrStaticTranslate(SYSINTR_STUART, IRQ_STUART);
最后在驱动程序里建立一个事件,然后用InterruptInitialize(sysintr, istevent, 0, 0)把系统中断号和事件关联, 那么硬件中断就能够进到系统中断进而设置事件. 驱动程序得到了事件, 就爱干嘛干嘛吧……
-----The End-----
Copyright @ Walzer 2005