引言
在理想世界里,USB 设备与 USB 集线器(hub)都会规规矩矩地工作,并能自行解决遇到的各种问题。但现实中,各类诡异的事情都可能发生,因此有必要做点什么来应对。通常的做法是把 USB 插头拔下来再重新插上。但如果这个过程需要自动化呢?
更极端的解决方案见另一篇网页:它会关闭并重启 USB 集线器的驱动,效果几乎等同于把电脑上所有 USB 设备拔掉再重新连接。这是最重的一把锤子,但有时确实需要用到。
本页介绍两种复位(reset)指定 USB 设备的方法。我先演示每种方法的操作步骤,并附少量必要的说明;之后会深入讲解大量技术细节。
这里的讨论仅限于 Linux,尽管第二种方法在其他平台上可能同样可行。
这里的很多资料取自 USB 2.0 规范(Universal Serial Bus Specification Revision 2.0)。这份文档的文件名通常是 usb_20.pdf。
方法一:让 Linux 内核复位该设备
有好几个工具可以做这件事,其中功能最全面的工具是 usbutils 的一部分。下载 usbreset.c,然后用 gcc 编译:
$ gcc -O3 -Wall -g usbreset.c -o usbreset
然后执行:
$ ./usbreset Usage: usbreset PPPP:VVVV - reset by product and vendor id usbreset BBB/DDD - reset by bus and device number usbreset "Product" - reset by product name Devices: Number 001/004 ID 045e:07b2 Microsoft® Nano Transceiver v1.0 Number 001/002 ID 04f3:0103 $ ./usbreset 001/004 Resetting Microsoft® Nano Transceiver v1.0 ... can't open [Permission denied] $ sudo ./usbreset 001/004 Resetting Microsoft® Nano Transceiver v1.0 ... ok $ sudo ./usbreset 045e:07b2 Resetting Microsoft® Nano Transceiver v1.0 ... ok
复位之后,内核日志中会出现下面一行:
usb 1-6: reset full-speed USB device number 4 using xhci_hcd
这段操作说明了几件事:
- 不携带任何参数运行 usbreset,它会列出所有 USB 设备。用 lsusb 也能看到同样的信息。
- 真正复位设备需要 root 权限(所以上例中使用了 sudo)。
- 可以通过总线地址(bus address,由总线编号和设备编号组成)来选择设备。注意,设备每次连接后,总线地址都可能变化。
- 也可以根据厂商 ID(Vendor ID)和产品 ID(Product ID)来选择设备。如果这类 USB 设备只有一台,这是首选的方法。
- 该设备在 USB 总线上的地址没有改变(它并没有因为这次复位而被重新枚举)。
那么 usbreset 究竟做了什么呢?归根结底,就是对 USB 设备的设备文件执行下面这个命令:
ioctl(fd, USBDEVFS_RESET, 0)
这会最终调用内核中的 usb_reset_device() 函数,而该函数做的事情远不止是复位设备。其设计目的是让整个过程平稳进行:如果设备驱动要求收到通知,它会在复位前后分别通知驱动;复位前会解绑(unbind)驱动,复位后再重新绑定(bind)回去。复位后还会重新加载设备的配置。否则,设备将不知道自己的总线地址,也无法为任何数据交换做好准备。
所以这几乎等同于重新连接设备,只是不再要求设备自报身份(因为这些信息已经知道了),也不会给它分配新的地址。
就 USB 3.x 而言,这是一种热复位(hot reset),属于效果较弱的那种复位方式。下文还会详细介绍热复位。
方法二:切换 USB 端口的电源状态
用于这个目的的工具也有好几个。下面用 hubpower 来演示。下载 hubpower.c,然后编译:
$ gcc -O3 -Wall -g hubpower.c -o hubpower
使用这个工具会稍微麻烦一些,因为命令并不是发送给 USB 设备本身,而是发送给该 USB 设备所连接的 USB 集线器。
因此第一步要弄清楚目标集线器是哪一个。先找到 USB 设备的总线地址:
$ lsusb Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub Bus 001 Device 004: ID 045e:07b2 Microsoft Corp. Bus 001 Device 002: ID 04f3:0103 Elan Microelectronics Corp. ActiveJet K-2024 Multimedia Keyboard Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
可以看到,设备位于总线编号 1、设备编号 4。注意,每次 USB 设备被枚举时,设备编号都会变化。
那么,这个 USB 设备到底连在哪个集线器上?又连在哪个端口上?
$ lsusb -t /: Bus 02.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/6p, 5000M /: Bus 01.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/12p, 480M |__ Port 6: Dev 4, If 0, Class=Human Interface Device, Driver=usbhid, 12M |__ Port 6: Dev 4, If 1, Class=Human Interface Device, Driver=usbhid, 12M |__ Port 6: Dev 4, If 2, Class=Human Interface Device, Driver=usbhid, 12M |__ Port 11: Dev 2, If 0, Class=Human Interface Device, Driver=usbhid, 1.5M |__ Port 11: Dev 2, If 1, Class=Human Interface Device, Driver=usbhid, 1.5M
因此设备编号 4 连接在 6 号端口上。该集线器的总线编号是 1,设备编号也是 1(它是主板上的 USB 控制器,扮演根集线器(root hub)的角色)。
来看看 hubpower 能告诉我们什么:
$ sudo ./hubpower 1:1 status
Port 1 status: 0100 Power-On
Port 2 status: 0100 Power-On
Port 3 status: 0100 Power-On
Port 4 status: 0100 Power-On
Port 5 status: 0100 Power-On
Port 6 status: 0103 Power-On Enabled Connected
Port 7 status: 0100 Power-On
Port 8 status: 0100 Power-On
Port 9 status: 0100 Power-On
Port 10 status: 0100 Power-On
Port 11 status: 0303 Low-Speed Power-On Enabled Connected
Port 12 status: 0100 Power-On
注意,“1:1”这部分是集线器的总线地址。如果这是外接集线器,那么每次集线器连接到电脑时,这个地址都会变化。
但端口编号永远不会变(只要设备始终连接在同一个物理端口上)。
既然知道了设备所在的端口号,下面就来复位它:
$ sudo ./hubpower 1:1 power 6 off Port 6 status: 0000 Power-Off $ sudo ./hubpower 1:1 power 6 on Port 6 status: 0100 Power-On
执行第一条命令后,内核日志会报告设备已断开连接:
usb 1-6: USB disconnect, device number 4
执行第二条命令后,电脑的表现就如同设备被物理连接上一样:
usb 1-6: new full-speed USB device number 5 using xhci_hcd
usb 1-6: New USB device found, idVendor=045e, idProduct=07b2, bcd Device= 7.04
usb 1-6: New USB device strings: Mfr=1, Product=2, SerialNumber=0
usb 1-6: Product: Microsoft® Nano Transceiver v1.0
usb 1-6: Manufacturer: Microsoft
[ ... ]
由于发生了重新枚举,设备被分配了一个新的总线地址:
$ lsusb
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 001 Device 005: ID 045e:07b2 Microsoft Corp.
Bus 001 Device 002: ID 04f3:0103 Elan Microelectronics Corp. ActiveJet K-2024 Multimedia Keyboard
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
那么,这是否等同于把 USB 设备物理拔下再插回去呢?答案也许是:在大多数情况下,hubpower 命令并不会真正切断 USB 设备的电源,设备仍会接收到 VBUS 电压(5V)。真正会切断电源的 USB 集线器很少。下文会详细说明。
对绝大多数集线器来说,第一条命令(“power off”)只是让端口进入一种会忽略设备的状态;第二条命令(“power on”)则让端口回到正常状态。结果是设备被检测到,并开始执行初始化流程。具体来说,设备会被复位、枚举,然后由其驱动程序完成初始化。
因此,物理断开通常比这些命令更有效,尤其是当设备依靠 USB 插头供电时。但如果集线器真的能切断 VBUS,hubpower 的效果与之相同。
注意,在这个例子中,命令是发送给主板上的 USB 控制器(即根集线器)的,因为该 USB 设备直接连接在电脑上。如果设备通过外接集线器连接电脑,则应该把命令发送给那个外接集线器。两种情况下 hubpower 的使用方法都一样。
遗憾的是,hubpower 不支持 USB 3.0(SuperSpeed)。下文还会继续讨论。
两种方法的区别
那么,第二种方法(hubpower)和第一种方法(usbreset)有什么不同呢?就解决设备问题而言,两种方法的本质是一样的:都发送一个复位命令。两者发送命令的方式差别巨大,但如果问题真能被解决,最终起作用的仍然都是这个复位命令。
但两者之间有几个重要差异:
- 即使设备在总线上不被识别,hubpower 也能起作用。例如,电脑尝试对设备进行多次枚举后放弃了。此时设备虽然在物理上连接着电脑,但因为没有总线地址,usbreset 将无能为力。
- 当 USB 端口被禁用时,只有 hubpower 能帮上忙:有时端口出现多次错误后,电脑会完全忽略这个端口。hubpower 应该能解决这种情况。
- hubpower 无法用于 SuperSpeed(USB 3.x)。这应该只是一个需要增加支持的问题。关于 SuperSpeed 的更多内容见下文。
- hubpower 还有可能控制 USB 设备的电源(但通常并不会真正切断)。如果设备需要重新上电(power recycle)才能解决问题,这就是一个优势。
- hubpower 使用起来更麻烦:必须先确定 USB 设备连接在集线器的哪个端口上,还要知道集线器的总线地址。
控制电气设备(?)
虽然本文的主要话题是如何解决 USB 设备的问题,但这还有一个有趣的副作用:有时可以控制 USB 端口的 5V 电源。换句话说,一个简单便宜的 USB 集线器,可以用来打开或关闭一个能承受 2.5 瓦功率的电源。
这用来控制一个机电式继电器(electromechanical relay)绰绰有余。因此,若要控制 110V / 220V 供电的电器,只需要再增加一个元件。当然,最好也加一个简单的二极管,以保护 USB 集线器不被损坏。除此之外,就不需要别的了。
遗憾的是,这种电源控制能力并不是必备的:根据 USB 2.0 规范第 11.11 节,集线器可以配备电源开关,在端口处于 Powered-Off(已断电)状态时切断该端口的 5V 电源;也可以用一个电源开关同时控制多个端口的电源,也就是“成组电源切换”(ganged power switching)。控制电源的主要目的是关闭电流过大的 USB 设备,使其余端口能继续正常工作。
但如前所述,物理上能否控制电源是可选的。hubpower 真正做的是改变端口的 PORT_POWER 属性的值(在 USB 规范中,这个属性称为一个 “feature”,可译作“特性”)。这一改变涉及集线器端口的两个不同方面:
- 必需方面:端口的逻辑状态。当 PORT_POWER 为零时,端口只能处于 Powered-Off(已断电)状态(或 Not Configured,即未配置状态)。换句话说,集线器必须表现得像端口上没有电压一样,因此要忽略已连接的设备(如果有的话)。
- 可选方面:端口的供电线上是否存在 VBUS(也就是 5V 电压)。如果集线器不支持这一特性,那么即使 PORT_POWER 为零,5V 电压也可能仍然存在。
下文会对 PORT_POWER 作更详细的说明。
我的集线器能控制电压吗?
你怎么知道自己的集线器是否真的会切断电压?唯一可靠的方法就是实测。接一个不是 USB 设备、但会从 USB 端口取电的东西来测试。真正的 USB 设备反而可能造成混淆。例如,光电鼠标在收到断电命令后通常会关闭 LED,即使 5V 电压仍然存在。
每个集线器都会在“lsusb -v”可见的信息中声明自己是否控制电压,以及控制到什么粒度。这段信息位于 Hub Descriptor(集线器描述符)中;该描述符在规范的 11.23.2.1 节中有定义。集线器可以声明自己完全不控制电压,也可以声明按端口组(“gang”)控制电压,还可以声明单独控制每个端口的电压。根据 USB 2.0 规范第 11.11 节:“带电源开关的集线器可以对所有端口成组(gang)切换电源,也可以单独切换每个端口的电源,还可以有任意数量的组,每组包含一个或多个端口。”
不过,这条信息并不可靠。我遇见过好几个宣称自己能够控制电压的集线器,结果没有一个真的能做到。
例如,下面是“lsusb -v”输出的一部分:
Bus 001 Device 073: ID 0bda:5411 Realtek Semiconductor Corp. Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 2.10 bDeviceClass 9 Hub bDeviceSubClass 0 Unused bDeviceProtocol 2 TT per port bMaxPacketSize0 64 idVendor 0x0bda Realtek Semiconductor Corp. idProduct 0x5411 bcdDevice 1.23 iManufacturer 1 Generic iProduct 2 4-Port USB 2.0 Hub iSerial 0 bNumConfigurations 1 Configuration Descriptor: [ ... ] Hub Descriptor: bLength 9 bDescriptorType 41 nNbrPorts 4 wHubCharacteristic 0x00a9 Per-port power switching Per-port overcurrent protection TT think time 16 FS bits Port indicators bPwrOn2PwrGood 0 * 2 milli seconds bHubContrCurrent 100 milli Ampere DeviceRemovable 0x00 PortPwrCtrlMask 0xff Hub Port Status: Port 1: 0000.0503 highspeed power enable connect Port 2: 0000.0503 highspeed power enable connect Port 3: 0000.0100 power Port 4: 0000.0100 power
看起来挺乐观,对吧?实际上,这个集线器根本不能控制电压。
相比之下,下面是一个普通主板集线器的输出:
Bus 005 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 2.00 bDeviceClass 9 Hub bDeviceSubClass 0 Unused bDeviceProtocol 1 Single TT bMaxPacketSize0 64 idVendor 0x1d6b Linux Foundation idProduct 0x0002 2.0 root hub bcdDevice 4.15 iManufacturer 3 Linux 4.15.0-20-generic xhci-hcd iProduct 2 xHCI Host Controller iSerial 1 0000:06:00.0 bNumConfigurations 1 Configuration Descriptor: [ ... ] Hub Descriptor: bLength 9 bDescriptorType 41 nNbrPorts 2 wHubCharacteristic 0x000a No power switching (usb 1.0) Per-port overcurrent protection TT think time 8 FS bits bPwrOn2PwrGood 10 * 2 milli seconds bHubContrCurrent 0 milli Ampere DeviceRemovable 0x00 PortPwrCtrlMask 0xff Hub Port Status: Port 1: 0000.0100 power Port 2: 0000.0100 power Device Status: 0x0001 Self Powered
所以,这个集线器坦承自己不支持任何电压控制。
注意,除了电源切换方面的信息之外,输出中还有每个端口的状态信息,也就是 “Hub Port Status”(集线器端口状态)。这些状态位定义在 USB 2.0 规范的表 11-21(第 11.24.2.7.1 节)中。hubpower 获取的正是这些信息。
uhubctl 工具
用 USB 集线器控制继电器的想法,促成了很多项目。值得一看的是 uhubctl,主要是因为该项目维护了一个能够控制电压的 USB 集线器列表。
这个工具显然只打算用于电压控制,而不是用来解决 USB 设备的问题。例如,uhubctl 默认会忽略那些没有声明支持逐端口独立控制电压的集线器。
下面的命令与前面 hubpower 的命令等效:
$ sudo ./uhubctl -f -l 1 -p 6 -a 0 Current status for hub 1 [1d6b:0002 Linux 5.16.0 xhci-hcd xHCI Host Controller 0000:00:14.0, USB 2.00, 12 ports, nops] Port 6: 0103 power enable connect [045e:07b2 Microsoft Microsoft? Nano Transceiver v1.0] Sent power off request New status for hub 1 [1d6b:0002 Linux 5.16.0 xhci-hcd xHCI Host Controller 0000:00:14.0, USB 2.00, 12 ports, nops] Port 6: 0000 off $ sudo ./uhubctl -f -l 1 -p 6 -a 1 Current status for hub 1 [1d6b:0002 Linux 5.16.0 xhci-hcd xHCI Host Controller 0000:00:14.0, USB 2.00, 12 ports, nops] Port 6: 0000 off Sent power on request New status for hub 1 [1d6b:0002 Linux 5.16.0 xhci-hcd xHCI Host Controller 0000:00:14.0, USB 2.00, 12 ports, nops] Port 6: 0100 power
uhubctl 的命令语法要麻烦得多。下面简要解释一下。
uhubctl 基于 libusb,因此也能在其他操作系统上运行。相比之下,hubpower 直接访问位于 /dev/bus/usb/(或 /proc/bus/usb/)下的集线器设备文件,因此只能在 Linux 上运行。
uhubctl 也支持 USB 3.x(参见这个 git 提交及其后续提交)。不过,似乎没有人关心当真有一个真实 USB 设备连接在集线器上时会发生什么:我尝试复位 SuperSpeed 设备时得到了一些奇怪的结果。电源恢复后,设备停在 Polling(轮询)状态,没有被枚举;而且在断电期间,“lsusb -v”还会卡住。可见那里确实出了些问题。
以下命令用于在 SuperSpeed 端口上关闭和重新打开电源。关于 SuperSpeed 的更多内容见下文。
# ./uhubctl -f -e -l 2 -p 4 -a 0 # ./uhubctl -f -e -l 2 -p 4 -a 1
- -f 表示即使集线器报告不支持逐端口电压控制,也照样对它操作。
- -e 表示“精确定位”(exact position),也就是把一个 SuperSpeed 端口当作两个集线器来计数。
- -l 2 表示第二个集线器(第一个集线器是我电脑上的 USB 2.0 集线器)。
- -p 4 表示 4 号端口。
- -a 0 表示关闭电源(动作 #0)。
- -a 1 表示打开电源(动作 #1)。
详解 PORT_POWER
当主机发出命令要把 PORT_POWER 改为零时,端口会无条件进入 Powered-off(已断电)状态。即使集线器仍在对设备提供 5V 电源,情况也是如此。PORT_POWER 变为零还有其他原因,尤其是端口出现过流(overcurrent)时,也就是设备抽取的电流过大。
要把 PORT_POWER 改为 '1',唯一的途径是收到来自主机的命令。这会使端口进入 Disconnected(未连接)状态。没有连接任何设备的 USB 端口通常就处在这个状态。当端口上检测到设备时,经过短暂延迟后会进入 Disabled(已禁用)状态。如果设备已经连接着,而 PORT_POWER 变为 '1',则会立即进入 Disabled 状态。
要从这个状态过渡到设备可用状态,唯一途径是复位端口(使用 PORT_RESET,见下文)。只有主机才能执行复位。因此,集线器唯一能做的,就是通知主机有设备连接着、需要处理。
这里就涉及端口的状态位之一:PORT_CONNECTION。当端口处于 Powered-off 或 Disconnected 状态时,该位必须为零。当端口从 Disconnected 状态转换到 Disabled 状态时,PORT_CONNECTION 会变为 '1'。PORT_CONNECTION 的变化会产生一个集线器事件(hub event),从而通知驱动程序:也就是说,内核 hub.c 中的 port_event() 函数会以启用 USB_PORT_FEAT_C_CONNECTION 的方式被调用。驱动程序的响应是复位端口(把端口的 PORT_RESET 位设为 '1'),并对所连接的设备进行枚举。
以上这些都和 USB 2.0 有关。USB 2.0 规范中的图 11-10 展示了集线器端口的状态转换过程。
直接控制 PORT_RESET 与 PORT_ENABLE
hubpower 还可以直接改变另外两个属性:PORT_RESET 和 PORT_ENABLE。实际上,这个能力是我加到我自己的这个工具 fork(分支)版本中的。但请注意,你用它唯一能做的,就是故意让 USB 设备出错(然后让电脑复位设备来修复这个问题)。下面更详细地说明:
根据 USB 2.0 规范第 11.5.1.5 节,主机应将 PORT_RESET 设为 '1',以发起端口复位。集线器在完成复位后会把 PORT_RESET 重新置回 '0'。集线器绝不会自行发起端口复位;同时,主机也不允许向该属性写入 '0'(第 11.24.2.7.1.5 节)。
如果端口处于 Powered-off(已断电)或 Disconnected(未连接)状态,集线器会忽略 PORT_RESET。
关于 PORT_ENABLE:当该属性变为 '0' 时,端口会进入 Disabled(已禁用)状态。这可能是主机请求的结果,也可能是 USB 设备断开、端口处于断电状态或复位过程中出现错误所致。
PORT_ENABLE 只有在主机发出端口复位请求后,才能变为 '1'(USB 2.0 规范第 11.24.2.7.1.2 节)。
因此,主机不允许把 PORT_RESET 改为 '0',也不允许把 PORT_ENABLE 改为 '1'。如果尝试这样做,集线器会返回错误响应(相关的 ioctl() 命令会返回错误状态)。
hubpower 可以把端口的 PORT_RESET 改为 '1',直接请求一次复位。这会复位 USB 设备,设备也会因此忘记自己的总线地址,从而不再可访问。
这条命令是直接发送给集线器的,因此 Linux 内核中的集线器驱动并不会知道发生了这件事。实际上,电脑在尝试访问该 USB 设备之前,不会察觉任何变化。接下来会发生什么,取决于设备驱动。设备会像发生了硬件错误一样被对待,于是会采取某种纠错措施。最可能的情况就是执行一次复位。
所以,用 hubpower 直接触发复位虽然很可能达到预期效果,但会带来很多不必要的麻烦。用 PORT_POWER 则优雅得多。直接使用 PORT_RESET 唯一可能的好处是,驱动可能会认为:“嘿,这个设备真的出问题了,我们得采取更彻底的措施来修复它。”而这也许确实能解决问题。
至于操作 PORT_ENABLE,也会发生类似的情况:设备会突然消失。即便如此,电脑也不会立刻知道发生了什么。根据 USB 2.0 规范第 11.24.2.7.2.2 节,只有在链路上发生错误导致端口被禁用时,才会触发 PORT_ENABLE 的变化通知(即 C_PORT_ENABLE)。规范明确指出,其他任何原因都不会触发此通知。
因此,把 PORT_ENABLE 改为零的效果与直接改变 PORT_RESET 大致相同,但有一个缺点:根据 USB 3.0 规范第 10.14.2.6.1 节,SuperSpeed 集线器“不支持 PORT_ENABLE”。
SuperSpeed(USB 3.x)
首先是最重要的一点:如果你是因为 USB 3.x 设备出了问题才读到这篇文章,请先问问自己,是否真的需要 USB 3.x 提供的数据速率。如果答案是否定的,试着把设备通过一个不支持 USB 3.x 的 USB 集线器(或一根较短的 USB 2.0 线缆)连接到电脑。仅此一项也许就能解决问题。
SuperSpeed USB(也就是 USB 3.x)与 USB 2.0 以并行方式共存。每个 SuperSpeed 设备实际上由两个设备组成:一个用于 SuperSpeed 的独立设备,以及另一个用于 USB 2.0 的独立设备。这两个 USB 版本分别使用 USB 线缆中不同的导线,在电气上和概念上彼此独立。
USB 规范要求每个 SuperSpeed 设备都由这两个设备组成,尽管在实际使用中并非必须如此。换句话说,一个不支持 USB 2.0 的 SuperSpeed 设备,连接到 SuperSpeed 端口时也能正常工作。
当一个 SuperSpeed 设备连接到 SuperSpeed 端口时,系统会首先尝试通过 SuperSpeed 接口进行连接。如果失败,则会尝试通过 USB 2.0 连接。在实际中(规范也是这样说的),USB 设备绝不会同时通过两个版本进行连接。不过,这种情况是可能的;如果真的发生,USB 设备会表现得像两个独立的设备一样。
SuperSpeed 集线器由两个并行的集线器组成:一个用于 USB 2.0,一个用于 SuperSpeed。当你把外接的 SuperSpeed USB 集线器连接到电脑时,系统中会增加两个集线器,它们表现为两个独立的设备。普通 USB 设备不允许同时使用两个版本,但集线器必须如此。
如果 SuperSpeed 集线器连接到 USB 2.0 端口,它就会像一个普通 USB 2.0 集线器那样工作。
一般来说,这两个集线器彼此独立工作。每个集线器都有自己的端口,而且每个端口也独立工作。尤其要注意,如果改变了其中一个并行集线器上某个端口的参数,不会影响另一个集线器的端口。
由此还可得出一个结论:如果“lsusb -t”显示某个设备通过 SuperSpeed 根集线器连接到电脑,那么它就是在以 SuperSpeed 设备的方式工作,也就是说它的数据速率是 5 Gbit/s 或更高。同样,如果设备是通过 USB 2.0 根集线器连接的,那么它的数据速率是 480 Mbit/s 或更低。
SuperSpeed 与 PORT_POWER
回顾一下:hubpower 实际所做的,就是改变端口的 PORT_POWER 属性。
但 SuperSpeed 集线器由两个并行的集线器组成。这两个集线器各自拥有相互独立的每端口 PORT_POWER 属性。那么,集线器应该在什么时候切断 VBUS 电源呢?每个物理电源开关都取决于两个 PORT_POWER 属性,分别来自两个并行集线器中的每一个。
USB 3.0 规范中的表 10-2 给出了集线器是否应启用电源的真值表(truth table)。可以概括如下:如果集线器仅作为 USB 2.0 集线器运行(例如连接到一个不支持 SuperSpeed 的电脑),则遵循 USB 2.0 的 PORT_POWER。如果作为 SuperSpeed 集线器连接(或者说两个并行集线器都已连接),那么只有当两个 PORT_POWER 都为零时,VBUS 才会被切断。
如果上面听起来已经很复杂了,其实那还算是容易的部分:集线器的 SuperSpeed 部分有着不同的内部状态机(state machine)。这并不意外,因为链路训练(link training)的方式不同。而且,当 PORT_POWER 为零时,这个状态机有三个可能的状态(而 USB 2.0 只有一个):
- DSPORT.Powered-off:表示端口完全处于非活动状态。
- DSPORT.Powered-off-detect:表示端口尝试检测 SuperSpeed 链路对端(link partner)。
- DSPORT.Powered-off-reset:表示端口对链路对端执行一次温复位(warm reset,下文还会详述)。
后两个状态的目的是:如果一个自带电源的 SuperSpeed 设备连接到了这个端口,要确保连接不会回退到 USB 2.0。如果没有这两个状态,就会发生回退,因为该设备没有任何办法知道自己连接的是一个 SuperSpeed 端口。
那么我们从这些内容里学到了什么?主要是:在 SuperSpeed 集线器上通过改变 PORT_POWER 来复位设备,并不像在 USB 2.0 集线器上那样简单。
SuperSpeed:温复位与热复位
USB 2.0 只有一种简单的复位设备方式:集线器把两条信号线都接地(SE0)并保持 10 ms。相比之下,SuperSpeed 设备有两种复位方式:温复位(warm reset)和热复位(hot reset)。不要把它们与 PowerOn Reset(上电复位)和 Inband Reset(带内复位)相混淆——这两个术语用于定义复位发生的原因。Inband Reset 表示复位是因为主机请求而发生的;根据请求类型的不同,它可能导致温复位或热复位(下面马上会细说)。
区分温复位和热复位十分重要:温复位需要停止数据流(data stream),并从零开始重新建立;热复位则是直接在数据流上发送的,并让数据流保持运行。因此热复位要快得多,但如果数据流的问题可以通过重启数据流来解决,那就需要温复位。这类问题的一个例子是物理层出现位错误(bit errors)。把比特流(bitstream)关闭再从头开始,或许能解决问题,原因可能是这样做能纠正均衡器(equalizer)不理想的调谐。
回顾一下,usbreset 是以 USBDEVFS_RESET 参数执行 ioctl()。在由此引发的一系列操作中,有一件是把 PORT_RESET 改为 0,无论设备属于哪个 USB 版本(就 Linux 内核 v5.16 而言)。
根据 USB 3.0 规范第 7.4.2 节,PORT_RESET 请求会导致热复位(Hot Reset)。这意味着,如果数据流处于活动状态,它不会被拆除,而是在这条数据流上发送复位命令。该规范还定义了 BH_PORT_RESET(特性编号 28),它会强制进行一次温复位(除非端口处于禁用状态)。这种复位更彻底:它会关闭数据流,并重新启动数据流的建立过程(借助 LFPS 信令)。重要的区别在于:如果数据流需要重启,BH_PORT_RESET 能做到,而 PORT_RESET 做不到。
连接一个新的 SuperSpeed 设备时会涉及一次温复位(Warm Reset)。所以,显然还没有什么工具能在 SuperSpeed 端口上利用 PORT_POWER 达到同样的效果,这挺遗憾的。如前所述,uhubctl 在技术上可以做到,但最终设备会处于一种乱七八糟的状态。
零散补充
下面这些零散的信息也许有用,但放在前面的任何地方都不太合适。
- 像 PORT_POWER 这类命令的编号,列在 USB 2.0 规范的表 11-17 和 USB 3.0 规范的表 10-8 中,称为 Hub 类特性选择符(Hub class feature selectors)。
- 内核中把 PORT_RESET 置位的函数是 hub_port_reset()。但要真正重新初始化一个端口,靠的是 usb_reset_device();它会让所属驱动为复位做好准备,然后调用 usb_reset_and_verify_device()。这些函数都定义在 drivers/usb/core/hub.c 中,并且只有 usb_reset_device() 是导出的。
- 在 drivers/usb/core/devio.c 中,usb_reset_device() 由 proc_resetdevice() 调用,而后者可以由针对相关设备文件执行 USBDEVFS_RESET ioctl() 来触发。usbreset 所做的就是这个(见上文)。
- 这与 libusb 在 linux_usbfs.c 中的 op_reset_device() 等价(它改用 IOCTL_USBFS_RESET,但该值与 USBDEVFS_RESET 相同,两者都是 20;可对照内核的 include/uapi/linux/usbdevice_fs.h)。libusb 的这个函数还会取消对接口的占用(unclaim interfaces)。而如果设备的任何一个接口已经被占用(claimed),proc_resetdevice() 会礼貌地拒绝执行该 ioctl()。
- 在 hub.c 中,port_event() 由 hub_event() 调用。前者负责检测端口状态位的变化。hub_event() 是一个工作项(work item),由 kick_hub_wq() 触发(具体来说是由 hub_irq() 触发;hub_irq() 会在端口状态变化及各种故障时触发,以响应到达集线器 IN 端点上的状态变化通知;该端点正是为此目的而设的)。