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

 

3.2 获取描述符

上一小节认识了USB 的描述符后,这一节就来说如何从 USB 设备获取它们。我列出了具体的代码,包括获取设备描述符、配置描述符和 String 描述符。看过代码后,你们会以为在 WDF 中作这些操做,动做很是简洁,堪称舒心。程序员

首先看获取设备描述符,一行代码足矣。编程

 

USB_DEVICE_DESCRIPTOR   UsbDeviceDescriptor;数组

WdfUsbTargetDeviceGetDeviceDescriptor(app

IN  pContext->UsbDevice, // WDF设备对象框架

                                     OUT & UsbDeviceDescriptor // 返回的设备描述符ide

);函数

 

接下来看获取配置描述符。配置描述符囊括了USB 配 置所要用到的所有信息:设备描述(区别于设备描述符)、类描述、接口描述、端点描述。和设备描述符的定长不一样的是,因为不一样的设备其配置布局,包含的接口 与端点数不尽相同,故而配置描述符的长度是不定的。应该先取得配置描述符的长度,根据长度分配内存缓冲,而后二次获取设备描述符内容。工具

 

// 首先得到配置描述符的长度。它是变 长 的,包含了所用接口描述符、端点描述符。布局

status = 测试

WdfUsbTargetDeviceRetrieveConfigDescriptor(pContext->UsbDevice, NULL, &size);

 

if(!NT_SUCCESS(status) && status != STATUS_BUFFER_TOO_SMALL)

break;

 

// 输出缓冲区不够长

if(OutputBufferLength < size)

break;

 

// 再次调用,正式取得配置描述符。

status = 

WdfUsbTargetDeviceRetrieveConfigDescriptor(pContext->UsbDevice, pBufferOutput, &size);

 

 

最后咱们看String 描述符的状况。 USB 设备的字符串描述符也是由设备固件定义,数量不限,甚至能够没有。它用来表述设备厂商( Vendor )对本设备的描述,这包括设备的制造商名称,产品名称,产品序列号,甚至包括接口的描述(不过 Windows 系统彷佛不支持这个特性)。不一样的字符串经过从 0 开始递增的 String ID 来区分。另外值得一提的是, USB 协议容许字符串描述符支持多国语言,这样同一个 String ID 能够对应于一个以上的描述符。 String ID 为 0 的字符串描述符专门用来描述 USB 设备说支持的语言(用 Language ID 表示,好比英语的 ID 为 0x0904 )。这样主机能够经过获取设备的 0 号字符串来分析它所支持的语言种类,并获取相应语言版本的字符串描述符。

和配置描述符同样,字符串描述符的长度不肯定。咱们也是分两次调用,第一次调用获取描述符长度,而后分配内存缓冲区,再二次调用获取描述符内容。下面是CY001_WDF 工程中 GetStringDes 函数的实现,咱们能够看到语言 ID 是怎么在这里起到做用的(惋惜 CY001 的固件代码目前还只支持英语一种语言,呵呵):

 

NTSTATUS GetStringDes(

USHORT shIndex, // String ID

 USHORT shLanID, // 语言 ID

 VOID* pBufferOutput,

 ULONG OutputBufferLength, ULONG* pulRetLen, PDEVICE_CONTEXT pContext)

{

NTSTATUS status;

 

USHORT  numCharacters;

PUSHORT  stringBuf;

WDFMEMORY  memoryHandle;

 

KDBG(DPFLTR_INFO_LEVEL, "[GetStringDes] index:%d", shIndex);

ASSERT(pulRetLen);

ASSERT(pContext);

*pulRetLen = 0;

 

// 因为 String 描述符是一个变长字符数组,故首先取得其长度

status = WdfUsbTargetDeviceQueryString(

pContext->UsbDevice,

NULL,

NULL,

NULL, // 传入空字符串

&numCharacters,

shIndex,

shLanID

);

if(!NT_SUCCESS(status))

return status;

 

// 判读缓冲区的长度

if(OutputBufferLength < numCharacters){

status = STATUS_BUFFER_TOO_SMALL;

return status;

}

 

// 再次正式地取得 String 描述符

status = WdfUsbTargetDeviceQueryString(pContext->UsbDevice,

NULL,

NULL,

(PUSHORT)pBufferOutput,// Unicode字符串

&numCharacters,

shIndex,

shLanID

);

 

// 完成操做

if(NT_SUCCESS(status)){

((PUSHORT)pBufferOutput)[numCharacters] = L'/0';// 手动在字符串末尾添加 NULL

*pulRetLen = numCharacters+1;

}

return status;

}

