摘要:本文从工程应用开发的角度着重研究利用DDK开发TMS320DM642的WDM驱动程序的设计与实现问题。根据驱动程序设计的特点深入探讨了在Windows2000下开发WDM设备驱动程序的方法,给出了TMS320DM642的WDM驱动程序实现方案,并且解决了驱动程序设计中的中断、EDMA实现及与Win32应用程序异步通信等关键问题。实验数据表明:使用DDK编写的驱动程序与EDMA传输方式相结合可使读写速率达到30MB/s, 有效保证了TMS320DM642与高性能PCI总线的数据通信。
关键字:WDM,驱动程序,TMS320DM642,DDK,PCI总线
1.引言
TI(Texas Instruments)公司C6000系列的TMS320DM642芯片以其高速处理能力、出色的对外接口,如PCI接口,而广泛应用于视频会议、视频监控等众多领域,如何将其数据与具有高性能、线性突发传输、极小的存取延误、不受处理器限制等优势的计算机PCI(Peripheral Component Interconnect)总线[1]相互传送就成了关键问题,解决此问题的主要瓶颈在于驱动程序的设计。本文给出了TMS320DM642的WDM(Windows Driver Model)驱动程序设计方案及关键技术。
2.WDM 驱动模型分析[2]
WDM设备驱动程序是微软公司为当前主流操作系统的驱动程序设计的一种架构[3],它规定了标准化的驱动模块。其主要目标是实现能够跨平台使用、更安全、更灵活、编制更简单的Windows设备驱动程序。
WDM驱动程序是分层的,不同层上的驱动程序有着不同的优先级。WDM引入了功能设备对象FDO(Functional Device Object)与物理设备对象PDO(Physical Device 0bject)两个类来描述硬件,一个PDO对应一个真实硬件。一个硬件只允许有一个PDO,却可以拥有多个FDO,在驱动程序中直接操作的不是硬件而是相应的PDO和FDO。在WDM模型中,每个硬件设备至少有两个驱动程序,其中一个就是所编写的功能驱动程序。另一个称之为总线驱动程序,它由系统提供,负责管理计算机与硬件连接。在对硬件访问的过程中,设备驱动程序只需与下层的总线驱动程序打交道,具体控制硬件的琐碎工作交给总线驱动程序去完成。在有的驱动程序中还有位于设备驱动程序和总线驱动程序之间的过滤器驱动程序,用于修改和监视IRP (I/O Request Pack)。
3.TMS320DM642的驱动程序设计与实现
3.1 驱动程序的结构和主要例程[4]
驱动程序的主要工作如下:初始化;创建和删除设备;即插即用处理;分发例程处理;处理Win32打开和关闭文件句柄的请求;处理Win32输入输出请求;访问硬件;调用其它驱动程序;取消I/O请求;超时I/O请求;处理电源管理。在PCI设备的WDM驱动程序中,一般是编写功能驱动程序,PCI总线驱动程序由操作系统实现。
3.2 软件开发及驱动程序核心进程的实现[5]
驱动程序开发需要专门的开发工具,开发WDM驱动程序的主要工具有微软提供的软件包(DDK) (Driver Development Kit),NuMega公司的DriverStudio。DriverStudio把DDK封装成了完整的C++函数库,在开发驱动程序之前就可以通过向导根据具体硬件生成框架代码,不用开发者过多考虑底层的实现细节,因此成为许多驱动程序开发者的首选工具。但使用DriverStudio开发会导致开发者因驱动程序的理解不够深刻,使得驱动程序效率不高。而采用DDK开发驱动程序在深入了解操作系统的内核工作方式的基础上,则可实现代码简洁、结构清晰、高效的设备驱动程序。考虑到驱动程序的效率,及在不同Windows平台下的兼容,本题选用了DDK和Visual C++的开发方式。
3.2.1 驱动程序获取资源、初始化和清除例程
(1) PCI设备资源的获得
当系统的 PNP (Plug and Play) 管理器在取得设备资源后会自动向驱动程序发出IRP_MN_START_DEVICE 的 IRP,在该 IRP (IO Request Packet) 栈中包含了设备的资源信息。 关键代码如下:
NTSTATUS StartDevice(IN PDM642_DEVICE_EXTENSION dx, IN PCM_RESOURCE_LIST AllocatedResourcesTranslated)
{...
for( ULONG i=0; i<NumResources; i++,resource++)
{…
switch( resource->Type)
{
case CmResourceTypePort: //获取TMS320DM642的端口资源
dx->IOStartAddress[dx->iIONum] = resource->u.Port.Start;//开始物理地址
dx->IOLength[dx->iIONum] = resource->u.Port.Length;//地址数量
dx->IONeedsMapping[dx->iIONum] = (resource->Flags & CM_RESOURCE_PORT_IO)==0;
dx->IOInIOSpace[dx->iIONum] = !dx->IONeedsMapping[dx->iIONum];
portbase = resource->u.Port.Start;
dx->nports = resource->u.Port.Length;
dx->portbase = (PUCHAR) portbase.QuadPart;
dx->iIONum++;
break;
…}
}
//调用IoConnectInterrupt 注册其ISR(Interrupt Service Routine)。作为响应,I/O 管理器调用内核以创建驱
//动程序的中断对象。
IoConnectInterrupt( &dx->InterruptObject, PKSERVICE_ROUTINE)InterruptHandler,
(PVOID)dx, NULL, dx->Vector, dx->Irql, dx->Irql, dx->Mode, dx->Irqshare, dx- >Affinity, FALSE);
DEVICE_DESCRIPTION dd; //构建DMA对象
RtlZeroMemory(&dd, sizeof(dd));
dd.Version = DEVICE_DESCRIPTION_VERSION;
dd.Master = TRUE;
dd.ScatterGather = FALSE;
dd.Dma32BitAddresses = TRUE;
dd.Dma64BitAddresses = FALSE;
dd.InterfaceType = bustype; // PCIBus;
dd.DmaWidth = Width32Bits; // PCI default width
dd.MaximumLength = 0xFFFF;
//获取DMA适配器对象指针
dx->AdapterObject = IoGetDmaAdapter(dx->pdo, &dd, &dx->nMapRegisters);
…}
(2) DriverEntry例程
I/O管理程序在驱动程序首次装入时调用该例程,DriverEntry例程执行所有的第一次初始化任务,诸如声明其它驱动程序例程的地址。只要告诉内核这些例程的名字,以后内核会自动调整响应例程。
(3) AddDevice添加设备例程
WDM驱动程序通过AddDevice例程创建设备,该例程首先选择并形成一个内部设备名,然后调用IoCreateDevice创建设备对象和设备扩展,接着注册一个或多个设备接口,以使应用程序能知道设备的存在,并给出设备名和创建符号连接,最后把新设备对象放到堆栈上,初始化设备对象的Flag成员,建立设备对象。本题针对TMS320DM642内部PCI模块特点采用基于系统包(PACKET)的DMA(Direct Memory Access)方式,其中,AddDevice例程关键代码如下:
NTSTATUS Dm642AddDevice(IN PDRIVER_OBJECT DriverObject, IN PDEVICE_OBJECT pdo)
{…
status = IoCreateDevice( DriverObject, sizeof(DM642_DEVICE_EXTENSION), NULL, FILE_DEVICE_UNKNOWN, 0, FALSE, &fdo); //创造 fdo
//初始化设备
PDM642_DEVICE_EXTENSION dx = (PDM642_DEVICE_EXTENSION)fdo->DeviceExtension;
dx->fdo = fdo;
dx->pdo = pdo;
KeInitializeEvent( &dx->StoppingEvent, NotificationEvent, FALSE);
…
POWER_STATE NewState; //初始化电源状态
NewState.DeviceState = dx->PowerState;
PoSetPowerState( fdo, DevicePowerState, NewState);
//注册使能设备接口
status = IoRegisterDeviceInterface( pdo, &DM642_GUID, NULL, &dx->ifSymLinkName);
fdo->Flags |= DO_DIRECT_IO; //设置 fdo 标志
fdo->Flags &= ~DO_DEVICE_INITIALIZING;
…
return STATUS_SUCCESS;
}
(4) Unload例程
I/O管理程序在驱动程序被动态卸载时调用该例程,Unload例程反转DriverEntry所执行的行为,删除所有设备名和所分配的资源。
3.2.2 I/O系统服务调度例程
I/O管理程序先把请求类型转换为一个代码,然后调用驱动程序中相应的Dispatch例程。
(1) 打开和关闭操作
用户程序发出CreateFile请求获取设备句柄,驱动程序调用相应的DispatchCreate例程处理该请求,完成后,用户程序调用CloseHandle来执行清除工作,相应的,驱动程序提供Dispatch例程完成该操作。
(2) 设备操作
Win32用户程序发出ReadFile,WriteFile,DeviceloControl等设备操作请求时,I/O管理程序为各个请求创建功能代码(IRP_MJ_READ,IRP_MJ_WRITE,IRP_MJ_DEVIC_CONTROL),并提供相应的驱动程序例程Dm642Read、Dm642Write或Dm642DeviceControl,其中,从设备读取数据的Dm642Read例程部分代码如下:
NTSTATUS Dm642Read (IN PDEVICE_OBJECT fdo, IN PIRP Irp)
{…
KeAcquireSpinLock(&BufferLock,&irql);//获取自旋锁
IoMarkIrpPending(Irp); //使I/0请求入对列
StartPacket(&dx->dqReadWrite, fdo, Irp, OnCancelReadWrite); DispatchCance1);
//调用StartIo例程开始传输数据
…
KeReleaseSpinLock(&BufferLock,irql); //释放自旋锁
return STATUS_PENDING; //返回状态值
}
3.2.3 数据传输例程
(1) Startlo例程
当设备开始启动数据传输时,I/O管理程序调用该例程,当一个高级的I/O请求结束而另一个请求等待在队列中时,由I/O管理程序产生该请求,即I/O管理程序使所有I/O请求进入队列并串行化所有的I/O请求,Startlo例程的部分代码如下:
VOID Dm642StartIo(IN PDEVICE_OBJECT fdo, IN PIRP Irp)
{…
switch( stack->MajorFunction)
{…
case IRP_MJ_READ:
mdl = Irp->MdlAddress;
dx->numxfer = 0;
dx->xfer = dx->nbytes = MmGetMdlByteCount(mdl);
dx->vaddr = MmGetMdlVirtualAddress(mdl);
//计算需要映射寄存器个数
nregs = ADDRESS_AND_SIZE_TO_SPAN_PAGES(dx->vaddr, dx->nbytes);
if (nregs > dx->nMapRegisters)
{ //如果需要映射寄存器个数大于系统分配个数,则一次只能传输系统分配的大小
nregs = dx->nMapRegisters;
dx->xfer = nregs * PAGE_SIZE - MmGetMdlByteOffset(mdl);
}
dx->nMapRegistersAllocated = nregs;
//调用适配器控制例程
status = (*dx->AdapterObject->DmaOperations->AllocateAdapterChannel)
(dx->AdapterObject, fdo, nregs, (PDRIVER_CONTROL) AdapterControl, dx);
…}
(2) AdapterControl(适配器控制例程)
AdapterControl例程通过系统DMA控制器在它的设备和系统物理内存之间完成传递操作。此例程调用系统提供的适配器对象支持例程以执行DMA传输。作为设备启动的一部分,这些驱动程序调用I/O 管理器,I/O管理器轮流调用平台特定的HAL 来创建一组适配器对象。AdapterControl例程的部分代码如下:
IO_ALLOCATION_ACTION AdapterControl(PDEVICE_OBJECT fdo, PIRP junk, PVOID regbase, PDM642_DEVICE_EXTENSION dx)
{…
(*dx->AdapterObject->DmaOperations->FlushAdapterBuffers)
( dx->AdapterObject, mdl, dx ->regbase, dx ->vaddr, dx ->xfer, TRUE);
PHYSICAL_ADDRESS address = (*dx->AdapterObject->DmaOperations->MapTransfer)
(dx->AdapterObject, mdl, regbase, dx->vaddr, &dx->xfer, !isread);
StartTransfer(dx, address, isread);
}
(3) StartTransfer(传输例程)
本例程根据TMS320DM642内部PCI模块要求,控制其EDMA(Enhanced Direct Memory Access)寄存器,启动数据传输。部分代码如下:
VOID StartTransfer(PDM642_DEVICE_EXTENSION dx, PHYSICAL_ADDRESS address, BOOLEAN isread)
{…
if (isread)
{…
//配置TMS320DM642寄存器启动EDMA传输数据
RtlCopyMemory((dx->MemBase[0]+0x00010),&udspma,sizeof(ULONG)); //DSPMA
RtlCopyMemory((dx->MemBase[0]+0x00014),&address.LowPart,sizeof(ULONG));//PCIMA
RtlCopyMemory((dx->MemBase[0]+0x00018),&upcimc,sizeof(ULONG));//PCIMC
}
…}
3.2.4 ISR 中断服务例程
在PCI总线中,很多设备共享一个中断,这就需要在中断处理函数要格外小心,处理不当就会导致系统崩溃。驱动程序首先要在IRP_MN_START_DEVICE中获得中断资源,然后需要连接到中断处理函数中,使其当有中断请求时,进入中断服务例程。连接中断的函数为IoConnectInterrupt。部分代码如下:
BOOLEAN InterruptHandler(IN PKINTERRUPT Interrupt, IN PDM642_DEVICE_EXTENSION dx)
{
KIRQL irql;
KeAcquireSpinLock(&dx->ReadIrpLock,&irql);
ULONG rstsrc = 0x10; //根据TMS320DM642结构清楚PCI中断
RtlCopyMemory(dx->MemBase[1]+0x400000,&rstsrc,sizeof(ULONG));//置TMS320DM642对应寄存器
KeReleaseSpinLock(&dx->ReadIrpLock,irql);
return FALSE;
}
3.2.5 DPC 延迟调用例程
DPC例程用来实现设备操作的后处理,在DMA传输时,如果本次DMA操作有更多的数据传输,则进行下一次传输,否则DPC例程将传输的数据大小和最后的状态信息放入IRP,通知I/O管理器已完成当前请求并准备接受下一个请求。
4.驱动程序测试结果
通过对硬件读、写大量数据,确认传输正确,传输速率达30MB/s,有效保证了TMS320DM642与高性能PCI总线的数据通信。
5.结束语
PCI设备为外设与外设、外设与主机之间实现高速数据传输提供了一个高效的途径,且 WDM驱动模型已经成为当今驱动程序开发的主流。本文给出了在Windows 2000环境下TMS320DM642的WDM驱动程序DDK具体实现,调试通过,运行稳定。
参考文献
[1]. 李贵山,戚德虎. PCI局部总线开发者指南[M].西安:西安电子科技大学出版社,2000:3.
[2]. 武安河,于洪涛.Windows2000/XP WDM设备驱动程序开发[M].北京:电子工业出版社,2003:
[3]. 尹勇,李宇.PCI总线设备开发宝典[M].北京:北京航空航天大学出版社,2005:99.
[4]. 贾涛,王铁岭.PCI数据采集卡的WDM 驱动程序开发[J].国外电子测量技术,2006,25(8):2
[5]. (美)[C.坎特]Chris Cant著;孙义[等]译;Windows WDM设备驱动程序开发指南[M].北京:机械工业出版社,2005.
作者简介
臧淼:女,1977年生,硕士,北方工业大学信息学院通信工程系讲师,主要研究方向为数字系统设计和多媒体通信。
联系方式:
联系人:臧淼
通信地址:北京石景山晋元庄路5号北方工业大学信息学院 100041
电话:010-88803049
手机:13693390959
电子信箱:zangmiao999@sina.com