近期在64位Win7下开发一款PCIe接口的多串口卡驱动程序,做个小结:
1. 因为在Win下对WDF不熟悉,加上市面上DDK、WDM书籍较多,故选用WDM框架;
2. 多串口卡的硬件接口为PCIe,因为在软件驱动层面上,PCIe和PCI兼容,直接借用常用的WDM即插即用框架。这里用《Windows驱动开发技术详解(张帆等编写)》第16章Test5中的InitMyPCI函数。该函数枚举了PCI总线的各种资源,如中断、IO口、内存等,直接原封不动的拿来即可用;
① PCI工具PciScope、RW,多多使用;
② WinDriver开发框架虽然简单,但效率不高,做初期验证不错;但随着对PCI等总线的深入了解,发现还不如自己控制来的直接;DriverStudio也是如此;
② 访问一个LONG型的reg,注意内存边界访问方式,:WRITE_REGISTER_ULONG((PULONG)(g_MemBar0+(i * 4) + UART_TX_DATA), dwVal);【这里g_MemBar0是一个unsigned char类型的】
3. 该串口卡没有使用中断,底层数据交换都是读写reg实现的。这种机制,驱动开发难度一下子减少了不少,故在内核驱动中启动一个线程,轮询PCIe定义的reg资源,调度线程每次Sleep为1ms(但经测试DebugView测试,实际在15ms左右)。这里和硬件设计人员沟通,每次休息的15ms中,串口卡FPGA中数据缓存足够大,不会丢失。但在串口使用层面看,写入或读出的数据会带来约15ms的延时(忽略其他延时),这是一个需注意的问题;
//延时函数,单位为ms void MySleep(LONG msec) { #define DELAY_ONE_MICROSECOND (-10) //1微秒 #define DELAY_ONE_MILLISECOND (DELAY_ONE_MICROSECOND*1000) //1毫秒 LARGE_INTEGER my_interval; my_interval.QuadPart = DELAY_ONE_MILLISECOND; my_interval.QuadPart *= msec; KeDelayExecutionThread(KernelMode, 0, &my_interval); }
由此看来,内核层的1ms睡眠,和Win32的(1)精度一样,不加高精度多媒体库时,都是约15ms。
(关于在Win32下测量Sleep的精度问题,参见我的另一篇文章:http://blog.csdn.net/dijkstar/article/details/23092747,里面有详细的测试方法和测试数据)
4. 驱动层面暴露给Win32的串口COM口,使用《Windows驱动开发技术详解(张帆等编写)》第19章的Virtual_COM示例。
借用这个框架,能看出市面上最常用的串口调试助手“sscom32.exe”,在读取数据时,调用了IOCTL_SERIAL_GET_COMMSTATUS,又调用了IOCTL_SERIAL_WAIT_ON_MASK(对应Win32的WaitCommEvent),所以,自己写的驱动里,必须要处理这两个请求:对第一个IOCTL_SERIAL_GET_COMMSTATUS,简单点处理,要将当前可读的数据个数赋值给AmountInInQueue成员;第二个请求,还用原框架内容:立刻Pending该请求,但要在轮询的线程里,轮询Fifo(后面介绍)发现有数据时,释放到这个请求。这个逻辑查看函数DriverCheckEvent怎么被调用的即可;
5. 如何多个设备dev的创建?传统的WDM是创建一个fdo,附加在底层的pdo之上。灵活改变一下,将第一个fdo创建时,调用IoAttachDeviceToDeviceStack,但第二个、第三个....,不要调用该函数,这样创建后,用DeviceTree查看创建的设备列表,发现他们都是“平行”关系,而不是“Att”关系(只有第一个是);
如此,在Win32应用层CreateFile、ReadFile、WriteFile时,访问的都是“当前”的那个dev,所以,要在驱动层里,自己维护一个全局的设备dev[n]数组;
6. WDM的初始化PCI设备函数是放在IRP_MN_START_DEVICE完成的,而IRP_MN_START_DEVICE又依赖于“某一个dev”的CreateFile完成的,怎么办?使用一个全局变量,当应用层CreateFile时第一个设备时(下标不一定是0),调用初始化PCI设备函数,将全局变量置false,下次再有应用层CreateFile不会调用InitMyPCI;
由此看出,“WDM即插即用”和“传统的NTDriver”并没有区分的那么明显,像上面的思路,就是把两者结合起来使用了;
7. 64位inf生成:只需要将张帆示例的inf添加个NTamd64即可,如下:
[Manufacturer] %MfgName%=Mfg0,NTamd64 [Mfg0.NTamd64] ; PCI hardware Ids use the form ; PCI\VEN_aaaa&DEV_bbbb&SUBSYS_cccccccc&REV_dd %DeviceDesc%=YouMark_DDI, PCI\VEN_9999&DEV_9999
8. 64位Win7的驱动需要数字签名,目前没有找到彻底解决办法,暂时:
① 每次启动按F8进入“禁用驱动签名”;
② 进入TestMode:bcdedit/set testsigning on(一定用管理员身份运行),但这样结果导致会出现桌面右下角水印;
③ 使用工具dseo13b.exe(http://www.ngohq.com/?page=dseo);
④ 找网上的“去水印”工具(Windows 7 Watermark Remover);
⑤ 张佩写的签名工具:64Signer V1.2.exe,使用文档、签名原理及下载处:http://www.yiiyee.cn/Blog/64signer/
9. IRP同步问题,做一个假设:假设用户应用程序不会开启两个线程同时调用ReadFile、WriteFile,因此在IRP_MJ_READ、IRP_MJ_WRITE处理里,不考虑STATUS_PENDING的情况,而是直接读写Fifo缓冲区,即为一个非阻塞的操作,无论有无数据都立刻返回。由上层用户启动线程时在调用ReadFile类似的函数时,自己Sleep操作;
如果假设应用层开启两个线程同时ReadFile操作,那么应该使用“串行化处理IRP”(StartIO例程)技术,基本思路是IRP_MJ_READ请求进来后,先Pending挂起该请求,将该请求入队,在StartIO例程里去处理该请求,自己做取舍;
10. 硬件Reg数据的使用:驱动中一个线程高速轮询Reg,不断的读写,读出或写入的数据不是和IRP_MJ_READ、IRP_MJ_WRITE交互,而是首先放到一个Fifo中再交互。这个Fifo要在驱动里自己实现,参见我的另一篇文章:http://blog.csdn.net/dijkstar/article/details/42361805(推荐一个VC下的FIFO实现源码CCircularFifo,附带测试程序),这个程序稍加改造,就可以在驱动层面中使用,记住在内核里创建内存干脆直接用NonPagedPool(非分页内存)吧,现在的机器内存都不少于4G了,没必要为区区一点“交换内存”而引起蓝屏崩溃。