得到了这些描述符以后,咱们就能够经过对它们的分析,获得USB 设备的详细信息了。好比设备的版本( 1.1仍是 2.0 ),有几个接口,接口中的端点数,端点的类型(控制、批量、中断或等时)。

运行CY001 开发板的 UsbKitApp.exe ,点击最上面的三个按钮,能够得到并打印出这些描述符的信息。以下图所示:

描述符按钮: 

打印信息:

连接地址 4. 设备初始化

作惯了WDM 驱动的人都知道,驱动初始化在入口函数,设备初始化在 AddDevice 函数,这确是不刊之论。WDF 框架中,驱动初始化咱们已经讲了它的入口函数。然则设备初始化,到底怎么作呢?它是否仍是对应到AddDevice 函数?回答是 NO 。

WdfDriverCreate 调用已经指明了,设备初始化在自定义的PnpAdd函数中完成。你们稍微上翻一两页,就能看到定义PnpAdd函数的地方,不妨再写出来:

WDF_DRIVER_CONFIG_INIT(&config, PnpAdd);

 

调用 WDF_DRIVER_CONFIG_INIT 宏,并不强制你必定要传入一个有效的函数指针,若是传入NULL指针也是能过去的,只是设备就没有地方能够初始化了。

回过头来讨论PnpAdd函数,你们确定脑子里已经在想,它和AddDevice是什么关系呢?看看它的函数申明先:

typedef  NTSTATUS 
  (*PFN_WDF_DRIVER_DEVICE_ADD)( 
    IN   WDFDRIVER    Driver , 
    IN   PWDFDEVICE_INIT    DeviceInit 
    );

 

第一个参数是驱动对象,就是DriverEntry 函数中被初始化的那个。

第二个参数,是WDFDEVICE_INIT 结构体。这个结构体颇为复杂, WDF 未能给出它的具体定义,只是暴露出了一系列 API 用来初始化这个结构体。具体来讲,它涉及到了设备初始化的方方面面,甚至更多。好比定义设备名、设备缓冲方式的定义,属于正常的设备对象属性;而注册 PNP 和 Power 回调函数,则已经超出了传统的设备对象属性范围,越界到驱动对象里去了(这些回调函数,更像是驱动对象的分发函数或者分发函数的变体。WDF 框架对 PNP 和 Power 管理有很是大的变更,内部机理,谁也不晓得,咱们顺其天然罢了)。

WDFDEVICE_INIT结构体的初始化 API 颇为丰富。分红了三个系列。对应于普通设备对象(简称 Devcice )的初始化,专门针对功能设备对象(简称 FDO )的初始化,和专门针对物理设备对象(简称 PDO )的初始化。总共加起来大概有 30 来个。对 USB 设备驱动而言,要用到的只是前二者系列 API 。物理设备对象的初始化 API通常由总线驱动或更底层的驱动使用,生成的物理设备对象,将被上层功能驱动所挂载。

一一弄明白这些API 接口,非常一件烦心事。好在这些 API 的定义到时很 Readable ,有时候看看名称到也可以猜到一二。我下面尽可能多分析几个。

PnpAdd函数所收到的这个 WDFDEVICE_INIT 结构体,是已经被初始化过的。最明显的一个理由是,经过它,能够调用 FDO 初始化 API 得到许多设备信息。好比:获取物理设备对象、获取注册表中的硬键、软键(也就是 Hardware 键和 Software 键)、获取物理设备对象的属性(设备 ID 、兼容 ID 等)。这些 API 列于下:

WdfFdoInitAllocAndQueryProperty

