编写基于 WinUSB 模板的 Windows 桌面应用

编写与 USB 设备通信的 Windows 桌面应用的最简单方法是使用 C/C++ WinUSB 模板。 对于此模板,你需要将 Windows 驱动程序工具包 (WDK) (与 Windows) 调试工具和 Microsoft Visual Studio (Professional 或 Ultimate) 集成的环境。 可以将模板用作起点。

准备工作

  • 若要设置集成开发环境,请先安装 Microsoft Visual Studio Ultimate 2019 或 Microsoft Visual Studio Professional 2019,然后安装 WDK。 可以在 WDK 下载页上找到有关如何设置 Visual Studio 和 WDK 的信息
  • 安装 WDK 时,将包含适用于 Windows 的调试工具。 有关详细信息,请参阅 下载和安装 Windows 调试工具

创建 WinUSB 应用程序

若要从模板创建应用程序,请执行以下操作:

  1. 在“ 新建项目 ”对话框顶部的搜索框中,键入 “USB”。

  2. 在中间窗格中,选择“ WinUSB 应用程序 (通用)

  3. 选择“下一页”。

  4. 输入项目名称,选择保存位置,然后选择“ 创建”。

    以下屏幕截图显示了 WinUSB 应用程序 (通用) 模板的新建项目”对话框。

    winusb 模板新建项目创建第一屏幕。

    winusb 模板新建项目创建第二屏幕。

    本主题假定 Visual Studio 项目的名称为 USB Application1

    Visual Studio 将创建一个项目和一个解决方案。 可以在“解决方案资源管理器”窗口中查看解决方案、项目和属于该项目的文件,如以下屏幕截图所示。 (如果解决方案资源管理器窗口不可见,请从“视图”菜单中选择“解决方案资源管理器”。) 解决方案包含名为 USB Application1 的 C++ 应用程序项目。

    winusb 模板解决方案资源管理器 1.

    USB Application1 项目具有应用程序的源文件。 如果想要查看应用程序源代码,可以打开“源文件”下显示的任何 文件

  5. 将驱动程序包项目添加到解决方案。 选择并按住 (或右键单击) 解决方案 (解决方案“USB Application1”) ,然后选择“ 添加新>项目 ”,如以下屏幕截图所示。

    winusb 模板创建第二个项目添加。

  6. 在“ 新建项目 ”对话框的顶部搜索框中,再次键入 “USB”。

  7. 在中间窗格中,选择“ WinUSB INF 驱动程序包”。

  8. 选择“下一页”。

  9. 输入项目名称,然后选择“ 创建”。

    以下屏幕截图显示了 WinUSB INF 驱动程序包模板的“新建项目”对话框。

    winusb 模板第二个项目创建第一屏幕。

    winusb 模板第二个项目创建第二屏幕。

    本主题假定 Visual Studio 项目的名称为 USB Application1 包

    USB Application1 包项目包含一个 INF 文件,该文件用于安装 Microsoft 提供的 Winusb.sys 驱动程序作为设备驱动程序。

    解决方案资源管理器现在应同时包含这两个项目,如以下屏幕截图所示。

    winusb 模板解决方案资源管理器 2.

  10. 在 INF 文件 USBApplication1.inf 中,找到以下代码: %DeviceName% =USB_Install, USB\VID_vvvv&PID_pppp

  11. 将 VID_vvvv&PID_pppp 替换为设备的硬件 ID。 从设备管理器获取硬件 ID。 在设备管理器中,查看设备属性。 在“ 详细信息 ”选项卡上,查看 “硬件 ID” 属性值。

  12. 解决方案资源管理器窗口中,选择并按住 (,或右键单击) 解决方案“USB 应用程序1” (两个项目中的 2 个) ,然后选择Configuration Manager。 为应用程序项目和包项目选择配置和平台。 在本练习中,我们选择“调试”和“x64”,如以下屏幕截图所示。

