はじめに
理想的な世界では、USB デバイスやハブは行儀よく動作し、直面した問題を何とか解決してくれます。しかし現実には、さまざまな奇妙なことが起こるもので、何かしらの対処が必要になります。よくある解決策は、USB プラグを抜いて挿し直すことです。では、これを自動的に行わなければならない場合はどうすればいいでしょうか?
もっとも劇的な解決策は、別の Web ページで紹介しています。これは USB ハブのドライバをいったんシャットダウンして再起動する方法です。パソコン上のすべての USB デバイスをいったん切断して再接続するのとほぼ同じ効果があります。最も重いハンマーですが、時には必要です。
このページでは、特定の USB デバイスをリセットする 2 つの方法を紹介します。まず各方法の実行手順を最小限の説明とともに示し、その後、テクニカルな詳細を詳しく見ていきます。
ここでの議論は Linux に限定します。ただし、後で紹介する 2 番目の方法は、他のプラットフォームでもおそらく可能です。
ここに書いた情報の多くは、Universal Serial Bus Specification Revision 2.0(USB 2.0 仕様書)から取っています。この文書のファイル名は通常 usb_20.pdf です。
方法 1: 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 を使います)。
- デバイスはバスアドレス(バス番号とデバイス番号)でも指定できます。このバスアドレスは、デバイスを接続するたびに変わります。
- デバイスはベンダ ID / プロダクト ID でも選択できます。同種の USB デバイスが 1 台しかない場合は、こちらが好ましい方法です。
- この方法では、デバイスの USB バス上のアドレスは変わりません(リセットによる再列挙は行われません)。
では、usbreset は何をやっているのでしょうか? 結局のところ、USB デバイスのデバイスファイルに対して、次の ioctl() を実行しています。
ioctl(fd, USBDEVFS_RESET, 0)
これは、カーネルの usb_reset_device() を呼び出すことになります。この関数は単にデバイスをリセットするだけではありません。一連の処理をスムーズに行うための仕掛けです。この関数は、ドライバが要求すれば、リセットの前後でドライバに通知を行います。また、リセットの前にはドライバをアンバインド(unbind)し、リセット後には再びバインドします。さらに、リセット後にはデバイスのコンフィギュレーションも読み込み直します。これがないと、デバイスは自分のバスアドレスを知ることができず、データ交換の準備もできないからです。
つまり、デバイスの再接続にほぼ等しい操作です。ただし、デバイスに自己識別を求めず(情報はすでに分かっているため)、新しいアドレスも割り当てません。
USB 3.x の場合、これはホットリセット(hot reset)に相当し、より効果が限定的なタイプのリセットです。ホットリセットの詳細は後述します。
方法 2: USB ポートの電源状態を切り替える
この目的のためのツールもいくつかあります。ここでは hubpower を使って説明します。hubpower.c をダウンロードしてコンパイルします。
$ gcc -O3 -Wall -g hubpower.c -o hubpower
このツールの使い方は少し厄介です。コマンドは USB デバイス自体に送られるのではなく、その 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 コントローラで、ルートハブとして機能しています)。
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
2 番目のコマンドを実行すると、コンピュータはデバイスが物理的に接続されたときと同じように動作します。
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
[ ... ]
この再列挙(re-enumeration)により、デバイスには新しいバスアドレスが割り当てられます。
$ 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)は、ポートを「デバイスを無視する」状態にするだけです。2 番目のコマンド(power on)でポートは通常の状態に戻ります。その結果、デバイスが検出され、初期化手順が始まります。具体的にはデバイスのリセットと列挙が行われ、ドライバが初期化を行います。
したがって、特にデバイスが USB プラグから電源供給を受けている場合は、物理的な切断のほうが通常は効果的です。しかし、ハブが本当に VBUS を遮断するなら、hubpower も同じだけ効果があります。
この例では、コマンドをマザーボードの USB コントローラ(ルートハブ)に送りました。これは USB デバイスがコンピュータに直接接続されていたからです。外付けハブを介して接続している場合は、その外付けハブにコマンドを送る必要があります。どちらの場合も hubpower の使い方は同じです。
残念ながら hubpower は USB 3.0(SuperSpeed)に対応していません。詳細は後述します。
2 つの方法の違い
では、2 番目の方法(hubpower)は 1 番目の方法(usbreset)とどこが違うのでしょうか。デバイスの問題を解決するという観点では、両者は本質的に同じことをしています。つまりリセットコマンドを送るのです。その送り方は大きく異なりますが、問題を解決するのは、結局そのリセットコマンドです(解決する場合の話ですが)。
ただし、重要な違いがいくつかあります。
- hubpower は、デバイスがバス上で認識されていない場合でも機能します。たとえば、コンピュータがデバイスの列挙を何度か試みた後、諦めてしまった場合です。こういうとき、デバイスは物理的に接続されていますが、バスアドレスがないため usbreset は役に立ちません。
- USB ポートが無効になってしまった場合に助けになるのは hubpower だけです。コンピュータは、特定の USB ポートで何度かエラーが発生すると、そのポートを完全に無視することがあります。hubpower ならこれを解決できるはずです。
- hubpower は SuperSpeed(USB 3.x)では動作しません。これはおそらくサポートを追加すれば済む話です。SuperSpeed については後述します。
- hubpower は USB デバイスの電源供給も制御できる可能性があります(ただし、通常は制御されません)。デバイスの問題を解決するのに電源の再投入(パワーリサイクル)が必要な場合は、これが利点になります。
- hubpower のほうが扱いは難しいです。USB デバイスがハブのどのポートに接続されているかを調べる必要があり、さらにハブのバスアドレスも調べる必要があります。
電気機器の制御(?)
このページの本題は USB デバイスの問題解決ですが、面白い副作用もあります。USB ポートの 5V 電源を制御できることがあるのです。つまり、安価な USB ハブを使えば、2.5W まで扱える電源のオン / オフを切り替えられます。
これは電磁リレー(electromechanical relay)を制御するには十分すぎる能力です。したがって、110V / 220V で動作する電気機器を制御するために追加する部品は、リレー 1 つだけで済みます。そうだ、USB ハブが壊れないように、フライバックダイオードを追加するのもよいでしょう。ただし、それだけです。
残念ながら、電源供給を制御できるかどうかはオプションです。USB 2.0 仕様のセクション 11.11 によれば、ハブには、ポートが Powered-Off 状態のときにそのポートへの 5V 電源を遮断するパワースイッチを実装してもよい(may)ことになっています。あるいは、複数ポートへの電源供給を 1 つのパワースイッチでまとめて制御する方式(ganged power switching)もあります。電源制御の主な目的は、過大な電流を消費する USB デバイスを遮断して、残りのポートが正常に動作し続けられるようにすることです。
ただし、すでに述べたように、物理的な電源制御はオプションです。hubpower が実際にやっているのは、ポートの PORT_POWER 属性(USB の仕様書では「フィーチャ(feature)」と呼ばれます)の値を変更することです。この変更が関係するのは、ハブのポートに関する次の 2 つの側面です。
- 1 つ目は必須の側面、ポートの論理状態です。PORT_POWER が 0 のとき、ポートは Powered-Off 状態(または Not Configured 状態)にしかなれません。つまり、ハブはポートに電圧がかかっていないかのように振る舞わなければならず、デバイスが接続されていてもそれを無視します。
- 2 つ目はオプションの側面、ポートの電源線に VBUS(5V)が存在するかどうかです。ハブがこの機能に対応していない場合、PORT_POWER が 0 であっても電圧がかかっていることがあります。
PORT_POWER の詳細は後述します。
自分のハブは電圧を制御している?
自分のハブが実際に電圧を遮断するかどうかは、どうすれば分かるのでしょうか? 確実に知る方法はテストしかありません。USB デバイスではないが、USB ポートから電力を消費するものを接続してみてください。実際の USB デバイスを使うと、かえって混乱することがあります。たとえば光学式マウスは、5V が供給されたままでも、電源オフのコマンドに反応して LED を消すのが普通です。
各ハブは、電圧を制御するかどうか、またどの単位で制御するかを、「lsusb -v」で表示される情報の中で宣言しています(この情報は、仕様のセクション 11.23.2.1 で定義されているハブディスクリプタ(Hub Descriptor)です)。ハブは「電圧を制御しない」「複数ポートをグループ(gang)として制御する」「ポートごとに個別に制御する」のいずれかを宣言します。USB 2.0 仕様のセクション 11.11 によれば、「パワースイッチを持つハブは、全ポートへの電源を 1 つのグループ / ギャングとして切り替えることも、ポートごとに個別に切り替えることも、1 つ以上のポートからなる任意の数のギャングとして切り替えることもできる」とあります。
しかし、この情報は信頼できません。電圧を制御すると宣言しているハブをいくつか見てきましたが、実際に制御していたものは 1 つもありませんでした。
たとえば、これは「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 は libusb ベースなので、他のオペレーティングシステムでも動作します。一方 hubpower は、/dev/bus/usb/(または /proc/bus/usb/)にあるハブのデバイスファイルに直接アクセスする実装なので、Linux でしか動作しません。
uhubctl は USB 3.x にも対応しています(git commit とそのフォローアップを参照)。ただし、実際の 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 ポートをハブ 2 つ分として数えます。
- -l 2 は 2 番目のハブを意味します(私のコンピュータでは、1 番目のハブが USB 2.0 ハブでした)。
- -p 4 はポート番号 4 を意味します。
- -a 0 は電源オフ(アクション #0)を意味します。
- -a 1 は電源オン(アクション #1)を意味します。
PORT_POWER の説明
ホストから PORT_POWER を 0 に変更するコマンドが届くと、ポートは無条件に Powered-Off 状態になります。ハブがデバイスに 5V を供給し続けている場合でも同じです。PORT_POWER が 0 になる理由はほかにもあります。特に、ポートの過電流状態(デバイスが過大な電流を消費した場合)が代表的です。
PORT_POWER を 1 に変更できるのは、ホストからのコマンドだけです。これによってポートは Disconnected 状態になります。何も接続されていない USB ポートは、通常この状態です。ポートにデバイスが検出されると、少し時間をおいて Disabled 状態に変わります。デバイスがすでに接続されている状態で PORT_POWER が 1 に変わった場合は、これが即座に起こります。
この状態からデバイスを有効化する唯一の道は、ポートをリセット(PORT_RESET。後述)することです。これはホストにしかできません。したがって、ハブにできるのは、接続されているデバイスが対応を必要としていることをホストに通知することだけです。
ここで、ポートのステータスビットの 1 つである PORT_CONNECTION が登場します。このビットは、ポートが Powered-Off 状態または Disconnected 状態のときは 0 でなければなりません。PORT_CONNECTION が 1 になるのは、ポートが Disconnected 状態から Disabled 状態へ遷移するときです。PORT_CONNECTION の変化はハブイベントを発生させるため、ドライバに通知されます(具体的には、カーネルの 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 という 2 つの属性を直接変更する機能があります。実はこの機能は、私自身がフォークした版に追加したものです。ただし注意してほしいのは、これでできることは、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 デバイスの取り外し、ポートが Powered-Off 状態になること、リセット処理中のエラーなどによっても起こります。
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)は、リンク上のエラーが原因でポートが Disabled になった場合にのみ発生します。仕様には、それ以外の理由では通知は行われないと明記されています。
したがって、PORT_ENABLE を 0 に変更するのは、PORT_RESET を直接変更するのとほぼ同じ効果があります。ただし欠点が 1 つあります。USB 3.0 仕様のセクション 10.14.2.6.1 によれば、PORT_ENABLE は「SuperSpeed ハブではサポートされない」のです。
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 デバイスは、実質的に 2 つのデバイスから構成されています。SuperSpeed 用の独立したデバイスと、USB 2.0 用の独立したデバイスです。これら 2 つの USB バージョンは、USB ケーブル内の別々の配線を使用しており、電気的にも概念的にも互いに独立しています。
USB 仕様書は、すべての SuperSpeed デバイスがこの 2 つのデバイスで構成されることを要求しています。ただし、実用上は USB 2.0 側は必須ではありません。言い換えると、USB 2.0 に対応していない SuperSpeed デバイスでも、SuperSpeed ポートに接続すれば正常に動作します。
SuperSpeed デバイスを SuperSpeed ポートに接続した場合、まず SuperSpeed インターフェイスでの接続が試みられます。それが失敗すると、USB 2.0 での接続が試みられます。実際には(そして仕様上も)、USB デバイスが両方のバージョンで同時に接続されることはありません。ただし、同時に接続すること自体は可能であり、その場合、USB デバイスは 2 つの別々のデバイスであるかのように動作します。
SuperSpeed ハブは、2 つのハブが並列に合体したものです。USB 2.0 用と SuperSpeed 用です。外付けの SuperSpeed USB ハブをコンピュータに接続すると、システムにはハブが 2 つ追加されます。それらは 2 つの別々のデバイスとして表示されます。通常の USB デバイスは両方のバージョンを並行して使うことはできませんが、ハブはそうする必要があります。
SuperSpeed ハブを USB 2.0 ポートに接続した場合、そのハブは通常の USB 2.0 ハブとして動作します。
一般に、これら 2 つのハブは互いに独立して動作します。各ハブは独自のポートを持っており、各ポートも独立して動作します。特に、並列した一方のハブであるポートのパラメータを変更しても、他方のハブのポートには影響しません。
ここから導かれるもう 1 つの結論は、「lsusb -t」でデバイスが SuperSpeed ルートハブ経由でコンピュータに接続されていると表示される場合、そのデバイスは SuperSpeed デバイスとして動作している、ということです。つまりデータレートは 5Gbit/s 以上です。同様に、USB 2.0 ルートハブ経由で接続されている場合、データレートは 480Mbit/s 以下です。
SuperSpeed と PORT_POWER
前述のとおり、hubpower が実際に行っているのは、ポートの PORT_POWER 属性を変更することです。
しかし SuperSpeed ハブは 2 つのハブが並列になったものです。その 2 つのハブは、それぞれポートごとに独立した PORT_POWER 属性を持っています。では、ハブはいつ VBUS を遮断すべきなのでしょうか。物理的なパワースイッチは、並列した 2 つのハブのそれぞれにある PORT_POWER 属性、つまり 2 つの PORT_POWER 属性に依存します。
USB 3.0 仕様の表 10-2 に、ハブが電源を供給すべきかどうかの真理値表があります。まとめると、次のようになります。ハブが USB 2.0 ハブとしてのみ動作する場合(例: SuperSpeed に対応していないコンピュータに接続されている場合)、USB 2.0 側の PORT_POWER に従います。SuperSpeed ハブとして接続されている場合(つまり並列した両方のハブが接続されている場合)、VBUS が遮断されるのは、両方の PORT_POWER が 0 のときだけです。
複雑に聞こえるかもしれませんが、ここまでが実は簡単な方です。SuperSpeed 側のハブは、内部ステートマシン(state machine)が異なります。リンクトレーニング(link training)のやり方が異なるのですから、当然といえば当然です。さらにこのステートマシンは、PORT_POWER が 0 のときに取り得る状態が 3 つあります(USB 2.0 では 1 つだけです)。その 3 つとは、次のとおりです。
- DSPORT.Powered-off: ポートが完全に非アクティブな状態。
- DSPORT.Powered-off-detect: ポートが SuperSpeed のリンクパートナーを検出しようとしている状態。
- DSPORT.Powered-off-reset: ポートがリンクパートナーに対してウォームリセット(warm reset)を実行する状態(詳細は後述)。
最後の 2 つの状態の目的は、自己電源を持つ SuperSpeed デバイスがポートに接続された場合に、接続が USB 2.0 にフォールバックしないようにすることです。自己電源デバイスには、SuperSpeed ポートに接続されたことを認識する手段がないため、放っておくとフォールバックしてしまうからです。
では、ここから何が分かるのでしょうか。主な教訓は、SuperSpeed ハブで PORT_POWER を変更してデバイスをリセットするのは、USB 2.0 ハブの場合ほど簡単ではないということです。
SuperSpeed: ウォームリセットとホットリセット
USB 2.0 のデバイスリセット方法は単純です。ハブが 2 本の信号線を 10ms の間グランドに接続します(SE0)。一方、SuperSpeed デバイスには、ウォームリセット(warm reset)とホットリセット(hot reset)という 2 つのリセット方法があります。これらを、リセットの「理由」を表す PowerOn Reset や Inband Reset と混同しないでください。Inband Reset は、ホストからの要求によってリセットが発生することを意味します。要求の種類によって、それがウォームリセットになるかホットリセットになるかが決まります(詳細はすぐ下で説明します)。
ウォームリセットとホットリセットの区別は重要です。特に、ウォームリセットはデータストリームを停止して、最初から立ち上げ直す操作を伴います。ホットリセットはデータストリーム自体に送られるもので、データストリームは流れ続けます。したがってホットリセットのほうがはるかに高速です。しかし、データストリームを再起動すれば解決するような問題がある場合は、ウォームリセットが必要になります。そのような問題の例としては、物理層でのビットエラーがあります。ビットストリーム(bitstream)をいったん落としてやり直すと、イコライザ(equalizer)の調整が最適でない場合などを修正でき、問題が解決することがあります。
usbreset は USBDEVFS_RESET を使った ioctl() を実行するのでした。その中で行われる処理の 1 つとして、デバイスの USB バージョンに関係なく PORT_RESET が 0 に変更されます(Linux カーネル v5.16 現在)。
USB 3.0 仕様のセクション 7.4.2 によると、PORT_RESET 要求を行うと Hot Reset になります。これは、データストリームがアクティブな場合、それを破棄せずに、そのデータストリーム上でリセットコマンドが送られることを意味します。同仕様は BH_PORT_RESET(フィーチャ番号 28)も定義しており、これは(ポートが Disabled 状態でない限り)ウォームリセットを強制します。こちらのリセットのほうがより根本的です。データストリームを停止し、立ち上げ直す手続き(LFPS シグナリングによる)を最初からやり直します。重要な違いは、データストリームの再起動が必要な場合、BH_PORT_RESET なら対応できるが、PORT_RESET では対応できないということです。
新しい SuperSpeed デバイスの接続にはウォームリセットが伴います。それなのに、SuperSpeed ポートの PORT_POWER を操作して同じ効果を得られるツールが(おそらく)存在しないのは残念です。前述のように、uhubctl は技術的には可能ですが、デバイスがおかしな状態になってしまいます。
雑多な覚え書き
以下は、役に立つかもしれないが、はっきりした文脈がない情報を並べたものです。
- PORT_POWER のようなコマンドのコードは、USB 2.0 仕様の表 11-17 と USB 3.0 仕様の表 10-8 に、Hub クラスのフィーチャセレクタ(feature selector)として記載されています。
- カーネル内で 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 では、proc_resetdevice() が usb_reset_device() を呼び出します。proc_resetdevice() は、該当するデバイスファイルに対する USBDEVFS_RESET の ioctl() によって起動できます。これが usbreset の正体です(前述のとおり)。
- これは libusb の linux_usbfs.c にある op_reset_device() に相当します(libusb は代わりに IOCTL_USBFS_RESET を使いますが、これは USBDEVFS_RESET と同値です。どちらも 20 で、カーネルの include/uapi/linux/usbdevice_fs.h と比較してください)。libusb の関数はインターフェイスのクレーム解除も行います。一方 proc_resetdevice() は、デバイスのいずれかのインターフェイスがクレームされている場合、丁寧にリセットを拒否します。
- hub.c では、hub_event() が port_event() を呼び出します。後者の port_event() がポートのビット変化を検出します。hub_event() はワークアイテムで、kick_hub_wq() によって起動されます(特に hub_irq() によって起動されます。hub_irq() は、この目的に指定されたハブの IN エンドポイントに状態変化通知が届くと、それに応答して「ポート状態の変化やさまざまな障害のときに発火」します)。