借り初めのひみつきち

仮ブログです。

最小限の USB Hub 対応 ver.2

以前 USB Hub の制御方法について記事を書きましたが、当時は USB2 Hub しか制御できていなかったのでアップデートします。

USB Hub の基本

USB ではそれぞれのデバイスに 1〜127 の 7bit のアドレスを割り当て、バス全体で最大 127 台までの USB デバイスを扱うことができます。 なお、アドレス 0 はコンフィグレーション用に予約されています。

実際の PC には多くても数個のポートしか繋ぐことができません。 それ以上のデバイスを接続するには USB Hub を使ってポートを拡張する必要があります。

USB Hub は Ethernet Hub とは異なり、独立したアドレスを持っているごく普通の USB デバイスです。 専用のドライバで制御しないとデバイスを接続してもホストと通信できません。

つまり 127 台の制限の中には Hub 自身も含まれるので、現実には Hub 以外のデバイスだけで 127 台接続することはできません。

FS / LS / HS / SS

USB 規格には大きく分けると4種類の転送速度が定められています。

  • FS / FullSpeed は元々 USB 規格で想定された規格で 12Mbps で通信可能です。 LS 以外の全ての USB デバイスは互換性のために FS として動作可能なモードを持つはずです。現代では中途半端な速度のため FS ネイティブのデバイスはマイナーとなっています。
  • LS / LowSpeed はキーボードやマウスなど低速なデバイスを低コストで実現するための規格で 1.5Mbps で通信可能です。電気仕様が FS と若干異なっていて接続した時点で認識できるようになっています。
  • HS / HighSpeed は USB 2 からサポートされた規格で 480Mbps で通信可能です。 FS を拡張したような規格で、接続直後は FS デバイスとして動作し、 Chirp という特殊なプロトコルネゴシエーションを行なってホストやハブとデバイスがお互いに HS 対応だった場合に HS に切り替わります。 XHCI ではこの動作はコントローラー側で隠蔽されるのでドライバ層で通常意識する事はありません。
  • SS / SuperSpeed は USB3 からサポートされた規格で 5Gbps 以上の速度で通信可能です。 USB3 系は転送速度の規格が若干複雑になっていて、まとめて ESS (Enhanced SuperSpeed) と表現することがあります。 USB3 は USB2 と物理層が全く異なっており、 USB3 デバイスは USB2 コネクタと別に追加された信号線で通信を行います。 *1

USB2 Hub と USB3 Hub

USB2 を乱暴に表現すると USB1 を高速化させた規格であるのに対し、 USB3 は様々な面で USB2 系とは異なるテクノロジーで構成されていて SS/ESS デバイスと FS/LS/HS デバイスの間にはいくつか明確な違いが存在します。

このことは USB Hub においても重要な違いがあり、 USB2 系 (FS/LS/HS) のデバイスツリーと USB3 系 (SS/ESS) のデバイスツリーはルートハブの時点でそれぞれ別のツリーとして扱われます。

つまり、HS Hub の下には FS/LS/HS デバイス のみが接続され、 SS デバイス が接続されることはありません。 同様に、SS Hub の下には SS/ESS デバイスのみが接続され、 FS/LS/HS デバイスが接続されることはありません。

実際の物理的な USB3 Hub には USB2 デバイスを接続することができますが、これは内部的に HS Hub と SS Hub の2つの Hub として別々に動作しているためで、 SS Hub に直接 USB2 デバイスが繋がっているわけではありません。

SS Hub は USB 規格ではオプションとなっている uuid を必ず持っていることになっていて、同一のデバイスであれば接続するプロトコルや動作モードに関わらず同じ uuid を返す決まりがあり、 uuid を比較することで相方になっている SS Hub と HS Hub を特定することが可能です。

ルートハブ

XHCI などの USB ホストコントローラー直下にあるポートのことを特別にルートハブと呼びます。ルートハブは USB デバイスとしての通常の USB Hub とは若干異なり、ホストコントローラーで直接制御します。

XHCI のルートハブのポートは USB2 のポートと USB3 のポートに分かれていて、 USB2 ポートには FS/LS/HS デバイスのみが、 USB3 ポートには SS/ESS デバイスのみが接続できます。 ステータスレジスタは USB2 ポートと USB3 ポートで同じインターフェースを使う関係上、 USB Hub のステータスレジスタと主要なフラグの相対関係は似ていますが若干内容が異なります。

通常、外部に公開されている USB3 コネクタはルートハブの USB2 ポートと USB3 ポートを組み合わせて物理的にはひとつのコネクタになっていますが、内部的な制御の流れは USB2 系と USB3 系で別になっています。

これらの接続状況は ACPI で定義されています。 myos では ACPI の対応が追いついていないのでこの辺りの情報は未対応です。

Compound Device

USB Hub 内蔵のキーボードや USB 接続の Dock のことを Compound Device と呼びます。

USB Hub は Compound Device を構成するための重要な要素にもなります。 Compound Device は物理的にはひとつのデバイスですが、 USB バスに接続されるとまずは USB Hub として接続され、 Hub のコンフィグレーションが終わるとその下にいくつかの追加の USB デバイスとして認識されます。