显示“Configuration Manager”窗口的屏幕截图,其中选择了“调试”和“x64”。

生成、部署和调试项目

到目前为止,在本练习中,你已使用 Visual Studio 创建项目。 接下来,需要配置设备连接到的设备。 该模板要求将 Winusb 驱动程序安装为设备的驱动程序。

测试和调试环境可以具有以下功能:

  • 两台计算机设置:主计算机和目标计算机。 在主计算机上的 Visual Studio 中开发和生成项目。 调试程序在主计算机上运行,并在 Visual Studio 用户界面中可用。 测试和调试应用程序时,驱动程序将在目标计算机上运行。

  • 单台计算机设置:目标和主机在一台计算机上运行。 在 Visual Studio 中开发和生成项目,并运行调试器和应用程序。

可以按照以下步骤部署、安装、加载和调试应用程序和驱动程序:

  • 两台计算机设置

    1. 按照预配 用于驱动程序部署和测试的计算机中的说明预配目标计算机。 注意: 预配会在目标计算机上创建名为 WDKRemoteUser 的用户。 预配完成后,你将看到用户切换到 WDKRemoteUser。
    2. 在主计算机上,在 Visual Studio 中打开你的解决方案。
    3. 在 main.cpp 中,在 OpenDevice 调用之前添加此行。
    system ("pause")
    

    行会导致应用程序在启动时暂停。 这在远程调试中很有用。

    1. 在 pch.h 中,包括以下行:
    #include <cstdlib>
    

    上一步中的调用需要 system() 此 include 语句。

    1. “解决方案资源管理器”窗口中,选择并按住 (或右键单击“) USB Application1 包”,然后选择“属性”。

    2. “USB Application1 包属性页” 窗口的左窗格中,导航到 “配置属性驱动程序 > 安装 > 部署”,如以下屏幕截图所示。

    3. 选中“部署前删除以前的驱动程序版本” 。

    4. 对于远程计算机名,请选择配置用于测试和调试的计算机名。 在本练习中,我们使用名为 dbg-target 的计算机。

    5. 选择 “安装/重新安装并验证”。 选择“应用”。

      winusb 模板部署。

    6. 在属性页中,导航到“配置属性调试”>,然后选择“Windows 调试工具 - 远程调试器”,如以下屏幕截图所示。

      winusb 模板远程调试器。

    7. 从“生成”菜单中选择“生成解决方案”。 Visual Studio 在 “输出 ”窗口中显示生成进度。 (如果“输出”窗口不可见,请从“视图”菜单中选择“输出”。) 在本练习中,我们为运行Windows 10的 x64 系统生成了项目。

    8. 从“生成”菜单中选择“部署解决方案”。

在目标计算机上,你将看到驱动程序安装脚本正在运行。 驱动程序文件将复制到目标计算机上的 %Systemdrive%\drivertest\drivers 文件夹。 请确认 .inf、.cat、测试证书和 .sys 文件以及其他任何必要的文件均位于 %systemdrive%\drivertest\drivers 文件夹下。 设备必须显示在设备管理器中且不会出现错误。

在主计算机上,你将在 “输出 ”窗口中看到此消息。

Deploying driver files for project
"<path>\visual studio 14\Projects\USB Application1\USB Application1 Package\USB Application1 Package.vcxproj".
Deployment may take a few minutes...
========== Build: 1 succeeded, 0 failed, 1 up-to-date, 0 skipped ==========

若要调试该应用程序

  1. 在主计算机上,导航到解决方案文件夹中的 x64 > Win8.1Debug

  2. 将应用程序可执行文件 UsbApplication1.exe 复制到目标计算机。

  3. 在目标计算机上启动应用程序。

  4. 在主计算机上的 “调试 ”菜单中,选择“ 附加到进程”。

  5. 在窗口中,选择“ Windows 用户模式调试器 ” (“Windows) 调试工具”作为传输,并将目标计算机的名称(在本例中为 dbg-target)作为限定符,如下图所示。

    winusb 模板调试设置。

  6. 从“ 可用进程 ”列表中选择应用程序,然后选择“ 附加”。 现在可以使用 即时窗口 或使用“调试”菜单中的选项 进行调试

