01signal.com

Resetting a USB device on Linux (and maybe control its power supply)

Introduction

In a perfect world, USB devices and hubs behave nicely, and manage to fix whatever problem they encounter. In reality, all kinds of weird things happen, and there's a need to do something about it. The common solution is to pull out the USB plug and connect the device again. But what if this needs to happen automatically?

The most dramatic solution is presented on a different web page: It causes a shutdown and restart of the USB hub's drivers. The effect is nearly equivalent to disconnecting and connecting back all USB devices on the computer. That's the heaviest hammer, and it's necessary sometimes.

This page presents two methods to reset one specific USB device. I'll start with presenting how to carry out each method, with a few minimal explanations. After this, I'll get into a lot of technical details.

The entire discussion is limited to Linux here, even though the second method that is presented here is probably possible on other platforms as well.

A lot of information here is taken from the Universal Serial Bus Specification Revision 2.0. The file name of this document is typically usb_20.pdf.

Method #1: Ask the Linux kernel to reset the device

There are several tools available for doing this. The most advanced tool is part of usbutils. Download usbreset.c and use gcc for its compilation:

$ gcc -O3 -Wall -g usbreset.c -o usbreset

And then:

$ ./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

In response to the resets, the following appears in the kernel log:

usb 1-6: reset full-speed USB device number 4 using xhci_hcd

What this session demonstrates:

So what does usbreset do? It boils down to this command on the USB device's device file:

ioctl(fd, USBDEVFS_RESET, 0)

This ends up with a function call to the kernel's usb_reset_device(), which does much more than just to reset the device. The idea is to make the whole process smooth: This function notifies the device's driver about the reset before and after, if so requested. It unbinds the driver before the reset, and binds it back afterwards. The device's configuration is also loaded after the reset. Without this, the device wouldn't know its bus address and wouldn't be ready for any data exchange.

So it's almost like reconnecting the device, but without asking it to identify itself (because the information is already known) and without assigning it with a new address.

For USB 3.x, this is a hot reset, which is the less efficient type. More on hot reset below.

Method #2: Toggle the power state of the USB port

There are several tools for this purpose as well. I shall demonstrate this method with hubpower. Download hubpower.c and perform a compilation:

$ gcc -O3 -Wall -g hubpower.c -o hubpower

Using this tool is a somewhat trickier, because the commands are not sent to the USB device itself. Instead, the requests are sent to the USB hub that the USB device is connected to.

So the first step is to figure out which hub this is. First, let's find the address of the USB device:

$ 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

So the device is on bus number 1 and device number 4. Note that the device number changes every time the USB device is enumerated.

Which hub is the USB device connected to, then? And to which port?

$ 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

So device number 4 is connected to port #6. The hub's bus number is 1 and its device number is 1 (it's the motherboard's USB controller, which functions as a root hub).

Let's see what hubpower has to say about this:

$ 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

Note that the "1:1" part is the hub's bus address. If this is an external hub, this address will change every time the hub is connected to the computer.

But the numbers of the ports never change (as long as the device is connected to the same physical port).

Now when we know the device's port number, let's reset it:

$ 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

After the first command, the device will be reported as disconnected in the kernel log:

usb 1-6: USB disconnect, device number 4

After the second command, the computer behaves as if the device was connected physically:

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
[ ... ]

Because of this re-enumeration, a new bus address is assigned to the device:

$ 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

So is this like physically disconnecting the USB device and connect it back? The answer is maybe: In most cases, the hubpower commands don't really turn off the USB device's power supply. The device will therefore continue to receive its VBUS voltage (5V). Very few USB hubs actually turn off the power. More about this below.

