借り初めのひみつきち

仮ブログです。

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のメモリをチップ内に実装するのは容易なはずで、わざわざメインメモリからバッファを確保して渡さないといけないのは納得できませんが

GPD MicroPC

正直、 GPD 社に期待はしてるけど今まで出した製品はあまり出来がいいと思ってない Nerry です。

エンジニア向けとして期待されていた GPD MicroPC ですが、やっと触る機会ができたので自作 OS の観点から調査してみましたヽ(•̀ω•́ )ゝ✧

結局、 Portrait なの? Landscape なの?

最近の小型 PC には避けて通ることができない Portrait 問題。

これは小型の液晶が基本的にスマホタブレットで使う前提で作られていることから Portrait (縦長) のものが多く、コストや性能の関係でスマホ用の液晶を止む無く横倒しにして使ってる事が多いことに起因する諸問題です。

GPD MicroPC が出る前後もこの問題で話題になり、 Landscape 液晶を使ってるのではという情報も飛び交った事がありました。

結論から言うと、少なくともデフォルトでは Portrait です。

通常起動すると GOP は解像度 720 x 1280 として報告し、実際 VRAM の構造もそのようになっています。最近のよくある小型 PC と変わらないですね。

問題は似非 Landscape モードがある点です。

内蔵の UEFI シェルから起動すると通常とは異なるドライバが読み込まれるようで、 GOP の報告する解像度が 1280 x 720 になり、 gop->Blt を呼び出した場合もそのように振舞います。

しかし、実際の VRAM 構造は Portrait のままなので、その状態で Windows を起動するとなんと横倒しのおかしな画面で起動します。(ディスプレイドライバが起動する前は)

これは本来

HorizontalResolution = 720
VerticalResolution = 1280
PixelsPerScanLine = 720

の画面モードを、単純に縦横入れ替えて

HorizontalResolution = 1280
VerticalResolution = 720
PixelsPerScanLine = 720

として報告しているようです。矛盾してます。

通常 PC の画面は HorizontalResolution <= PixelsPerScanLine が成立するので*1、拙作の OS では HorizontalResolution > PixelsPerScanLine だった場合に縦横を入れ替える処理を入れて対処しました。

USB とキーボードあれこれ

xHCI が特殊でうまく通信できないという前情報があったのでその調査がメインのつもりでしたが、拙作の OS では外部 USB キーボードが普通に動いたので、特に変わった挙動はなかったように思います。

内蔵キーボードが曲者で、ルートハブに繋がっている VID:PID = 6080:8061 という奇妙な ID *2 でプロダクト名も 「USB KEYBOARD」 と名乗っているデバイスのインターフェース #0 クラス 3.1.1 (ブートプロトコル、キーボード) からはデータがうまく読み出せず、インタフェース #1 クラス 3.0.0 (汎用 HID) から読み出せるデータはタッチパッドのデータのようでした。お前は一体何者なんだ。

一方 LPC バスに PS/2 コントローラーも存在し、ほとんどの PS/2 コマンドに対して意味のある応答があり、キーボードが存在するかのような応答はあるのですが、実際のキー信号がなぜかうまく取れません。
2回だけ、なぜかちゃんとキーボードが入力できた事があったのですが、再現性が不明で再起動後は同じコードでも何も起きず、ポーリングしてもキーコードを返してくれませんでした。

また、 UEFI シェルから ACPI ENABLE するコマンドを実行してみると USB 関連が正常に動作しなくなりますが、内蔵キーボードは相変わらず動作していました。

以上の経緯から内蔵キーボードは PS/2 接続してる可能性が濃厚ですが、 USB ・ PS/2 ともにキーボードとみられるデバイスにうまくアクセスできず、拙作の OS からはキー入力がちゃんとできていない状態です。

ACPI とレガシーモード

HW reduced ACPI ではありません!!(超重要)
先述のように ACPI ENABLE コマンドを実行すると UEFI の USB ドライバの挙動が変わります。

BIOS 設定画面で UEFI モードかレガシーモードか選択できるのですが、レガシーモードで起動すると起動デバイスが見つからないので結局使い道がありませんでした。ドライバが足りないだけで CSM は入ってるんでしょうか?

全体的にハードウェアは近年のよくある小型デバイスよりもレガシーな雰囲気があります。

TL; DR

GPD って変なデバイス好きですね。

*1:稀にパディングを入れて HorizontalResolution < PixelsPerScanLine になる機種があるが、ほとんどの機種では WindowsUEFI 要件によって HorizontalResolution = PixelsPerScanLine になる

*2:検索してもベンダーIDがまったく見つからない

BitTest 命令

x86 には BitTest というマイナーな命令があります。

具体的には BT BTS BTR BTC の4種類あって、それぞれ特定のビットをテスト(1かどうか返す)、セット(1)する、リセット(0)する、反転する(0⇔1)命令になります。

