支持在总线驱动程序中进行 PnP 和电源管理
某些设备永久插入系统,而其他设备可以在系统运行时插入和拔出电源。 总线驱动 必须识别并报告连接到其总线的设备,并且他们必须发现并报告系统中设备的到达和离开情况。
总线驱动程序标识和报告的设备称为总线的 子设备。 标识和报告子设备的过程称为 总线枚举。 在总线枚举期间,总线驱动程序会为其子 设备创建设备对象 。
总线驱动程序本质上是同时处理总线枚举的功能驱动程序,或者很少是Filter驱动程序。 总线驱动程序通常是总线适配器的功能驱动程序,但它不是连接到总线的子设备的功能驱动程序。
总线驱动程序还具有与功能驱动程序相同的 PnP 和电源管理职责。
枚举总线上的设备
总线枚举 是确定哪些子设备连接到父设备的行为。 父设备通常是总线适配器,但它也可以是支持多种功能(例如声音卡)的设备,每个功能需要一组单独的驱动程序。
Kernel-Mode 驱动程序框架 (KMDF) 支持两种类型的总线枚举:
- 静态枚举易于实现,如果子设备的数量和类型不是特定于系统的,并且硬件接通后不会更改,则静态枚举是理想的;
- 当子设备的数量或类型从一台计算机更改为另一台计算机时,应使用动态枚举;
总线驱动程序可以使用或两种类型的总线枚举。
静态枚举
静态枚举 是驱动程序在系统初始化期间检测和报告设备是否存在的功能,并且报告系统配置的后续更改的能力有限。
如果设备或功能子单元的数量和类型是预先确定的和永久的,并且不依赖于运行驱动程序的系统的配置,则总线驱动程序可以使用静态枚举。
例如,声音卡的驱动程序可以充当总线驱动程序, PDO为卡的每个功能(例如 MIDI、音频和游戏杆)创建单独的物理设备对象。
静态子列表
框架通过提供静态子列表,使驱动程序能够支持静态枚举。 每个静态子列表表示连接到父设备的子设备列表。 父设备的总线驱动程序必须标识父设备的子设备,将它们添加到父设备的静态子设备列表中,并为每个子设备创建 PDO。
创建静态子列表
每次驱动程序创建一个框架设备对象,该框架对象表示 (设备的 FDO) 功能设备对象时,框架都会为设备创建一个空的静态子列表。
1. 当框架调用总线驱动程序的 EvtDriverDeviceAdd 回调函数时,回调函数必须调用 WdfDeviceCreate 为父设备创建 FDO。
2. 然后,驱动程序必须枚举父设备的子级,为子级创建 PDO,并将子级添加到子级列表。
3. (可选)驱动程序可以调用 WdfDeviceSetBusInformationForChildren ,为框架提供有关总线的信息。 建议这样做,因为这样可以更轻松地让子设备和应用识别总线。
若要为检测到的子设备创建 PDO,总线驱动程序必须:
- 调用 WdfPdoInitAllocate 以获取 WDFDEVICE_INIT 结构;
- 初始化WDFDEVICE_INIT结构;
- 调用 WdfDeviceCreate 以创建表示 PDO 的框架设备对象;
调用 WdfDeviceCreate 后,驱动程序必须调用 WdfFdoAddStaticChild 以将子设备添加到子列表。
修改静态子列表
由于驱动程序应仅对预先确定的永久性设备配置使用静态子列表,因此驱动程序在创建静态子列表后几乎不需要修改它。 如果驱动程序确定子设备变得不可访问,驱动程序可以调用 WdfPdoMarkMissing。 如果子设备仍可访问,但变得无响应且不可用,则驱动程序应将 WDF_DEVICE_STATE 结构的 Failed 成员设置为 WdfTrue,然后调用 WdfDeviceSetDeviceState。
遍历静态子列表
如果需要检索静态子列表的内容,驱动程序可以通过执行以下操作遍历该列表:
- 调用 WdfFdoLockStaticChildListForIteration;
- 根据需要多次调用 WdfFdoRetrieveNextStaticChild ;
- 调用 WdfFdoUnlockStaticChildListFromIteration;
动态枚举
动态枚举 是驱动程序能够检测和报告在系统运行时连接到系统的设备的数量和类型的更改。
如果连接到父设备的设备数量或类型取决于系统的配置,则总线驱动程序必须使用动态枚举。 其中一些设备可能始终连接到系统,有些设备可能在系统运行时接通电源并拔下电源。
例如,插入系统 PCI 总线的设备的数量和类型取决于系统,但它们是永久性的,除非用户关闭电源、打开外壳,并使用螺丝刀添加或删除设备。 另一方面,用户可以通过在系统运行时插入或拔下电缆来添加或删除 USB 设备。
动态子列表
框架通过提供框架子列表对象,使驱动程序能够支持动态枚举。 每个子列表对象表示连接到父设备的子设备的列表。 父设备的总线驱动程序必须标识父设备的子设备,将它们添加到父设备的子列表中,并为每个子设备创建物理设备对象 (PDO) 。
每次驱动程序创建表示设备的 FDO 的框架设备对象时,框架都会为设备创建一个空的默认子列表。 驱动程序可以通过调用 WdfFdoGetDefaultChildList 来获取设备默认子列表的句柄。 通常,如果要编写枚举设备子级的总线驱动程序,驱动程序可以将子级添加到默认子列表。 如果需要创建其他子列表,驱动程序可以调用 WdfChildListCreate。
在驱动程序可以使用子列表之前,它必须通过初始化 WDF_CHILD_LIST_CONFIG 结构并将结构传递给 WdfFdoInitSetDefaultChildListConfig对于默认子列表或 WdfChildListCreate对于其他子列表来配置子列表对象。
动态子级说明
每次总线驱动程序标识子设备时,都必须将子设备的说明添加到子列表中。 子说明由必需的标识说明和可选的地址说明组成。
标识说明 是一种结构,它包含唯一标识驱动程序枚举的每个设备的信息。 驱动程序定义此结构,但其第一个成员必须是 WDF_CHILD_IDENTIFICATION_DESCRIPTION_HEADER 结构。
通常,标识说明包含设备的 设备标识字符串(可能是序列号)以及有关设备在总线上的位置的信息,例如槽号。
驱动程序可以提供以下一组回调函数,这些函数允许框架操作标识说明中的信息:
- EvtChildListIdentificationDescriptionCompare,用于比较两个标识描述结构的内容;
- EvtChildListIdentificationDescriptionCopy,将一个标识说明结构的内容复制到另一个标识说明结构;
- EvtChildListIdentificationDescriptionDuplicate,它通过复制现有标识说明结构并在必要时分配其他缓冲区来创建新的标识说明;
- EvtChildListIdentificationDescriptionCleanup,它解除分配由 EvtChildListIdentificationDescriptionDuplicate 回调函数分配的缓冲区;
通常,如果驱动程序的标识说明结构包含指向动态分配的缓冲区的指针,则需要提供这些回调函数。
地址说明 地址说明是一种结构,它包含驱动程序所需的信息,以便它可以访问其总线上的设备,如果信息可以在设备接通电源时发生更改。 驱动程序定义此结构,但其第一个成员必须是 WDF_CHILD_ADDRESS_DESCRIPTION_HEADER 结构。
地址说明是可选的。 如果设备地址信息在设备接通电源和拔出电源之间无法更改,则设备的所有地址信息都可以存储在标识说明中。 例如,USB 控制器在设备接通电源时将地址分配给设备,并且这些地址不会更改。
另一方面,一些总线使用可能会更改的寻址信息。 例如,IEEE 1394 总线使用“代数”,即已发生的总线重置次数。 向 IEEE 1394 设备发送的每个异步 I/O 请求都必须包含生成计数。 由于此地址信息可能会更改,因此驱动程序必须将其存储在地址说明中。
驱动程序可以提供以下回调函数集来操作地址说明中的信息:
- EvtChildListAddressDescriptionCopy,将一个地址说明结构的内容复制到另一个地址说明结构;
- EvtChildListAddressDescriptionDuplicate,它通过复制现有地址说明结构并在必要时分配其他缓冲区来创建新的地址说明;
- EvtChildListAddressDescriptionCleanup,可解除分配由 EvtChildListAddressDescriptionDuplicate 回调函数分配的缓冲区;
通常,如果驱动程序的地址描述结构包含指向动态分配的缓冲区的指针,则需要提供这些回调函数。
将设备添加到动态子列表
当框架调用总线驱动程序的 EvtDriverDeviceAdd 回调函数时,回调函数必须调用 WdfDeviceCreate 为父设备(通常是总线适配器)创建 FDO。 然后,驱动程序必须枚举父设备的子级,并将子级添加到子列表中。
(可选)驱动程序可以调用 WdfDeviceSetBusInformationForChildren ,为框架提供有关总线的信息。 建议这样做,因为它使子设备和应用更容易识别总线。
若要将子级添加到子列表,驱动程序必须为找到的每个子设备调用 WdfChildListAddOrUpdateChildDescriptionAsPresent 。 此调用通知框架驱动程序已发现连接到父设备的子设备。 当驱动程序调用 WdfChildListAddOrUpdateChildDescriptionAsPresent 时,它会提供标识说明和地址说明(可选)。
在驱动程序调用 WdfChildListAddOrUpdateChildDescriptionAsPresent 来报告新设备后,框架会通知 PnP 管理器新设备存在。 然后,PnP 管理器为新设备生成设备堆栈和驱动程序堆栈。 在此过程中,框架调用总线驱动程序的 EvtChildListCreateDevice 回调函数。 此回调函数必须调用 WdfDeviceCreate 才能为新设备创建 PDO。
通常,多个子设备连接到父设备,因此总线驱动程序需要多次调用 WdfChildListAddOrUpdateChildDescriptionAsPresent 。 执行此操作的最有效方法是:
- 调用 WdfChildListBeginScan;
- 为每个子设备调用 WdfChildListAddOrUpdateChildDescriptionAsPresent ;
- 调用 WdfChildListEndScan;
如果使用对 WdfChildListBeginScan 和 WdfChildListEndScan 的调用包围驱动程序的动态枚举,框架会将所有更改存储到子列表,并在驱动程序调用 WdfChildListEndScan 时通知 PnP 管理器所做的更改。 稍后,框架会为子列表中的每台设备调用总线驱动程序的 EvtChildListCreateDevice 回调函数。 此回调函数调用 WdfDeviceCreate 为每个新设备创建 PDO。
当驱动程序调用 WdfChildListBeginScan 时,框架会将以前报告的所有设备标记为不再存在。 因此,驱动程序必须为驱动程序可以检测的所有子级,而不仅仅是新发现的子级调用 WdfChildListAddOrUpdateChildDescriptionAsPresent 。 若要将单个子级添加到子列表,驱动程序可以调用 WdfChildListUpdateAllChildDescriptionsAsPresent ,而无需先调用 WdfChildListBeginScan。
更新动态子列表
有两种常见方法来更新动态子列表中的信息:
- 当父设备收到指示子项到达或删除的中断时,如果设备已接通电源,则驱动程序的 EvtInterruptDpc 回调函数将调用 WdfChildListAddOrUpdateChildDescriptionAsPresent ;如果设备已拔出电源,则 调用 WdfChildListUpdDescriptionAsMissing ;
- 驱动程序可以提供 EvtChildListScanForChildren 回调函数,每次父设备进入其工作 (D0) 状态时,框架都会调用该函数。 此回调函数应通过调用 WdfChildListBeginScan、 WdfChildListAddOrUpdateChildDescriptionAsPresent (或 WdfChildListUpdateAllChildDescriptionsAsPresent) 和 WdfChildListEndScan 来枚举所有子设备;
可以在驱动程序中使用其中一种或两种技术。
遍历动态子列表
如果希望驱动程序检查子列表的内容,它可以使用以下方法之一遍历列表:
1. 若要获取每个子设备说明的内容(一次一个),驱动程序可以:
调用 WdfChildListBeginIteration。
根据需要多次调用 WdfChildListRetrieveNextDevice。
调用 WdfChildListEndIteration。
调用 WdfChildListBeginIteration 时,驱动程序指定 WDF_RETRIEVE_CHILD_FLAGS类型的标志,该标志指示框架是应检索所有设备说明还是仅检索子集。 当 WdfChildListRetrieveNextDevice 找到匹配项时,它将检索子设备的标识和地址说明,以及其设备对象的句柄。
2. 如果需要获取当前包含在子设备说明中的地址说明,驱动程序可以调用 WdfChildListRetrieveAddressDescription,并指定标识说明。 框架遍历子列表,直到找到具有匹配标识说明的子设备,然后检索地址说明。
3. 如果需要获取与特定子设备关联的框架设备对象的句柄,驱动程序可以调用 WdfChildListRetrievePdo。 框架遍历子列表,直到找到具有匹配标识说明的子设备,然后返回设备对象句柄。 请务必使用 WdfChildListBeginIteration 和 WdfChildListEndIteration 包装调用,以防止调用方在另一个线程上突然删除 PDO。
访问 PDO 的标识和地址说明
驱动程序可以调用以下方法来访问 PDO 的标识说明或地址说明:
- WdfPdoRetrieveIdentificationDescription,用于检索与 PDO 关联的标识说明;
- WdfPdoRetrieveAddressDescription,用于检索与 PDO 关联的地址说明;
- WdfPdoUpdateAddressDescription,用于更新与 PDO 关联的地址说明;
处理重新枚举请求
支持动态枚举的基于框架的总线驱动程序可以接收通过 REENUMERATE_SELF_INTERFACE_STANDARD 接口恢复特定子设备的请求。
处理枚举请求
PnP 管理器可以随时请求总线驱动程序枚举其子级。 (如果熟悉 WDM 接口,则枚举请求 IRP_MN_QUERY_DEVICE_RELATIONS 关系类型为 BusRelations.) Framework 驱动程序的请求看不到这些请求。 相反,框架使用存储在设备的子列表中的信息来处理请求。 驱动程序负责使子列表保持最新状态,以便框架可以在 PnP 管理器请求枚举时提供正确的信息。
支持动态枚举的基于框架的总线驱动程序可以接收请求,要求恢复特定子设备。 在驱动程序检测到设备故障后,子设备的功能驱动程序可能会发送此类请求。 框架通过实现 REENUMERATE_SELF_INTERFACE_STANDARD 接口来支持这种类型的请求,该接口是在 wdm.h.
支持动态枚举的基于框架的总线驱动程序可以提供 EvtChildListDeviceReenumerated 回调函数,框架在收到来自子设备的驱动程序的恢复请求时调用该函数。 如果此回调函数返回 TRUE 或不存在,框架会将子设备标记为不再存在,并通知 PnP 管理器总线驱动程序的子列表已更改。 因此,PnP 管理器请求恢复,框架调用驱动程序的 EvtChildListCreateDevice 回调函数,该函数为子设备创建新的 PDO。