前回に引き続き、『ゼロからのOS自作入門』(通称:ミカンOS本)を読み進めています。「6.4 ポーリングでマウス入力」ではUSB接続のマウスに対応します。
USBホストコントローラを制御するためのドライバの実装は書籍では省略されていますが、ソースコードが公開されているので、それを写経しながら勉強しました。
エミュレータ(QEMU)上では順調に動いたのですが実機で動かすのに手こずった、、という日記です。けど動きました!
処理の概要
USBマウスとOSの橋渡しとなるハードウェアとしてUSBコントローラという制御チップがPCに搭載されている。この制御チップを制御するソフトウェアがドライバ。
ドライバ部分のソースコードと、ドライバを利用するKernel部分のソースコードを自作する。
USBホストコントローラーの規格として、USB1.1用の OHCI , UHCI 、USB2.0用の EHCI 、USB3.x用の xHCI がある。xHCIに準拠したホストコントローラのことを xCI と呼ぶ。
書籍のMikanOSでは xHCI 規格のみサポートしているとのこと。
めちゃくちゃ大雑把に言うと、以下のような処理を実装する。
- PCに搭載されているxHC(=USBコントローラ)を見つけ出す
- xHCのMMIOアドレスを読む
- ドライバの処理を呼び出し、xHCを初期化・起動する
- 画面上のカーソルを動かす
ハードウェアの構成
ハードウェアとして以下のような構成になってる。

上図のPCIバスにつながるPCIデバイス(USBコントローラやGPUなどなど...)までが、PCの端末内に収まってるイメージ。
PCIデバイスは PCIコンフィギュレーション という情報を持っており、その中の Class Code を読み取ることでPCIデバイスがどういうものなのか判定できる。