通常の USB Hub と Compound Device は Hub Descriptor に含まれる属性値で区別することができますが、実際のところデバイスマネージャの見た目に影響するだけで、通常の USB Hub とデバイスとして扱っても特に問題ないと思います。

Compound Device とよく似た言葉に Composite Device がありますが、 Composite Device は USB バス上では完全にひとつのデバイスとして振る舞う点が Compound Device と異なります。

RouteString

USB3 では Hub に繋がっているデバイスのルーティングのために RouteString という値を使います。

RouteString はルートハブ以外の経路のハブのポート番号を 4bit で表現したパスのような値で、最大5つまでの Hub を表現するため 4 x 5 = 20bit の値になっています。

SS Hub は RouteString を使ってルーティングするため、最大ポート数が 4bit で表現可能な 15 までに制限されます。

HS Hub は RouteString に依存しないため規格上 16 以上のポート番号を扱うことが可能で、その場合 RouteString は正確な経路を表現できません。

現実的には USB 規格でポートの消費電流の最低 100mA と最大 500mA の要件を満たすため、アップストリームから 500mA を供給してもらってハブ自身が消費する 100mA と各ダウンストリームポートに 100mA を分配できる4ポートが通常の Hub の上限となり、それ以上のポートをサポートするには電源を確保する必要があり扱いづらくなるため、4ポートより多い USB Hub はあまり存在しません。


HS Hub (USB2) の使い方

USB Hub にはひとつの Interrupt 転送エンドポイントがあり、ポートの抜き差しや異常発生時などのイベントを通知するのに使用します。 基本的な USB Hub の制御は、コントロール転送で各ポートに対して GET_STATUS でステータスを取得し、 SET_FEATURE または CLEAR_FEATURE でオン・オフを制御します。

コンフィグレーション

まずは、通常の USB デバイスと同じように Device Descriptor や Configuration Descriptor を使って通常のコンフィグレーションを行います。 Device Descriptor のクラスコードを見て USB Hub であることを認識したら、クラスドライバをロードして USB Hub 固有の初期化を行います。

USB Hub はデバイスクラスのベースクラスが 09 で、 09_00_00 が FS hub、 09_00_01 と 09_00_02 が HS hub、 09_00_03 が SS Hub を指しています。

Base Class Sub Class Protocol
09 00 00 FS Hub
09 00 01 HS Hub with single TT
09 00 02 HS Hub with multi TTs
09 00 03 SS Hub

FS Hub は基本的に HS Hub と同じ方法で扱えると思いますが、現代では入手困難なので確認できていません。 HS Hub の TT というのは FS/LS デバイスを接続した場合の速度変換用バッファのことで、 multi TTs 対応の Hub は複数の FS デバイスを接続したときの性能がシングルの時よりも若干向上するようです。 SS Hub は HS Hub と違いが多いので別項にまとめます。

Hub Descriptor

USB Hub 固有の初期化では、まず GET_DESCRIPTOR リクエストで Hub Descriptor を取得します。 Hub Descriptor のサイズはポート数によって可変長なので正確なデータを得るためには取得方法を工夫する必要があります。*2

Hub Descriptor にはいくつかの属性がありますが、最も重要なメンバーはポート数を表す bNbrPorts です。

USB2 Hub ではポート数が実質無制限 *3 なので Hub Descriptor も可変長になりますが、現実の Hub は4ポート以下が主流です。

XHCI ではデバイスが Hub の場合に特別な設定が必要なので、 Hub Descriptor を元に Slot Context の Hub、 MTT、 Number of Ports、 TTT あたりの Hub に関する設定を行い、最後に EVALUATE_CONTEXT コマンドを XHCI に送信して設定を反映します。

GET_STATUS SET_FEATURE CLEAR_FEATURE と各ポートの初期化

ハブの構成が終わったら各ポートの初期化を行います。

まずは、全てのポートに対して SET_FEATUREPORT_POWER を送信して電源をオンにします。

for port in self.hub_desc.ports() {
    self.set_port_feature(UsbHub2PortFeatureSel::PORT_POWER, port).await?;
}

次に、全てのポートに対して CLEAR_FEATUREC_PORT_CONNECTION を送信します。 これは念の為で送らなくてもいい気がします。

最後に、全てのポートに対して GET_STATUS でステータスを取得して PORT_CONNECTION ビットを調べ、1だった場合は最初からデバイスが繋がっているので個別にポートリセット(後述)します。

Interrupt 転送ループ

USB Hub の初期化が完了したら Interrupt 転送エンドポイントのデータを読み込む無限ループに突入します。

Interrupt 転送エンドポイントから読み出せるデータは各ポートで状態変更イベントがあったことを示す数バイトのビットマップとなり、 0 以外の値を読み出した場合はどこかのポートで何かしらのイベントが発生したことを示しています。

