本書は USB 3.0 ホストコントローラの規格である Extensible Host Controller Interface(xHCI)のドライバ作成に必要となる情報を解説する本です.最終的に,自作 OS 上で USB キーボードから文字を入力することを目指します.xHCI 規格のすべてを網羅的に説明するのではなく,実験用のドライバを作るのに最低限の知識を解説するようにしました.ドライバ作成の各段階に沿って説明してあるので,本書を上から読むと一通りの事柄を学ぶことができます.
歴史的には USB1.1 に対応する UHCI,OHCI があり,USB2.0 に対応する EHCI がありました.EHCI は USB1.1 に対応するためにコンパニオンコントローラとして UHCI か OHCI を搭載していて,USB1.1 のデバイスの制御はそちらに任せるような仕組みでした.一方で USB3.0 用の xHCI はコンパニオンコントローラという考え方が無くなり,xHCI 自身が USB1.1 と USB2.0 のデバイスも直接制御できるようになっています.
一口に USB ホストドライバと言っても,細かく見ると次のような階層に分けることができます.上にあるものほどアプリケーション層に近く,下にあるものほど物理層に近いです.本書では各階層について一通り説明します.
本書で説明した内容をもとに作ったサンプルコードは https://github.com/uchan-nos/seccamp-os/tree/xhci*1にあります.サンプルコードは QEMU と実機(MinnowBoard Turbot)で動作確認をしてあります.主なソースコードの位置とごく簡単な説明を次に示します.
[*1] 本文で示した URL は xhci ブランチの HEAD ですから,本書執筆後にソースコードが変更される可能性があります.本書執筆当時の内容を見るには URL 中の tree/xhci
を tree/128f544c0c527f5c71ce7e76b5fc6cfb8292fa10
に変えてアクセスしてください.
Xhci
関数が xHCI ドライバの処理の起点となっています.PCI バスを検索し xHCI デバイスを見つけ,MSI の設定を行い,xHCI の初期化処理を呼び出します.DeviceManager
クラスを含みます.InterruptHandler
と事後処理用関数 PostInterruptHandler
が定義されています.InterruptHandler
は xHC の割り込みを受けて呼び出され,割り込み要因を特定してからメッセージをキュー usb_queue
に積みます.main.cpp 内のメインループ内で usb_queue
から取り出す処理があります.取り出されたメッセージは,最終的に PostInterruptHandler
に渡されます.Device
クラスを含みます.また USB デバイスから取得した情報を元にクラスドライバを生成したり,割り込みメッセージをクラスドライバへ配送する処理などを含みます.PCI バス(ソフトウェア互換性のある PCIe バスを含む)は現代の x86 アーキテクチャにおいて非常に重要な役割を果たしています.現代の多くの周辺機器,例えば I/O APIC,USB コントローラ,SATA コントローラ,NVMe コントローラ,グラフィックボードなどは PCI バス接続が前提であり,PCI バスなしではコンピュータはただの箱と言っても過言ではありません.
PCI バスを扱った書籍やサイトはいくつかありますので,本書では詳しくは扱いません.参考となる書籍やサイトを次に紹介します.
PCI デバイス(PCIe デバイスを含む)は必ず PCI コンフィグレーション空間という 256 バイトのレジスタ空間を持っています.PCI コンフィグレーション空間はいくつかのタイプに標準化されており,xHCI はタイプ 0 です*1.xHCI の PCI コンフィグレーション空間を図2.1に示します.PCI デバイスの種類(デバイスクラス)を表すレジスタや,デバイス固有のレジスタ空間を指すベース・アドレス・レジスタなどがあります.
[*1] 他には PCI-PCI ブリッジのためのタイプ 1 が定義されています.
PCI コンフィグレーション空間へのアクセスには 2 つの方法があります.PCI 時代からある I/O ポート経由でアクセスする方法(コンフィグレーションメカニズム 1 と呼ぶ)と,PCIe で導入されたメモリ空間にマップしてアクセスする方法です.後者をサポートするデバイスでは前者もサポートされているはずですから,本書では前者のやり方を説明します.
コンフィグレーションメカニズム 1 では,I/O 空間の CONFIG_ADDRESS(0xCF8)にアクセスしたいレジスタのアドレスを設定してから CONFIG_DATA(0xCFC)を読み書きすることで,その読み書きが PCI コンフィグレーション空間のレジスタに伝わる仕組みになっています.CONFIG_ADDRESS のフィールド構造を表2.1に示します.PCI 仕様書[2]の 3.2.2.3.2 に正確な仕様があります.
ビット | フィールド | 値 |
---|---|---|
1..0 | 0(4 バイト整列のため下位 2 ビットは 0) | |
7..2 | レジスタ | 読み書き対象のレジスタアドレス(4 バイト整列) |
10..8 | ファンクション | ファンクション番号 |
15..11 | デバイス | デバイス番号(0 - 31) |
23..16 | バス | バス番号(0 - 255) |
30..24 | 予約済み | |
31 | 有効化ビット | 1 = 有効 |
有効化ビットを 1 に設定したうえで CONFIG_DATA レジスタを 32 ビット幅でアクセスすることで,それが PCI コンフィグレーション空間へのアクセスだとみなされます.バス番号とは PCI バスの番号で,0 から 255 の値を取ります.デバイス番号はその PCI バスに接続されたデバイスに割り振られた番号で,0 - 31 の値を取ります.ファンクション番号はそのデバイスが持つ機能に割り当てられた番号で,デバイスは必ずファンクション 0 を持つことになっています.
バス番号とデバイス番号が決まればファンクション 0 のコンフィグレーション空間をアクセスできます.コンフィグレーション空間のベンダ ID フィールドが 0xFFFF の場合,それは無効なベンダ ID を表しています.そのことを利用し,すべてのバス番号・デバイス番号をしらみつぶしにチェックし,ベンダ ID が 0xFFFF 以外のデバイスを探すことで,簡単に PC に接続された PCI デバイスの一覧表を作成できます.
xHCI のクラスコードは,基本クラス 0x0C,サブクラス 0x03,インターフェース 0x30 です.接続された PCI デバイスの一覧からクラスコードを頼りに xHCI デバイスを探すことができます.xHCI デバイスを検索できたら,その xHCI の BAR を利用して xHCI の初期化処理を行っていきます.
PCI デバイスは一般にデバイス固有のレジスタ群を持ちます.それらが PCI コンフィグレーション空間のベンダ固有領域(0x40 - 0xFF)に収まらない場合には,メモリ空間または I/O アドレス空間にデバイス固有レジスタ群をマップして使うことになっています.その際,マップ先の先頭アドレスを設定するレジスタが BAR: Base Address Register です.BAR のフィールド構造はメモリマップ用(ビット 0 が 0)と I/O マップ用(ビット 0 が 1)で異なります.メモリマップ用 BAR の構造を表2.2に示します.
ビット | フィールド | 値 |
---|---|---|
0 | メモリ空間インジケータ | 0 |
2..1 | タイプ | 0 = 32 ビット空間,2 = 64 ビット空間 |
3 | プリフェッチ許可 | 1 = プリフェッチを許可する |
31..4 | ベースアドレス | マップ先ベースアドレス |
BAR を適切な値に設定するのは多少複雑な手順ですが,幸いなことに UEFI が起動時に適切な値に設定しているはずなので,本書ではその設定をそのまま使うこととします.つまり,自作 OS の中では BAR を読み取って下位 4 ビットをマスクすれば,それがそのままレジスタ群の先頭アドレスとなるわけです.
xHCI では 64 ビットでメモリマップを行うために,メモリマップ用の BAR を 2 つ(BAR0 と BAR1)使うことになっています.BAR0 が下位 32 ビット,BAR1 が上位 32 ビットを表します.次の式でメモリマップの先頭アドレス MMIO_BASE を求めることができます(BAR1 はビットマスクをする必要がありません).MMIO_BASE の先頭から xHCI Capability Registers が配置されます.
MMIO_BASE = (BAR0 & 0xFFFFFFF0) | (BAR1 << 32)
PCI では信号線による割り込みと MSI 割り込みの 2 種類の割り込みが規定されています.信号線による割り込みは,PCI デバイスにある INTA から INTD の 4 本のうち,いずれかの割り込み信号線の状態を変えることでホストに割り込みを通知します.MSI および拡張規格の MSI-X では,信号線は使わず,メモリ書き込みサイクルを発生させることでホストに割り込みを通知します.
信号線による割り込みは信号線が 4 本しかなく PCI デバイス同士で割り込み信号線が共有されてしまう,割り込み対象の CPU を指定できないのでマルチ(コア)CPU でうまく分散処理ができない,などの欠点があります.
一方で MSI では指定されたアドレスに対してメモリ書き込みを発生させることで割り込みを通知します.信号線による割り込みではホストに通知できる情報は 1 ビット(割り込みの有無)しかありませんでしたが,MSI は 32 ビットのデータを書き込むことになっているので,どの PCI デバイスからの割り込みであるか,さらにはどの割り込み要因であるかを割り込み発生と同時にホストへ伝えられます.幸いなことに,PCI Rev 2.2*2以上の規格に準拠したデバイスであれば MSI か MSI-X に対応しているので,本書では MSI および MSI-X を使った割り込み方法だけを説明します.
[*2] PCI Rev 2.2 は 1999 年に策定されましたので,これ以降のデバイスであれば基本的には MSI が使えます.
MSI: Message Signaled Interrupt は PCI によって規定されている割り込み方式です.信号線による割り込みと違い,メモリ書き込みサイクル*3によって CPU に割り込みます.
[*3] メモリバスにアドレスとデータを出力し,書き込みストローブ信号を出力する一連の流れのことです.
x86 系 CPU は MSI 割り込み用の特殊なメモリマップトレジスタを持っており,そのアドレスを PCI デバイス側の MSI レジスタに設定しておきます.また,割り込み発生時に書き込むデータも前もって MSI レジスタに設定しておきます.割り込みすべき状況になると,PCI デバイスは設定されたアドレスに設定されたデータを書き込みます.この仕組みのお陰で割り込み対象の CPU を指定できるばかりか,中間の回路(主に IO APIC)をだいぶ飛ばすことができます.MSI による割り込みの仕組みを図2.2に示します.
PCI デバイスの MSI レジスタはコンフィグレーション空間の Capability List 内に存在します.Capability List は様々な拡張設定をリンクリストで表現したもので,Capabilities Pointer からたどることができます.Capabilities Pointer はリストの最初の要素を指すポインタで,コンフィグレーション空間の先頭からのバイト数を表します.それぞれの要素は 0 バイト目が Capability ID,1 バイト目が Next Pointer です.Next Pointer は次の要素がコンフィグレーション空間の先頭から何バイト目にあるかを表していて,リンクリスト終端では 0 になります.
フィールド | 値 |
---|---|
Capability ID | 5(MSI Capability) |
Next Pointer | 次の Capability レジスタへのポインタ |
Message Control | アドレスのビット幅や MSI 有効化フラグなど |
Message Address | 書き込み先アドレス |
Message Data | 書き込みデータ |
Capability ID は MSI が 5,MSI-X が 17 です.MSI Capability レジスタの構造は亜種がたくさんあり複雑です.正確な仕様は PCI 仕様書[2]の 6.8.1 および 6.8.2 を参照してください.参考までに,Intel Atom E3826*4に内蔵された xHC のレジスタ構造を図2.3に,レジスタの説明を表2.3に示します.
[*4] MinnowBoard Turbot という小型ボードに搭載されています.
Message Control は MSI の設定を行うフィールドです.各ビットの意味を表2.4に示します.Multiple Message Capable/Enable は 2 のべき数(2^nのn)で指定します.xHCI では Interrupter という仕組みで複数の割り込み要因を発生させることができますが,このとき使用できる Interrupter の個数は Multiple Message Enable で設定したベクタ数までです.本書では Primary Interrupter のみを使うことにしているので,Multiple Message Enable = 0 に設定しておけば大丈夫です.
ビット | フィールド | R/W | 値 |
---|---|---|---|
0 | MSI Enable | R/W | 1 = MSI 有効 |
3..1 | Multiple Message Capable | R | デバイスが要求する割り込みベクタ数 |
6..4 | Multiple Message Enable | R/W | 実際に有効にする割り込みベクタ数 |
7 | 64 Bit Address Capable | R | 1 = 64 ビットアドレス |
8 | Per-vector Masking Capable | R | 1 = per-vector マスクをサポート |
15..16 | Reserved | R | 0 |
Message Address は書き込み先のメモリアドレスを,Message Data は書き込むデータを設定するレジスタです.それぞれの具体的なフォーマットは PCI 規格では定められておらず各アーキテクチャに任せられています.x86 系 CPU では図2.4に示すフォーマットになっています.
レジスタ | フィールド | 値 |
---|---|---|
Message Control | MSI Enable | 1 |
Message Control | Multiple Message Enable | 0 |
Message Address | Destination ID | 割り込み対象プロセッサの Local APIC ID |
Message Address | RH (Redirection Hint) | 0(Destination ID に示したプロセッサに割り込む) |
Message Address | DM (Destination Mode) | 0(RH = 0 のとき DM は無視される) |
Message Data | TM (Trigger Mode) | 1(レベルトリガ) |
Message Data | LV (Level) | 1(Assert) |
Message Data | DM (Delivery Mode) | 0(Fixed Mode) |
Message Data | Vector | 割り込みベクタ番号 |
少し複雑ですが,とりあえず使うには表2.5に示す設定を行えば動きます.この設定は,Destination ID で指定したプロセッサに対して割り込みを発生させるような設定です.
現代の x86 アーキテクチャでは割り込みは APIC: Advanced Programmable Interrupt Controller が処理します.APIC は各 CPU コアに付属する Local APIC と,外部割り込みを受け付ける I/O APIC から構成されます.Local APIC は CPU の一部なので Intel SDM[1]に詳しい仕様が載っていますが,I/O APIC はあくまでも PCI バス経由で接続されているデバイスですので,Intel SDM にはほとんど情報がありません.
Message Address レジスタの Destination ID には,RH = 0 であれば Local APIC ID をそのまま指定します.Local APIC ID はコア毎に異なる値を持っているため,割り込みを発生させるコアを任意に指定できます.
Local APIC ID を取得するには,対象の CPU コアの Local APIC ID レジスタを読む必要があります.このレジスタは 0xFEE00020 にある 32 ビットのレジスタです.Local APIC ID はレジスタの上位 1 バイトですので,次のように読み取ることができます.
ここで,Local APIC ID は各コアごとに異なるのに Local APIC ID レジスタが 1 個しかないのは変だと思うかもしれません.しかしそれは誤解で,Local APIC ID レジスタはコア毎に存在しているのです.上記のプログラムを実行すると 0xFEE00020 からデータを読むための mov 命令があるコア上で実行されることになります.その mov を実行したコアのレジスタが読み出されるのです.
OS が起動した直後はシングルコアモードになっており,そのコアを BSP: Bootstrap Processor と呼びます.したがって,マルチコアについての設定を特にしていない状態でリスト2.1を実行すると BSP の Local APIC ID が取得されます.実験段階では,とりあえず BSP を割り込み対象プロセッサとして設定すれば十分でしょう.割り込みの負荷分散をしたくなったら,それぞれのコアにバランスよく割り込みがかかるように設定します.
x86 アーキテクチャでは 0 番から 255 番までの割り込み要因を使用できます.その中の 0 から 31 まではシステム割り込み*5,32 から 255 まではユーザー定義割り込みで使います.Message Data レジスタの Vector フィールドにはいずれかの割り込み要因の番号(ベクタ番号)を設定します.
[*5] 有名なシステム定義の割り込みとしては,一般保護例外(13 番)やページフォルト(14 番)などがあります.
割り込みベクタの設定は IDT: Interrupt Descriptor Table で行います.IDT の役割や設定方法の詳しい説明は「30 日でできる!OS 自作入門」[8]や「はじめて読む 486」[9]に書いてありますので,ここではごく簡単に説明します.
IDT とは大雑把に言うと,割り込みルーチンのアドレス表です.IDT のデータ構造を図2.5に,IDT エントリ(ベクタ)のデータ構造(64 ビットモード)を図2.6に示します.ある割り込み要因が発生した際,CPU はこの IDT の中の割り込み要因に対応するベクタを見てジャンプ先を決定します.MSI に対応するためには,PCI デバイスからの割り込みを処理する関数を作り,IDT の中の未使用のベクタに関数の先頭アドレスを登録し,Message Data レジスタの Vector フィールドにそのベクタ番号を設定すれば設定完了です.