上述说明使用 适用于 Windows 的调试工具 - 远程调试器调试应用程序。 如果要使用 Visual Studio) 附带的调试 器 (远程 Windows 调试器 ,请按照以下说明操作:

  1. 在目标计算机上,将 msvsmon.exe 添加到允许通过防火墙的应用列表。
  2. 启动位于 C:\DriverTest\msvsmon\msvsmon.exe 中的 Visual Studio 远程调试监视器。
  3. 创建工作文件夹,例如 C:\remotetemp。
  4. 将应用程序可执行文件 UsbApplication1.exe 复制到目标计算机上的工作文件夹。
  5. 在主计算机上的 Visual Studio 中,右键单击 “USB Application1 包 ”项目,然后选择“ 卸载项目”。
  6. 选择并按住 (或右键单击) USB Application1 项目,在项目属性中展开 “配置属性” 节点,然后选择“ 调试”。
  7. 调试器更改为启动远程 Windows 调试器
  8. 按照 本地生成的项目的远程调试中提供的说明,更改项目设置以在远程计算机上运行可执行文件。 确保 工作目录远程命令 属性反映目标计算机上的文件夹。
  9. 若要调试应用程序,请在“ 生成 ”菜单中选择“ 开始调试”,或按 F5。
  • 单台计算机设置:

    1. 若要生成应用程序和驱动程序安装包,请从“生成”菜单中选择“生成解决方案”。 Visual Studio 在 “输出 ”窗口中显示生成进度。 (如果“输出”窗口不可见,请从“视图”菜单中选择“输出”。) 在本练习中,我们已为运行Windows 10的 x64 系统生成项目。

    2. 若要查看生成的驱动程序包,请在 Windows 资源管理器中导航到 USB Application1 文件夹,然后导航到 x64 > 调试 > USB Application1 包。 驱动程序包包含多个文件:MyDriver.inf 是 Windows 在安装驱动程序时使用的信息文件,mydriver.cat 是安装程序用于验证驱动程序包的测试签名的目录文件。 以下屏幕截图中显示了这些文件。

      winusb 应用程序模板。

      包中不包含驱动程序文件。 这是因为 INF 文件引用 Windows\System32 文件夹中的内置驱动程序 Winusb.sys。

    3. 手动安装驱动程序。 在 设备管理器 中,通过在包中指定 INF 来更新驱动程序。 指向解决方案文件夹中的驱动程序包,如上一部分所示。 如果看到错误 DriverVer set to a date in the future,请设置 INF 包项目设置 > Inf2Cat > 常规 > 使用本地时间 > 是

    4. 选择并按住 (或右键单击) USB Application1 项目,在项目属性中展开 “配置属性” 节点,然后选择“ 调试”。

    5. 调试器更改为启动本地 Windows 调试器

    6. 选择并按住 (或右键单击) USB Application1 包项目,然后选择 “卸载项目”。

    7. 若要调试应用程序,请在“ 生成 ”菜单中选择“ 开始调试”,或按 F5

模板代码讨论

模板是桌面应用程序的起点。 USB Application1 项目具有源文件 device.cpp 和 main.cpp。

main.cpp 文件包含应用程序入口点(_tmain)。 device.cpp 包含打开和关闭设备句柄的所有帮助程序函数。

该模板还有一个名为 device.h 的头文件。 此文件包含稍后) 讨论的设备接口 GUID (定义,以及存储应用程序获取的信息的DEVICE_DATA结构。 例如,它存储由 OpenDevice 获取并用于后续操作的 WinUSB 接口句柄。

typedef struct _DEVICE_DATA {

    BOOL                    HandlesOpen;
    WINUSB_INTERFACE_HANDLE WinusbHandle;
    HANDLE                  DeviceHandle;
    TCHAR                   DevicePath[MAX_PATH];

} DEVICE_DATA, *PDEVICE_DATA;

获取设备的实例路径 - 请参阅 device.cpp 中的 RetrieveDevicePath

为了访问 USB 设备,应用程序通过调用 CreateFile 为设备创建有效的文件句柄。 对于该调用,应用程序必须获取设备路径实例。 为了获取设备路径,应用使用 SetupAPI 例程并在用于安装 Winusb.sys 的 INF 文件中指定设备接口 GUID。 Device.h 声明名为 GUID_DEVINTERFACE_USBApplication1 的 GUID 常量。 通过使用这些例程,应用程序枚举指定设备接口类中的所有设备,并检索设备的设备路径。

HRESULT
RetrieveDevicePath(
    _Out_bytecap_(BufLen) LPTSTR DevicePath,
    _In_                  ULONG  BufLen,
    _Out_opt_             PBOOL  FailureDeviceNotFound
    )
/*++

Routine description:

    Retrieve the device path that can be used to open the WinUSB-based device.

    If multiple devices have the same device interface GUID, there is no
    guarantee of which one will be returned.

Arguments:

    DevicePath - On successful return, the path of the device (use with CreateFile).

    BufLen - The size of DevicePath's buffer, in bytes

    FailureDeviceNotFound - TRUE when failure is returned due to no devices
        found with the correct device interface (device not connected, driver
        not installed, or device is disabled in Device Manager); FALSE
        otherwise.

Return value:

    HRESULT

--*/
{
    BOOL                             bResult = FALSE;
    HDEVINFO                         deviceInfo;
    SP_DEVICE_INTERFACE_DATA         interfaceData;
    PSP_DEVICE_INTERFACE_DETAIL_DATA detailData = NULL;
    ULONG                            length;
    ULONG                            requiredLength=0;
    HRESULT                          hr;

    if (NULL != FailureDeviceNotFound) {

        *FailureDeviceNotFound = FALSE;
    }

    //
    // Enumerate all devices exposing the interface
    //
    deviceInfo = SetupDiGetClassDevs(&GUID_DEVINTERFACE_USBApplication1,
                                     NULL,
                                     NULL,
                                     DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);

    if (deviceInfo == INVALID_HANDLE_VALUE) {

        hr = HRESULT_FROM_WIN32(GetLastError());
        return hr;
    }

    interfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);

    //
    // Get the first interface (index 0) in the result set
    //
    bResult = SetupDiEnumDeviceInterfaces(deviceInfo,
                                          NULL,
                                          &GUID_DEVINTERFACE_USBApplication1,
                                          0,
                                          &interfaceData);

    if (FALSE == bResult) {

        //
        // We would see this error if no devices were found
        //
        if (ERROR_NO_MORE_ITEMS == GetLastError() &&
            NULL != FailureDeviceNotFound) {

            *FailureDeviceNotFound = TRUE;
        }

        hr = HRESULT_FROM_WIN32(GetLastError());
        SetupDiDestroyDeviceInfoList(deviceInfo);
        return hr;
    }

    //
    // Get the size of the path string
    // We expect to get a failure with insufficient buffer
    //
    bResult = SetupDiGetDeviceInterfaceDetail(deviceInfo,
                                              &interfaceData,
                                              NULL,
                                              0,
                                              &requiredLength,
                                              NULL);

    if (FALSE == bResult && ERROR_INSUFFICIENT_BUFFER != GetLastError()) {

        hr = HRESULT_FROM_WIN32(GetLastError());
        SetupDiDestroyDeviceInfoList(deviceInfo);
        return hr;
    }

    //
    // Allocate temporary space for SetupDi structure
    //
    detailData = (PSP_DEVICE_INTERFACE_DETAIL_DATA)
        LocalAlloc(LMEM_FIXED, requiredLength);

    if (NULL == detailData)
    {
        hr = E_OUTOFMEMORY;
        SetupDiDestroyDeviceInfoList(deviceInfo);
        return hr;
    }

    detailData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
    length = requiredLength;

    //
    // Get the interface's path string
    //
    bResult = SetupDiGetDeviceInterfaceDetail(deviceInfo,
                                              &interfaceData,
                                              detailData,
                                              length,
                                              &requiredLength,
                                              NULL);

    if(FALSE == bResult)
    {
        hr = HRESULT_FROM_WIN32(GetLastError());
        LocalFree(detailData);
        SetupDiDestroyDeviceInfoList(deviceInfo);
        return hr;
    }

    //
    // Give path to the caller. SetupDiGetDeviceInterfaceDetail ensured
    // DevicePath is NULL-terminated.
    //
    hr = StringCbCopy(DevicePath,
                      BufLen,
                      detailData->DevicePath);

    LocalFree(detailData);
    SetupDiDestroyDeviceInfoList(deviceInfo);

    return hr;
}