ポート番号は1から始まるのでビット番号も1から始まり、ビット0は Hub 本体でイベントが発生したことを示す特殊なビットになります。

状態変更イベントがあったことを検知したら、該当するポートのステータスを GET_STATUS で取得します。

GET_STATUS で取得するデータは 32bit 値で、下位 16bit は現在のステータス、上位 16bit はステータスビットに変更があったことを示すフラグとなります。基本的には下位 16bit と上位 16bit は同じビットが同じステータスに対応しています。 例えば、ポートの接続状態が変化すると PORT_CONNECTION ビットが変化し、同時に C_PORT_CONNECTION ビットが1になります。

ステータスの C_PORT_CONNECTION ビットが1になっていた場合は、次に PORT_CONNECTION ビットの値を調べます。 PORT_CONNECTION が1になっていた場合はデバイスが接続されたイベントなのでポートリセット(後述)を行なってデバイスのコンフィグレーションを行います。 0の場合はデバイスが切断されたのでデバイスツリーから該当のポートのデバイスを削除します。

ただし、 C_PORT_CONNECTIONPORT_CONNECTION だけで抜き差しを判断しようとすると、チャッタリングやノイズの影響でデバイスが存在しないのにデバイスを抜いたと判断するような誤動作が発生することが稀にあるので、実際の抜き差しの判断はドライバ内部で管理している接続状態も考慮した方が良さそうです。

C_PORT_CONNECTION 以外も本来はエラーのリカバリなどを行うべきですが、いずれかのビットが1の間は常にデータを送信してくるので、 C_ で始まるビットはとりあえず最後に CLEAR_FEATURE でクリアしておきましょう。

ポートリセットとコンフィグレーション

USB Hub の各ポートにデバイスが接続されたことを検知したら、デバイスを使うための準備が必要です。

まずは、 SET_FEATUREPORT_RESET を送信します。

少し待ってから GET_STATUS でポートのステータスを取得し、 PORT_ENABLE が1になっていることを確かめます。 デバイスのリセットが成功して Default フェーズになり、 USB バス上では「アドレス 0 」で通信できる特殊な状態になっています。

次に、先ほど取得したステータスからデバイスの速度を判別します。 PORT_LOW_SPEED が1の場合は LS デバイスPORT_HIGH_SPEED が1の場合は HS デバイス、それ以外の場合は FS デバイスです。 先述の通り HS Hub に SS デバイスが接続されることはないので考慮する必要はありません *4

ホストコントローラーに Hub 自身の情報とポート番号とデバイスの速度を伝えて新しいデバイスを接続する準備をします。

XHCI の場合、ここで ENABLE SLOT コマンドでデバイススロットを割り当て、 Hub からもらった情報をもとに Device Context や Slot Context を作ります。 ルートハブに繋がっているデバイスと違い、ルーティング情報としてルートハブに繋がっているポート番号や RouteString を設定する必要があります。 デバイスの速度によっては parent Hub の情報も設定する必要があります。 最後に ADDRESS DEVICE コマンドを実行すると Device Context が有効になり、この時デバイスSET_ADDRESS リクエストが送信されてアドレスが割り当てられます。

ここまで終わると該当の USB デバイスAddress フェーズになっているので、通常の USB デバイスと同じようにコンフィグレーションを進めると Configured になってデバイスが使えるようになります。

なお、 Default フェーズのデバイスが存在する状態で他のスロットでコマンドを実行すると宛先がよくわからなくなって USB Transaction Error が起こりやすくなります。ポートリセットからコンフィグレーションの間は排他制御した方が良いです。


SS Hub (USB3) の使い方

SS Hub では Hub Descriptor 、ステータスレジスタ、 Feature セレクタなど HS Hub と内容が異なりますが、基本的な考え方は HS Hub とあまり変わらないので差分を調整する程度で同じような流れで対応できます。

USB3 には FS/LS/HS の区別がないなど削除された機能や代わりに追加された機能もあるためステータスのビット位置も若干変わっています。

また、 SS Hub はポート数の上限が 15 までと決まっているため Hub Descriptor のサイズが固定長となります。

HS Hub と SS Hub の制御で大きく異なる点は、 SET_HUB_DEPTH リクエストで Hub 自身の Route String の長さを設定しないと動作しません。


いかがでしたか?

ここまで実装することで、 myos はとりあえず USB Hub が動くようになって多くの USB デバイスを接続できるようになりました。

*1:USB2 は1対の差動信号で半二重通信します。 USB3-A では追加された2対の差動信号で全二重通信し、 USB-C ではさらに追加の2対の差動信号を使って転送速度を増やしたり映像信号を流すことができます。

*2:実際のところ後ろの方の DeviceRemovable 等は無視して最小サイズで取得しても実害はあまりないです

*3:bNbrPorts が 8bit で上限 255 に対して USB アドレスは 7bit で上限が 127 なのでバスの上限を超えます

*4:実は USB3-A コネクタをゆっくり挿入すると USB2 の回路が先に繋がって USB2 デバイスとして認識する場合があり、その場合はおそらく HS デバイスとして動作します