首页 > 代码库 > 基于WDF的PCI/PCIe接口卡Windows驱动程序(5)-如何为硬件移植驱动程序

基于WDF的PCI/PCIe接口卡Windows驱动程序(5)-如何为硬件移植驱动程序

原文地址:http://www.cnblogs.com/jacklu/p/6139347.html

正如前几篇博客所说,使用WDF开发PCIe驱动程序是我本科毕业设计的主要工作。在读研的两年,我也分别为所在课题组移植了自己编写的驱动程序,在Windows 32位和64位平台下的PXI、PXIe、PCI、PCIe板卡分别得到了验证。

这篇文章根据自己最新编写的驱动代码(源代码请找博主索取),主要讲述如何为自己的硬件板卡移植驱动程序,并简单讲述如何使用Altera系列FPGA配置PCI IP核,然后对INF文件作简要描述,最后描述如何使用Qt编写上位机软件调用底层驱动。

准备去读博了,这一篇将作为这个系列的完结,之后将把更多精力放到机器学习上来。 

1概述

所编写的驱动代码程序包括7个源代码文件,分别是Device.h, Driver.h, Public.h, Trace.h, Device.c,Driver.c, Queue.c。其中Device.h 定义了与硬件相关的地址偏移;Public.h定义了DeviceIoControl 用到的用户自定义命令字, 此头文件由上层应用程序和驱动程序共同使用;Queue.c定义了I/O回调例程,分别使用了read、write和I/O Control 三个队列。 除了这三个文件外, 不建议更改其他4个文件的代码。三个源文件函数列表分别如图 1-1、1-2、1-3所示:

技术分享

2驱动程序移植说明

2.1 Public.h说明

  • 代码中定义了GUID值,开发者可以使用 VS2013 下的工具 GUIDGen.exe 生成 GUID值,该GUID标识驱动程序,应用程序根据这个GUID值来找到对应的驱动程序。
  • 代码中定义了CTL_CODE, I/O 处理例程 DeviceIoControl 的第二个参数dwIoControlCode 就是由 CTL_CODE 宏定义的。 CTL_CODE 是一个用于创建一个唯一的32 位系统I/O控制代码的宏,这个控制代码包括4部分组成:DeviceType(设备类型,高16位(16-31 位)),Access(访问限制,14-15 位),Function(功能2-13位),Method(I/O访问内存使用方式)。CTL_CODE 定义中有一个Method域,该2域定义了驱动程序中获取应用程序数据缓冲区的地址方式。 已经定义的 CTL_CODE 命令字说明如表 2-1 所示, 用户可以根据格式自定义 CTL_CODE, 实现不同功能。

技术分享

什么是 CRA 寄存器组? 打开 quartus 的 sopc builder,可以看到sopc架构如图 2-1 所示,在PCI IP核一栏中有 Control Register Access 寄存器组,地址范围0x00000000-0x00003fff。 里面关键的寄存器地址如图 2-2 所示。 通过读黄色标识的寄存器,可以通过驱动程序调试验证 PCI 核。 关于 CRA 寄存器组的配置说明会在 2.3 节详细说明。

技术分享

技术分享

2.2 Device.h说明

  • 代码对 FPGA 上硬件资源的偏移地址进行宏定义,在 Altera 系列的 FPGA 里,这些偏移地址也叫 Avalon 地址, 在 sopc builder 可以自定义分配, 如图 2-1 中的 Base和End 所示, “ 小锁头” 标志表示地址锁定, 点击该标志解锁后可以自定义便宜地址。这些地址必须与驱动程序中所用的地址一一对应;
  • 代码定义了设备对象结构体, 对几个重要的成员变量注释如下:

技术分享

  • 代码对一些事件回调例程进行了说明, 一般不需要用户进行二次修改;

2.3 Queue.c说明

  • 代码是用户需要针对功能开发的代码。 以从应用程序向驱动程序写入偏移地址为例,即原代码第 xx-xx 行 , 首先在 Public.h 文件里定义 IoControlCode 为qd41_IOCTL_WRITE_OFFSETADDRESS, 第 48-53 行为获取应用程序输入缓存数据的指针( 代码中为 inBuffer) , 通过赋值语句将 inBuffer 指向的数据内容赋给设备对象的成员变量 OffseAddressFromApp, 即完成了本功能;
  • 代码定义了写队列功能, 对 DMA 的写功能配置在此函数中完成。 Altera的 DMA IP 核共有 5 个寄存器, 如图 2-3 所示。

