谈到中断使用PCI总线来作为例子是最合适的,在Windows发展过程中,PCI作为最成功的底层总线,集成了大量的外设,不夸张的说,目前PCI几乎是唯一的总线选择,故大部分情况下,只有PCI设备驱动程序会遇到需要使用中断的情况。
PCI的升级版本是PCIe,但是在协议上,PCIe兼容PCI总线,在编程上,这两者几乎一致,PCIe兼容了PCI的结构和定义,以至于仅看代码很难理解这些知识,这个文档推迟了很久,是因为我还需要一些PCI方面的文档。
直接架构于PCI总线的设备类型包括: USB总线、1394总线、网络适配器、显示适配器等等,PCI可能目前最广泛的总线体系,这套体系不仅仅是软件也是硬件。
PCI总线最早采用的中断机制是INTx,这是基于边带信号的。后续的PCI/PCI-X版本,为了消除边带信号,降低系统的硬件设计复杂度,逐渐采用了MSI/MSI-X的中断机制。INTx一般被称为传统的(Legacy)PCI中断机制,MSI和MSIX中断则属于PCIE后来使用的中断方式。
注意: 由于PCI和PCIe是前向兼容的,所以在下面的讨论中,讨论PCI就是指PCIe。
PCI配置空间
PCI配置空间是PCI(Peripheral Component Interconnect)设备特有的一个物理空间,它支持即插即用功能,允许操作系统自动配置PCI设备的参数。PCI配置空间总长度为256个字节,分为两部分:
配置首部:前64个字节的配置空间称为配置头,对于所有设备都一样,主要功能是用来识别设备、定义主机访问PCI卡的方式(I/O访问或存储器访问,以及中断信息);
本地配置空间:其余的192个字节称为本地配置空间,主要定义卡上局部总线的特性、本地空间基地址及范围等;
PCI分为两种,一种成为PCI设备,另一种则是PCI桥,PCI设备容易理解,PCI桥则是用于将几个PCI总线连接起来的,一般认为每个PCI桥上可以有多个PCI设备。它们都使用相同的首部,不过PCI设备的首部称为type 0,PCI桥则是type 1,不过,遗憾的是,PCI的配置空间实际上是0x0或者0x81,位于PCI配置空间的14字节。
学习PCI最好实际看看PCI的配置空间,下图是我截取我系统上的PCI网卡的配置空间图;:
同样的PCI桥的配置空间如下:
在后续的文档中会详细解释这部分。 遗憾的是,我无法直接修改PCI的中断寄存器,无法演示两种中断模式。
INTx中断
在设备早期的开发中,很可能采用这种方式的中断,但是这种有边带信号触发的中断,有许多缺点: 系统其实无法准确分辨是PCI桥中具体哪一个设备引发的中断,需要调用所有的中断处理程序、系统每次收到中断的时候,数据其实并未准备好;每个设备最多有4个引脚可以触发传统中断。
但是无论如何,这种中断方式在硬件体系设计的早期是非常方便的,设备设计的早些版本很容易触发一个INTx中断,从而传输数据,从INTx中断升级到MSIX中断非常简单,并且对于主机侧来说,代码改动并不算太复杂,也不会改变程序结构。
目前的PCIe设备之间的中断信息传输中使用的并非边带信号INTx,而是基于消息(Message)的。其中Assert_INTx消息表示INTx信号的下降沿;Dessert_INTx消息表示INTx信号的上升沿。当发送这两种消息时,PCIe设备还会将配置空间的相关中断状态bit的值也进行更新。接收中断的物理设备的驱动程序会注册一个或多个中断服务例程, ISR为中断提供服务。 系统每次收到该中断时都会调用 ISR。
当驱动程序收到 IRP_MN_START_DEVICE 请求时,驱动程序将可以处理 IO_STACK_LOCATION 结构的 Parameters.StartDevice.AllocatedResources 和 Parameters.StartDevice.AllocatedResourcesTranslated 成员中的原始中断和已转换的硬件资源。 为了连接其中断,驱动程序使用 AllocatedResourcesTranslated.List.PartialResourceList.PartialDescriptors[] 中的资源。 驱动程序必须扫描部分描述符数组中 CmResourceTypeInterrupt 类型的资源。
如果驱动程序为 SpinLock 提供存储,则必须先调用 KeInitializeSpinLock ,然后才能将其中断旋转锁传递给 IoConnectInterrupt。
从成功调用 IoConnectInterrupt 返回后,如果在驱动程序的设备上启用了中断,或者 ShareVector 设置为 TRUE,则可以调用调用方 ISR。 在 IoConnectInterrupt 返回之前,驱动程序不得启用中断。
注意: 原始中断和已经分配的中断都是系统提供的,一般都会存储它们来为后面的一系列操作做准备。
有两个中断函数,不过先介绍第一个中断函数:
NTSTATUS IoConnectInterrupt([out] PKINTERRUPT *InterruptObject,[in] PKSERVICE_ROUTINE ServiceRoutine,[in, optional] PVOID ServiceContext,[in, optional] PKSPIN_LOCK SpinLock,[in] ULONG Vector,[in] KIRQL Irql,[in] KIRQL SynchronizeIrql,[in] KINTERRUPT_MODE InterruptMode,[in] BOOLEAN ShareVector,[in] KAFFINITY ProcessorEnableMask,[in] BOOLEAN FloatingSave
);
- [out] InterruptObject: 指向指向一组中断对象的指针的驱动程序提供的存储地址的指针。 此指针必须在后续调用 KeSynchronizeExecution 中传递;
- [in] ServiceRoutine: 指向驱动程序提供的 InterruptService 例程的入口点的指针;
- [in, optional] ServiceContext: 指向驱动程序确定的上下文的指针,该上下文将在调用时提供给 InterruptService 例程。 ServiceContext 区域必须位于驻留内存中:在驱动程序创建的设备对象的设备扩展中、驱动程序创建的控制器对象的控制器扩展中,或位于设备驱动程序分配的非分页池中;
- [in, optional] SpinLock:指向初始化的旋转锁的指针,驱动程序为其提供存储,该锁将用于同步对由其他驱动程序例程共享的驱动程序确定数据的访问。 如果 ISR 处理多个向量或驱动程序有多个 ISR,则此参数是必需的。 否则,驱动程序无需为中断旋转锁分配存储,并且输入指针为 NULL;
- [in] Vector: 指定在CM_PARTIAL_RESOURCE_DESCRIPTOR的 u.Interrupt.Vector 成员处的中断资源中传递的中断向量;
- [in] Irql: 指定在 CM_PARTIAL_RESOURCE_DESCRIPTOR 的 u.Interrupt.Level 成员处的中断资源中 传递的 DIRQL;
- [in] SynchronizeIrql: 指定运行 ISR 的 DIRQL。 如果 ISR 处理多个中断向量或驱动程序具有多个 ISR,则此值必须是每个中断资源中 u.Interrupt.Level 传递的 Irql 值的最高值。 否则, Irql 和 SynchronizeIrql 值是相同的;
- [in] InterruptMode: 指定设备中断是 LevelSensitive 还是 Latched;
- [in] ShareVector: 指定中断向量是否可共享;
- [in] ProcessorEnableMask: 指定一个 KAFFINITY 值,该值表示此平台中设备中断可能发生的处理器集。 此值在 u.Interrupt.Affinity 处的中断资源中传递;
- [in] FloatingSave: 指定在驱动程序的设备中断时是否保存浮点堆栈。 对于基于 x86 和基于 Itanium 的平台,此值必须设置为 FALSE;
中断例程函数原型如下:
KSERVICE_ROUTINE KserviceRoutine;BOOLEAN KserviceRoutine([in] _KINTERRUPT *Interrupt,[in] PVOID ServiceContext
)
{...}
下面我们从一个实际的案例来看中断:
NTSTATUS
NICMapHWResources(__in PFDO_DATA FdoData,__in PIRP Irp)
{PCM_PARTIAL_RESOURCE_DESCRIPTOR resourceTrans;PCM_PARTIAL_RESOURCE_LIST partialResourceListTranslated;PIO_STACK_LOCATION stack;ULONG i;NTSTATUS status = STATUS_SUCCESS;DEVICE_DESCRIPTION deviceDescription;ULONG MaximumPhysicalMapping;PDMA_ADAPTER DmaAdapterObject;ULONG maxMapRegistersRequired, miniMapRegisters;ULONG MapRegisters;BOOLEAN bResPort = FALSE, bResInterrupt = FALSE, bResMemory = FALSE;ULONG numberOfBARs = 0;
#if defined(DMA_VER2) // To avoid unreferenced local variables errorULONG SGMapRegsisters;ULONG ScatterGatherListSize;
#endifstack = IoGetCurrentIrpStackLocation (Irp);PAGED_CODE();if (NULL == stack->Parameters.StartDevice.AllocatedResourcesTranslated) {status = STATUS_DEVICE_CONFIGURATION_ERROR;goto End;}partialResourceListTranslated = &stack->Parameters.StartDevice.\AllocatedResourcesTranslated->List[0].PartialResourceList;resourceTrans = &partialResourceListTranslated->PartialDescriptors[0];// 此处我们可以遍历所有IRP提供的硬件资源,一般是寄存器、端口、中断这三类for (i = 0;i < partialResourceListTranslated->Count;i++, resourceTrans++) {switch (resourceTrans->Type) {case CmResourceTypePort:numberOfBARs++;DebugPrint(LOUD, DBG_INIT,"I/O mapped CSR: (%x) Length: (%d)\n",resourceTrans->u.Port.Start.LowPart,resourceTrans->u.Port.Length);if(numberOfBARs != 2) {DebugPrint(ERROR, DBG_INIT, "I/O mapped CSR is not in the right order\n");status = STATUS_DEVICE_CONFIGURATION_ERROR;goto End;}FdoData->IoBaseAddress = ULongToPtr(resourceTrans->u.Port.Start.LowPart);FdoData->IoRange = resourceTrans->u.Port.Length;FdoData->ReadPort = NICReadPortUShort;FdoData->WritePort = NICWritePortUShort;bResPort = TRUE;FdoData->MappedPorts = FALSE;break;case CmResourceTypeMemory:numberOfBARs++;if(numberOfBARs == 1) {DebugPrint(LOUD, DBG_INIT, "Memory mapped CSR:(%x:%x) Length:(%d)\n",resourceTrans->u.Memory.Start.LowPart,resourceTrans->u.Memory.Start.HighPart,resourceTrans->u.Memory.Length);ASSERT(resourceTrans->u.Memory.Length == 0x1000);FdoData->MemPhysAddress = resourceTrans->u.Memory.Start;FdoData->CSRAddress = MmMapIoSpace(resourceTrans->u.Memory.Start,NIC_MAP_IOSPACE_LENGTH,MmNonCached);if(FdoData->CSRAddress == NULL) {DebugPrint(ERROR, DBG_INIT, "MmMapIoSpace failed\n");status = STATUS_INSUFFICIENT_RESOURCES;goto End;}DebugPrint(LOUD, DBG_INIT, "CSRAddress=%p\n", FdoData->CSRAddress);bResMemory = TRUE;} else if(numberOfBARs == 2){DebugPrint(LOUD, DBG_INIT,"I/O mapped CSR in Memory Space: (%x) Length: (%d)\n",resourceTrans->u.Memory.Start.LowPart,resourceTrans->u.Memory.Length);FdoData->IoBaseAddress = MmMapIoSpace(resourceTrans->u.Memory.Start,resourceTrans->u.Memory.Length,MmNonCached);if(FdoData->IoBaseAddress == NULL) {DebugPrint(ERROR, DBG_INIT, "MmMapIoSpace failed\n");status = STATUS_INSUFFICIENT_RESOURCES;goto End;}FdoData->ReadPort = NICReadRegisterUShort;FdoData->WritePort = NICWriteRegisterUShort;FdoData->MappedPorts = TRUE;bResPort = TRUE;} else if(numberOfBARs == 3){DebugPrint(LOUD, DBG_INIT, "Flash memory:(%x:%x) Length:(%d)\n",resourceTrans->u.Memory.Start.LowPart,resourceTrans->u.Memory.Start.HighPart,resourceTrans->u.Memory.Length);} else {DebugPrint(ERROR, DBG_INIT,"Memory Resources are not in the right order\n");status = STATUS_DEVICE_CONFIGURATION_ERROR;goto End;}break;case CmResourceTypeInterrupt:ASSERT(!bResInterrupt);bResInterrupt = TRUE;//// 将所有中断特定信息保存在设备中// 因为我们稍后在电源暂停和恢复期间需要它来断开和连接//FdoData->InterruptLevel = (UCHAR)resourceTrans->u.Interrupt.Level;FdoData->InterruptVector = resourceTrans->u.Interrupt.Vector;FdoData->InterruptAffinity = resourceTrans->u.Interrupt.Affinity;if (resourceTrans->Flags & CM_RESOURCE_INTERRUPT_LATCHED) {FdoData->InterruptMode = Latched;} else {FdoData->InterruptMode = LevelSensitive;}ASSERT(FdoData->InterruptMode == LevelSensitive);DebugPrint(LOUD, DBG_INIT,"Interrupt level: 0x%0x, Vector: 0x%0x, Affinity: 0x%x\n",FdoData->InterruptLevel,FdoData->InterruptVector,(UINT)FdoData->InterruptAffinity); // casting is done to keep WPP happybreak;default:DebugPrint(LOUD, DBG_INIT, "Unhandled resource type (0x%x)\n",resourceTrans->Type);break;}}if (!(bResPort && bResInterrupt && bResMemory)) {status = STATUS_DEVICE_CONFIGURATION_ERROR;goto End;}NICDisableInterrupt(FdoData);IoInitializeDpcRequest(FdoData->Self, NICDpcForIsr);status = IoConnectInterrupt(&FdoData->Interrupt,NICInterruptHandler,FdoData, // ISR ContextNULL,FdoData->InterruptVector,FdoData->InterruptLevel,FdoData->InterruptLevel,FdoData->InterruptMode,TRUE, // shared interruptFdoData->InterruptAffinity,FALSE);if (status != STATUS_SUCCESS){DebugPrint(ERROR, DBG_INIT, "IoConnectInterrupt failed %x\n", status);goto End;}MP_SET_FLAG(FdoData, fMP_ADAPTER_INTERRUPT_IN_USE);RtlZeroMemory(&deviceDescription, sizeof(DEVICE_DESCRIPTION));#if defined(DMA_VER2)deviceDescription.Version = DEVICE_DESCRIPTION_VERSION2;
#elsedeviceDescription.Version = DEVICE_DESCRIPTION_VERSION;
#endifdeviceDescription.Master = TRUE;deviceDescription.ScatterGather = TRUE;deviceDescription.Dma32BitAddresses = TRUE;deviceDescription.Dma64BitAddresses = FALSE;deviceDescription.InterfaceType = PCIBus;miniMapRegisters = ((NIC_MAX_PACKET_SIZE * 2 - 2) / PAGE_SIZE) + 2;maxMapRegistersRequired = FdoData->NumTcb * NIC_MAX_PHYS_BUF_COUNT;MaximumPhysicalMapping = (maxMapRegistersRequired-1) << PAGE_SHIFT;deviceDescription.MaximumLength = MaximumPhysicalMapping;DmaAdapterObject = IoGetDmaAdapter(FdoData->UnderlyingPDO,&deviceDescription,&MapRegisters);if (DmaAdapterObject == NULL){DebugPrint(ERROR, DBG_INIT, "IoGetDmaAdapter failed\n");status = STATUS_INSUFFICIENT_RESOURCES;goto End;}if(MapRegisters < miniMapRegisters) {DebugPrint(ERROR, DBG_INIT, "Not enough map registers: Allocated %d, Required %d\n",MapRegisters, miniMapRegisters);status = STATUS_INSUFFICIENT_RESOURCES;goto End;}FdoData->AllocatedMapRegisters = MapRegisters;FdoData->NumTcb = MapRegisters/miniMapRegisters;FdoData->NumTcb = min(FdoData->NumTcb, NIC_MAX_TCBS);DebugPrint(TRACE, DBG_INIT, "MapRegisters Allocated %d\n", MapRegisters);DebugPrint(TRACE, DBG_INIT, "Adjusted TCB count is %d\n", FdoData->NumTcb);FdoData->DmaAdapterObject = DmaAdapterObject;MP_SET_FLAG(FdoData, fMP_ADAPTER_SCATTER_GATHER);#if defined(DMA_VER2)status = DmaAdapterObject->DmaOperations->CalculateScatterGatherList(DmaAdapterObject,NULL,0,MapRegisters * PAGE_SIZE,&ScatterGatherListSize,&SGMapRegsisters);ASSERT(NT_SUCCESS(status));ASSERT(SGMapRegsisters == MapRegisters);if (!NT_SUCCESS(status)){status = STATUS_INSUFFICIENT_RESOURCES;goto End;}FdoData->ScatterGatherListSize = ScatterGatherListSize;#endifFdoData->AllocateCommonBuffer =*DmaAdapterObject->DmaOperations->AllocateCommonBuffer;FdoData->FreeCommonBuffer =*DmaAdapterObject->DmaOperations->FreeCommonBuffer;
End:return status;
}
可以看到,在上面的例子中,我们会调用IoConnectInterrupt函数来连接中断。
这里的代码中有一部分遗憾,由于没有使用自旋锁,所以调用SpinLock之前需要初始化自旋锁这一点并未体现。
还有另外一个遗憾就是,中断本身适合处理器相关的,但是IoConnectInterrupt的返回值中,可能出现STATUS_INVALID_PARAMETER,这个错误的本质是没有指定合适的处理器,但是它的返回值上没有说明这一点,参数ProcessorEnableMask指的就是处理器组,它的解释如下:
KAFFINITY 类型在 32 位版本的 Windows 上为 32 位,在 64 位版本的 Windows 上为 64 位。
如果组包含 n 个逻辑处理器,则处理器的编号从 0 到 n-1。 组中的处理器编号 i 由关联掩码中的位 i 表示,其中 i 在 0 到 n-1 的范围内。 与逻辑处理器不对应的关联掩码位始终为零。
例如,如果 KAFFINITY 值标识组中的活动处理器,如果处理器处于活动状态,则处理器的掩码位为 1;如果处理器不处于活动状态,则为0。
关联掩码中的位数确定组中逻辑处理器的最大数目。 对于 64 位版本的 Windows,每个组的最大处理器数为 64。 对于 32 位版本的 Windows,每个组的最大处理器数为 32。 调用 KeQueryMaximumProcessorCountEx 例程以获取每个组的最大处理器数。 此数字取决于多处理器系统的硬件配置,但永远不能超过 64 位和 32 位版本的 Windows 分别设置的固定 64 处理器和 32 处理器限制。
GROUP_AFFINITY 结构包含一个相关性掩码和一个组编号。 组号标识应用相关性掩码的组。
禁止和启用中断
和我们的直觉相反,禁止和启用中断非常频繁,这有几个原因:
首先,中断等级直接等于代码运行等级,windows明确规定中断不会被同级别的中断打断,只能被更高级别的中断打断;其次,边带信号可以理解为引脚上拉这种操作,这对于硬件那边也很好设计;最后,中断可以共享,因为系统内可以使用的中断向量只有两百多个,但是明显系统需要使用的中断远多于这个,故共享中断的时候,也需要注意避免中断之间的干扰。
中断的禁用也非常简单:
__inline VOID
NICDisableInterrupt(__in PFDO_DATA FdoData)
{FdoData->CSRAddress->ScbCommandHigh = SCB_INT_MASK;
}KSYNCHRONIZE_ROUTINE NICEnableInterrupt;__inline
BOOLEAN
NICEnableInterrupt(PVOID Context)
{PFDO_DATA FdoData = Context;FdoData->CSRAddress->ScbCommandHigh = 0;return TRUE;
}
此处解释一下, CSRAddress->ScbCommandHigh是已经映射进系统空间的中断寄存器的地址,对它修改之后,系统会检测到这种行为并设置,硬件那边在发起中断之前可以查询这个寄存器来决定要不要中断。
我们由此也能知道什么时候禁用和启用中断:
在中断例程正在执行时;
系统进入深度睡眠(< S4)同时设备不需要唤醒时;
设备禁用和启用;设备进入D3同时不需要唤醒时;