With the vast majority of hubs, the first command ("power off') only puts the port in a state that causes it to ignore the device. The second command ("power on") returns the port to its natural state. The result is that the device is detected, and its initialization procedure begins. Among others, the device is reset and enumerated, and its driver initializes it.

So a physical disconnection is usually more effective than these commands, in particular if the device receives its power supply from the USB plug. But if the hub really shuts down VBUS, hubpower is as effective.

Note that in this example, the commands were sent to the motherboard's USB controller (the root hub). The reason is that the USB device was connected directly to the computer. If this device is connected to the computer through an external hub, the commands should be sent to this external hub. In both cases, hubpower is used in the same way.

hubpower doesn't support USB 3.0 (SuperSpeed), unfortunately. More on that below.

Differences between the two methods

So how is the second method (with hubpower) different from the first method (with usbreset)? For the purpose of solving a problem with a device, both methods do essentially the same: They send a reset command. The way this happens is dramatically different, but it's still this reset command that will probably solve the problem, if at all.

But there are a few important differences:

Controlling electrical equipment (?)

Even though the main topic of this page is how to fix a problem with a USB device, there's an interesting side effect too: It's sometimes possible to control the USB port's 5V power supply. In other words, a simple and cheap USB hub can be used to turn on and off a power supply that is capable to handle 2.5 Watts.

This is more than enough to control an electromechanical relay. So only one component needs to be added in order to control an electrical appliance that runs on 110V / 220V. Well, it's a good idea to add a simple diode too, in order to protect the USB hub from getting damaged. But that's it.

Unfortunately, the ability to control the power supply is optional: According to section 11.11 in the USB 2.0 specification, a hub may have power switches that turn off the 5V power supply to a port when the port is in the Powered-Off state. Alternatively, a power switch may be used to control the power supply of several ports ("ganged power switching"). The purpose of controlling the power supplies is primarily to shut off USB devices that draw too much current, so that the remaining ports can continue to work normally.

But as already mentioned, the physical power control is optional. What hubpower actually does is to change the value of the port's PORT_POWER attribute (it's called a "feature" in the USB specifications). This change relates to two different aspects of the hub's port:

PORT_POWER is explained in more detail below.

Does my hub control the voltage?

How do you know if your hub actually turns off the voltage? The only way to know for sure is to test it. Connect anything that isn't a USB device, but consumes power from the USB port. Real USB devices may add confusion. For example, an optical mouse will usually turn off its LED in response to the power-off command, even if the 5V voltage remains.

Each hub declares whether it controls the voltage, and to what granularity, in the information that is visible with "lsusb -v" (in the Hub Descriptor, which is defined in section 11.23.2.1 of the specification). The hub may declare that it doesn't control the voltage, or that it controls the voltage in groups of ports ("gangs"), or that it controls the voltage of each port individually: According to section 11.11 in the USB 2.0 specification, "a hub with power switches can switch power to all ports as a group/gang, to each port individually, or have an arbitrary number of gangs of one or more ports".

However, this information is not reliable. I've encountered several hubs that declare that they control the voltage, but none of them did.

For example, this is an of the output of "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

Looks optimistic, doesn't it? In reality, this hub doesn't control the voltages at all.

By contrast, this is a plain motherboard's hub:

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

So this hub admits that it doesn't support any voltage control.

Note that besides the information about the power switching, there's also status info about each port as "Hub Port Status". These status bits are defined in table 11-21 of the USB 2.0 specification (section 11.24.2.7.1). This is the same information that is fetched by hubpower.

The uhubctl tool

The idea to control a relay with a USB hub is the motivation for a lot of initiatives. It's worth to look at uhubctl, mainly because this project maintains a list of USB hubs that control the voltage.

This tool is clearly intended only for voltage control, and not for solving problems with a USB device. For example, by default uhubctl ignores hubs that don't declare the ability to control the voltages individually for each port.

These commands are equivalent to the hubpower commands above:

$ 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

The command syntax is much less convenient. It's explained briefly below.

uhubctl is based upon libusb, so this tool works with other operating systems as well. By contrast, hubpower accesses the hub's device file in /dev/bus/usb/ (or /proc/bus/usb/) directly, which works only on Linux.

uhubctl also supports USB 3.x (see the git commit and its follow-up). However, it seems like nobody cared what happens when a real a USB device is connected to the hub: My attempts to reset a SuperSpeed device resulted in weird results: When the power was returned, the device was left in the Polling state, and it wasn't enumerated. Also, while the power was off, "lsusb -v" was stuck. So something bad happened there.

These are the commands to turn the power off and on again on a SuperSpeed port. More about SuperSpeed below.

# ./uhubctl -f -e -l 2 -p 4 -a 0
# ./uhubctl -f -e -l 2 -p 4 -a 1

PORT_POWER explained

When a command arrives from the host to change PORT_POWER to zero, the port unconditionally enters the Powered-off state. This is true even if the hub continues to feed the device with its 5V power supply. There are other reasons for PORT_POWER to become zero, in particular an overcurrent condition on the port (the device draws too much current).

The only way to change PORT_POWER to '1' is by virtue of a command from the host. This puts the port in the Disconnected state. USB ports that have nothing connected to them are normally in this state. When a device is detected on the port, the state will change to Disabled after a short delay. If the device is already connected, and PORT_POWER changes to '1', this happens immediately.