BitTest 命令をレジスタに使った場合はちょっとだけビット操作が楽になりますが、別に AND OR XOR でもいいよね感がありました。

実は BitTest 命令はレジスタに対して実行した場合とメモリに対して実行した場合重要な違いがあります。

x86 CPU はシフト命令などでビットを指定するオペランドは 32bit (64bitオペランドの場合は64bit) に制限され、例えばシフト命令に 100 を指定しても実際には 100回もシフトしませんでした。
BitTest 命令をレジスタに対して実行した場合はシフト命令などと同様に 32bit の制限がかかっていますが、メモリに対して実行した場合は 32bit の制限がありません。
つまり、何百何万もある巨大な配列の先頭を指定して BitTest 命令を実行するとそこから何百何万番目の遥か遠くにあるビットに対してビット操作する事ができます。

また、マルチスレッドプログラミングにおいても重要な機能があり、 BitTest 命令は実行前の対象ビットの値を CF レジスタに返します。
つまり、 LOCK プレフィックスとセットで使うと Mutex などのメモリ上の特定のビット操作をアトミックに扱う事ができます。

マイナーだけど、すごい命令です。

exFAT について

exFAT というファイルシステムがあります。
FAT ファイルシステムの次世代ファイルシステムとしてリムーバブルメディアなどに採用実績がありますが、仕様が公開されておらず特許が絡むために Microsoft 製以外の OS からはあまり使い勝手がよくありませんでした。

しかし、仕様が公開され、特許の条件も緩和されようとしているようです。

docs.microsoft.com

exFAT は名前の通り FAT を改良したファイルシステムで、以下のような FAT によく似た特徴があります。

  • クラスタという単位でセクタを管理しており、最初のクラスタ番号は2である
  • FAT というテーブルによりクラスタチェインを管理している
  • 32バイト単位のディレクトリエントリでファイルを管理している

一方で以下のような相違点もあります。

  • 空き領域はビットマップで管理されており、 FAT 上で管理されていないファイルが存在する
  • 8.3 ファイル名が存在せず、通常のファイルはディレクトリエントリを複数必要とする
  • FAT には存在しなかったメタデータ拡張機能が存在する

FAT はフロッピーディスクの時代に設計されたもので小容量ではそれなりにうまく動いていましたが、メディアが GB 単位の時代になってくると色々と不都合が増えてきました。 exFAT はより大きなメディア、特にソリッドステートメモリーでうまく扱えることを前提に設計されています。

クラスタ

FAT におけるクラスタとは、生のセクタを直接扱うと量が多すぎて管理が大変になるので、複数のセクタをまとめてクラスタという名前の論理セクタとして扱うことでトータルの論理セクタ数を減らして管理を楽にするための概念です。
よく使われるクラスタサイズとして 4KiB や 32KiB などがあります。4KiBはページングのページサイズ、32KiBはよくあるフラッシュメモリーのページサイズと一致するため管理の都合が良い数値です。

クラスタ単位でディスクアクセスすることでセクタ単位でアクセスするよりも高速で効率的なディスクアクセスが可能になります。
一方で、クラスタサイズはファイルの最小単位でもあり、1バイトでも内容のあるファイルはディスク上で最低1クラスタを占有するため、クラスタサイズが大きすぎると必ずしもディスクを効率的に扱えるとは限りません。

クラスタ番号は歴史的な事情で2から開始します。*1 クラスタ番号から実際のセクタ番号に変換する場合はオフセットを考慮する必要があります。

FAT とクラスタチェーン

クラスタサイズを超えるファイルを格納するには2つ以上のクラスタが必要です。
1番目のクラスタ番号はディレクトリエントリから辿れますが、2番目以降のクラスタを探す方法が別途必要になります。
この時 FAT 系ファイルシステムは FAT というテーブルを参照する事で2番目以降のクラスタを探します。

例えばクラスタ番号123のクラスタの次のクラスタを探す場合、 FAT テーブルの123番目の要素を調べ、そこに456とあった場合は次のクラスタは456と決めることができます。

FAT では FAT の要素のサイズによって FAT12FAT16FAT32 というバリエーションがありました。*2
exFAT は稀に FAT64 という俗称で呼ばれることがありますが FAT の要素サイズは 32bit となっています。

FAT ファイルシステムでは FAT テーブルはクラスタの利用状況を管理するためにも使い、 FAT テーブル上に 0 と記述されたクラスタは空き領域、 0xF...F7 と記述されたクラスタ不良セクタや予約セクタを含んでいるのでデータとして使用禁止、という風にクラスタを管理していました。

一方、exFAT では利用状況の管理には FAT テーブルを利用せずビットマップで管理しており、クラスタが連続していることが保証されている属性のファイルは FAT 上にクラスタチェーンを形成しません。
つまり、exFAT には FAT によるクラスタチェーンで管理しているファイルと FAT を利用しないファイルの2種類のファイルがあります。

