WDM下, AddDevice 调用成功后, Pnp 管理器会紧接着发送 PNP_MN_START_DEVICE 函数,我们一般会为这个子分发定义一个函数,比如 StartDevice 。 WDF 的 StartDevice 在哪里呢?其实我们上面已经在 Pnp/Power 回调函数中定义过来,就是 PnpPrepareHardware 函数。它 紧跟着 PnpAdd 函数,是我们自己定义的,第二个被WDF调用的函数。此时设备的加电过程尚未完成,处于一种“未初始化的”D0状态。我们现在就来把这个“未”字抹掉,还它一个“初始化完成”!
在 PnpPrepareHardware 函数中,我们主要做了两件事情:配置设备、初始化设备电源策略。另外做了两件辅助的事情:获取总线接口、枚举系统资源。
下面我们分别来看吧。
前面的准备工作都完备后,最后才讲到设备配置内容。
大家晓得,设备描述符中有一个值,用来表示USB 设备支持几个配置描述符。并且 USB 协议支持设备在多个配置描述符之间进行切换的。其实这个功能在 WDM 框架下,是可以实现的。但不幸的是,很多人已经发现, WDF框架 下找不到合适的方法,用来在多个 配置 选项间进行切换 , 而 总是默认选择 0号 配置; 后来我终于找到了一个比较隐晦的方法(使用 WDF_USB_DEVICE_SELECT_CONFIG_PARAMS_INIT_URB ,见后面章节)。但用起来比较麻烦。 我没 能 找到 可以方便调用的接口,也许WDF 以后会增加接口的,但谁也不确定 。 实际上,还是有很多人希望能够在多配置间来回切换的,谁知道呢。
如果不需要选择非0 配置, WDF 到是能带来好事一桩,因为它总是在 PnpPrepareHardware 被调用之前,就已经把0号配置给选择好了。 这样一来,就省去了代码若干行了。
接口总是需要配置的。接口配置分两种情况:单接口配置,多接口配置。WDF 为单接口、多接口的配置分别给出了不同的宏定义,这是为了方便编程的缘故。但可以记住一点:用来配置多接口的宏,也可以用来配置单接口。所谓的多接口,当把这个“多”的数量设为 1 的时候,也就是单接口了。
请看下面的代码和注释:
//
// 接口配置。 WDF 提供了多种接口配置的初始化宏,对于单一接口的 USB 设备,使用下面的宏即可。
// 代码非常精简,几乎令长期受累于 WDM 编程折磨的程序员们,大跌眼镜;
// 因为同样的操作,使用WDM 来写的话,需要上百行代码。
numInterfaces = WdfUsbTargetDeviceGetNumInterfaces(DeviceContext->UsbDevice);
if(1 == numInterfaces) //单接口
{
WDF_USB_DEVICE_SELECT_CONFIG_PARAMS_INIT_SINGLE_INTERFACE(&usbConfig);
}
else if (2 == numInterfaces)
{
settingPairs = ExAllocatePoolWithTag(PagedPool,
sizeof(WDF_USB_INTERFACE_SETTING_PAIR) * 2, '1234');
if (settingPairs == NULL)
return STATUS_INSUFFICIENT_RESOURCES;
InitSettingPairs(DeviceContext->UsbDevice, settingPairs, 2);
WDF_USB_DEVICE_SELECT_CONFIG_PARAMS_INIT_MULTIPLE_INTERFACES(
&usbConfig, 2, settingPairs);
}else // 开发套件只用 1 、 2 接口两种情况。
return STATUS_UNSUCCESSFUL;
// 发送配置命令
status = WdfUsbTargetDeviceSelectConfig(DeviceContext->UsbDevice,
WDF_NO_OBJECT_ATTRIBUTES, &usbConfig);
if(!NT_SUCCESS(status))
{
KDBG(DPFLTR_INFO_LEVEL, "WdfUsbTargetDeviceSelectConfig failed with status 0x%08x/n", status);
return status;
}
上面代码中,在进行多接口配置的时候,需要用到一个InitSettingPairs 函数。它主要是用来初始化 WDF_USB_INTERFACE_SETTING_PAIR 对象,用来设置多个接口的Alt 值。
// 多接口设备配置初始化 Alter 值
UCHAR MultiInterfaceSettings[2] = {1, 0};
void InitSettingPairs(IN WDFUSBDEVICE UsbDevice, // USB设备对象
OUT PWDF_USB_INTERFACE_SETTING_PAIR Pairs, // OUT指针。
IN ULONG NumSettings ) // 接口个数
{
UCHAR i;
// 最多支持个接口,把多余的切掉。
if(NumSettings > MAX_INTERFACES)
NumSettings = MAX_INTERFACES;
// 配置每个接口的可选值( Alternate Setting )
for(i = 0; i < NumSettings; i++)
{
Pairs[i].UsbInterface =
WdfUsbTargetDeviceGetInterface(UsbDevice, i);// 接口句柄设置
Pairs[i].SettingIndex =
MultiInterfaceSettings[i];// 可选值设置
}
}
因为涉及到了多接口,这里不妨多费一点口舌讲解一下。对USB 驱动而言,一个功能驱动按照常理是只能驱动 USB 设备的一个接口;但 Windows 的 USB 驱动架构规定了,如果某几个接口能够按照某种规定组成接口组( Interface Collection ),则可以共享同一个功能驱动。否则多个设备接口必须由多个 USB 功能驱动来控制。
四 种情况下多接口可以组成接口组:
1 最普遍通用的方法,通过USB 设备接口的接口相关描述符( interface association descriptors ,简称 IADs )进行接口组的组织工作。它总是第一位的。如果IADs 存在,它总是被优先考虑,即使其他几种情况的依据也可能同时存在。通用父驱动在使用 IADs 的时候,把具有相同 IADs 的接口组织到同一个接口组中。
2 向通用父驱动(Generic father driver )注册厂商回调函数( Vender-Supplied Callback Function )。 由于接口组的组合权,掌握在通用父驱动手里。这个回调函数被注册后,它的执行时间,比通用父驱动要早,能够代替通用父驱动做部分工作。一般设备厂商是很少 使用这种方法的,因为它一般要求提供一个设备过滤函数,位于功能驱动和通用父驱动之间。增加了开发的难度。但它的优先级最好,也最有效,能把毫无关系的接 口都统一在一起。
3 CDC(通信设备类)或者 WMCDC (无线移动通信设备类)设备,通用父驱动会通过接口的 Union Functional Descriptors 值,来组织接口。
4 对于音频设备,有特殊的组织方法。音频设备的功能ID 、子功能 ID ,此时被用作判别其所属关系的依据。但这种方法的优先级最低,如果设备提供了 IADs ,即使通用父驱动知道当前是音频设备,也会选择 IADs 作为判别依据的。使用这种方法的时候,凡是能够被组织进入同一个接口组的接口,必须具备三个条件:第一,组内接口的接口 ID 必须是连续的,必须 1 、 2 、 3 … 这样排下去,如果中间掉了一个,那就不能成立(比如1 、 2 、 4 ,中间缺了 3 ,那么只能 1 、 2 做成一组, 4 另外算);第二,组内接口的类值( bInterfaceClass )必须都为指定的音频类值(1 );第三,接口组中的第一个接口的子类值 (bInterfaceSubClass) ,必须不同于组中所有其他接口的子类值(在实际设计中,为首的接口总是 AudioContro 接口,子类值为 0 ,后续接口类型都为 AudioStreaming ,子类值为 1 )。满足了这三个条件的一组接口才能被组织为一个接口组,一旦三个条件中有一条不满足,就立刻结束当前接口组,开始下一个可能存在的接口组的查找。如果对音频设备感兴趣,参考协议文档《 USB Device Class For Audio Class 》会得到更深的理解。
所以我们这一节里,遇到的情况就是,存在一个接口组,组中自然存在着多个接口,我们的功能驱动要为这些接口做配置工作。
WDF为这种情况下的多接口配置封装了一个结构体 WDF_USB_INTERFACE_SETTING_PAIR ,结构体里面定义了一个数组,数组的每个入口依次对应了USB 设备的各个接口,我们在这些入口中为各个接口做配置定义。入口中包含两个值:接口句柄和接口可选值( Alternate Value )。
CY001开发板中提供的固件文件 CY001(2Interfaces).hex 是一个多接口版本固件代码文件,加载到开发板中之后,设备将拥有两个不同的接口(接口 0 和接口 1 )。但我们在设计的时候,没有采用接口组来组织这两个接口,而是让它们相互独立。这样读者在使用的时候,就会在设备管理器中看到两个设备同时出现的情况。
5.2 设备电源策略
CY001在电源策略上,做了两个动作:支持闲时休眠、支持远程唤醒。
所谓闲时休眠,就是如果当USB 设备在一定时间内没有工作,就据此判断在接下来更长的时间内,也不会有实际工作需要做,就自动进入休眠状态以节约能量;
而远程唤醒是指设备进入休眠状态后,通过软件或者硬件上的操作,使其回复到工作状态。
实现这两个功能的函数为 InitPowerManagement 。
闲时休眠是通过结构体 WDF_DEVICE_POWER_POLICY_IDLE_SETTINGS 来设置的。
typedef struct _WDF_DEVICE_POWER_POLICY_IDLE_SETTINGS {
ULONG Size ;
WDF_POWER_POLICY_S0_IDLE_CAPABILITIES IdleCaps ;
DEVICE_POWER_STATE DxState ;
ULONG IdleTimeout ;
WDF_POWER_POLICY_S0_IDLE_USER_CONTROL UserControlOfIdleSettings ;
WDF_TRI_STATE Enabled ;
} WDF_DEVICE_POWER_POLICY_IDLE_SETTINGS;
通过 WDF_DEVICE_POWER_POLICY_IDLE_SETTINGS_INIT 宏调用,对结构体变量进行初始化。宏调用的第二个参数是一个 WDF_POWER_POLICY_S0_IDLE_CAPABILITIES 类型的枚举量,这个值被赋值给 IdleCaps 成员变量。此变量用来判断设备的这样一种能力:当设备已经休眠(离开了D0 状态),而系统未休眠(仍处于 S0 状态),这种情况下,设备是否有能力让自己从休眠状态中醒来。值 IdleUsbSelectiveSuspend 只能用于USB 设备,它把判断的权力让给了 USB 总线,所以总是能从 S0 状态中醒来的。
另外需要设置Idle 时间,只需要直接设置 WDF_DEVICE_POWER_POLICY_IDLE_SETTINGS 的idleTimeOut 变量就可以了。 idleTimeOut 值是以毫秒为单位的,所以设置 1 秒钟,应该赋值 1000 。 Idle 时间设好后, USB 设备就会在经历了规定长时间的 Idle 状态后自动进入休眠状态。 CY001 驱动把 Idle 时间设为 10 秒。
通过调用函数 WdfDeviceAssignS0IdleSettings ,把上述设置应用到设备对象中。
远程唤醒是通过结构体 WDF_DEVICE_POWER_POLICY_WAKE_SETTINGS_INIT 来 设置的。远程唤醒包含了唤醒设备自身,和唤醒系统两个方面。我们的CY001开发板是两者同时支持的。从设备上看,在CY001固件代码被加载到开发板 后,系统会在10秒闲时后自动进入休眠状态,七段LED灯显示字符‘S’。此后,软件和硬件的任何操作,都能令设备自身醒来;如果设备休眠后,操作系统本 身也进入休眠状态,则通过硬件上的操作(按下开发板上的“WakeUp”按钮),可令设备和系统同时醒来。
typedef struct _WDF_DEVICE_POWER_POLICY_WAKE_SETTINGS {
ULONG Size ;
DEVICE_POWER_STATE DxState ;
WDF_POWER_POLICY_SX_WAKE_USER_CONTROL UserControlOfWakeSettings ;
WDF_TRI_STATE Enabled ;
} WDF_DEVICE_POWER_POLICY_WAKE_SETTINGS;
DxState是当系统进入休眠状态后,设备进入的电源状态。这个值不能为 D0 ,也就是说,如果系统休眠了,连接到系统的硬件设备不能仍处于运行状态。默认情况下, DxState 被设置为 DevicePowerMaximum 。这不是一个有效的电源状态值,这时候系统将会参照其他设置来自行判断。
Enabled 用来设置是否允许设备唤醒系统。
UserControlOfWakeSettings 值用来设置用户控制能力,也就是用户能否通过在注册表中指定项中设置自定义值,来改变Enabled 值所确定的电源唤醒策略。默认情况下是允许的。
最终我们通过传入 WDF_DEVICE_POWER_POLICY_WAKE_SETTINGS 结构体参数并调用方法 WdfDeviceAssignSxWakeSettings 来设置设备的唤醒策略 。
设备可拥有多个电源状态,除了D0 为工作状态为,从 D1 开始到 D3 ,都为休眠状态。但 D1-D3 之间又存在差别,他们的供电量一级比一级低。但即使在 D3 状态下,设备也是有电源供应的,而不是完全断电。另外, D0 是必须具备的状态, D1 到 D3 不必全部都支持,有选择地支持一种及以上即可。
系统的电源策略使用另外一套表示方式,S0 为完全工作状态, S0 以外还存在 S1 到 S5 层次不等的低电源状态。我们在设置 WDF_DEVICE_POWER_POLICY_WAKE_SETTINGS 结构提的DxState 值时知道系统电源状态和设备电源状态之间存在着映射关系。也就是当系统电源处于 Sx 状态的时候,设备电源应当处于什么状态(也就是 Dx )。下图是我截取的 CY001 的电源映射图:
从上图可以看出,S0 状态映射到设备的 D0 状态(实际上, S0 映射到所有设备的 D0 状态),其他时候都映射到 D3 状态。这表明了每次当系统进入 S0 工作状态的时候,都会自动为 CY001 提供最大电源供应,将其电源状态调整到 D0 ;当系统离开 S0 状态( S1 到 S5 ),就会自动将给 CY001 的供电降到最小的 D3 状态。
(从略)
就这样,设备启动就算完成了。 PnpAdd 的作用类似于WDM驱动中的AddDevice, PnpPrepareHardware 类似于WDM 驱动中的 StartDevice 函数。当然等号是不能划的,可以用一个约等号吧。
6. 其他设备操作
我们在PnpAdd 函数中,除了注册了 PnpPrepareHardware 外,还注册了一下其他的接口。我们这个小节就是简单地介绍一下它们。
如果 PnpPrepareHardware 执行错误导致函数返回失败,我们就会在设备管理器中看到对应的设备那一栏中被标上一个黄色感叹号,表示此设备不可用。如 PnpPrepareHardware 返回错误,系统会调用 DeviceReleaseHardware 释放系统资源。
DeviceReleaseHardware 有点类似WDM框架下,IRP_MN_STOP_DEVICE所对应的子分发函数。
如果设备被异常拔除,回调函数 PnpSurpriseRemove 函数会被调用。值得注意的是,如果 PnpSurpriseRemove 被调用的话,紧接着 DeviceReleaseHardware 也一定会被调用。所以虽然我们也可以在 PnpSurpriseRemove 中释放系统资源,但如果把它们都统一到 DeviceReleaseHardware 中的话会更好。如果没有特殊需求,一般可忽略这个回调。同样,这个回调函数,对应于WDM框架下的IRP_MN_SURPRISE_REMOVE子分发ID所对应的处理函数。
PwrD0Entry 和 PwrD0 Exit 两个回调比较重要。前者当设备电源状态进入D0时被调用,后者当设备电源状态离开D0时被调用。这两个回调函数合在一起,恰可对于WDM框架下 IRP_MN_SET_POWER子分发所对应的处理函数。联系到我们为CY001注册的两个电源策略:休眠和唤醒。我们就可以这样来说:当设备休眠的时 候,PwrD0Exit将被调用;当设备唤醒的时候, PwrD0Entry 将被调用。
6.1 USB设备反 配置
DeviceReleaseHardware 函数会把USB设备反配置,也就是把配置描述符内容选空。正反相对,有正必有反,呵呵。 反配置就是把前面的设备配置操作取消掉。在配置的时候,USB 总线会为 USB 设备分配总线资源;而在反配置的时候,总线收回已分配的一切。
反配置操作的代码非常简单,代码如下:
WDF_USB_DEVICE_SELECT_CONFIG_PARAMS_INIT_DECONFIG(&configParams); //反配置
status = WdfUsbTargetDeviceSelectConfig(pDeviceContext->UsbDevice ,
WDF_NO_OBJECT_ATTRIBUTES,
&configParams);
第一行初始化USB 配置参数块( WDF_USB_DEVICE_SELECT_CONFIG_PARAMS ),告知是反配置;
第二行进行实际的配置/ 反配置操作。
代码逻辑和配置操作极为类似。关于USB 配置参数块,上面不曾详细讲,这里不妨多讲一些。这个结构体由五个联合 union 组成,分别对应于不同的配置场合,也就是说, WDF 支持用多种方式对设备进行配置和反配置。对应于五个 union ,有五个不同的宏初始化这个结构体。这五个宏,一个用来反配置,这里已经讲过了,另四个用来配置。四个配置宏中, WDF_USB_DEVICE_SELECT_CONFIG_PARAMS_INIT_SINGLE_INTERFACE 和 WDF_USB_DEVICE_SELECT_CONFIG_PARAMS_INIT_MULTIPLE_INTERFACES 上面也已经讲过了,下面简单讲讲其他的两个宏:
WDF_USB_DEVICE_SELECT_CONFIG_PARAMS_INIT_INTERFACES_DESCRIPTORS :
这个宏用起来比比较复杂,要求用户从配置描述符析取全部的接口描述符。用下面的方法析取接口的描述符:1 ,调用 WdfUsbTargetDeviceGetInterface 获取接口句柄,2 ,调用 WdfUsbInterfaceGetDescriptor 获得接口描述符。循环使用此方法,以获取所有的接口描述符。
WDF_USB_DEVICE_SELECT_CONFIG_PARAMS_INIT_URB
这个宏要求用户输入一个类型为 _URB_SELECT_CONFIGURATION 的URB 。这个宏只为一些特别的需求而设计,是 WDF 和 WDM 混合使用的方法,在 WDF 中极为少见。幸运地是,我们正可以通过它来避过默认配置为 0 的限制,为 USB 设备选择其他的非 0 配置。这个宏用起来最为复杂,容易出错。因为宏定义是没有参数类型检查的,所以如果你不小心把 URB 参数弄错了,并不会被报错,却会在运行时出错。
如何使用这个宏,以及如果使用它来避过默认配置0 的限制,我暂时没有示例代码。读者可以自己进行尝试。
7. 数据I/O 操作
首先讲控制端口。
控制端口是先天存在的,其他端口则是后期定制的。一个USB 设备中,至少有一个 0 号控制端口,而其他的端口则安需要生成。向控制端口发送的命令称作控制命令,控制命令有规定的格式叫 Setup 包。 Setup 包共 7 个字节,表示了命令属性、命令 ID 、命令对象等信息。通过命令属性,可以把命令归类为:标准命令、类命令、自定义命令。根据命令对象,可把命令归类为:设备处理命令、接口处理命令、端点处理命令。
不要把不认识的命令发给设备,那样设备返回STALL 握手信号,总线可能因此而将你的驱动挂起,再有其他什么命令,都不能够被正常完成,直到物理设备移除总线。遇到这种情况是很糟糕的,使用 CY001 的时候就表现为任何命令都将返回失败,甚至 UI 失去响应。
有的控制命令很简单,只要发送一个Setup 包就可以了。有的则复杂一些,还需要传送一段数据。 Setup 包中的 wLength 值表示需要传输的数据长度。如果 wLength 为 0 ,说明没有数据需要传送。
USB设备如果成功处理了一个控制命令,会返回 ACK 握手包给主机,主机凭此而知道命令已被正确处理。如果 USB 设备未能正确处理一个控制命令,它会返回 STALL 握手包给主机,主机会因此而一直等待。
再讲控制命令。
控制命令由一个8 字节的 SETUP 结构体定义。我在开发包的结构体定义中,对它做了一点点整理,更便于了解结构体各个部分的作用:
typedef struct
{
union {
struct {
BYTE recepient:2; // 接受者:设备、接口、端口
BYTE Reserved:3; // 不用
BYTE Type:2; // Standard、 Class 、 Vendor
BYTE bDirInput:1; // 方向
} Request;
BYTE type;
} type;
UCHAR req; // 命令码
USHORT value;
USHORT index;
USHORT length;
}USB_CTL_REQ ;
结构体中第一个字节很重要,被我定义成一个Union ,它定义了控制命令的类型。这个字节的各个比特位,都有特殊的意义。第 7 位表示命令的方向, 1 为输入, 0 为输出。 5 、 6 两位表示了设备类型。 2-4 位是保留位。 0 和 1 位是接 收 位。
CY001_WDF工程中, UsbControlRequest 函数对type 字节做了详细的分析: 0 、 1 字节组合值, 00 代表设备, 01 代表接口, 10 代表端口, 11 代表其他; 5 、 6 字节组合值, 00 代表标准 USB 命令, 01 代表 Class 命令, 10 代表用户自定义命令, 11 代表其他。
举例来说,0x80 代表了一个接收者为设备的标准输入控制命令。其二进制值为 :1000 0000 。
再以CY001 中自定义的设备 LED 明灭命令来说,它的 Type 值必须被设置成 0x40 ,其二进制位 0100 0000 。
读者在有空的时候可对照图例分析这两个例子。
USB_CTL_REQ 结构体中第二个字节定义了Request 值。这个值对于标准命令和 Class 命令,都是由协议预先定义好的; Vendor 命令则由用户自行定义,没有特别的限制。
Value、 Index 和 Length 值,对不同的控制命令,所对应的值意义不尽相同。 USB 协议对他们的使用没有特别的规定。
使用CY001 开发包工具 UsbKitApp ,可以通过用户界面向设备发送各种控制命令,其界面和 BusHound 很类似。下图就是一个例子,向设备发送获取设备描述符的控制命令:
最后获得的设备描述符结构,以16 进制字符的形式显示在编辑框中。有些朋友对数字怀有偏执狂一样的热爱,能从 滚动的 二进制数字中看出美女 图 片 来 。这个小工具会很合他们的口味。