WdfFdoInitOpenRegistryKey

WdfFdoInitQueryProperty

WdfFdoInitWdmGetPhysicalDevice

这些API 的具体的使用方法很简单,确实起到了简化操做的目的。 WDF 文档中都有示例代码的。注意,这些API 必须在 WdfDeviceCreate 被调用以前调用。由于一旦 WdfDeviceCreate 被调用后, WDFDEVICE_INIT 结构的内容可能就已经变了甚至不存在了。

FDO初始化 API 中剩下的三个,两个是为过滤驱动准备的( WdfFdoInitSetEventCallbacks 和WdfFdoInitSetFilter ),一个为总线驱动准备( WdfFdoInitSetDefaultChildListConfig ),咱们就不用管它们了。

 

回头来看Devcice 初始化系列 API 。这里面涉及最多的是设置 Pnp 和 Power 属性、回调的 API ,由此也可见这二者的复杂程度。 CY001 中用到了一个 WdfDeviceInitSetPowerPolicyEventCallbacks ,下面会讲到 。

另外一类是类型注册,用来在注册表中修改物理设备安装属性的(包括Type 、 GUID 、特性等)。它们使得设备即便在被安装后,也能改变它的 class ID 、 device type 这些安装时设定的设备属性。这确实是一件很实惠的事情。拿 CY001 为例,用 inf 文件安装好后,它的给定类 ID 是: {9048DC75-B91C-4392-925A-44A7269D6BD4} ,类名称是: CY001 Sample 。打开设备管理器,正以下图所能看到的:

 

但若是我在PnpAdd 函数中,调用 WdfDeviceInitSetDeviceType 函数并传入参数FILE_DEVICE_SERIAL_PORT ,那下次再看到CY001 的时候,它的位置就会列于串口设备下面去了。

说一说这些API 的内部机理吧。 Windows 的安装( Setup )模块是一套挺复杂的东西,我就很少嘴多舌了。对于已经在系统中安装好的设备,它们的信息是统一被列在注册表中 Enum 和 Class 下的,也就是你们所说的硬件键和软件键。系统的 Setup 系统,正是从这些地方保存并查找设备的。而咱们如今所讲到的这一系列的 API ,其工做就是修改设备对象这两个键的位置与值,这样 Setup 系统下次就会把它当成另一我的看了。

这些API 列于下:

 

WdfDeviceInitSetCharacteristics // 好比软盘设备: FILE_FLOPPY_DISKETTE

WdfDeviceInitSetDeviceClass // 好比系统设备类: GUID_DEVCLASS_SYSTEM

WdfDeviceInitSetDeviceType // 好比改为串口类型 FILE_DEVICE_SERIAL_PORT

WdfDeviceInitSetExclusive // 独占打开,即一次只能建立设备对象的一个实例

//(对应于应用程序的Handle)

 

下面具体讲,如何进行USB 设备初始化、配置。

 

连接地址 4.1  初始化过程

 

之前写USB 驱动,程序员大倒苦水,缘由之一是 USB 设备的配置太麻烦了。这不由让我想起了写文件过滤驱动的时候,里面有一个卷设备挂载操做,反反复复,这般那般,简直没完没了。代码还没开始写呢,脑子先被他转晕了。还好 USB 的设备配置任务虽然重(我指的是代码多),但总算都是些基本概念,不用太难为本身的脑细胞。

从USB 设备插入 PC 主机开始,到它能被操做系统识别,要通过一些特定的过程,枚举以下:

a)  设备插入主机后,USB 设备进行复位操做,将物理地址置 0 。

b)  主机检测到有物理设备接入,便经过地址查找的方式,查找地址为0 的 USB 设备;找到后,向 USB 设备发送请求,获取它的设备描述符。

c )  主机分析设备描述符,并根据实际状况,为新插入设备从新分配一个物理地址(非0 );并把这个新地址,经过 Set Address 命令发送给设备。

d )  设备收到并保存新地址,此后当主机查询设备的时候,USB 设备即当以此新地址来回应查询请求。

e )  Set Address成功后,主机向刚分配地址的 USB 设备再次发送请求,获取设备描述符。

