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控制码,获取对应根集线器的设备路径,将根集线器加入待遍历的集线器队列。 - 集线器端口递归遍历逻辑
对队列中的每一个集线器设备句柄,执行以下操作:- 发送
IOCTL_USB_GET_NODE_INFORMATION控制码,获取集线器属性,包括总端口数量、集线器类型(根集线器/外部扩展集线器) - 按序号遍历所有端口(注意端口序号从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
相关产品推荐
相关产品推荐

