DriverStudio开发PCI设备DMA数据传输

时间:2023-01-22 15:19:10

DriverWizard向导可以创建基本的wDM驱动程序框架,包括总线类型,地址空间,中断源,DMA资源,以及IOCTL(i/o控制代码)的定义等等。详细情况可参看DriverStudio的帮助文档,以下主要介绍如何用DriverWorks编写DMA方式的驱动程序。

DriverWorks关于DMA操作。封装了三个类:KDmaAdapter, KDmaTransfer和KCommonCmaBuffer。KDmaAdapter类用于建立一个DMA适配器。说明DMA通道的特

性; KDmaTransfer类用于控制DMA传输: KCommonCmaBuffer类用于申请公共缓冲区。

DriverWorks中很少涉及到映射寄存器。只是提到KDmaTransfer类“由于硬件或者缓冲区大小的限制。将一个DMA请求分段进行。为每个段提供了一个传输段描述符数组,当设备不支持分散/集中时,这个数组中的描述符只有一个。每个描述符包含一个物理地址和相应的字节数。其结构如下:

typedef struct TRANSFERDESCRIPTOR

{

PHYSICAL_ADDRESS td_PhysAddr;,

ULONG tdLength;

} PrRANSFERDESCRIPTOR;

其中的物理地址就是要提供给总线控制器的,实际上就是前面所说的逻辑地址;而字节数也就是相应的连续逻辑地址范围的长度。DriverWorks通过封装,隐藏了映射寄存器的操作,简化了编程。

DriverWorks关于DMA的驱动编程步骤如下:

(1)定义适配器对象。在驱动程序基类的成员变量中定义一个KDmaAdapter类的适配器对象,并且在启动设备的时候对其初始化,正确的建立适配器描述符(DEVICE_DIS—CRItrflON),说明是否为总线主设备、是否支持分散/集中以及总线类型等特征,详细定义参考DDK文档。

(2)定义KDmaTransfer类的对象。可在全局范围内静态定义,也可定义为驱动程序基类的成员变量,或者在需要时在内存堆上分配。进~TDMA传输的设备使用Queuelrp串行IRP,于是DMA传输通常在成员函数Startlo中初始化,同时定义一个DMA回调例程(作为KDmaTransfer类初始化成员函数的一个参数)。

(3)写DMA回调例程。如前所述,KDmaTransfer类的一个重要功能就是根据硬件或者缓冲区大小的限制将一个传输分解成段。每当准备传输一个新段时,KDmaTransfer的对象将通知驱动程序调用回调例程。回调函数的原型typedef DMAREADY— CALLBACK指定, 通常使用宏DEVMEM—BER— DMAREADY,声明回调函数为驱动程序基类的成员函数。调函数首先检查传输是否完成。调用成员函数BytesRemaining,如果返回为0, 回调函数将调用成员函数Terminate,完成相应的IRP。否则,回调函数继续传输。

(4)编写处理当前DMA段传输结束的代码。一段传输结束,通常硬件以一个DMA中断通知系统。在中断服务例程中判断是否为DMA中断,如果是,就请求DPC (延迟调用过程)。在DPC中,驱动程序必须调用成员函数Continue通知传输对象此段传输完成。这个成员函数按照需要清空缓冲区,更新传输状态。如果整个传输并没有完成。则建立下一段传输调用DMA回调例程。

利用DriverWizard生成驱动程序框架共需要11个步骤,其中比较重要的包括:

(1)在第四步中选中PCI.并在Device ID和Vendor ID中分别输入设备号和厂商号.还需要填入PCI Subsystem ID和PCI Revision ID。这些ID都保存在PCI配置寄存器PCR中。3.1版中的C DriverWizard提供一个PnP View可以查看这些值,另外也可以使用PCI Tree等免费软件查看。

(2)第九步是比较关键的一步。首先在Resources中添加资源.在Name中输入变量名.在PCI Base Address中输入0—5的序列号.0—5和BARO—BAR5一一对应。实例中有两个IoPort资源。在设置中断对话框中.在name栏写人中断服务程序的名称,选中创建中断服务程序ISR(Interrupt Service Route),选中创建延迟程序调用DPC(Deferred Processing Call),选中Make ISR/DPC class functions。

然后选中Buffer选取读写方式.用于描述与I/O操作相关的数据缓冲区。实例中不需要传送大量数据,因此采用Buffered方式。

