Exception Process in wince&arm 作者: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的低地址了, 只能选用高地址放置.
在WINCE500PRIVATEWINCEOSCOREOSNKKERNELARMarmtrap.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, 它位于wince500privatewinceoscoreos
kkernellogger.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在publiccommonoakinc
kintr.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, 可以在wince500privatewinceoscoreoscoredllCoredll.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 @_@ 看不懂. 期待高人指点
另外wince500publiccommonoakincMkfuncs.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 (wince500privatewinceoscoreos
kkernelintrapi.c) -> DoInterruptEnable (同上intrapi.c) -> OEMInterruptEnable (wince500platformcommonintrcommonoem.c) -> OALIntrEnableIrqs (wince500platformcommonintrpxa27xintr.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.首先在platformwindowtvsrccommonpxa27xinculverde_intr.h中检查是否定义了IRQ_STUART, 所定义的中断号是否和pxa27x manual上面的一样. 如果没有就自己添加进去。
2.在 platformwindowtvsrccommonintrpxa27xintr.c 中的g_InPriorties数组里, 加入IRQ_STUART. 该数组在OALIntrInit()中初始化Interrupt Priority Registers的时候会用到
3.在platformwindowtvsrcincsp_cfg,h中添加系统中断号SYSINTR_STUART. 这里要用define也行.
4.platformwindowtvsrckerneloalintr.c中, 函数BSPIntrInit()里添加软硬件中断号的关联 OALIntrStaticTranslate(SYSINTR_STUART, IRQ_STUART);
最后在驱动程序里建立一个事件,然后用InterruptInitialize(sysintr, istevent, 0, 0)把系统中断号和事件关联, 那么硬件中断就能够进到系统中断进而设置事件. 驱动程序得到了事件, 就爱干嘛干嘛吧……
-----The End----- Copyright @ Walzer 2005 |