引用元 ハイパーバイザの作り方~ちゃんと理解する仮想化技術~ 第15回 PCIパススルーその1「PCIパススルーとIOMMU」
xHCに対応したUSBコントローラなら 0x0C0330 である。
IOアドレス空間でPCIコンフィギュレーションを読み書きする
PCIコンフィギュレーション を読むと言ってもどうすればいいのか。
メインメモリのメモリアドレス空間とは別に、周辺機器用のIOアドレス空間というのがある。空間といってもアドレスと物理的なレジスタの割り当て表のようなイメージ。この割り当て表はホストブリッジが把握している。
CPUにはIOアドレス空間の読み書き専用のIO命令がある。C++では直接IO命令を使えないので、アセンブリ言語で書く(https://github.com/kariyayo/mikan_os/blob/222ae4b7e0b5bd91040cfcb6186371228d5f1003/kernel/asmfunc.asm)。これをC++から呼び出す。
PCIコンフィギュレーション を読み書きするために、ホストブリッジが持つ CONFIG_ADDRESSレジスタ と CONFIG_DATAレジスタ を利用する。これらのレジスタは、IOアドレス空間の特定のアドレスに対応づけられている。
これらを以下のように扱うことで PCIコンフィギュレーション を読み書きできる。
CONFIG_ADDRESSレジスタに読み書きしたい位置を設定してからCONFIG_DATAレジスタを読み書きする
例えば、 PCIコンフィギュレーション のClassCodeを取得する ReadClassCode() 関数を見てみる。 PCIコンフィギュレーション は4バイトずつ読むので 0x08が読み取り開始位置。この読み取り開始位置を WriteAddress() 関数 → アセンブリ言語で書かれたIoOut32() で IOアドレス空間上の 0x0cf8 (= CONFIG_ADDRESSレジスタ )に書き込む。それから ReadData() 関数 → アセンブリ言語で書かれたIoIn32() でIOアドレス空間上の 0x0cfc (= CONFIG_DATAレジスタ )を読み取る、という流れ。
/** CONFIG_ADDRESSレジスタのIOアドレス空間上のアドレス */ const uint16_t kConfigAddress = 0x0cf8; /** CONFIG_DATAレジスタのIOアドレス空間上のアドレス */ const uint16_t kConfigData = 0x0cfc; void WriteAddress(uint32_t address) { IoOut32(kConfigAddress, address); } uint32_t ReadData() { return IoIn32(kConfigData); } ClassCode ReadClassCode(uint8_t bus, uint8_t device, uint8_t function) { WriteAddress(MakeAddress(bus, device, function, 0x08)); // 4バイト単位 auto reg = ReadData(); ClassCode cc; cc.base = (reg >> 24) & 0xffu; cc.sub = (reg >> 16) & 0xffu; // ClassCodeのsub部分は16~23なので、16ビット右シフトして上位8ビットを捨てることで取得 cc.interface = (reg >> 8) & 0xffu; return cc; }
実機で動かしたい!
ここまでで、「PCに搭載されているxHC(USBホストコントローラ)を見つけ出す」部分が実装できた。他の部分はごにょごにょと実装して、QEMUで動かすと、問題なくマウスカーソルが動いた!
実機PCでも動かしたい・・・!
実機がxHCI非サポートだった
私がMikanOSを実機で動かすときに使っているのは、だいぶ前から家に放置されてた ThinkPad X220。動くだろうか。。マウスカーソルが動かない。。
プリントデバッグしたところ、PCIデバイスを発見できているが、その中からxHC(USBコントローラ)が見つからないようだ。ググってみてもThinkPad X220はUSB3.xには対応してなさそう。
うーん、どうしようかな。実機で動かすのは諦めたくないけど、新しいPC用意するのもなぁ。ということで、PCI ExpressカードでUSB3.0に対応してみよう!
以下のような構成になる。

事象:Address Device Command が Code 17 で失敗する
PCI Expressカードを導入して実機環境で起動すると、、まだマウスカーソルが動かない!
色々プリントデバッグしたところ、USB マウスの初期化処理中に以下のようなエラーが出てOSが停止していた。
AddressDeviceCommand failed. Code: 17
xHCI の仕様書で確認すると、Code 17 は Parameter Error 。QEMU では問題なく動くコードが、なぜ実機のコントローラだと弾かれるのだろう。
原因判明:CSZ(Context Size)
引き続きプリントデバッグ。ホストブリッジが持つレジスタはIOアドレス空間を使っていた。ホストブリッジが持つレジスタを操作することで、PCIデバイス(=xHC(USBコントローラ))が持つ PCIコンフィギュレーション の情報を読み取った。xHC(増設したPCIeカード)のレジスタはMMIOという機能によってメインメモリのメモリアドレス空間でアクセスできるように初期化している。メモリアドレス空間のどこにあるかは PCIコンフィギュレーション のBase Address Register 0に記録されている、ということがxHCIの仕様で決められている。
- ホストブリッジ
CONFIG_ADDRESSレジスタCONFIG_DATAレジスタ
- xHC(USBコントローラ)
- PCIコンフィギュレーション
- 色々なレジスタ
PCIコンフィギュレーションからBase Address Register 0 を読み取って、そのアドレスを用いてメインメモリを読み取る、結果的にMMIOによってxHCのレジスタを読み取れる。
xHCのレジスタを色々出力させていたところ、USBコントローラの機能を示すレジスタ HCCPARAMS1 の値が以下のようになってた。
HCCPARAMS1: 01405cf, CSZ: 1
これが原因っぽい。xHCI 仕様書の「HCCPARAMS1」の項目を見ると、Bit 2 に CSZ (Context Size) というフラグがある。
CSZ = 0: 1つのコンテキストのサイズは 32バイトCSZ = 1: 1つのコンテキストのサイズは 64バイト
MikanOS のデフォルト実装は、QEMU や一部のコントローラに合わせて 32バイト(CSZ=0)前提の固定サイズの構造体 で実装されている。
しかし、私の PCIe 拡張カードは CSZ=1(64バイト)を要求していた。
OS 側が「32バイト目」にエンドポイント設定を書き込んでも、ハードウェア側は「64バイト目」を読み取ります。そのズレによってハードウェアは Parameter Error を返していたのではないか。
解決:CSZに対応
固定サイズの C++ 構造体を使うのをやめ、コンテキストを単なるバイト配列(uint8_t buf[...])として確保するように修正。
// context.hpp // HCCPARAMS1 を読んで CSZ が 1 かどうかを保持しておく extern bool is_csz1; struct DeviceContext { uint8_t buf[2048]; // 64バイトになっても入り切るサイズを確保 EndpointContext* GetEndpointContext(int i) { // ハードウェアの要求に合わせて 64バイト / 32バイト 幅でポインタを計算 return reinterpret_cast<EndpointContext*>( buf + (is_csz1 ? 64 : 32) + i * (is_csz1 ? 64 : 32) ); } };
これで QEMU(CSZ=0)でも実機(CSZ=1)でも、ハードウェアが期待する正しい位置にデータを書き込めるようになったはず。
実行したところ、ついに実機の PCIe 拡張カード越しにマウスのカーソルが動いた!嬉しい!!
※余談:構造体のサイズを変更した際、make clean がサブディレクトリの .o を消さない仕様だったため、一部のファイルだけ古いサイズのままリンクされてしまい、謎のメモリ破壊(ポインタの消失)に数時間悩まされるという罠もありました。。
おしまい
ハードウェアや規格の仕様書は偉大。原因特定まで時間がめっちゃ掛かったー、もっと上手にやりたいところですなぁ。。