技术分享

  • 在配置 DMA 前需要配置 PCI CRA 寄存器, 使能 PCI 中断, 配置 Avalon-PCI 地址转换表, 如代码所示;
  • 为什么需要配置 Avalon-PCI 地址转换表? 因为 PCI IP核一端是 PCI 总线,一端是Avalon总线,地址转换过程图 2-4 所示,类似MMU地址翻译原理,不在赘述,此时需要把PC机获得的DMA传输缓存物理地址的高16位地址写入地址转换表;

技术分享

  • 配置好 PCI 后就可以配置 DMA 控制寄存器了, 首先将状态寄存器和控制寄存器清零,如代码所示;
  • 然后将读写地址分别写入读写寄存器, 注意PC机上的内存地址为低16位,而高16位要配置在 Avalon-PCI 地址转换表中, 如代码所示, 其中 0x20000 为 PCI IP核的 Avalon 地址, 如图 2-1 所示;
  • 向长度寄存器写入 DMA 传输长度(单位:字节) , 如代码所示;
  • 向控制寄存器写入 DMA 控制字 0x8c, 即长度寄存器降低为0时传输完成, 使能DMA,32 位字传输, 如代码所示;

3应用程序如何调用驱动程序

3.1 GUID说明

GUID( Globally Unique Identifier) 是微软推出的全局唯一标识符, 通过使用某个特定的算法( 比如根据时间或地点等信息) 生成一组128 位二进制数,来标识某一个实体,比如硬盘上的一张图片。 GUID 广泛应用于微软的产品中, 用于识别接口、文件等对象。开发者可以使用 VS2013 下的工具 GUIDGen.exe 生成 GUID 值, 该 GUID 标识驱动程序, 应用程序根据这个 GUID 值来找到对应的驱动程序。

应用程序总体流程设计为: 首先通过 Win32API 函数 CreateFile 打开设备, 然后调用DeviceIoControl 函数与驱动程序通信,即读写数据,当应用程序退出时,调用CloseHandle函数关闭设备。

应用程序根据底层设置的 GUID 获取设备路径, 从而与指定设备建立连接: 首先调用SetupDiGetClassDevs 函数获得符合传入参数 GUID 的设备信息集合 hDevInfo; 然后根据设备信息集合 hDevInfo 和 GUID 调用 SetupDiEnumDeviceInterfaces 函数枚举设备信息集合中的设备,并输出设备接口数据信息 DeviceInterfaceData ; 再 根 据 hDevInfo和 DeviceInterfaceData 调用 SetupDiGetDeviceInterfaceDetail函数得到保存设备接口详细信息DeviceInterfaceDetailData 的缓冲区大小 ,接着为其开辟空间 , 最后再调用一次SetupDiGetDeviceInterfaceDetail 函数获得设备接口详细信息 pDeviceInterfaceDetailData; 最后根据设备接口详细信息的设备接口路径 pDeviceInterfaceDetailData->DevicePath 调用CreateFile 函数来创建设备句柄。

3.2 应用程序如何打开设备

在测试程序 Source.c 的代码第 62-70 行,完成打开设备句柄功能, 不需要用户改动。

3.3 应用程序如何读设备

成功打开设备后, 调用 DeviceIoControl 即可通过 IOControl 队列与驱动程序通信, 以读32bit 数据为例, 首先向驱动程序写入需要读的存储单元地址, 如代码第 257-272 行所示,然后向驱动程序传入读数据缓存 outBuffer, 如代码第 274-289 所示, 即可获得FPGA 上对应偏移地址的数据。

3.4 应用程序如何写设备

成功打开设备后, 调用 DeviceIoControl 即可通过 IOControl 队列与驱动程序通信, 以写32bit 数据为例, 首先向驱动程序写入需要写的存储单元地址, 如代码第 303-317 行所示,然后向驱动程序传入缩写数据缓存 inBuffer, 如代码第 322-336 所示, 即可将数据写入 FPGA上对应偏移地址的内存单元。

3.5 应用程序如何对设备进行DMA传输

通过Win32API 函数ReadFile和WriteFile对设备句柄进行文件读写操作, 在内核驱动中会调用驱动程序的 qd41EvtIoRead和qd41EvtIoWrite,实际的DMA配置在这两个驱动回调例程中实现, ReadFile和WriteFile 只是完成了数据从用户层到内核层的搬运。详细代码在第 340-378 行,不再赘述。

4 INF文件与如何调用Qt编写上位机软件调用底层驱动

技术分享

技术分享

技术分享

技术分享

我把C语言编写的应用程序每一个独立的控制命令编译成一个可执行文件,这样Qt可以在新的进程中调用 C 程序编写的命令字读写程序。使用Qt 封装好的进程类 QProcess, 使用其成员函数 start 即可调用一个外部程序。

基于WDF的PCI/PCIe接口卡Windows驱动程序(5)-如何为硬件移植驱动程序