SerCx 或 SerCx2 托管串行端口的设备接口发布

从 Windows 10 版本 1903 及更高版本开始,SerCx 和 SerCx2 包括对发布GUID_DEVINTERFACE_COMPORT设备接口的支持。 系统上的应用程序和服务可使用此设备接口与串行端口进行交互。

如果 UART 作为物理端口公开,或者如果常规应用程序(UWP 或 Win32)需要与连接到 UART 的设备直接通信,则可以在基于 SoC 的平台上启用此功能,这些平台具有集成的 UART 和 SerCx/SerCx2 客户端驱动程序。 这与通过连接 ID 访问 SerCx/SerCx2 控制器不同,连接 ID 只能通过专用外围驱动程序来访问 UART。

当 SerCx/SerCx2 托管串行端口使用该功能时,不会为这些设备分配 COM 端口号,也不会创建符号链接,这意味着应用程序必须使用本文档中描述的方法将串行端口作为设备接口打开。

建议使用设备接口 (GUID_DEVINTERFACE_COMPORT) 来发现和访问 COM 端口。 使用旧版 COM 端口名称容易造成名称冲突,并且不会向客户端提供状态更改通知。 不建议使用旧版 COM 端口名称,SerCx2 和 SerCx 也不支持这种名称。

启用设备接口创建

以下是启用设备接口创建的说明。 请注意,串行端口是独占的,这意味着如果串行端口可作为设备接口访问,则 ACPI 中的连接资源不应提供给任何其他设备,例如,不应向系统中的任何其他设备提供 UARTSerialBusV2 资源;应通过设备接口使端口可独占访问。

ACPI 配置

系统制造商或集成商可通过修改现有 SerCx/SerCx2 设备的 ACPI (ASL) 定义来启用此行为,为 UUID daffd814-6eba-4d8c-8a91-bc9bbf4aa301 的键值设备属性添加一个 _DSD 定义。 在此定义中,属性 SerCx-FriendlyName 被定义为串行端口的系统特定描述,例如 UART0UART1 等。

示例设备定义(不包括定义设备所需的供应商特定信息):

    Device(URT0) {
        Name(_HID, ...)
        Name(_CID, ...)

        Name(_DSD, Package() {
            ToUUID("daffd814-6eba-4d8c-8a91-bc9bbf4aa301"),
            Package() {
                Package(2) {"SerCx-FriendlyName", "UART0"}
            }
        })
    }

必须使用指定的 UUID (daffd814-6eba-4d8c-8a91-bc9bbf4aa301),并且必须为 SerCx/SerCx2 定义条目 SerCx-FriendlyName 才能创建设备接口。

注册表项

出于开发目的,SerCxFriendlyName 也可以配置为注册表中设备硬件键的属性。 CM_Open_DevNode_Key 方法可用于访问设备的硬件密钥,并为设备添加属性 SerCxFriendlyName,SerCx/SerCx2 将使用该属性来检索设备接口的友好名称。

不建议通过扩展 INF 设置该密钥,它主要用于测试和开发目的。 建议的方法是通过 ACPI 启用该功能,如上文所述。

设备接口

如果使用上述方法定义 a,SerCxFriendlyName/SerCx2 将为控制器发布GUID_DEVINTERFACE_COMPORT设备接口。 此设备接口的 DEVPKEY_DeviceInterface_Serial_PortName 属性将设置为指定的友好名称,应用程序可使用该名称来查找特定的控制器/端口。

启用非特权访问

默认情况下,只有特权用户和应用程序才能访问控制器/端口。 如果需要从未获授权的应用程序访问,SerCx/SerCx2 客户端必须在调用 SerCx2InitializeDeviceInit()SerCxDeviceInitConfig() 之后,但在调用 SerCx2InitializeDevice()SerCxInitialize() 之前覆盖默认安全描述符,此时应用的安全描述符将传播到控制器 PDO。

下面举例说明如何在 SerCx2 客户控制器驱动程序 EvtDeviceAdd 中启用 SerCx2 的非特权访问。

SampleControllerEvtDeviceAdd(
    WDFDRIVER WdfDriver,
    WDFDEVICE_INIT WdfDeviceInit
)
{
    ...

    NTSTATUS status = SerCx2InitializeDeviceInit(WdfDeviceInit);
    if (!NT_SUCCESS(status)) {
        ...
    }

    // Declare a security descriptor allowing access to all
    DECLARE_CONST_UNICODE_STRING(
        SDDL_DEVOBJ_SERCX_SYS_ALL_ADM_ALL_UMDF_ALL_USERS_RDWR,
        L"D:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;GA;;;UD)(A;;GRGW;;;BU)");

    // Assign it to the device, overwriting the default SerCx2 security descriptor
    status = WdfDeviceInitAssignSDDLString(
                WdfDeviceInit,
                &SDDL_DEVOBJ_SERCX_SYS_ALL_ADM_ALL_UMDF_ALL_USERS_RDWR);

    if (!NT_SUCCESS(status)) {
        ...
    }

    ...
}

使用设备接口时的行为变化

选择使用此功能会导致 SerCx/SerCx2 中出现以下行为变化(与通过连接 ID 访问 SerCx/SerCx2 控制器相反):

  • 端口不会应用默认配置(速度、奇偶校验等)。 由于 ACPI 中没有描述这种情况的连接资源,因此端口开始时处于未初始化状态。 与设备接口交互的软件必须使用定义的串行 IOCTL 接口配置端口。

  • 从 SerCx/SerCx2 客户端驱动程序调用查询或应用默认配置将返回失败状态。 此外,由于没有指定要应用的默认配置,向设备接口发出的 IOCTL_SERIAL_APPLY_DEFAULT_CONFIGURATION 请求也将失败。