在前面的函数中,应用程序通过调用以下例程获取设备路径:

  1. SetupDiGetClassDevs 用于获取 设备信息集的句柄,该数组包含与指定设备接口类匹配的所有已安装设备的信息,GUID_DEVINTERFACE_USBApplication1。 数组中称为 设备接口 的每个元素都对应于已安装并注册到系统的设备。 设备接口类是通过传递 INF 文件中定义的设备接口 GUID 来标识的。 函数将 HDEVINFO 句柄返回到设备信息集。

  2. SetupDiEnumDeviceInterfaces 用于枚举设备信息集中的设备接口,并获取有关设备接口的信息。

    此调用需要以下项:

    若要枚举设备信息集中的所有设备接口,请在循环中调用 SetupDiEnumDeviceInterfaces ,直到函数返回 FALSE 并且失败的错误代码ERROR_NO_MORE_ITEMS。 可以通过调用 GetLastError 检索ERROR_NO_MORE_ITEMS错误代码。 每次迭代时,都会递增成员索引。

    或者,可以在调用方分配的SP_DEVINFO_DATA结构中调用 SetupDiEnumDeviceInfo,该设置枚举设备信息集并返回有关由索引指定的设备接口元素的信息。 然后,可以在 SetupDiEnumDeviceInterfaces 函数的 DeviceInfoData 参数中传递对此结构的引用。

  3. SetupDiGetDeviceInterfaceDetail 以获取设备接口的详细数据。 信息以 SP_DEVICE_INTERFACE_DETAIL_DATA 结构返回。 由于 SP_DEVICE_INTERFACE_DETAIL_DATA 结构的大小各不相同, 因此将调用 SetupDiGetDeviceInterfaceDetail 两次。 第一次调用获取要为 SP_DEVICE_INTERFACE_DETAIL_DATA 结构分配的缓冲区大小。 第二次调用使用接口的详细信息填充分配的缓冲区。

    1. 调用 DeviceInterfaceDetailData 参数设置为 NULL 的 SetupDiGetDeviceInterfaceDetail 函数在 requiredlength 参数中返回正确的缓冲区大小。 此调用失败并显示ERROR_INSUFFICIENT_BUFFER错误代码。 此错误代码是预期代码。
    2. 根据在 requiredlength 参数中检索到的正确缓冲区大小为SP_DEVICE_INTERFACE_DETAIL_DATA结构分配内存。
    3. 再次调用 SetupDiGetDeviceInterfaceDetail ,并将引用传递给 DeviceInterfaceDetailData 参数中初始化的结构。 函数返回时,结构中会填充有关接口的详细信息。 设备路径位于 SP_DEVICE_INTERFACE_DETAIL_DATA 结构的 DevicePath 成员中

