中断处理
《设备驱动及BSP开发指南》与《工程实践完全解析》笔记。
转载请注明出处:http://blog.csdn.net/renpine/archive/2009/10/03/4628346.aspx
1、中断处理内部结构流程
中断处理内部结构图
①、硬件设备向Kernel发送中断异常的代码,如果检测到这个中断异常,就会被Kernel层的异常处理所截获;
②、中断服务调度程序会调用OAL例程中的OEMInterruptDisable函数,这个函数会通知硬件在处理完这一中断前关闭特殊的中断,但其他的中断仍然处于开放状态;
③、中断服务例程ISR被调用以决定如何来处理这一中断;
④、Kernel接收到ISR的返回值(SYSINTR)以得知如何处理这一中断。它的响应结果之一是忽略掉这一中断不作处理(SYSINTR_NOP),另一结果是准备执行IST。
⑤、Kernel引发中断服务调度程序来唤醒中断服务线程去工作。IST是常规的Win32线程,一旦启动后,它会创建必要的EVENT然后等待该EVENT被激发。中断服务调度通过调用PulseEvent函数来激发EVENT,从而唤醒IST线程运行;
⑥、当唤醒以后,IST会对中断进行必要的处理如将数据移动到缓冲区或其他有意义的事;
⑦、如果需要的话,IST会借助于I/O支持例程访问硬件设备;
⑧、当IST处理完成后,它会调用InterruptDone函数通知Kernel;
⑨、Kernel调用OEMInterruptDone函数完成此次中断的处理过程,OAL例程通知硬件设备重新启用中断。
2、对ARM硬件产生中断到ISR之前的分析
对ARM较熟悉,就看看ARM的硬件产生中断到进入ISR之前的流程。一般而言, 硬件的异常产生后,CPU将跳转到0x00000000地址访问中断向量表(normal exception vectors), 但ARM920T / ARM9 / ARM10 系列的CPU支持把中断向量表放到高地址0xFFFF0000(high exception vectors)。该跳转地址的决定因素为协处理器的CP15:BI13。即CP15:BIT13 = 0时, 跳转到低地址; CP15:BIT13 = 1时, 跳转到高地址。此时的PC与MMU的根本没关系,因为PC只要指向了高地址处,那么MMU自身去解释而已,去找到向量表的位置。Wince的中断向量表只支持存放在高地址,从0x00000000~0x00001000这段是reserved的,留作其它用途。
对2410其中断映射到了ArmHigh区域内的虚拟地址0xFFFF0000,因此当硬件IRQ中断产生,迫使PC指向了IRQHandler处即0Xfff0018,在该处存放了一条跳转指令,到达异常向量表0Xffff03F8,如下图所示。
内核中跟中断相关的工作主要有以下几个部分:
定义异常处理函数,其实现文件为:
C:/WINCE600/PRIVATE/WINCEOS/COREOS/NK/KERNEL/ARM/armtrap.s。
创建中断向量表,其实现文件为:
C:/WINCE600/PRIVATE/WINCEOS/COREOS/NK/KERNEL/ARM/exvector.s
中断向量的初始化,其实现在文件:
C:/WINCE600/PRIVATE/WINCEOS/COREOS/NK/KERNEL/ARM/mdarm.c
从IRQHandler里面直接就调用了OEMInterruptHandler函数。
3、ISR
由于外部IRQ中断数目,致使需要一个或者更多的ISR来处理,导致处理方式的不同,有了ISR的两种模型:
名称 | 描述 |
单ISR硬件平台 | ISR为OEMInterruptHandler,仅此一个 |
多ISR硬件平台 | 没有OEMInterruptHandler,必须通过HookInterrupt注册ISR |
其实也没这么绝对,对X86来说有多个IRQ,使用了多ISR的方式,ARM平台其实也可以看作有两个IRQ的方式(一IRQ一FIQ),并且是混合使用的,里面也调用了NKCallIntChain去调用其它的ISRHandler,但具体的ISR调用的函数在OemGlobal全局变量里面:
处理器类型 | |
X86 | OEMNMIHandler(系统非屏蔽中断) |
ARM | OEMInterruptHandler OEMInterruptHandlerFIQ |
分为静态与动态,静态清楚了再分析动态。
静态ISR:
即固定了物理中断号与逻辑中断号的映射,这在系统初始化时候的OALIntrInit里面实现,就两个数组:g_oalSysIntr2Irq和g_oalIrq2Intr,从字面意思即可知一个是从系统中断到Irq的转换,一个是Irq到系统中断的转换,其实就是将对应的中断号作为下标,得到该元素的值。
动态ISR:
即可安装的ISR,是在运行时候安装的,比较灵活,但必须清楚是一个IRQ可以绑定多个ISRHandler,最多有256个IRQ。查看NKCallIntChain函数的源码,可以看到里面使用了数组pIntChainTable[256],每个指针作为一个链表首指针,其类型为指针数组:
struct _INTCHAIN {
struct _INTCHAIN* pNext; //下一节点
PMODULE pMod;
//NKLoadKernelLibrary (giisr.dll),如果用默认giisr.dll
FARPROC pfnHandler; //ISRHandler,默认
DWORD dwInstData; //InstanceIndex
BYTE bIrq; //中断号
BYTE bPad[3];
};
再看看giisr.dll输出入口:
EXPORTS
ISRHandler
CreateInstance
DestroyInstance
IOControl
这里对各个函数大致介绍:
CreateInstance对g_Info[MAX_GIISR_INSTANCES]数组返回一个下标序号,这里数组的宏为32,也即最多每个安装中断有32个实例,DestroyInstance则相反。
IOControl在驱动里面都使用KernelLibIoControl来调用。两个case条件IOCTL_GIISR_PORTVALUE和IOCTL_GIISR_INFO,一般用后者,在外面将信息全部填好过后直接拷贝给g_Info[InstanceIndex]。
看一个info配置使用的的例子:
// Set up ISR handler
Info.SysIntr = dii.dwSysintr;
Info.CheckPort = TRUE;
Info.PortIsIO = (dwIOSpace) ? TRUE : FALSE;
Info.UseMaskReg = TRUE; //使用掩码寄存器
Info.PortAddr = PhysAddr + 0x0C; //判断IO端口地址
Info.PortSize = sizeof(DWORD);
Info.MaskAddr = PhysAddr + 0x10; //掩码地址
再将信息填入g_Info数组中:KernelLibIoControl(pPddObject->IsrHandle, IOCTL_GIISR_INFO, &Info, sizeof(Info), NULL, 0, NULL),这就是使用注册过程。
ISRHandler通过IO端口或者内存映射端口读取状态,再与Mask与后得到是否是本中断号。例如IO端口类型,ISRHandler从Info.MaskAddr读取屏蔽值,再与Info.PortAddr地址的值进行与即知道是否是合适中断号了。
NKCallIntChain函数知道,这里面会直接调用ISRHandler,如若判定是该中断则将返回逻辑中断号,否则处理后就返回SYSINTR_CHAIN告知结束,OEMInterruptHandler将会继续后面其它处理。下面是OEMInterruptHandler调用到返回得到逻辑中断号的过程,模拟器里面是在这OEMInterruptHandler里面计算出IRQ的值:
4、如何使用中断
也分两种,静态与动态ISR的使用。
这里说明几个有点有时让人有点晕的点。逻辑中断号跟物理中断号的挂钩,为了灵活起见,一般做法是:把物理中断号(Irq)放入注册表项里面,在驱动初始化时再行读取,自动获取逻辑中断号,当然这是除开默认系统中断号之外的(SYSINTR_DEVICES+8之下的保留逻辑中断号)。比如模拟器的键盘,注册表里面是如此:
[HKEY_LOCAL_MACHINE/HARDWARE/DEVICEMAP/KEYBD]
"DriverName"="kbdmouse.dll"
"Irq"=dword:1
"IOBase"=dword:B1600000
"SSPBase"=dword:B1900000
这里有了Irq,接下来在驱动里面将会读取该值,在IsrThreadProc里面有:
ReadRegDWORD( TEXT("HARDWARE//DEVICEMAP//KEYBD"), _T("Irq"), &dwIrq_Keybd );
这样取得了Irq值dwIrq_Keybd,后面KernelIoControl就转换得到逻辑中断号g_dwSysIntr_Keybd,后面再用函数InterruptInitialize将g_dwSysIntr_Keybd与事件m_hevInterrupt挂钩,这样键盘驱动就能够响应硬件中断了。
静态ISR:
讲述这个的比较多,就给个别人的实例:
① 驱动初始化
pGPIOInfo->hGPIOEvent1 = CreateEvent(0,FALSE,FALSE,NULL);
其次创建一个处理事件的线程(IST)
pGPIOInfo->hGPIOThread1 = CreateThread(NULL, 0, GPIOFuncThread1, pGPIOInfo, 0, NULL);
然后使用InterruptInitialize让虚拟中断号pGPIOInfo->dwIntID1与创建的事件pGPIOInfo->hGPIOEvent1挂钩。
InterruptInitialize(pGPIOInfo->dwIntID1, pGPIOInfo->hGPIOEvent1, NULL, 0)
那么,当GPIO的中断到来,与GPIO虚拟中断挂钩的事件pGPIOInfo->hGPIOEvent1就会被设为Active。
② ISR到来
OEMInterruptHandler调用sysIntr = OALIntrTranslateIrq(irq),转换为逻辑中断号,再返回给异常处理,发送事件给IST;
③ IST的执行
线程中,WaitForSingleObject(pGPIOInfo->hGPIOEvent1, INFINITE);被唤醒,然后执行下一条指令,当中断处理结束以后,必须使用 InterruptDone(pGPIOInfo->dwIntID1);通知系统已经完成中断处理,那么下一次的中断到来,事件pGPIOInfo->hGPIOEvent1就才会再次被设为Active。
④ 驱动的卸载
驱动卸载时,需要释放申请的事件及线程CloseHandle(pGPIOInfo->hGPIOEvent1);CloseHandle(pGPIOInfo->hGPIOThread1)
动态ISR:
Giisr.dll并没有加入common.bib因此加入platform.bib中,主要步骤:
① 加入platform.bib giisr.dll $(_FLATRELEASEDIR)/giisr.dll NK SHK;
② 驱动初始化时,首先读取该驱动注册表项,并从该项下的"IsrDll"="giisr.dll","IsrHandler"="ISRHandler"与Irq=N等子键得到IRQ等信息,调用LoadIntChainHandler()函数以安装一个ISR;
③ 与静态IST相似,进行事件与逻辑中断号关联,这里初始化结束,IST等待中断触发。
④ IRQ到来,OAL的OEMInterruptHandler调用NKCallIntChain()函数执行ISR来获得逻辑中断号sysIntr;
⑤ 这里跟静态ISR处理完全相同了。