From this state, the only path towards activating the device is by resetting the port (with PORT_RESET, see below). Only the host can do that. Hence the only thing that the hub can do is to notify the host that there is connected device that needs attention.

This is where one of the port's status bits comes in: PORT_CONNECTION. This bit must be zero when the port is in the Powered-off state or the Disconnected state. PORT_CONNECTION changes to '1' when the port transitions from the Disconnected state to the Disabled state. A change in PORT_CONNECTION generates a hub event, so the driver is notified (i.e. a function call to port_event() in the kernel's hub.c is made with USB_PORT_FEAT_C_CONNECTION enabled). The driver responds with resetting the port (by changing the port's PORT_RESET bit to '1') and enumerating the connected device.

All this relates to USB 2.0. Figure 11-10 in the USB 2.0 specification shows how a hub's port changes states.

Controlling PORT_RESET and PORT_ENABLE directly

hubpower also has the capability to change two other attributes directly: PORT_RESET and PORT_ENABLE. In fact, I added this capability to my own fork of this tool. Note, however, that all you can do with this is to deliberately make the USB device fail (and hence make the computer reset the device to fix that). Now in more detail:

PORT_RESET should be set to '1' by the host in order to initiate a port reset according to section 11.5.1.5 in the USB 2.0 specification. The hub changes PORT_RESET back to '0' after completing the reset. The hub never starts a port reset on its own initiative, and the host is not allowed to write '0' to this attribute (section 11.24.2.7.1.5).

This hub will ignore PORT_RESET if the port is in the Powered-off or Disconnected states.

Regarding PORT_ENABLE: When this attribute changes to '0', the port changes to the Disabled state. This can occur due to a request from the host, as well as disconnection of the USB device, the port being in a powered-off state, or an error during the reset process.

PORT_ENABLE can change to '1' only as a result of a port reset request from the host (section 11.24.2.7.1.2 in the USB 2.0 specification).

The host is hence not allowed to change PORT_RESET to '0' or to change PORT_ENABLE to '1'. Trying to do this will result in an error response from the hub (the relevant ioctl() command will return an error status).

hubpower can request a reset directly, by changing the port's PORT_RESET to '1'. This will reset the USB device, and the device will forget its bus address as a result. So the device will not be accessible anymore.

This command is sent directly to the hub, so the hub's driver in the Linux kernel will not know that this has happened. In fact, the computer will not notice that anything has changed until it attempts to access the USB device. What will happen next depends on the device's driver. The device will be treated as if it has some hardware error. Accordingly, some kind of error-correcting measure will be taken. Most likely, this will include a reset.

So using hubpower to cause a reset directly will probably achieve the desired result, but with a lot of unnecessary drama. PORT_POWER does this more elegantly. The only possible advantage of a direct PORT_RESET is that the driver might say "hey, something is really wrong with this device, let's do something drastic to fix it". And that might help.

As for manipulating PORT_ENABLE, the same will happen: The device will suddenly disappear. Even in this case, the computer will not know immediately that anything has happened: According to section 11.24.2.7.2.2 in the USB 2.0 specification, a change notification on PORT_ENABLE (i.e. C_PORT_ENABLE) is triggered only if the port becomes disabled because of an error on the link. The spec explicitly says that this notification is not made for any other reason.

So changing PORT_ENABLE to zero will have roughly the same effect as changing PORT_RESET directly. With one disadvantage: According to section 10.14.2.6.1 of the USB 3.0 specification, PORT_ENABLE "is not supported by SuperSpeed hubs".

SuperSpeed (USB 3.x)

First and foremost: If you're reading this because you have a problem with a USB 3.x device, ask yourself if you really need the data rate that USB 3.x offers. If the answer is negative, try connecting the device to the computer through a USB hub that doesn't support USB 3.x (or a short USB 2.0 cable). This alone might solve the problem.

SuperSpeed USB (which means the same as USB 3.x) coexists in parallel with USB 2.0. Every SuperSpeed device effectively consists of two devices: One separate device for SuperSpeed, and another separate device for USB 2.0. Each of these two USB versions rely on separate wires of the USB cable. They are mutually independent, electronically and conceptually.

The USB specifications requires that every SuperSpeed device consists of these two devices, even though this is practically not required. In other words, a SuperSpeed device that doesn't support USB 2.0 works properly when it's connected to a SuperSpeed port.

When a SuperSpeed device is connected to a SuperSpeed port, the first attempt is to connect through the SuperSpeed interface. If that fails, an attempt to connect through the USB 2.0 is made. In practice (and according to the specification), a USB device never connects through both versions at the same time. However, this is possible, and will make the USB device behave as if it was two separate devices.

A SuperSpeed hub consists of two hubs in parallel: One for USB 2.0 and one for SuperSpeed. When you connect an external SuperSpeed USB hub to a computer, two hubs are added to the system. They appear as two separate devices. A plain USB device is not allowed to use both versions in parallel, but a hub must do this.

If a SuperSpeed hub is connected to a USB 2.0 port, it behaves like a regular USB 2.0 hub.

Generally speaking, each of these two hubs operates independently of the other. Each hub has its own ports, and each of these ports operates independently. In particular, if the parameters of a port are changed on one of these parallel hubs, this has no effect on the other hub's ports.

Another conclusion is that if "lsusb -t" shows that a device is connected to the computer through the SuperSpeed root hub, it operates as a SuperSpeed device. In other words, its data rate is 5 Gbit/s or more. Likewise, if this device is connected through the USB 2.0 root hub, its data rate is 480 Mbit/s or less.

SuperSpeed and PORT_POWER

Recall that what hubpower actually did was to change the port's PORT_POWER attribute.

But a SuperSpeed hub consists of two hubs in parallel. Each of these two hubs has its own independent PORT_POWER attribute for each port. So when should the hub turn off the VBUS power? Each physical power switch depends on two PORT_POWER attributes, one from each of the parallel hubs.

Table 10-2 of the USB 3.0 specification gives the truth table for whether the hub should enable the power supply or not. It can be summarized as follows: If the hub is behaves as a USB 2.0 hub only (e.g. connected to a computer that doesn't support SuperSpeed), it follows the USB 2.0's PORT_POWER. If it's connected as a SuperSpeed hub (or both parallel hubs are connected), VBUS is turned off only if both PORT_POWER are zero.

If that sounded complicated, that was actually the easy part: The SuperSpeed part of the hub has a different internal state machine. That's quite expected, because the link training is done differently. But this state machine has three different states (instead of one, like USB 2.0) that are eligible when PORT_POWER is zero:

The purpose of the two last states is to ensure that if a SuperSpeed device that has its own power supply is connected to the port, the connection will not fall back to USB 2.0. This would happen because the device would not have any way to recognize that it's connected to a SuperSpeed port.

So what did we learn from this? Mainly that resetting a device by changing PORT_POWER on a SuperSpeed hub is not as simple as with a USB 2.0 hub.

SuperSpeed: warm reset and hot reset

USB 2.0 has one simple way to reset the device: Both wires are connected by the hub to ground (SE0) for 10 ms. SuperSpeed devices, on the other hand, have two ways to reset a device: warm reset and hot reset. These should not be confused with PowerOn Reset and Inband Reset, which are terms that are used to define the reason for the reset. Inband Reset means that the reset occurs because of a request from the host. This can result in a warm reset or a hot reset, depending on the request's type (more on this just below).

It's important to distinguish between a warm reset and a hot reset: In particular, a warm reset involves stopping data stream and bringing it up from the beginning. A hot reset is sent on the data stream itself, and keeps this data stream running. The hot reset is hence much faster, but if there is a problem with the data stream that can be fixed by restarting it, a warm reset is required. An example to such a problem is if there are bit errors on the physical layer. Taking down the bitstream and start over again may fix this, possibly because it can correct a suboptimal tuning of the equalizer.

Recall that usbreset performs a ioctl() with USBDEVFS_RESET. Among all other things that happen, this changes PORT_RESET to zero, regardless of the device's USB version (as of Linux kernel v5.16).

According to the USB 3.0 specification, section 7.4.2, a PORT_RESET request results in a Hot Reset. This means that if the data stream is active, it is not torn down, but the reset command is sent on this data stream. This spec also defines BH_PORT_RESET (feature number 28), which forces a warm reset (unless the port is disabled). This reset is more fundamental: It brings down the data stream, and restarts the procedure for bringing it up again (by virtue of LFPS signaling). The important difference is that if the data stream needs a restart, BH_PORT_RESET will do it, but PORT_RESET won't.

A connection of a new SuperSpeed device involves a Warm Reset. So it's a shame that there's apparently no tool that can do the trick with PORT_POWER on a SuperSpeed port. As mentioned above, uhubctl can do this technically, but the device ends up in a messy state.


Leftover jots

These are random pieces of information that may be useful, but has no proper context.

Copyright © 2021-2024. All rights reserved. (b4b9813f)