编写 UCSI 客户端驱动程序
USB Type-C 连接or 系统软件接口(UCSI)驱动程序充当带有嵌入式控制器(EC)的 USB Type-C 系统的控制器驱动程序。
如果实现平台策略管理器(PPM)的系统,如 UCSI 规范中所述,请在连接到系统的 EC 中通过以下方式进行:
- ACPI 传输不需要编写驱动程序。 加载 Microsoft 提供的内置驱动程序(UcmUcsiCx.sys和UcmUcsiAcpiClient.sys);
- 非 ACPI 传输(如 USB、PCI、I2C 或 UART)需要为控制器编写客户端驱动程序;
- 如果 USB Type-C 硬件没有处理电源交付(PD)状态机的功能,请考虑编写 USB Type-C 端口控制器驱动程序;
从 Windows 10 版本 1809 开始,添加了 UCSI(UcmUcsiCx.sys)的新类扩展,该扩展以与传输无关的方式实现 UCSI 规范。 只需编写极少量的代码,驱动程序(即 UcmUcsiCx 的客户端)即可通过非 ACPI 传输来与 USB Type C 硬件通信。 本主题介绍 UCSI 类扩展提供的服务,以及客户端驱动程序的预期行为。
UCSI 类扩展体系结构
UCSI 类扩展 UcmUcsiCx 允许编写驱动程序,该驱动程序使用非 ACPI 传输与其嵌入式控制器通信。 控制器驱动程序是 UcmUcsiCx 的客户端驱动程序。 UcmUcsiCx 又是 USB 连接器管理器(UCM)的客户端。 因此,UcmUcsiCx 不自行做出任何策略决策。 而是实现 UCM 提供的策略。 UcmUcsiCx 实现状态机来处理来自客户端驱动程序的平台策略管理器(PPM)通知,并发送命令来实现 UCM 策略决策,从而允许更可靠的问题检测和错误处理。
OS 策略管理器 (OPM)
OS 策略管理器(OPM)实现与 PPM 交互的逻辑,如 UCSI 规范中所述。 OPM 负责:
- 将 UCM 策略转换为 UCSI 命令,将 UCSI 通知转换为 UCM 通知;
- 发送初始化 PPM、检测错误和恢复机制所需的 UCSI 命令;
处理 UCSI 命令
典型的操作涉及由 UCSI 复杂硬件完成的多个命令。 例如,让我们考虑GET_CONNECTOR_STATUS命令。
- PPM 固件将连接更改通知发送到 UcmUcsiCx/客户端驱动程序;
- 作为响应,UcmUcsiCx/客户端驱动程序将GET_CONNECTOR_STATUS命令发送回 PPM 固件;
- PPM 固件执行GET_CONNECTOR_STATUS,并异步将命令完成通知发送到 UcmUcsiCx/客户端驱动程序。 该通知包含有关实际连接状态的数据;
- UcmUcsiCx/客户端驱动程序处理状态信息并将ACK_CC_CI发送到 PPM 固件;
- PPM 固件执行ACK_CC_CI,并异步将命令完成通知发送到 UcmUcsiCx/客户端驱动程序;
- UcmUcsiCx/客户端驱动程序将GET_CONNECTOR_STATUS命令视为已完成;
与平台策略管理器(PPM)的通信
UcmUcsiCx 抽象化了将 UCSI 命令从 OPM 发送到 PPM 固件以及从 PPM 固件接收通知的详细信息。 它将 PPM 命令转换为 WDFREQUEST 对象,并将其转发到客户端驱动程序。
- PPM 通知:客户端驱动程序向 UcmUcsiCx 通知固件中的 PPM 通知。 驱动程序提供包含 CCI 的 UCSI 数据块。 UcmUcsiCx 将通知转发到 OPM 和其他组件,这些组件基于数据采取适当的操作;
- 客户端驱动程序的 IOCTL:UcmUcsiCx 将 UCSI 命令(通过 IOCTL 请求)发送到客户端驱动程序以发送到 PPM 固件。 驱动程序负责在将 UCSI 命令发送到固件后完成请求;
处理电源转换
客户端驱动程序是电源策略所有者。如果客户端驱动程序由于 S0-Idle 而进入 Dx 状态,则当 UcmUcsiCx 向客户端驱动程序的电源托管队列发送包含 UCSI 命令的 IOCTL 时,WDF 会将驱动程序引入 D0。 当固件中有 PPM 通知时,S0-Idle 中的客户端驱动程序应重新进入电源状态,因为在 S0-Idle 中,仍会启用 PPM 通知。
开始之前
- 根据硬件或固件是否实现 PD 状态机和传输,确定需要写入的驱动程序类型。
- 安装 Windows 10 桌面版(家庭版、专业版、企业版和教育版)。
- 在开发计算机上安装最新的 Windows 驱动程序工具包(WDK)。 该工具包具有用于编写客户端驱动程序所需的头文件和库,具体而言,需要:库(UcmUcsiCxStub.lib)。 该库转换客户端驱动程序发出的调用,并将其传递给类扩展;头文件 Ucmucsicx.h。
客户端驱动程序在内核模式下运行,并绑定到 KMDF 1.27 库。
1.向 UcmUcsiCx 注册客户端驱动程序
在EVT_WDF_DRIVER_DEVICE_ADD实现中。
- 设置即插即用和电源管理事件回调函数(WdfDeviceInitSetPnpPowerEventCallbacks)后,调用 UcmUcsiDeviceInitInitInitialize 初始化WDFDEVICE_INIT不透明结构。 调用将客户端驱动程序与框架相关联;
- 创建框架设备对象(WDFDEVICE)后,调用 UcmUcsiDeviceInitialize 以向 UcmUcsiCx 注册客户端 diver;
2. 使用 UcmUcsiCx 创建 PPM 对象
在EVT_WDF_DEVICE_PREPARE_HARDWARE的实现中,收到原始和已翻译的资源列表后,请使用资源来准备硬件。 例如,如果传输为 I2C,请读取硬件资源以打开信道。 接下来,创建 PPM 对象。 若要创建对象,需要设置某些配置选项。
2.1. 为设备上的连接器集合提供句柄。
通过调用 UcmUcsi连接orCollectionCreate 创建连接器集合;通过调用 UcmUcsi连接orCollectionAdd连接or 枚举设备上的连接器并将其添加到集合:
// Create the connector collection.
UCMUCSI_CONNECTOR_COLLECTION* ConnectorCollectionHandle;
status = UcmUcsiConnectorCollectionCreate(Device, //WDFDevice
WDF_NO_OBJECT_ATTRIBUTES,
ConnectorCollectionHandle);
// Enumerate the connectors on the device.
// ConnectorId of 0 is reserved for the parent device.
// In this example, we assume the parent has no children connectors.
UCMUCSI_CONNECTOR_INFO_INIT(&connectorInfo);
connectorInfo.ConnectorId = 0;
status = UcmUcsiConnectorCollectionAddConnector ( &ConnectorCollectionHandle,
&connectorInfo);
2.2. 确定是否要启用设备控制器。
2.3. 配置并创建 PPM 对象。
通过提供在步骤 1 中创建的连接器句柄来初始化UCMUCSI_PPM_CONFIG结构。将 UsbDeviceControllerEnabled 成员设置为步骤 2 中确定的布尔值。在WDF_OBJECT_ATTRIBUTES中设置事件回调。通过传递所有配置的结构来调用 UcmUcsiPpmCreate:
UCMUCSIPPM ppmObject = WDF_NO_HANDLE;
PUCMUCSI_PPM_CONFIG UcsiPpmConfig;
WDF_OBJECT_ATTRIBUTES attrib;
UCMUCSI_PPM_CONFIG_INIT(UcsiPpmConfig, ConnectorCollectionHandle);
UcsiPpmConfig->UsbDeviceControllerEnabled = TRUE;
WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&attrib, Ppm);
attrib->EvtDestroyCallback = &EvtObjectContextDestroy;
status = UcmUcsiPpmCreate(wdfDevice, UcsiPpmConfig, &attrib, &ppmObject);
3. 设置 IO 队列
UcmUcsiCx 将 UCSI 命令发送到客户端驱动程序以发送到 PPM 固件。 这些命令以 WDF 队列中的这些 IOCTL 请求的形式发送。
- IOCTL_UCMUCSI_PPM_标准版ND_UCSI_DATA_BLOCK
- IOCTL_UCMUCSI_PPM_GET_UCSI_DATA_BLOCK
客户端驱动程序负责通过调用 UcmUcsiPpmSetUcsiCommandRequestQueue 来创建和注册该队列到 UcmUcsiCx。 队列必须进行电源管理。
UcmUcsiCx 保证 WDF 队列中最多可以有一个未完成的请求。 客户端驱动程序还负责在将 UCSI 命令发送到固件后完成 WDF 请求。
通常,驱动程序在实现EVT_WDF_DEVICE_PREPARE_HARDWARE时设置队列。
WDFQUEUE UcsiCommandRequestQueue = WDF_NO_HANDLE;
WDF_OBJECT_ATTRIBUTES attrib;
WDF_IO_QUEUE_CONFIG queueConfig;
WDF_OBJECT_ATTRIBUTES_INIT(&attrib);
attrib.ParentObject = GetObjectHandle();
// In this example, even though the driver creates a sequential queue,
// UcmUcsiCx guarantees that will not send another request
// until the previous one has been completed.
WDF_IO_QUEUE_CONFIG_INIT(&queueConfig, WdfIoQueueDispatchSequential);
// The queue must be power-managed.
queueConfig.PowerManaged = WdfTrue;
queueConfig.EvtIoDeviceControl = EvtIoDeviceControl;
status = WdfIoQueueCreate(device, &queueConfig, &attrib, &UcsiCommandRequestQueue);
UcmUcsiPpmSetUcsiCommandRequestQueue(ppmObject, UcsiCommandRequestQueue);
此外,客户端驱动程序还必须调用 UcmUcsiPpmStart 来通知 UcmUcsiCx 驱动程序已准备好接收 IOCTL 请求。 建议在创建用于接收 UCSI 命令的 WDFQUEUE 句柄后,在 EVT_WDF_DEVICE_PREPARE_HARDWARE 中发出该调用,通过 UcmUcsiPpmSetUcsiCommandRequestQueue。 相反,当驱动程序不想再处理任何请求时,它必须调用 UcmUcsiPpmStop。 执行此操作在EVT_WDF_DEVICE_RELEA标准版_HARDWARE实现中。
4.处理 IOCTL 请求
请考虑将 USB Type C 合作伙伴附加到连接器时发生的事件的此示例序列。
- PPM 固件确定附加事件并向客户端驱动程序发送通知;
- 客户端驱动程序调用 UcmUcsiPpmNotification 将通知发送到 UcmUcsiCx;
- UcmUcsiCx 通知 OPM 状态机,并将 Get 连接or Status 命令发送到 UcmUcsiCx;
- UcmUcsiCx 创建一个请求,并将IOCTL_UCMUCSI_PPM_标准版ND_UCSI_DATA_BLOCK发送到客户端驱动程序;
- 客户端驱动程序处理请求命令并将其发送到 PPM 固件。 驱动程序以异步方式完成此请求,并将另一个通知发送到 UcmUcsiCx;
- 成功发出命令完成通知后,OPM 状态机将读取有效负载(包含连接器状态信息),并通知 UCM Type C 附加事件;
在此示例中,有效负载还指示固件与端口合作伙伴之间的电源交付协商状态更改已成功。 OPM 状态机发送另一个 UCSI 命令:获取 PDO。 与 Get 连接or Status 命令类似,当 Get PDO 命令成功完成时,OPM 状态机会通知 UCM 此事件。
EVT_WDF_IO_QUEUE_IO_DEVICE_CONTROL的客户端驱动程序处理程序类似于此示例代码。
void EvtIoDeviceControl(
_In_ WDFREQUEST Request,
_In_ ULONG IoControlCode
)
{
...
switch (IoControlCode)
{
case IOCTL_UCMUCSI_PPM_SEND_UCSI_DATA_BLOCK:
EvtSendData(Request);
break;
case IOCTL_UCMUCSI_PPM_GET_UCSI_DATA_BLOCK:
EvtReceiveData(Request);
break;
default:
status = STATUS_NOT_SUPPORTED;
goto Exit;
}
status = STATUS_SUCCESS;
Exit:
if (!NT_SUCCESS(status))
{
WdfRequestComplete(Request, status);
}
}
VOID EvtSendData(
WDFREQUEST Request
)
{
NTSTATUS status;
PUCMUCSI_PPM_SEND_UCSI_DATA_BLOCK_IN_PARAMS inParams;
status = WdfRequestRetrieveInputBuffer(Request, sizeof(*inParams),
reinterpret_cast<PVOID*>(&inParams), nullptr);
if (!NT_SUCCESS(status))
{
goto Exit;
}
// Build a UCSI command request and send to the PPM firmware.
Exit:
WdfRequestComplete(Request, status);
}
VOID EvtReceiveData(
WDFREQUEST Request
)
{
NTSTATUS status;
PUCMUCSI_PPM_GET_UCSI_DATA_BLOCK_IN_PARAMS inParams;
PUCMUCSI_PPM_GET_UCSI_DATA_BLOCK_OUT_PARAMS outParams;
status = WdfRequestRetrieveInputBuffer(Request, sizeof(*inParams),
reinterpret_cast<PVOID*>(&inParams), nullptr);
if (!NT_SUCCESS(status))
{
goto Exit;
}
status = WdfRequestRetrieveOutputBuffer(Request, sizeof(*outParams),
reinterpret_cast<PVOID*>(&outParams), nullptr);
if (!NT_SUCCESS(status))
{
goto Exit;
}
// Receive data from the PPM firmware.
if (!NT_SUCCESS(status))
{
goto Exit;
}
WdfRequestSetInformation(Request, sizeof(*outParams));
Exit:
WdfRequestComplete(Request, status);
}