はじめに
MikanOSをRustでやってますが、しんどいです。 の続き。
RustでMikanOSをやっていて、いよいよUSBマウス入力をとるぞという段階になって半詰みになりました。 筆者が提供しているUSBライブラリはC++であり、使えないからです。 筆者も言ってるように、USBライブラリを作るのはそれ自体がかなりややこしい領域であり、 OS自作本としてはスコープ外というのは正しく、だからこそライブラリを提供しているわけですが、 Rustaceanに対してはこのような優しさはないため、他の方法でなんとかしなければいけません。 はっきりいうと、Mikan本はRustベースで書くべきだったと思います。C++での実装にもう価値はないと思います。
まず考えたのが、既存のライブラリをなんとかして使うことです。 探してみると、色々なライブラリが見つかりました。 xHCIのライブラリはまぁまぁ理解出来ました。 その他、レポートディスクリプタなども実装したかなり本格的なライブラリもありました。 しかし残念なことに、一体各コンポーネントがどこからどこまでの機能を実装したものなのか、 それが自分のほしいものなのかどうかも判断がつきません。 なんとなくですが、xHCIを初期化することはライブラリでなんとか出来る可能性はあるが、 その先は他の方法でなんとかする方法があるだろうという感想を持ちました。
筆者のソースコードを半分脳死で写経するというのも手かも知れませんが、 そこまではしたくないと思いました。
そこで、脳死写経は無駄だが、USBについて本を読んで学ぶことは無駄ではないだろうと思い、 著者が過去に書いたUSB 3.0 ホストドライバ自作入門 という本を読むことにしました。 全体像を理解すれば、ライブラリの理解にも繋がると思ったからです。
本は50ページ程度と薄めなのですが、プロトコルの説明(どこどこレジスタに何を書くだとか) がぎっしり敷き詰められていて、全体像が見えにくいです。 おれが知りたいのは全体像であり、少なくとも全体像を1ページにまとめた文書があった方が 詳細を理解する上でも役立つだろうと思ったので、本の中の詳細は一切無視してふわっとした流れだけをまとめたいと思います。
抽象層
USBマウスと通信を行うには、CPUから見るとxHCが窓口となる。 しかし、xHCを使うという抽象を隠蔽しておいた方が、色々と都合が良い。 そこで、
- クラスドライバ:HIDなどUSBのクラスに特化した実装を提供する。L7に相当
- バスドライバ:エンドポイントというCPUとデバイスをつなぐパスを定義する。L4に相当
- ホストコントローラドライバ:xHCを初期化し、データのやりとりをしたり、割り込みを発生させたりする。L2に相当
というレイヤリングを行う。 OSIモデルとの対応は厳密ではないが、イメージをわかりやすくするために書いた。 これを低レイヤ側から順に設定していく。
xHC
リングバッファ
xHCは、以下3種類のリングバッファを使って外とやりとりする。
エントリはTRB(Transfer Request Block)と呼ばれる。
- Command Ring:xHCを初期化・設定するため
- Transfer Ring:CPUとエンドポイントとのデータ通信用。エンドポイントごとに存在する
- Event Ring:エンドポイントに入力があった時などにここにTRBが挿入され、CPU割り込みが発生する
Transfer RingとEvent Ringは、io-uringのSubmission QueueとCompletion Queueのようなもの。
リングバッファは、サイクルビットというデータを使い実装されている。
CPU側からCommand RingやTransfer Ringにエントリを挿入したことをxHCに通知するには、 ドアベルレジスタというのを使う。
xHCの初期化
xHCを動作させるためには、以下のようにデータを設定する必要がある。 デバイスコンテキストは、USBデバイスごとに必要で、この先頭アドレスを格納するための DBCAAという配列をあらかじめソフトウェア側で用意する必要がある。 まだUSBデバイスは差し込まれていないため、要素はNULLで初期化すればよい。
xHCの初期化は、
- リングバッファの用意
- DBCAAの用意
- 割り込みの設定(本ではMSIとLocal APICを使う)
を行う。
これらは、メモリマップされたレジスタを使って行う。
デフォルトコントロールパイプの取得
xHCの初期化を行うと、リングバッファを使って通信することが可能になる。
これにより、USBデバイスがポートに接続された時にEvent Ringを通じて 「ポートにUSBデバイスが差し込まれた」ことを知ることが出来るようになる。 その後、 スロットの割当、デバイスコンテキストの割当を Command Ringを通じて行うことで、 USBデバイスに対するデフォルトコントロールパイプ(エンドポイント0)を得る。
このデフォルトコントロールパイプはどのUSBデバイスも必ず持っている。
HID
ディスクリプタの取得
デフォルトコントロールパイプを取得したら、 Transfer Ringを使ってコントロール転送を行うことで、 各種ディスクリプタを取得出来る。
ディスクリプタは階層的であり、
- デバイスディスクリプタ
- コンフィギュレーションディスクリプタ
- インターフェイスディスクリプタ
- エンドポイントディスクリプタ
の順に取得出来る。
HIDの初期化
インターフェイスディスクリプタを見ると、 クラスコードからHIDクラスであることがわかり、 サブクラスコードや、プロトコルから、 どのようなプロトコルで通信するかがわかる。
HIDクラスでは一般に レポートディスクリプタを使って、プロトコルを決めることが出来る。 これにより、プロトコルを自由に定義することが出来る。 マウスとキーボードに関しては事前定義されたブートインターフェイスというものが存在し、 これにより容易にアクセスすることが出来る。
HIDクラスは3つのエンドポイントを持つ。
- Control:デフォルトコントロールパイプ(In/Out)
- InterruptIn:デバイスからの非同期入力
- InterruptOut:デバイスへの非同期出力
InterruptInは、HIDデバイスに対する入力を 非同期的に受け取るインターフェイスであるが、 その仕組みは擬似的なものである。 CPU側からあらかじめNormal TRBをTransfer Ringに挿入しておくと、 xHCは新しいデータをデバイスに対してポーリングし、データがある場合に読み込み転送をし、 Event RingにTransfer Eventを挿入する。(それによってCPU割り込みが発生する)
入力データは、InterruptInエンドポイントを使わずとも、 Controlエンドポイントに対して コントロール転送を発行することにより取得することも出来る。