f )  获取设备描述符成功后,主机发送请求获取配置和报告描述符。

g )  根据获取的描述符,主机配置此USB 设备。

h )  配置完成,设备正常工做。

 

上面的这个过程,凡是讲PNP 管理器的书籍,大抵都会讲。我这里仅仅简单列一下,详细透彻的说明,你们去找书看,《 Windows Internal 》就讲得很是详细。 a->d 这四个步骤,是设备被系统识别的过程,是由系统(总线驱动或其余的系统模块)和 USB 设备交互完成的。 e->g 这三个步骤由功能驱动负责来作。

我上面也说过了,之前用WDM 来完成这五个步骤,是比较烦难的。弄弄就是一大堆代码,虽然没有什么灵活机变的地方,但很容易一不当心就搞错了。在这篇文档中,我为了比较可能会举一些 WDM 的示例代码。但我主要想指给你们的路,是一条用 WDF 铺出的林中碎石密径,轻快、干净还漂亮。因此会有好多 WDF 代码示例,教你走,领着看。

连接地址 4.2   建立WDF 设备

提到设备对象,让人一会儿就想到DEVICE_OBJCET 结构体。更有些人还会马上想到《Undocument Windows 2k 》 里面列出的关于这个结构体每一个成员的详细解释。设备对象是最基本的内核对象之一。设备对象未必都对应到一个物理设备。好多“设备”都是存在于逻辑上的,比 如“卷”设备;还有一些设备对象,则连逻辑设备也不是,好比每一个驱动均可能会有一个控制设备对象,它们纯粹只是一个“结构体”而已。

但对于表明物理设备的物理设备对象而言,系统经过操做这些对象,起到了实际控制物理设备自己的做用。

从结构体自己而言,DEVICE_OBJCET 够底层,够强大,够 Undocument 。另外,它还够难理解,够难使用,够易出错。用好它的人够厉害,用坏它的人,嗯,够不幸。处于对无数不幸人士的体贴, WDF 提供了封装对象 WDFDVICE 。对于 WDFDEVICE ,它彻底 undocument (别沮丧),但无比易用,几乎不会出错。

哦,不要忘了,WDF 除了 WDFDVICE 外,还进一步又封装了一个 WDFUSBDEVICE 对象。从从属关系来讲, WDFUSBDEVICE 已是 DEVICE_OBJCET 的孙子辈了。对于 USB 驱动,这个对象真是太好用了!

WDF对象封装得过于严实。到目前为止,我还不晓得有谁破译出它们内部的定义。这种状况下,***们大概是不太欢喜的。

调用WDF 驱动初始化函数后,框架就为驱动对象生成一个 WDFDEVICE 对象。这个对象句柄在 XXX 函数中做为参数传入。能够不保存这个句柄,由于咱们须要根据这个对象句柄,生成 WDFUSBDEVICE 对象,只要保存后者就能够了。

要找一个可用来保存自有数据的地方。WDF 为每一个框架对象都设计了一个特殊的“环境变量”——不只仅是这里讲到的设备对象,而是全部框架对象——正可用来保存这些数据。这个“环境变量”,用起来有点像 WDM设备对象中的设备扩展。但用起来要麻烦不少。

首先要申明“环境变量”的类型和大小。根据大小,框架为设备对象申请一块内存。

其次定义一个函数指针,经过这个函数能够获取“环境变量”。这可真麻烦。但这也是没有办法,由于框架对象是彻底密封的,没有办法像设备扩展指针同样直接获取。这项技术提及来仍是挺有趣的,我非要给你们说个明白不可。

注:咱们使用KMDF 框架进行编程,通常不直接使用原始的 WDM 对象。在这里,咱们把 WDM 对象称做原始对象( RAW ),而把 KMDF 对象称做封装对象( Wrapped )。只要愿意,能够对 RAW 对象进行各类形式的封装。你们初学的时候遇到这些东西会感受比较麻烦,但熟悉以后却能带来编程上的便利,它们都带有定义良好的接口。