これによるメリットは2つあります。
ビットマップは FAT テーブルに比べてサイズが小さいので空き領域を割り当てる検索処理が素早くできます。空き領域をビットマップで管理するのは他のファイルシステムでも割とよく使われる方法です。
また、ファイルアクセス時に毎回 FAT 上のクラスタチェーンを辿るのはコストがかかるため、クラスタチェーンを辿らなくても連続したクラスタにアクセスできることで高速化に寄与します。

exFAT におけるファイル管理は基本的には FAT 上にクラスタチェーンを生成しないように連続した領域を割り当て、どうしても断片化してしまった時に改めてクラスタチェーンを生成するのが効率的な管理になります。 exFAT という名前でありながら基本的には FAT を利用しないのが exFAT です。

ディレクトリエントリ

FAT も exFAT も32バイト単位のディレクトリエントリという構造体でファイルの情報を管理します。

FAT のディレクトリエントリは FAT12 の時代に設計されたもので、 FAT32 の時代になるとかなり無理のある使い方をしてすでに限界に近い状態でした。
当初、ファイル名は8.3形式の短いファイル名のみサポートしていましたが、その後特殊な属性のディレクトリエントリを繋げることで長いファイル名をサポートし、互換性のために短いファイル名と長いファイル名の2つでファイルを管理していました。

exFAT でもディレクトリエントリの基本サイズは32バイトですが、ディレクトリエントリの構造を大幅に変更することで FAT よりも多くの情報を含めることができるようになり、タイムスタンプの2秒問題などが改善しています。
通常のファイルは1つのディレクトリエントリに情報が収まりきらないため3つ以上のディレクトリエントリを繋げて使用します。
8.3形式のファイル名には対応しておらず、 FAT の長いファイル名と同等の制限の緩いファイル名を使うことができます。

また、細かい違いとして従来サブディレクトリに存在した「.」「..」という存在理由のよくわからないディレクトリエントリは exFAT のボリューム上には存在しません。

メタデータ、大文字テーブル

exFAT では従来の FAT ファイルシステムにはなかったメタデータが増えています。
例えば、先述の空き領域ビットマップはメタデータとして専用のディレクトリエントリがルートディレクトリにあります。

また、 exFAT の特徴的なメタデータとして大文字テーブルというものがあります。
FAT ファイルシステムはもともとファイル名を大文字で管理し、ファイルの検索は大文字小文字を区別しません。
exFAT では大文字に変換するためのテーブルがボリューム上のメタデータとして存在します。*3
大文字テーブルで変換したファイル名からハッシュ値を計算し、ディレクトリエントリのハッシュ値と比較することで大小文字を無視したファイルの検索が高速にできるのが他のファイルシステムにはないユニークな特徴となっています。この部分が特許に絡んでいた気がしますが、ソースを見つけることができませんでした。

ソリッドステートメモリーとの親和性

exFAT は先述のようにクラスタが連続しているファイルは FAT によるクラスタチェーンを経由せず高速にアクセスできます。
また、クラスタサイズやオフセットを調整してソリッドステートメモリーのページサイズと一致させることで効率の良いアクセスが可能になっています。
そのためにソリッドステートメモリーの特性を記述できる拡張属性が定義されています。

*1:なぜ2から始まるのかはよくわかりません

*2:実は FAT32 は要素サイズは 32bit ですが 32bit 全て使っている訳ではありません

*3:しかし仕様書によると内容は固定のようです...

MOE は MEG-OS MOE になりました。

MOE は MEG-OS MOE になりました。

f:id:neriring16:20190901000042p:plain

MEG-OS Dawn

もともと MOE は内部コードネーム「MEG-OS Dawn」として企画された MEG-OS ファミリーの最も新しい OS でした。

MEG-OS Dawn の主な要件は以下の通りです。

  • 新しい 64bit OS
  • UEFI で起動し、 PIC や PIT に依存しないモダンな OS を目指す
  • マルチコアに対応する
  • 当初は PS/2 キーボード・マウスに対応し、将来は USB 対応を目指す

企画自体は数年前から存在していましたが、新しい試みが多く開発するための余裕や決意が弱くなかなか進みませんでした。
そして、とある契機で今から約一年ほど前に moe として始まることになりました。

コードネーム warbler

moe は開発が順調に進んで v0.5 になり、そろそろ USB 対応しようとしたところで細かい問題にぶつかって開発がうまく進まなくなりました。
そこで少し休眠した後 v0.6 からリフレッシュして開発再開し、ついに当初の目標を達成することができました。

古代の meg-os は v0.5 で開発が止まって v0.6 になることはできませんでした。
moe は v0.6 になった現在も精力的に開発が進んでおり、現在は v0.7 以降へ向けてロードマップの整備中です。

