WDF开发USB设备驱动教程(1)

PDF下载地址(1.2版):链接地址

CY001开发板讨论帖:链接地址

注:本文档新版本已出,请在博客中查找,或下载PDF全文文档。

 

链接地址WDF开发USB设备驱动教程

by 张佩

 

文档说明

 

    作者写作此文档的初衷,是为了配合 CY001 USB 内核驱动开发套件,更好地让使用者入门并熟悉 USB 驱动开发。但本文档完全可以从开发板中独立出来,因为这里面说讲到的绝大部分内容都是通用的 USB 技术知识。

USB接口越来越流行并已经发展到3.0版本了,他的强势甚至让1394不得不折腰。USB规范以其易用性和廉价性,应用遍及电子产业方方面面。

OSR推广的OSR FX2 USB开发板,对于美国人自己来说,可能不算贵(70美金),但依我的愚见,乘以汇率后的价格对于中国的程序员来说太贵了。我们因此而推出了CY001 USB内核驱动开发套件,板子+代码+文档,堪称完美的组合。

 

链接地址一. KMDF简介

KMDF的特点

KMDF研发的始末

KMDF的注意点

(尚未完成)

 

 

链接地址二. USB 设备硬件结构

 

USB硬件拓扑结构分成三层,分别为:硬件接口、统筹模块、多个功能模块。借助于这种结构可以实现一个接口,多个功能。通过一个设备接口,实现多个独立的功能模块。使用这种方式,可以把游戏柄和鼠标集成在同一块USB芯片中实现。本质上它们是同一个设备,但因为增加了分工,而扩展了应用功能。

这种结构可以被想象成一把打开的扇子(我试图画出一把扇子来,终究画得不像),一把扇子(唯一的设备),打开来,对应着许多的骨架(功能)。从硬件自身来看,所有的功能模块混于同一设备中,难以被独立拎出。但站在主机的立场上(代表USB系统总线驱动),它为了便于管理USB接入设备,将物理设备分而治之,为每个功能模块创建一个单独的“设备对象”(区别于物理设备)。这样一个物理设备在主机中可能对应着多个“物理设备”。而从使用者的观点来看,发现操作系统中同时冒出来多个USB设备来,会认为真的存在好几个USB物理设备。

“逻辑设备”是一个抽象模块,可能是设备中的某几行代码和判断语句。它的作用是分配设备资源到不同的功能模块,或者把功能模块的请求汇总到设备。它既能识别和区分设备中的多个功能,又能将多个功能的实现对应到真实设备,起到了桥梁的作用。

接口、端点在设备中被定义为一些ID或者符号。它们体现了不同的功能。根据命令所指向的接口或端点,设备做分别的处理,亦即实现了不同的功能。这很类似于C语言中的switch…case语句,每个case后面的值就是接口或者端点ID,而一个case分支代码则相当于某个具体的功能实现。

一般我们不考虑USB芯片中固件代码的具体实现,作为基本知识,软件程序员必须知道,USB芯片中运行着程序代码,并由这些代码来管理设备,处理从主机发送过来的命令。USB协议定义了USB设备和HOST的关系是“从主”关系(USB设备为从,Host为主)。所谓“从”即奴仆,“主”不发话,“从”绝不敢擅自作主。即使数据链路是输入模式,也就是说USB设备发送数据给Host,也一定是Host先发送一个Input命令,USB设备响应这个命令并进行相关的数据处理。

从这里就牵扯到了另外一个话题,那就是USB的中断传输模式。众所周知,它是一种“伪中断”。如果你还没有参与到这个“众所周知”的行列中来,那面现在就要赶紧参与进来了:)。

真正的中断,不管硬中断也好,软中断也好,一定是主动发生的。即中断源出现了,相应的中断事件即随之产生。中断是一件“意外”,并“主动”发生。我用一个不太雅观的比喻来解释一下:中断好比小学生上课,忽然尿急,举手报告老师‘我要上厕所’;尿急是一件很“意外”的事,并非时常都会发生;学生憋不住了,“主动”举手,而不是等到憋晕过去,让老师来过问;另外,学生上厕所是对正常上课过程的“中断”,他的尿急恰是“中断源”。