咱们要找到一个保存WDFUSBDEVICE 对象句柄的地方。 WDF 设备的“环境变量”,至关于 WDM 驱动中的设备扩展,是一个理想的地方。

 

// 建立WDFUSB 设备

status = WdfUsbTargetDeviceCreate(Device, WDF_NO_OBJECT_ATTRIBUTES, &DeviceContext->UsbDevice);

if(!NT_SUCCESS(status))

{

   KDBG(DPFLTR_INFO_LEVEL, "WdfUsbTargetDeviceCreate failed with status 0x%08x/n", status);

return status;

}

    

上例中调用 WdfUsbTargetDeviceCreate 时候的Device 句柄,是初始化的时候由系统建立的。这个句柄表明了一个WDFDEVICE 对象,也就是说系统其实已经为咱们建立了一个 WDF 设备对象了,咱们如今在它的基础上再封装出一个 WDF USB 设备对象。

建立WDF 设备对象是比较简单的,复杂的地方在于设置初始化结构体。咱们能够分两个步骤来实现初始化: 1. 注册 PNP 、 Power 回调函数; 2.  设备命名;

对于设备驱动来说,PNP 、 Power 分发是顶顶重要的,这一点和过滤驱动不一样。如何处理好 PNP 、 Power 分发,是设备驱动开发过程当中很头疼的事情。不只事繁,并且事艰。 WDF 框架顶好的一个优势就是为全部的 PNP、 Power 分发写了默认处理方法。这样咱们只要注册少许感兴趣的回调函数,即能将它们轻松处理了。

 

// 注册PNP与Power回调函数。

WDF_PNPPOWER_EVENT_CALLBACKS_INIT(&pnpPowerCallbacks);

pnpPowerCallbacks.EvtDevicePrepareHardware   = PnpPrepareHardware; // 在此为设备驱动申请系统资源

pnpPowerCallbacks.EvtDeviceReleaseHardware = PnpReleaseHardware;

pnpPowerCallbacks.EvtDeviceSurpriseRemoval   = PnpSurpriseRemove;  // 异常移除

pnpPowerCallbacks.EvtDeviceRelationsQuery    = PnpRelation;

pnpPowerCallbacks.EvtDeviceD0Entry   = PwrD0Entry; // 进入D0电源状态(工做状态),好比初次插入、或者唤醒

pnpPowerCallbacks.EvtDeviceD0Exit    = PwrD0Exit;  // 离开D0电源状态(工做状态),好比休眠或设备移除

WdfDeviceInitSetPnpPowerEventCallbacks(DeviceInit, &pnpPowerCallbacks); // 注册回调

 

// 读写请求中的缓冲区访问方式。默认为Buffered,还包括Direct和Neither。

WdfDeviceInitSetIoType(DeviceInit, WdfDeviceIoBuffered);

上面代码的所有任务,就是初始化结构体对象 WDFDEVICE_INIT 。WDK文档没有给出这个结构体的定义。但有一系列的宏或者方法,被定义了用来对它进行设置。上面的代码仅用到了其中的两个。这个结构体也是至关复杂的,你们仍是结合WDK本身参透吧。

连接地址 4.3 设备命名

这一小节乃是从《建立设备》节中分出来的,为了醒目的缘故。

和WDM 驱动同样,设备对象是能够选择被命名的。就是说,设备对象能够被命名,也能够不命名,由程序员本身决定。命名的目的是为了可以被识别和使用,若是无此须要则命名可没必要进行。

功能设备对象老是须要命名的,由于功能驱动是用来被User 程序使用的。

 

>>>>>>>>>>>>>>>>>>>>>>>>>>>

附:《查看WDM 设备对象名》

若是扯得远一点,我想和读者交流一下怎么在知道了一个设备对象地址后,手动查看这个设备对象名(首先要确认是否有用设备名)。 这部份内容纯属附加,不感兴趣的朋友 可 绕过。

假设如今知道了某个设备对象的地址为0xe1016b a0 ,咱们能够经过下面的步骤手动查看它的设备名称(仅在XP 下测试):

