编写基于 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. 在“新建Project”对话框中的顶部搜索框中,键入 USB。

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

  3. 选择“下一页”。

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

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

    winusb template new project creation first screen.

    winusb template new project creation second screen.

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

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

    winusb template solution explorer 1.

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

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

    winusb template creation second project addition.

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

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

  8. 选择“下一页”。

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

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

    winusb template second project creation first screen.

    winusb template second project creation second screen.

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

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

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

    winusb template solution explorer 2.

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

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

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

Screenshot that shows the

生成、部署和调试项目

到目前为止,在本练习中,你已使用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 template deployment.

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

      winusb template remote debugger.

    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 template debug setting.

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

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

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

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

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

      winusb application template.

      注意 包中不包含驱动程序文件。 这是因为 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 包项目) ,然后选择“卸载Project”。

    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 用于枚举设备信息集中的设备接口并获取有关设备接口的信息。

    此调用需要以下项:

    • 初始化的调用方分配 SP_DEVICE_INTERFACE_DATA 结构,其 cbSize 成员设置为结构的大小。
    • 步骤 1 中的 HDEVINFO 句柄。
    • 在 INF 文件中定义的设备接口 GUID。

    SetupDiEnumDeviceInterfaces 查找设备接口的指定索引的设备信息集数组,并使用有关接口的基本数据填充初始化 的SP_DEVICE_INTERFACE_DATA 结构。

    注意 若要枚举设备信息集中的所有设备接口,请在循环中调用 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 Functions 使用此句柄来标识目标设备,而不是文件句柄。 若要获取 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;
}

后续步骤

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

USB 设备的 Windows 桌面应用
为驱动程序部署和测试预配计算机