USB设备的中断原理与此有别。USB设备总是把数据保存在设备的某个地方,等待HOST来索取;如果Host来索取,就把中断数据交给它;否则要么一直存在某处,要么被更新的数据覆盖掉。套用上面的“尿急”比喻,可以继续讲一下:老师上课,担心学生尿急而不敢举手,便每隔五分钟主动问一次‘用人要上厕所否?’;果真有学生尿急者,必须等到老师问的时候才能回答曰‘我要去’;如果老师不问,不可主动提出,只能等着;而如果自己憋了一阵把尿憋回去了,则放弃机会(数据被覆盖了)。

所以,中断端口和批量端口从本质上讲极为相似,中断端口在执行的时候,加上了一个轮询的操作。可以这样地理解:批量端口+端口轮询 ≈ 中断端口。

举例来说,Host向USB设备发送请求以获取设备描述符,这个过程经历了这样几个步骤:HOST向USB设备发送一个Setup格式数据包;USB设备得到Setup包后分析得知Host需要获取设备描述符,把描述符数据传回HOST;Host得到描述符后,向USB设备发送握手信号,告知我已取得想要的内容,谢谢;如果Host久等得不到回音,就把开始的Setup包再发一遍给设备,若多次尝试后仍无反应,即认为此次通信失败。

USB协议中定义了Setup数据包的具体格式,它包含了一条Host发向USB设备的控制命令的详细内容,包括:数据传输方向(主机将发送数据到设备,或设备将发送数据到主机),命令处理对象(设备、接口还是端点须处理此命令)。

CY001开发套件中包含一份固件代码,用C51编程。C51是一种类似C语言的编程语言,所以在开发的时候,会感觉很亲切。固件开发是一件很困难的工作,需要仔细调试,如果在固件层出了BUG,上层驱动软件将毫无办法。

下面再来看看USB设备和主机的连接情况。USB协议是一个整套协议,它不仅定义了设备芯片应当如何实现,还定义了USB设备之间如何连接,以及总线驱动的实现,命令格式等。

从上图我们了解到主机和USB设备连接需要涉及到哪些接口模块。主机上会有多个USB控制器,这些控制器上都有一个小的USB Hub,从Hub上延伸出一个或多个USB A型接口。外部USB设备就连接到这些A型接口上。

上图从设备管理器中截取。可以从软件层面上窥探到硬件的层次。最上层的驱动,是控制器的,中间是USB Hub的驱动,最低层的驱动才是USB设备功能驱动。

 

链接地址三. USB软件结构

 

软件结构比硬件来的复杂很多。因为它包含了许多从表面上看不到的层次。比如类驱动、过滤驱动、通用父驱动。套用社会学的话,这体现了功能应用中的分工和统筹。下面我们逐层来看它们。

 

链接地址1. USB类驱动

 

所以出现类驱动,体现了USB总线在应用上的繁荣景象。只有用得多了,才有被归类的可能。就像人类社会中有几百个国家,几千个民族,正是体现了人类这个团体的多样性与繁荣。只有在“多”的基础上,分类才是有必要的;少数人的小群体,再怎么独立特行,也都不足以被分类,甚至定义为“XX民族”。

USB设备包含很多的通用的功能类,比如:USB 集线器设备,USB HID设备,USB音频设备,USB MIDI设备,USB存储设备。为了让开发工作变得更加简单,Windows操作系统为很多设备类型提供了系统级别的“类驱动”,这样在开发相关的设备驱动的时候,大量的接口部分的开发工作就可以省略掉了,而只要做设备本身特有的那些东西即可。

本质上,类驱动和功能驱动应该归为一体,作为一个大概念上的“功能驱动”。这种观点如果有朋友不同意,是可以商榷的。我所以持这种观点,是因为类驱动其实是把多个功能驱动的类似部分的提取与合并,早期的操作系统版本中,在某些类驱动还没有被开发之前,类驱动的工作都归功能驱动来完成。

 

链接地址2. 功能驱动

 

不是所有的USB设备都有类驱动,但功能驱动却是它们唯一的身份证。没有功能驱动,设备就不足以在系统中存在。它的作用是为设备创造一个独一无二的内核设备对象(DEVICE_OBJCET),并因此而在需要的时候,系统能够通过此内核设备对象找到它。

