借り初めのひみつきち

仮ブログです。

moe の xHCI / USB 実装

現在の moe の xHCI / USB 実装について軽く解説します。
実装の解説がメインで、各レジスタやビットの意味などあまり詳細に解説すると本が書ける量になってしまうので省略ご了承ください。

xhci.c, xhci_init() PCI エニュメレーションとコンフィグレーション

xHC は通常の PC では PCI バスに接続されているため、まず xHCI ドライバが PCI バスから xHC を探します。
本来は PCI ドライバが各デバイスをエニュメレーションしてからデバイスに適合するドライバを呼び出すのが筋だと思いますが、現状 PCI バスを利用しているのが xHCI ドライバだけであり、 xHC は現代ではどのような PC にも搭載されている基本的なデバイスだと思うのでこのような実装になっています。

xHC を見つけたら PCI コンフィグレーション空間にある BAR (Base Address Register) から xHC のベースアドレスを取得します。
moe を起動した時点では PCIバイスのページテーブルは自動的に設定されていないので API を呼び出してメモリにマッピングします。

xhci.c, xhci_init_dev() xHC 初期化

xHC をメモリにマッピングしたら各レジスタにアクセスして xHC を初期化します。

xHCI は非常にたくさんのバッファーを必要とするので各バッファーの確保と初期化もほとんどここで実行します。*1

xHC からの割り込みには MSI (Message Signaled Interrupts) という仕組みを利用します。
MSI は割り込み通知専用の信号を使わずに特定のメモリアドレスに特定のデータを書き込んだ時に割り込みが発生する仕組みで、特定のデバイスに依存しないために実際に設定するアドレスなどの詳細仕様は決まっていません。 PC における MSIAPIC が処理する前提となっていて設定するアドレスやデータの内容は LocalAPIC のレジスタによく似ています。実際に設定する内容は xHCI ドライバでは定義せずに APIC ドライバから取得します。

全ての準備ができたら USB_CMD レジスタの Run / Stop ビットをセットして xHC を開始します。これ以降いくつかの設定レジスタにはアクセスできなくなります。

xHC を開始したら全てのポートをスキャンしてポートリセットを実行します。
本来はポートリセットしなくても接続イベントが発生するはずですが、 qemu はリセットしないとイベントが発生しないのでこのような実装になっています。

ここまでで初期化は終了します。

xhci.c, xhci_msi_handler() 割り込み

moe の xHCI 割り込みハンドラはとてもシンプルで、イベントスレッドのセマフォにシグナルを送信するだけとなっていて、実際のイベント処理はイベントスレッドで行います。

xhci.c, xhci_event_thread() イベントスレッド

イベントスレッドでは xHC からのイベントを処理します。
イベントは主に3種類ありますが、イベントスレッドはイベントを必要とするスレッドに通知するのみで具体的な処理はほとんどしません。

  • ポートステータスチェンジイベント
    • 主に USB の抜き差しなどで USB ポートの状態が変化した時に発生するイベントです。ポート設定スレッドに通知します。
  • コマンドコンプリーションイベント
    • コマンドの実行結果を返すイベントです。コマンドを実行したスレッドに結果を通知します。
  • トランスファーイベント
    • 転送コマンドの結果を返すイベントです。転送コマンドを実行したスレッドに結果を通知します。

xhci.c, xhci_config_thread() ポート設定スレッド

ポートステータスチェンジイベントを受け取ったポートの設定をするためのスレッドです。
ポートリセット処理中に他のポートリセット操作が混在するとうまく動作しない機種があるらしいのでこのスレッドで1ポートずつ順番に処理します。
コマンド実行待ちの間も別のイベントが発生するため、イベントスレッドとは別に処理します。

まずは、初期化またはデバイスを接続した時にイベントが発生するのでそこでポートリセットします。
リセットが終わったら xHC のデバイススロットを有効化します (ENABLE_SLOT_COMMAND)
その後、 ADDRESS_DEVICE_COMMAND を実行します。このコマンドは少し複雑なコマンドで、 USB ポートと xHC のデバイススロットを紐付け、コントロール転送エンドポイントの設定と USB デバイスにアドレスの割り当てをします。
ここまで実行するとデバイスと通信できるようになるので、 USB ドライバにデバイス接続したことを通知してデバイスのコンフィグレーションを行います。
ここで XHCI は表舞台から姿を消して以降は USB レイヤーの話になります。

usb.c, usb_new_device() USB デバイスのコンフィグレーション

USB はプラグアンドプレイを実現するために全てのデバイスは自分がどんなデバイスでどんな機能をサポートしているか記述された各種ディスクリプターを持っていて、ディスクリプターの内容にしたがってデバイスの初期化や適合するデバイスドライバを決定したりします。

まずはデバイスディスクリプタを読み出します。
FS デバイスxHCI ドライバの設定したデフォルトのパケットサイズでは以降の処理がうまくできないので、デバイスディスクリプタに記述されている値でパケットサイズを設定し直します。

次にコンフィグレーションディスクリプタを読み出します。
コンフィグレーションディスクリプタの実体はたった 9 バイトですが、インターフェースディスクリプタやエンドポイントディスクリプタもセットでくっついた不定長になるので、一度先頭だけ読み取って全体のサイズを取得してから改めて全体を読み直し、各ディスクリプタをパースしていきます。

ディスクリプターをパースする最中にコンフィグレーションの有効化、インターフェースの列挙、エンドポイントの設定などをします。

通常の USB デバイスはコンフィグレーションはひとつしか存在しませんが、複数のインターフェースを持っている場合があります。
とくに HID デバイスなどは本質的にはコンポジットデバイスで、デバイスディスクリプタにはクラスコード000000が記述されているので、インターフェースディスクリプタに設定されているクラスコードで識別する必要があります。

ディスクリプタの処理が終わったらデバイスクラスやインターフェースクラスに適合するクラスドライバーを検索します。

usb.c, hid_start_class_driver() HID クラスドライバの初期化

キーボードやマウスなど主に人間がコンピューターを操作するためのデバイスを HID (Human Interface Device) といい、 USB キーボードやマウスは全て HID という規格に則ったデバイスです。

HID インターフェースを検出したら HID クラスドライバーを初期化します。
現在の実装では OS側にノンブロッキング I/O を実現する手段がないのでインターフェースごとにスレッドを作成しています。この方法はオーバーヘッドが大きいのでそのうち修正したいです。
ここまできてやっとデバイスと単純なデータの通信ができる準備が整います。

usb.c, hid_thread() HID キーボードやマウスの読み取り

キーボードやマウスに対してデータ読み込みコマンドを実行します。

すべての HID デバイスは読み込み用のエンドポイントを持っていて、読み込みを実行するとレポートという構造体を返します。この構造体の構造は3種類ありますが、現在の moe では BIOS 向けに用途が固定されたブートプロトコルのキーボードとマウスのレポートのみ扱います。
より汎用的なレポートプロトコルバイスは HID ディスクリプタのパースが面倒なので当面は対応しません。ただし、定期的に読み込みを行わないとブートプロトコル側のデータ送信も止まってしまうデバイスが存在するので、その対策として全ての HID デバイスはとりあえず空読みしてデータが止まることがないようにしています。

レポート構造体を読み込んだら HID マネージャに渡すとパースしてキーボードやマウスのイベントに変換してくれます。

*1:現代の技術ならたかだか数十KBのメモリをチップ内に実装するのは容易なはずで、わざわざメインメモリからバッファを確保して渡さないといけないのは納得できませんが