OnStartDevice(Klrp I)

参数I中包含了系统分配的两种资源配置信息,一种是原始的资源分配信息(AllocatedResources),一种是转换后的资源分配信息(AllocatedResources Translated)。因为I/O总线与CPU在寻址物理硬件的方式上不同.所以存在着两种资源列表。在WDM出现之前.内核模式驱动程序从注册表、PCI配置空间、或其它地方获取原始的资源值,并通过调用诸如HalTranslateBusAddress或HalGetInterruptVector函数转换这些数值。现在。接收和转换工作全部由PnP

管理器来完成.WDM驱动程序需要做的仅是从设备启动IRP中获取这些资源。

PCM_RESOURCE_LIST pRaw = I.AllocatedResources();

PCM_RESUOURCE_LIST pTranslated = I.TranslatedResources();

接下来.程序员需要根据自己硬件分配的资源.初始化相应的寄存器和中断等。采集卡中只有BARl(PCI I/O访问基地址)和BAR2(PCI本地空间0基地址)两个地址有效。定义和初始化如下:

KIoRange m_IoConfig;

KIoRange m_LocalAddrSpace0;

NTSTATUS status = m_IoConfig.Initialize(pTranslated,pRaw,0);

NTSTATUS status = m_LocalAddrSpace0.Initialize(pTranslated,pRaw,1);

其中第3个参数为I/O口对应的基地址.用来转换成特定端口资源的序数。在将硬件中断与中断服务例程连接之前.先要关闭PCI采集卡中断.方法是将9052控制芯片I/0基地址偏移0x4Ch的INTCSR寄存器中断使能位(第6位)屏蔽。方法如下:

m_IoConfig&=~PLX9052_INTCSR]&=~PLx9552_INTCSR_IE.其中PLX9052 INTCSR、PLX9052 INTCSR IE定义分别为

#define PLX9052_INTCSR ULONG(0x004C)

#define PLX9052_INTCSR_IE ULONG(1<<6)

然后利用InitializeAndConnect()初始化并连接中断。

接下来在PCI一6038中定义设置中断模式为下降沿触发.设置通道一为读方式,具体操作是向PCI一6038特定偏移的地址中写入特定的控制字节,最后开中断。

中断服务例程

当外部中断到来时,调用中断服务例程。这时必须首先判断该中断是否是用户期待的中断源所产生。读取9052控制芯片INTCSR寄存器的LINTil Status(第2位)的值,若为1,表明中断有效;否则,直接返回。