为设备创建文件句柄

请参阅 device.cpp 中的 OpenDevice。

若要与设备交互,需要将 WinUSB 接口句柄指向设备上的第一个 (默认) 接口。 模板代码获取文件句柄和 WinUSB 接口句柄,并将其存储在DEVICE_DATA结构中。

HRESULT
OpenDevice(
    _Out_     PDEVICE_DATA DeviceData,
    _Out_opt_ PBOOL        FailureDeviceNotFound
    )
/*++

Routine description:

    Open all needed handles to interact with the device.

    If the device has multiple USB interfaces, this function grants access to
    only the first interface.

    If multiple devices have the same device interface GUID, there is no
    guarantee of which one will be returned.

Arguments:

    DeviceData - Struct filled in by this function. The caller should use the
        WinusbHandle to interact with the device, and must pass the struct to
        CloseDevice when finished.

    FailureDeviceNotFound - TRUE when failure is returned due to no devices
        found with the correct device interface (device not connected, driver
        not installed, or device is disabled in Device Manager); FALSE
        otherwise.

Return value:

    HRESULT

--*/
{
    HRESULT hr = S_OK;
    BOOL    bResult;

    DeviceData->HandlesOpen = FALSE;

    hr = RetrieveDevicePath(DeviceData->DevicePath,
                            sizeof(DeviceData->DevicePath),
                            FailureDeviceNotFound);

    if (FAILED(hr)) {

        return hr;
    }

    DeviceData->DeviceHandle = CreateFile(DeviceData->DevicePath,
                                          GENERIC_WRITE | GENERIC_READ,
                                          FILE_SHARE_WRITE | FILE_SHARE_READ,
                                          NULL,
                                          OPEN_EXISTING,
                                          FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
                                          NULL);

    if (INVALID_HANDLE_VALUE == DeviceData->DeviceHandle) {

        hr = HRESULT_FROM_WIN32(GetLastError());
        return hr;
    }

    bResult = WinUsb_Initialize(DeviceData->DeviceHandle,
                                &DeviceData->WinusbHandle);

    if (FALSE == bResult) {

        hr = HRESULT_FROM_WIN32(GetLastError());
        CloseHandle(DeviceData->DeviceHandle);
        return hr;
    }

    DeviceData->HandlesOpen = TRUE;
    return hr;
}
  1. 应用调用 CreateFile ,通过指定之前检索到的设备路径,为设备创建文件句柄。 它使用 FILE_FLAG_OVERLAPPED 标志,因为 WinUSB 依赖于此设置。
  2. 通过使用设备的文件句柄,应用将创建 WinUSB 接口句柄。 WinUSB 函数 使用此句柄来标识目标设备,而不是文件句柄。 为了获取 WinUSB 接口句柄,应用通过传递文件句柄来调用 WinUsb_Initialize 。 在后续调用中使用收到的句柄从设备获取信息,并将 I/O 请求发送到设备。

释放设备句柄 - 请参阅 device.cpp 中的 CloseDevice

模板代码实现用于释放设备的文件句柄和 WinUSB 接口句柄的代码。

VOID
CloseDevice(
    _Inout_ PDEVICE_DATA DeviceData
    )
/*++

Routine description:

    Perform required cleanup when the device is no longer needed.

    If OpenDevice failed, do nothing.

Arguments:

    DeviceData - Struct filled in by OpenDevice

Return value:

    None

--*/
{
    if (FALSE == DeviceData->HandlesOpen) {

        //
        // Called on an uninitialized DeviceData
        //
        return;
    }

    WinUsb_Free(DeviceData->WinusbHandle);
    CloseHandle(DeviceData->DeviceHandle);
    DeviceData->HandlesOpen = FALSE;

    return;
}

后续步骤

接下来,阅读以下主题以发送获取设备信息并将数据传输发送到设备: