You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Windows系统下如何枚举USB集线器及级联端口连接的所有USB设备

问题根因

现有实现仅通过SetupDi*系列API枚举三类USB相关设备接口的顶层注册实例,没有解析USB总线的物理拓扑连接关系,既无法识别集线器端口下的挂载设备,也无法追溯多级级联链路。要实现全量USB设备枚举(含拓扑层级遍历),必须基于Windows USB用户态IOCTL控制接口,从主机控制器出发顺着拓扑链路逐层向下遍历。

需要补充的核心实现逻辑
  • 设备句柄打开逻辑
    拿到每个设备接口的设备路径后,调用CreateFileW打开设备操作句柄,调用时需指定FILE_SHARE_READ | FILE_SHARE_WRITE共享标志,创建方式选OPEN_EXISTING,无特殊操作需求时无需申请读写权限,避免因权限不足打开失败。所有打开的句柄在遍历完成后需及时调用CloseHandle释放,避免句柄泄漏。
  • 根集线器枚举逻辑
    对每个打开的USB主机控制器句柄,发送IOCTL_USB_GET_ROOT_HUB_NAME控制码,获取对应根集线器的设备路径,将根集线器加入待遍历的集线器队列。
  • 集线器端口递归遍历逻辑
    对队列中的每一个集线器设备句柄,执行以下操作:
    1. 发送IOCTL_USB_GET_NODE_INFORMATION控制码,获取集线器属性,包括总端口数量、集线器类型(根集线器/外部扩展集线器)
    2. 按序号遍历所有端口(注意端口序号从1开始,不是0),对每个端口发送IOCTL_USB_GET_NODE_CONNECTION_INFORMATION_EX控制码,查询端口连接状态:
      • 端口无设备连接、或设备处于未就绪状态时直接跳过
      • 若端口连接设备类型为集线器(判断设备描述符bDeviceClass字段值为0x09即USB集线器类),发送IOCTL_USB_GET_NODE_CONNECTION_NAME控制码获取该下级集线器的设备路径,打开句柄后加入遍历队列,递归执行端口遍历流程
      • 若端口连接普通终端USB设备,发送IOCTL_USB_GET_NODE_CONNECTION_DRIVERKEY_NAME获取设备驱动键,拼接为标准格式的设备路径,存入结果列表
  • 结果去重逻辑
    原有SetupDi枚举逻辑拿到的直连设备,和拓扑遍历过程中拿到的设备会存在重复,需要对存入m_connectedUSB的设备路径做去重校验,避免重复记录。
关键实现注意事项
  • 所有传递给DeviceIoControl的USB相关结构体必须提前按要求初始化字段,比如查询端口状态时必须先给USB_NODE_CONNECTION_INFORMATION_EX结构的ConnectionIndex字段赋值为当前遍历的端口号,否则IOCTL调用会直接返回失败。
  • 枚举主机控制器、根集线器等系统设备时需要管理员权限,建议程序内嵌manifest申请requireAdministrator权限等级,避免因权限不足无法打开设备句柄导致枚举不全。
  • 遍历过程中需要兼容设备热插拔场景,对单个端口查询失败、句柄打开失败的情况做异常处理,不要因为单个节点访问失败中断整个枚举流程。
  • 原代码中过滤设备路径的startsWith("\\\\?\\usb#vid")逻辑可以保留,但要注意部分USB主机控制器、根集线器的设备路径不符合该前缀规则,需要根据需求判断是否保留这类节点。
核心逻辑代码示例
#include <usbiodef.h>
#include <usbioctl.h>

// 递归遍历集线器端口的示例函数
void EnumHubPorts(HANDLE hHub, QStringList& deviceList)
{
    USB_NODE_INFORMATION hubInfo = {0};
    DWORD retBytes = 0;
    if (!DeviceIoControl(hHub, IOCTL_USB_GET_NODE_INFORMATION,
        &hubInfo, sizeof(hubInfo), &hubInfo, sizeof(hubInfo),
        &retBytes, nullptr))
        return;

    ULONG portTotal = hubInfo.HubInformation.HubDescriptor.bNumberOfPorts;
    for (ULONG portIdx = 1; portIdx <= portTotal; portIdx++)
    {
        USB_NODE_CONNECTION_INFORMATION_EX connInfo = {0};
        connInfo.ConnectionIndex = portIdx;
        if (!DeviceIoControl(hHub, IOCTL_USB_GET_NODE_CONNECTION_INFORMATION_EX,
            &connInfo, sizeof(connInfo), &connInfo, sizeof(connInfo),
            &retBytes, nullptr))
            continue;

        if (connInfo.ConnectionStatus != DeviceConnected)
            continue;

        // 判断是否为集线器设备
        if (connInfo.DeviceDescriptor.bDeviceClass == USB_DEVICE_CLASS_HUB)
        {
            // 获取下级集线器名称
            ULONG bufSize = sizeof(USB_NODE_CONNECTION_NAME) + MAX_PATH * sizeof(WCHAR);
            USB_NODE_CONNECTION_NAME* connName = (USB_NODE_CONNECTION_NAME*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, bufSize);
            connName->ConnectionIndex = portIdx;
            if (DeviceIoControl(hHub, IOCTL_USB_GET_NODE_CONNECTION_NAME,
                connName, bufSize, connName, bufSize, &retBytes, nullptr))
            {
                QString hubPath = QString("\\\\?\\%1").arg(QString::fromWCharArray(connName->NodeName));
                // 打开下级集线器句柄,递归遍历
                HANDLE hChildHub = CreateFileW(hubPath.toStdWString().c_str(), 0,
                    FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr);
                if (hChildHub != INVALID_HANDLE_VALUE)
                {
                    EnumHubPorts(hChildHub, deviceList);
                    CloseHandle(hChildHub);
                }
            }
            HeapFree(GetProcessHeap(), 0, connName);
        }
        else
        {
            // 普通终端设备,获取设备路径后加入列表
            ULONG keyBufSize = sizeof(USB_NODE_CONNECTION_DRIVERKEY_NAME) + MAX_PATH * sizeof(WCHAR);
            USB_NODE_CONNECTION_DRIVERKEY_NAME* keyName = (USB_NODE_CONNECTION_DRIVERKEY_NAME*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, keyBufSize);
            keyName->ConnectionIndex = portIdx;
            if (DeviceIoControl(hHub, IOCTL_USB_GET_NODE_CONNECTION_DRIVERKEY_NAME,
                keyName, keyBufSize, keyName, keyBufSize, &retBytes, nullptr))
            {
                // 拼接设备路径,按原有逻辑做前缀过滤后加入列表
                QString devPath = QString("\\\\?\\usb#%1").arg(QString::fromWCharArray(keyName->DriverKeyName));
                if (devPath.startsWith("\\\\?\\usb#vid") && !deviceList.contains(devPath))
                    deviceList.push_back(devPath);
            }
            HeapFree(GetProcessHeap(), 0, keyName);
        }
    }
}

内容的提问来源于stack exchange,提问作者Bill Sellers

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.03 10:45:38