1.  打开WinDBG ,运行在 local kernel 模式下。在控制窗口中输入命令 : 

dt nt!_object_header 0xe1016b a0 -0x18

 

这时候提示画面会出现内核结构体OBJECT_HEADER (未文档的结构体)的内容。 XP 下OBJECT_HEADER 的大小为 0x18 字节,而且其位置正好就在 DEVICE_OBJECT 上面。因此咱们经过上面的 WinDBG 命令,能够获得一个正确的 OBJECT_HEADER 结构体内容。

 

2.  找到结构体中成员变量NameInfoOffset 的位置,看他的值。如今咱们能够根据这个值判断设备对象是否有名字:若是 NameInfoOffset 值为 0 ,说明这个对象未被命名;不然,就是拥有一个名称的,而且保存其名称的地方,就在 OBJECT_HEADER 上面某处( NameInfoOffset 即为偏移)。

我假设你看到的内容和我下面的截图是同样的:

 

咱们能够根据这个值,找到系统保存对象名称的地方。在控制窗口中运行这个命令:

dd 0xe1016b a0 -0x18-0x10

获得一串内存数值后,第三个DWORD 值,就是保存设备名称的缓冲区地址。

上图是我电脑中运行后的结果,第三个DWORD 内容为 0xe1016ba0 。

 

3.  再运行db 命令,查看地址 0xe1016ba0 所指示 缓冲区 中的 内容 :

lkd>  db  0x e1016ba0

0x e1016ba0   XXXXXXXXXX CY001_0.... //找到的设备名称

0xe1016bb0 .......................... ........................

 

成功!

>>>>>>>>>>>>>>>>>>>>>>>>>>>

 

咱们在CY001_WDF 驱动程序中,为设备命名形如“ CY001_X ”这样的名称,末位 X ,是区间 [0, 8] 的整数。由于不知道某个名字是否已经在系统中存在,因此须要一个循环尝试的过程。经过判断 WdfDeviceCreate调用返回的错误值是否为STATUS_OBJECT_NAME_COLLISION ,能够知道当前尝试的名称是否在系统中引发了名字冲突;若是发生冲突,咱们就须要从新尝试。最多尝试到名称“ CY001_8 ”,若是 CY001_8 也已经注册了,就让驱动初始化失败。这样的话,咱们的驱动目前最多支持同时 8 个 CY001 设备链接到系统中。 

 

// 目前驱动支持同时 8 个实例,便可以同时有 8 个开发板连接在 PC 上,驱动对它们给予并行支持。

// 不一样的设备,各以其名称的尾数( 0-7 )相别,并将尾数做为设备的 ID 。

// 下面的操做中,咱们为当前设备寻找一个未使用的 ID 。

for(nInstance = 0; nInstance < MAX_INSTANCE_NUMBER; nInstance++){

wcsDeviceName[nLen-1] += nInstance;// 修改末尾的数字,使从 0 至 7 。

 

// 调用 WdfDeviceInitAssignName 接口,尝试着为当前设备命名;

// 此函数在系统中查找此名称是否惟一,如已存在则返回失败,不然以成功返回。

status = WdfDeviceInitAssignName(DeviceInit, &DeviceName);

 

// 建立 WDF 设备。上面所作的设置在这一步方能发挥到实质性做用。

status = WdfDeviceCreate(&DeviceInit, &attributes, &device);  

if(!NT_SUCCESS(status))

{

if(status == STATUS_OBJECT_NAME_COLLISION)// 名字冲突

KDBG(DPFLTR_ERROR_LEVEL, "Invalid name: %wZ", &DeviceName);

else

{

KDBG(DPFLTR_ERROR_LEVEL, "WdfDeviceCreate failed with status 0x%08x!!!", status);

return status;

}

}else{

KdPrint(("Found valid name: %wZ", &DeviceName));

break;// 成功即退出

}

}

 

一旦命名成功,那么对应的名称就会出如今系统名称空间中。使用WinOBJ 工具,就能在 Device 子目录下看到了。

下面来看看WDF 环境下如何为设备建立符号连接或设备接口。(省略)

相关文章
相关标签/搜索