如果要让用户层也能够知道并使用USB设备,功能驱动更加不可少。它为设备在用户程序可见的名字空间中,为它起一个别名,这个别名可以是一个符号链接,也可以是一个由GUID定义的设备Interface。通过对这个别名进行操作,也就是对设备本身进行操作。

OK,上面的话说得太满了,有例外的!唯一的例外是以RAW 模式驱动的设备。这种设备直接由总线驱动来驱动其工作,不需要功能驱动。这种例子真的不多见,也许只有很很底层的控制器设备、Hub设备之类,才会这样做。对于RAW模式驱动的设备,当收到IRP_MN_QUERY_CAPABILITIES查询请求的时候,在返回的DEVICE_CAPABILITIES结构体中,必须将RawDeviceOK位设置为TRUE。

建议读者在需要的时候使用WinOBJ.exe工具查看系统空间中的设备与别名。

 

链接地址3. 过滤驱动

 

过滤驱动无处不在。在很多时候,它被称作Hook,是一种Hack手段;很多时候它又是必不可少的,这种技术甚至被操作系统自己使用。没有哪一个杀防毒软件不使用过滤驱动,我们在使用网上银行的时候,会用到的控件,也一般使用或包含了过滤驱动技术。

过滤驱动可以位于任何一层驱动的上面,或下面。过滤的对象也包括已经存在于系统中的其他的过滤驱动。当它位于某层驱动(D驱动)上面的时候,所有目标发往D驱动的请求,都首先被它截取;当它位于某层驱动下面的时候,所有和D驱动相关的从更底层驱动反馈回来的的“完成消息”都预先被过滤驱动截取。这正是它威力强大的原因所在。对于被过滤的驱动来说,过滤驱动简直就是它的先知了。

但使用过滤驱动,要很慎重。很容易把系统搞得很不稳定。读者在写过滤驱动的时候,要明白这样一件事:你想过滤谁,得先了解谁;好像你追求一个人,要先认识这个人。否则死机蓝屏都会与你不期而遇。

 

链接地址4. USB驱动栈、设备栈

 

讲完了上面的内容,回头再讲驱动栈。首先,请大家不要把这里的“栈”理解成“程序堆栈”的那个栈,朋友们请回到这个字的本意来理解它。

驱动栈、设备栈本质上是并行概念。产生问题的关键点在于,同一个驱动,可以生成多个设备对象。这就可能会导致交叉,使得同一个驱动会出现在多个不同的栈中。最明显的例子是类驱动。

经常使用WinDBG工具,会熟悉!devnode命令。下图就是我运行这个命令后,截取的一段和我们开发板有关的栈内容。

 

 

请大家看我用红色框标出的三个驱动,正好和上图可以对号入座,从上到下分别是控制器驱动(usbEHCI),集线器驱动(usbHUB),和功能驱动(CY001)。这里面内容的详细解析,读者自行研读文档为佳。

 

我从MSDN中截取了上图。是一个设备栈的例子。第一次看到这幅图,好多人都会蒙。我教大家应当这样来看:图中的最小单位是由圆圈数字标注的方框,每方框又由三个小方框所组成。这三个小方框,分别各有所指:最上面的圆角矩形,为栈中全部的上部设备(到当前大方框为止)所形成的设备栈(尚不完整),中间方框为当前设备驱动生成的设备,被上面的设备栈所挂(Attach),最下的方框为设备栈中下层设备的设备对象,它又为中间方框说代表的设备挂仔(Attached)。

我们就拿最底层的驱动设备来讲吧(方框1所代表者)。最底层方框代表的是控制器驱动,它上面是HUB驱动,下面则是PCI系统总线驱动了。控制器驱动本身自然是要生成一个设备对象的,即中间方框所代表者。控制器生成一个设备对象FDO(功能设备对象),位于三者中间。控制器连接到PCI系统总线上,底部方框中的设备即代表PCI BUS驱动生成的PDO(物理设备对象)。FDO的上面则是自“功能驱动”始至“HUB驱动”止所生成的设备栈,向下挂载于FDO上;FDO又向下挂载于PDO上;三者合力,乃最终构成设备栈的全貌!

 

 

 

 

 

 

 