そこで moe を v0.6 から正式な meg-os の後継にし、今後は独立したコードネームを付ける事にします。

新しい MEG-OS MOE をこれからもよろしくお願いします。

UEFI自作OS日記 v0.6.1 ゆ。

前回の記事で紹介したように v0.6 は大幅なリメイクとなりましたが、それには大きな目的がありました。

github.com

現代の PC ではペリフェラル接続に主に USB を利用しています。
キーボードやマウスもデスクトップ PC では USB 接続が主流です。
モバイル PC は 10 年くらい前は内部的に PS/2 接続のものが多かったですが、ここ数年は USB 接続のものが増えてきています。
BIOS の時代は PS/2 ポートは正義でしたが、まもなく完全に消滅してしまうでしょう。

自作 OS も BIOSPS/2 の時代が終わって UEFI と USB の時代がきます。

USB が使えるようになれば色々なペリフェラルが接続できるようになります。 USB には未来があります。 USB に対応しないと未来はありません。

実は v0.5 の非公開バージョンで USB 対応を初めていてある程度のところまで動いていたものの、あと一歩のところで不可解なバグに悩まされる日々が続いていました。

そんなわけで、余計な肉を削ぎ落として安定させた上で USB 対応するのが v0.6 の最大の目的でした。

f:id:neriring16:20190825114723p:plain

PS/2 の時代は決まった I/O ポート (0x60 など) から入出力するだけでかんたんにキーコードを取得できましたが、 USB はいくつかの層に分かれた比較的複雑な構造になっており、「デバイスと1ビットのデータをやりとりする」状態にいくまでが大変です。

一方で「デバイスと1ビットのデータがやりとりできる」状態になってしまえばキーボードなんてたかだか数バイトのデータやりとりするだけので特別難しいことはありません。

USB キーボードは HID という規格の一部です。 HID 規格自体は人間とデバイスの間のあらゆるインターフェースを定義しようとした巨大な規格なので全てを網羅するのはけっこう大変ですが、多くの機種ではキーボードとマウスはブートプロトコルという機能を制限したモードで使えるようになっているので、そちらを使えば比較的かんたんに利用できます。*1

というわけで USB 対応のうちの多くの時間は xHCI との格闘の歴史だったわけですが、筆者がはまった箇所を覚えてる限りいくつか紹介しておきます。

ポート ID とスロット ID のオリジン

xHCI では USB デバイスはルートハブの各ポートに接続しており、これをポート ID で区別します。ルートハブのポートは全て筐体の外に出てるとは限らず内蔵のデバイスの接続にも利用しています。
各ポートに接続されたデバイスは適切な初期化をすると xHCI 内部のスロットという内部データ構造に割り当てられ、スロット ID で区別します。
初期化以降はスロット ID を通して通信する事になります。

初期化時やデバイスの抜き差しでポートの状態に変化があると Port Status Change Event というイベントが発生しますが、このイベントで通知されるポート ID は1から始まります。
一方、実際の xHCI のポートに相当する PORTSC などのレジスタ

ベース + 0x10 * (ポートID - 1)

のような計算式で計算します。実質0オリジンです。
この「-1」を忘れると、イベントの発生したポートと実際にレジスタを操作するポートが1ずれる事になっておかしな事になります。

また、スロット ID も1から始まりますが、実際のスロットに相当する DCBAA という配列の項目も1から始まります。
0番目の項目は Scratchpad Buffer という xHC が内部で利用する内部で利用するバッファーの指定に使います。
これが指定されていないと、ポートの初期化などは一見普通に動作するものの xHC が Scratchpad Buffer に依存しているコマンドを実行しようとした時に再起動などの怪しい挙動します。

コントロール転送の Status Stage

USB のコントロール転送には Setup, Data, Status という3つのステージがあります。
USB プロトコルでは全ての転送命令の方向が決まっていて、 Setup Stage は常にホストからデバイスに対する出力となります。
Data Stage の方向は Setup Stage で指定する値によって異なりますが、入力したいのか出力したいのかはあらかじめ決まっているのでそんなに難しくないでしょう。
問題は最後の Status Stage ですが、なんと Data Stage の方向によって変わります。また Data Stage は存在しないこともよくあります。

これをまとめると以下のようになります。

f:id:neriring16:20190825124100p:plain

方向を正しくを設定しないと USB Transaction Error となって以降の通信がうまくいかなくなります。

キーコードの変換

実は moe は当初から USB 対応を見込んで設計されていてキーコードも PS/2 のコードではなく HID 規格のものをベースとして変換しているので、特に問題なく動作しました。

TL; DR

まだまだ問題も多いですが、とりあえず USB キーボードから入力できるようになりました。

*1:ブートプロトコルにしか対応してないキーボードや逆にブートプロトコルに対応していないキーボードもあります。