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

PDF全文下载:http://bbs.driverdevelop.com/read.php?tid-120461.html

 

3.2 获取描述符

上一小节认识了USB 的描述符后,这一节就来讲如何从 USB 设备获取它们。我列出了具体的代码,包括获取设备描述符、配置描述符和 String 描述符。看过代码后,大家会觉得在 WDF 中做这些操作,动作非常简洁,堪称舒心。

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

 

USB_DEVICE_DESCRIPTOR   UsbDeviceDescriptor;

WdfUsbTargetDeviceGetDeviceDescriptor(

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

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

);

 

接下来看获取配置描述符。配置描述符囊括了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 环境下如何为设备创建符号链接或设备接口。(省略)

你可能感兴趣的:(框架,api,object,header,null,attributes)