链接地址四. 内核开发

 

这一节开始,我们讲内核开发的内容。知识点的讲解紧贴着概念走;示例代码则取材于CY001开发板套件,乃紧贴着开发板走。我将讲的内容包括:接口函数,设备配置,数据传输,控制操作等。USB数据传输包含了控制方式、中断方式、批量方式、等时方式。

因为有初学者问起这个问题,所以要声明一下:开发仅涉及功能驱动,最多提一下过滤驱动内容,我没有本事也不可能去实现一个类驱动甚至端口驱动J。

链接地址1. 入口函数

让人首先想到的是入口函数。驱动程序加载由System进程完成,System进程首先在驱动文件的PE结构中寻找名称为DriverEntry的函数地址,然后调用它。WDF包装了绝大部分WDM接口,却唯独没办法替换掉DriverEntry这个名称。因为要做到这一步,除非改System进程的实现。如果可以的话,他们或许很愿意换一个更好听点的名字,我想——但DriverEntry函数里的内容,WDF却真的换了大包装。

让我们回想一下如何编写WDM入口程序的吧。很多读者都看着楚狂人的驱动入门文档开始学习的,那里面介绍DriverEntry的内容可真多。主要的工作是初始化驱动对象,有可能的话,还需要创建并初始化一个“控制设备对象”。由于驱动对象和设备对象,结构都较大,所以手动初始化的工作也就很麻烦。

典型的示例会像下面这样:

 

NT_STATUS DriverEntry(DRIVER_OBJECT Driver)

{

For(int i = 0; i < MAX_IRP; i++)

Driver->DispatchFunction[] = DefaultDispatch;

// 另定义需特殊处理的分发函数,比如Create分发

Driver->DispatchFunction[IRP_MJ_CREATE] = myCreate;

 

Driver->AddDevice = AddDevice;

 

// 现在可能需要创建控制设备对象,并为之创建符号链接

IoCreateDeviceObject();

IoCreateSymbolLink();

 

// 不要忘记对设备对象,有一些Flag值需要设置

Device->Flag &= !INIT;

}

 

这就导致了这个现象:即使一个空壳(什么都不做)驱动,也有一大堆的代码需要写,否则要么就什么都做不了,要么就是导致致命错误。

曾有个网友问我,他写了一个文件过滤驱动,几乎什么都没有做,只是挂载到了硬盘设备上,代码都是抄的经典代码,为什么每次都蓝屏?后来发现,原来他忘记初始化fast IO函数了。他说他没有用到它们,以为就不用写了。呵呵。他可错了,你用不到的东西,系统却可能用到;系统找不到自己要的东西,往往会报复程序员,用一个彻底的蓝屏。

如果把DriverEntry和作控制台程序中的main函数进行比较的话(很多人在内心中把它们进行过这样的类比,是吗?),我想人们会更偏爱main函数。因为即使main函数什么都不写,它也是正确的,并可运行的。而DriverEntry拖家带口一大堆,着实让不少人减少了寿命。

终于出现了WDF框架,不少人可以大呼万岁。因为WDF大大减少了他们的代码书写量,因此而节约了时间,保证了他们的生命质量(省下时间看碟片?)。

WDF的出发点是这样的:实现一个接口函数,完成驱动程序的默认初始化。用户只需在默认初始化的基础上,增加自定义的初始化;如果没有,则WDF的默认初始化即能保证驱动程序的正常稳定运行。

虽然不能一行代码都不写,让如果只需要写两三行代码,那也是可接受的吧。

代码如下:

 

熟悉小端口编程的朋友,会发现WDF的风格和很多小端口框架很类似。确实如此。这些框架(不管小端口还是WDF)都是冲着一个目的而被设计出来的:减少代码书写复杂性、加强程序强壮性。AVStream小端口框架的入口函数,类似于WDF的XXX函数,其初始化也只需调用KsInitializeDriver函数即可。

至于说到可增加代码强壮性,完全可理解:自己写的代码越少,错误也就越少,强壮性也就越强;千万不要有人为微软的代码而担心(比如为WDF担心),首先他们一般足够好,其次如果有Bug,他们会及时让你安装补丁的。