访问串行端口设备接口

对于 UWP 应用程序,可以使用 Windows.Devices.SerialCommunication 命名空间 API 访问发布的接口,就像访问其他合规的串行端口一样。

对于 Win32 应用程序,设备接口通过以下流程定位和访问:

  1. 应用程序调用 CM_Get_Device_Interface_ListW 获取系统中所有 GUID_DEVINTERFACE_COMPORT 类设备接口的列表
  2. 应用程序为每个返回的接口调用 CM_Get_Device_Interface_PropertyW,以查询所发现的每个接口的 DEVPKEY_DeviceInterface_Serial_PortName
  3. 通过名称找到所需的端口后,应用程序使用 (1) 中返回的符号链接字符串,以便通过 CreateFile() 打开端口句柄

此流的代码示例:

#include <windows.h>
#include <cfgmgr32.h>
#include <initguid.h>
#include <devpropdef.h>
#include <devpkey.h>
#include <ntddser.h>

...

DWORD ret;
ULONG deviceInterfaceListBufferLength;

//
// Determine the size (in characters) of buffer required for to fetch a list of
// all GUID_DEVINTERFACE_COMPORT device interfaces present on the system.
//
ret = CM_Get_Device_Interface_List_SizeW(
        &deviceInterfaceListBufferLength,
        (LPGUID) &GUID_DEVINTERFACE_COMPORT,
        NULL,
        CM_GET_DEVICE_INTERFACE_LIST_PRESENT);
if (ret != CR_SUCCESS) {
    // Handle error
    ...
}

//
// Allocate buffer of the determined size.
//
PWCHAR deviceInterfaceListBuffer = (PWCHAR) malloc(deviceInterfaceListBufferLength * sizeof(WCHAR));
if (deviceInterfaceListBuffer == NULL) {
    // Handle error
    ...
}

//
// Fetch the list of all GUID_DEVINTERFACE_COMPORT device interfaces present
// on the system.
//
ret = CM_Get_Device_Interface_ListW(
        (LPGUID) &GUID_DEVINTERFACE_COMPORT,
        NULL,
        deviceInterfaceListBuffer,
        deviceInterfaceListBufferLength,
        CM_GET_DEVICE_INTERFACE_LIST_PRESENT);
if (ret != CR_SUCCESS) {
    // Handle error
    ...
}

//
// Iterate through the list, examining one interface at a time
//
PWCHAR currentInterface = deviceInterfaceListBuffer;
while (*currentInterface) {
    //
    // Fetch the DEVPKEY_DeviceInterface_Serial_PortName for this interface
    //
    CONFIGRET configRet;
    DEVPROPTYPE devPropType;
    PWCHAR devPropBuffer;
    ULONG devPropSize = 0;

    // First, get the size of buffer required
    configRet = CM_Get_Device_Interface_PropertyW(
        currentInterface,
        &DEVPKEY_DeviceInterface_Serial_PortName,
        &devPropType,
        NULL,
        &devPropSize,
        0);
    if (configRet != CR_BUFFER_SMALL) {
        // Handle error
        ...
    }

    // Allocate the buffer
    devPropBuffer = malloc(devPropSize);
    if (devPropBuffer == NULL) {
        // Handle error
        free(devPropBuffer);
        ...
    }

    configRet = CM_Get_Device_Interface_PropertyW(
        currentInterface,
        &DEVPKEY_DeviceInterface_Serial_PortName,
        &devPropType,
        (PBYTE) devPropBuffer,
        &devPropSize,
        0);
    if (configRet != CR_SUCCESS) {
        // Handle error
        free(devPropBuffer);
        ...
    }

    // Verify the value is the correct type and size
    if ((devPropType != DEVPROP_TYPE_STRING) ||
        (devPropSize < sizeof(WCHAR))) {
        // Handle error
        free(devPropBuffer);
        ...
    }

    // Now, check if the interface is the one we are interested in
    if (wcscmp(devPropBuffer, L"UART0") == 0) {
        free(devPropBuffer);
        break;
    }

    // Advance to the next string (past the terminating NULL)
    currentInterface += wcslen(currentInterface) + 1;
    free(devPropBuffer);
}

//
// currentInterface now either points to NULL (there was no match and we iterated
// over all interfaces without a match) - or, it points to the interface with
// the friendly name UART0, in which case we can open it.
//
if (*currentInterface == L'\0') {
    // Handle interface not found error
    ...
}

//
// Now open the device interface as we would a COMx style serial port.
//
HANDLE portHandle = CreateFileW(
                        currentInterface,
                        GENERIC_READ | GENERIC_WRITE,
                        0,
                        NULL,
                        OPEN_EXISTING,
                        0,
                        NULL);
if (portHandle == INVALID_HANDLE_VALUE) {
    // Handle error
    ...
}

free(deviceInterfaceListBuffer);
deviceInterfaceListBuffer = NULL;
currentInterface = NULL;

//
// We are now able to send IO requests to the device.
//
... = ReadFile(portHandle, ..., ..., ..., NULL);

请注意,应用程序还可以订阅设备接口到达和设备移除的通知,以便在设备可用或不可用时打开或关闭控制器/端口的句柄。