ULONG IntCS=m_IoConfig[PLX8052_INTCSRl;

if ((IntCS&PLX9052_INTCSR_LINTlS)&&(IntCS&PLX9052_INTCSR_LINTlE))

{

t<<’’LINTl arrived!“<<BOL:

//采集时间

}

其中PLX9052_INTCSR_LINTlS定义为:

#define PLX9052_INTCSR_LINTlS ULONG(1<<2)

由于中断服务例程运行在执行高于DISPATCH_LEVEL的中断运行级上.因此ISR中使用的代码和数据必须存在于非分页内存中。此外。ISR能调用的内核模式函数有限。为了提高系统性能,ISR应该尽可能快地执行。基本上.只做服务硬件所需的最小量的工作.然后立即返回。如果有额外的工作需要做,应该交给DPC来完成。

延迟调用例程

ISR中采集了B码发送的GPS时间.其余工作由DPC完成,包括清除中断、通知用户应用程序时间采集完成等。

由于PCI一6038提供了中断清除寄存器.因此只要向该寄存器中写入0x00.就可以清除当前中断。在中断操作过程中,plx9052控制寄存器中的INTCSR寄存器中,除了INTl(bit#8)和INT2(bit#9)使能外,其余bit#0一bit#6均保持默认设置。

驱动程序调试

使用SoftIce作为调试工具.基本过程如下:

(1)使用Svmb01 Loader加载驱动程序水.Bins文件,然后激活SoftIce,设置断点跟踪调试;

(2)用Genint命令产生虚拟中断,单步跟踪中断服务例程;

(3)改变B码发送频率.检验驱动程序响应速率。在驱动程序的调试过程中,经常出现系统“死机”、“蓝屏”等现象,这些情况可能由于内存访问分页错误、寄存器非法操作等因素造成。

加载存储板的操作都是经过PCI9054的配置,但是在驱动程序设计过程中,一般会遇到DMA传输、内存映射、文件读写、中断处理等问题。在此模块,应注意的是读写访问和内存映射问题,对于局部总线地址进行的读写访问,它的部分驱动程序如下:

NTSTATUS SIGNALDevice::SIGNAL_MEM2_WRITE_Handler(KIrp I)

{⋯⋯ULONG *length,*offset;

ULONG plength,poffset;

Offset = (ULONG*)I.IoctlBuffer();      //数据在局部地址上的偏移量

poffset = *offset;

length=(ULONG*)I.IoetlBuffer()+l;      //数据传输个数

plength=*length:

m_MemoryRange2.outd(poffset,(unsigned long*)I.IoctlBuffer()+2,plength);

…..}

对于内存映射问题,因为内存管理和数据缓存相互对应,数据传输时使用缓冲区的方式就是内存映射的方式,在驱动程序开发过程中,有3种内存映射的方式:

(1)Buffered方式:I/O管理器先创建一个与用户模式数据缓冲区大小相等的系统缓冲区,驱动程序将使用这个系统缓冲区工作。I/O管理器就负责在用户模式缓冲区与系统缓冲区之间复制数据;

(2)Direct方式:I/O管理器锁定了包含用户模式数据缓冲区的物理内存页,并创建一个称为MDL(内存描述符表)的辅助数据结构来描述锁定页。本模块采用这种方式;

(3)Neither方式:以上两种方式之外的方式,在这个策略中,I/O管理器把调用者的输入缓冲区的地址放IRP当前的I/O堆栈单元的Parameters.DeviceIoContr01.TypelnputBuffer域中,把输出缓冲区的地址存放到IRP UserBuffer域中。

打开中断和关闭中断

在应用程序中,若想打开中断,先通过ReadFile获取中断使能寄存器的值,如值为0,则通过WriteFile往该寄存器写入1打开中断。若想关闭中断,先ReadFile获取中断使能寄存器的值,如值为1,则通过WriteFile往该寄存器写人0关闭中断。

应用程序中的ReadFile和WriteFile并不能直接访问设备,它们是通过驱动程序的read和write例程来访问设备的。

NTSTATUS PcicardDevice::Read(KIrp I)

{

PUCHAR pBuffer=(PUCHAR)I.BufferedReadDest();  //获取数据缓冲区地址

*pBuffer=m_IoPortRange.inb(IntEnableReg);     //从端口读中断使能寄存器的值

}

Write例程与Read例程相似,只是获得缓冲区地址的语句和访问设备的语句不同。

PUCHAR pBuffer=(PUCHAR)I.BufferedWriteSource();  //获取数据缓冲区地址

m_ioPonRange.outb(IntEnableReg,*pBuffer);    //往端口写一个字节数据,

I/O端口的访问

当驱动程序设置事件后,应用程序通过WaitForSingleObject(hEvent,0)函数接到“通知”,开始利用DeviceloControl函数对设备发起读操作。

DeviceloControl(hDevice,PCICARD_IOCTRL_EAD,NULL,0,DataBuffer,IOCTRL_OUTBUF_SIZE,&nOutput,NULL)

驱动程序在处理该IRP时,并不是直接去读设备,而是利用函数SynchronizeInterrupt(&m_Irq,LinkTo(ReadDataFromDevice),PIRP(I))调用中断同步例程ReadDataFromDevice。

B00LEAN PcicardDevice::ReadDataFromDevice(PVOID pIrp)

{

PUCHAR pBuffer=(PUCHAR)I.IoetlBuffer();//获得应用程序数据缓冲区地址

m_MemoryRange.inb(Offset,pBuffer,Count);

I.Information()=Count;

m_bNotifyApp=TRUE;  //当完成读操作后,给m_bNotifyApp赋值为TRUE,当数据采集卡再次发出中断时,在中断服务例程及延时处理例程中重新设置事件。

return TRUE;

}

断处理例程

中断处理需要声明并在P11P启动例程中初始化KInterrupt和KDeferredCall类实例,还需要声明中断服务例程和延迟过程调用例程。当创建驱动程序框架时,若有中断资源,这些都是自动生成的。图1是中断处理流程图。

在中断服务例程中,首先判断该中断是否是自己的设备产生的,若不是,返回FALSE;若是,进行必要的处理,请求一个DPC,然后返回TRUE。

BOOLEAN PcicardDevice::Isr_Irq(void)      //中断服务例程.

{

if((m_IoPortRange.inb(OxO)&&m_IoPortRange.inb(0x1))==0)

return FALSE;              //判断是否为本设备产生的中断

m_IoPortRange.outb(OxO,O);    //清除中断

if(!m_DpcFor_Irq.Request(NULL,NULL)){}   //请求延迟过程调用

return TRUE;

}

在延迟过程调用例程中进行相应的中断处理。

if(m_pEvent)

{

SynchStatus=SynchronizeInterrupt(&m_Irq,LinkTo(TestAndClearNotifyApp),&Notify);     //调用中断同步例程

TestAndClearNotifyApp

if(Notify)

m_pEvent->Set();    //设置事件

}

在应用程序中用CreateEvent函数创建自动重置事件,并调用DeviceIoControl函数把事件的句柄传递给驱动程序。

m_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);    //创建事件

驱动程序接收到事件的句柄后,重新构造系统事件m_pEvent,应用程序通过WaitForSingleObject(hEvent, 0)函数来等待事件发生。

NTSTATUS PcicardDevice::DeviceControl(KIrp I)

{

HANDLE hEvent = (ANDLE*)I.IoctlBuffer();  //应用程序创建的事件句柄

if (m_pEvent)

delete m_pEvent;

m_pEvent = new (NonPagedPool)KEvent(hEvent, OBJECT_TYPE_ACCESS);   //构造系统事件函数

}

一旦系统事件被设置(m_pEvent->set),则应用程序发起读操作获得数据并作后期处理。

件资源访问

DriverW6rks 中提供DriverWizard, 利用DriverWizard可以很方便生成驱动程序框架,共有10个步骤,其中关键步骤如下:

①在第2步,因为使用的操作系统是Windows2000,选中WDM项。

②在第6步中选择IRP(I/O Request Packet)队列排队方法,决定驱动程序检查设备的方法;若选用SystemManaged方式,则所有的IRP排队都由系统(即I/O管理器)来完成,此方法虽然灵活性较小,但可减少难度。

③在第8步,选中Bu疏r以选取读写方式:

Direct、Buffer、非Direct和Buffer。在选取读写方式时应注意,当需要快速传送大量数据时,用Direct I/O,反之用Buffer I/O。

④在第9步,定义系统与WDM驱动程序通信控制命令。视设备需求而定,如:定义CAN_INIT(CAN初始化)、CAN_RECEIVEMSG(CAN接收数据)、CAN_SENDMSG(CAN发送数据)等命令。

PCI9054的中断寄存器和DMA寄存器都采用了I/O映射方式,利用DriverWorks软件提供的KIoRange类,产生该类的一个实例,在设备启动例程(OnStartDevice)中初始化该实例,即可实现对硬件的两个I/O地址空间的映射,其中一个I/O地址空间用于访问桥接芯片PCI9054的寄存器,另一个I/0地址空间用于访问本地端的设备。如果映射成功,就可以使用KIoRange类的成员函数访问硬件资源,常用的成员函数有inb、inw、ind、outb、outw和outd。

以inb和outb为例说明如下:

VOID inb(ULONG Byteoffset,PUCHAR Buffer,ULONG Count):从偏移为Byteoffset的地址开始连续读取Count个字节数据放在Buffer缓冲区中。

VOID outb(ULONG Byteoffset,PUCHAR Buffer,ULONG Count):从Buffer中连续写Count个字节数据放在偏移地址为Byteoffset的地方。

其他的函数只是读写数据类型不同,W代表字(16位),d代表双字(32位)。

DriverWorks提供了3个类用于实现DMA操作,KDmaAdapter类用于建立一个DMA适配器说明DMA通道的特性,KDmaTransfer类用于DMA传输控制,KCommonDmaBuffer类用于申请系统提供的通用缓冲区。

①产生DMA适配器对象。在设备启动例程(OnStartDevice)中创建一个KDmaAdapter类的实例,并且在适配器对象描述表中正确描述适配器对象。描述表中的信息要与硬件一致,然后用KDmaAdapter类的成员函数Initialize初始化该DMA适配器。

②在设备启动例程(OnStartDevice)中创建一个KCommonDmaBuffer类实例,调用该类的成员函数Initialize初始化,需要用到DMA适配器,并指定通用缓冲区的大小。

③创建一个KDmaTransfer类实例,并调用该类的成员函数Initialize初始化,根据驱动程序使用的内存类型的不同有两种不同的初始化方式。驱动程序使用的内存类型分为“packet”和“common buffer”两种。“packet”形式的驱动程序采用直接I/O内存缓冲策略,应用程序将存放数据的源地址或目的地址传人驱动程序,驱动程序用IRP中的MDL信息获取;“commOB buffer”是通用缓冲区,它是系统分配的一个物理连续内存缓冲区,处理器和设备都可以存取,这种方法

适合经常传输数据的总线主控设备适配器,适用于不支持scatter/gather的设备传输大数据。鉴于硬件设备不支持scatter/gather,而且每收到一次本地中断就要进行一次DMA传输256 kBytes数据,数据传输频繁,所以码流播放卡的驱动程序中使用的内存类型是“common bufier”o

④编写KDmaTransfer类实例的回调函数OnDmaReady,由宏DMAREADY_CALLBACK定义。回调函数调用KDmaTransfer类的成员函数BytesRemaining,根据其返回值是否为0来判断数据传输是否已经完成。如果传输已完成,则调用该类的成员函数Terminate完成相应的IRP;如果未完成,则调用该类的成员函数GetTransferDescriptors,获得当前传输数据的物理地址、传输数据的字节数,然后进入StartDMA例程设置硬件的桥接芯片PCI9054的DMA寄存器,包括DMA模式、PCI端的地址、本地端的地址、数据传输方向和需要传输的字节数,开启DMA通道,开始真正的数据传输。

中断处理部分

①数据传输完成时PCI9054的DMA中断控制器产生中断(称为DMA中断),驱动程序的中断服务例程(ISR)做出响应;前面提到了FIFO中的数据量少于一半容量时FPGA产生本地中断,驱动程序同样会进人中断服务例程,两种中断发生时驱动程序要执行不同的操作,这就要求在中断服务例程中区分这两种中断。两种中断依靠桥接芯片PCI9054芯片DMA中断寄存器不同的比特发生变化来区分,在ISR例程中读取DMA中断寄存器,判断中断类型并设置驱动程序中的一个全局变量标识bcInt,然后清除本次中断,连接延迟过程调用(DPC)例程。

②在DPC例程中根据全局变量标识LocInt的不同,执行不同的操作:如果是硬件的本地中断(代表了FIFO中的数据量少于一半容量),则通知应用程序发送数据再次进行数据传输;如果是DMA传输完成的中断,则调用KDmaTransfer类的Continue()成员函数,进而又进入回调函数OnDmaReady,此时数据传输已经完成,调用KDmaTransfer类的成员函数Terminate并且完成相应的IRP。

DveiceIoControI函数调用时,驱动程序根据I/O挖制命令来决定如何获取应用程序的缓冲区地址。I/O控制命令中数据访问方式的定义有以下四种:METHOD_BUFFERED、METHOD_IN_DIRECT、METHOD_OUT_DIRECT和METHOD_NEITHER四种定义分为缓冲I/O、直接I/O、其他I/O方式。大部分IOCTL操作传输的数据少于一个内存叶,应该使METHOD_BUFFERED方式。在实例中I/O控制命令定义为METHOD_BUFFERED方式。使用这种方式时,I/O错理器创建一个足够大的内核模式

拷贝缓冲区(与用户模式输入和输出缓冲区中最大的容量相同)。用户模式的输入数据被复制到这个拷贝缓冲区。驱动程序通过KIrp::IoctlBuffer访问这个缓冲区。在这个IRP完成之前,应该向拷贝缓冲区填入需要发往应用程序的输出数据字节数。当IRP完成时,I/O管理器把数据复制到用户模式缓冲区并设置反馈变量。

阁为本设计在资源配置时已经指定映射为存储器模式,所以采用了内存访问。在DriverWizard生成驱动程序框架时声明KmemoryRange类的对象m_MemoryRangeO_FbrBaseO,并且在驱动程序的OnStartDevice()函数中对m_MemoryRange0_ForBase0实现Initialize的转换。这样可以 把驱动层序得到的PCI总线配置机构分配的物理内存转换为系统可访问的非分页内存。具体的实现如下:

Status = m_MemoryRange0_ForBase0_.Initialize(pResListTranslated, pResListRaw, PciConfig.BaseAddressIndexToOrdinal(0))