Exception Process in WinCE & ARM

我被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)把系统中断号和事件关联, 那么硬件中断就能够进到系统中断进而设置事件. 驱动程序得到了事件, 就爱干嘛干嘛吧……

本文转自Walzer博客园博客,原文链接:http://www.cnblogs.com/walzer/archive/2006/02/05/325537.html,如需转载请自行联系原作者

点赞