链接地址2. 再说USB驱动

USB驱动属于设备驱动。设备驱动和另外一种驱动形式“内核功能模块”不同。文件驱动和内核服务,都是内核功能模块;可能是为了实现一个内核API的Hook,或者挂载到其他的某个设备,截取别人的数据做一些分析或篡改。目的比较杂,比较散,但不外乎在OS内部打转。设备驱动则目的专一,只是为了让某个物理设备能正常工作,和系统结为紧密的一体。

专一性往往等同于特殊性。就像成衣定制,是专门为有钱人服务的,袖口、领角处的别致,和在卖场里的衣服大有不同。拿USB和1394两种设备驱动来说,他们的编程套路,从初始化到设备操作,完全不同。这是由设备协议本身决定的,不同的总线,不同的数据格式,不同的传输方式。

总而言之,设备驱动需要搞定这些事情:设备配置,数据操作,设备控制。

设备配置,就是初始化设备。首先能识别到设备,一般这由下面的总线驱动和PNP管理器去搞定,很麻烦的。然后获取设备信息、状态,如对USB设备而言,需要知道他的设备描述,接口数量,端点数量和类型。最后配置设备,并让总线为设备分配带宽或其他的系统资源。

数据操作,它可以被分为两大类(不要笑):输入和输出。用正确的方法,把正确的数据输入或输出设备,是设备驱动的终极目标。

设备控制,包括很多操作,因不同的设备而异。USB设备控制包括设备Reset,管道Reset,接口设置等。另外,设备配置的过程,就是调用许多设备控制操作的过程。

链接地址3. 描述符

USB协议中的描述符,包括设备描述符和配置描述符,String描述符,还有报告(Report)描述符。近身了解描述符的最好办法,是读一读USB设备的固件代码。阅读过CY001的固件代码后,读者应该有这样的感觉:描述符本身是独立的,不受物理设备的约束,只要合乎规矩,简直随便怎么乱写都可以。至于配置描述符中要安排几个接口,接口中需要安排几个端点(但一个USB设备中最多只能有16个端点,不能超过,因为端点地址为4 bit长度),都是悉听尊便的。唯一约束着描述符定义的是硬件设计者对物理设备本身的定义,几个接口和端点能符合需求。

CY001可以只定义一个接口,这对应于开发包中的固件文件CY001(1Interface).hex;也可以定义两个接口,对应于开发包中的固件文件CY001(2Interfaces).hex;还可以定义更多的接口,只要不超过256个即可以了(接口地址为8Bit,即不超过2的8次方)。有人见此可能会想,你大概瞎说吧,我从来也没有见到那么多接口嘛!呵呵,凡事需要看菜吃饭,一般来说,一个接口对应一个功能,需要有那么多的实际功能才需要定义那么多的接口啊。 

描述符没什么好讲的,纯粹是结构体定义。下面简单说一说(暂时从略)。

链接地址3.1 描述符

3.1.1 设备描述符:

 

3.1.2 配置描述符:

 

3.1.3 接口描述符:

 

3.1.4 端点描述符:

 

3.1.5 字符串描述符

 

USB_STRING_DESCRIPTOR结构定义如下:

typedef struct _USB_STRING_DESCRIPTOR {
  UCHAR  bLength ;
  UCHAR  bDescriptorType ;
  WCHAR  bString[1] ;
} USB_STRING_DESCRIPTOR;

第一个字节bLength表示整个结构体的长度,值是变长的,为实际字符串的长度(见bString)加上2(即开头的两个字节)。

第二个字节bDescriptorType总是0x3。

bString是真正保存字符串内容的地方,为宽字符形式,它的实际长度决定字符串描述符的长度。

调用WDF的WdfUsbTargetDeviceQueryString函数获取字符串描述符的时候,会把前两个字节剔除掉,只返回bString字符串指针(同时还返回其长度)。

 

3.1.6 报告描述符

报告描述符不是必须的,也就是说不是所有的设备都需要具备报告描述符。一般在HID(Human Interface device)类型设备中会看到它,鼠标、键盘、游戏棒都属于HID类型设备。从略。

猜你喜欢

转载自blog.csdn.net/zb774095236/article/details/83818619