借り初めのひみつきち

仮ブログです。

最小の UEFI Hello World

UEFI は実行ファイルに PE/COFF という形式を採用しています。

PE/COFF 形式はご存知の方も多いかと思いますが Windows で現在主流の実行ファイル形式で、名前の通り昔 UN*X で使われていた COFF 形式の派生です。
COFF から PE/COFF になる際にオリジナルの COFF の機能のいくつかは廃れ、逆に Windows に必要な情報がいくつか追加されました。その際、元の COFF にあった廃れたフィールドはそのまま残し、新たに追加されたフィールドは Optional Header に追記する形で拡張されました。
またその後 Windows の進化に合わせて PE/COFF にも新しい機能が追加されたり廃れたりしました。
このような経緯のため PE/COFF のヘッダーにはほとんど使われていないフィールドが多数存在します。*1

また、 PE/COFF は DOS や古い Windows との互換性のために DOS Stub というダミーの DOS プログラムが含まれています。*2


UEFI では仕様を決める際に実行形式として PE/COFF を利用することが決まりました。
Windows のために色々な機能がてんこ盛りされた PE/COFF ですが UEFI ではほとんどの機能が不要なので使われていません。
つまり、適当なツールで適当に生成された UEFI の PE/COFF には無駄がたくさん詰まってるということになります。


というわけで、最小の UEFI Hello World に挑戦してみたのが以下になります。345 バイトになりました。

A Experimental of Minimal EFI Hello World · GitHub

これ以上小さくするには使われていないヘッダフィールドに別のヘッダーを埋め込むようなテクニックが必要になりますが、 BaseOfCode を 0x1000 より小さくするのが難しそうだったのでヘッダーに埋め込むのは無理かもしれません。


ところで、 PE/COFF には無駄がたくさん詰まっているということは偉い人も気づいていたようで、 EFI の初期化環境である PEI 仕様では TE (Terse Executable) という形式がサポートされています。
TE 形式は PE/COFF から DOS Stub を廃止して COFF Header と Optional Header から最小限の重要な情報だけを抜粋した独自のヘッダーに差し替えたものです。

f:id:neriring16:20191104194532p:plain:w300

UEFI BIOS ROM イメージをダンプしてみると TE 形式のファイルが埋め込まれているのが確認できます。

f:id:neriring16:20191104195304p:plain:w300


PE 形式から TE 形式にコンバートするだけで COFF Header と Optional Header の無駄なフィールドがほとんど消えて数百バイト節約でき、理論上 Hello world が 120 バイト前後まで小さくできます。
しかし、試しに手元でバイナリーをいじってみたところうまく実行できませんでした。
ヘッダーの書き方が悪かったのか、それとも通常の EFI Application 実行環境ではサポートされていないのかいまのところ不明です。

*1:ただし明確に使用しないと明言されているフィールドは少なく、使われているのかどうか不明なフィールドも多数存在します。

*2:多くの場合 DOS Stub はメッセージを出すだけの小さなプログラムですが、 EXE 形式で実現可能なプログラムは何でも入れることができるのでまれに DOS Stub の部分だけでひとつのアプリケーションになっている場合もありました。

DirectInput と XInput

USB ゲームパッドは大きく分けると DirectInput 形式と XInput 形式の2種類存在しています。
いったい何が違うのでしょうか?

DirectInput

DirectInput 形式の名前の由来は Windows APIDirectInput からきていて、規格としては USB HID (03 00 00) をベースにしています。
XInput 形式よりはるか昔からあるので、古いゲームパッドは基本的に DirectInput 形式に対応しています。

HID 規格というのはユーザーの操作をスイッチやセンサーを通じてコンピューターにデータ入力するあらゆるデバイスを統括しようとしたとても壮大な規格で、 USB キーボードやマウスも USB HID 規格の一部です。

HID 規格のゲームパッドはどのボタンをどこにどう配置するのか決まっていないのでハードウェアを設計する人が自由に決めることができます。
逆に、どのボタンがどこに配置されているのかという情報はソフトウェア側からわかりません。
そのため DirectInput 形式のゲームパッドに対応するゲームではボタンの割り当てをユーザーが設定できるものが多いです。

XInput

XInput 形式は比較的最近登場した新しい形式で、規格としてはベンダー定義の独自プロトコル (FF 5D 01〜04) となっています。

XBox で使われているゲームパッドがベースになっているのでボタンの個数や配置などがある程度決まっています。
Windows で扱う場合はあらかじめ要件が決まっているので扱いやすいのだと思います。

仕様が一般に公開されておらず、第三者によるプロトコル解析によるとバリエーションが数種類あるようです。
バリエーションの判別方法や今後別のバリエーションが増える可能性を考えると Windows 以外では正常に動作する保証がありません。

最小限の USB Hub 対応

USB バスはスター型トポロジーになっていて規格上は最大127台のデバイスを接続することができることになっていますが*1、実際のコンピューターには多い機種でも数個、ほとんどのモバイル PC にはたった1個の USB ポートしかコネクタがありません。
それ以上の機器を繋ぐには USB Hub を使ってポートを拡張する必要があります。

USB Hub とは。

USB Hub とは USB ポートを拡張して最大15台*2 の子 USB デバイスを接続できるようにするデバイスです。 USB Hub の下にさらに別の USB Hub を繋ぐことで規格上は5段まで拡張することができます。

USB Hub の主な役割は、ポートの抜き差し監視と通知、 USB パケットのルーティング、速度の異なるデバイスプロトコル変換等です。

Ethernet のハブとは異なり USB Hub はホストから見るとアドレスを持った単なる1デバイスで接続可能台数に含めます。
通常のデバイスと同様にコンフィグレーションを行ってクラスコードで USB Hub を認識したらクラスドライバに引き渡します。
クラスコードは以下のように定義されています。

f:id:neriring16:20191024094252p:plain

なお、この図には載っていませんが USB3 (SS) Hub のクラスコードは 9, 0, 3 になります。 USB 公式サイトからの引用ですが情報が古くあまり更新されていないようです。
single TT / multiple TTs の TT というのは HS 対応ハブに LS/FS デバイスを接続した時の変換用バッファのことで、 multiple TTs の方が若干パフォーマンスがよくなります。
LS/Low Speed デバイスというのは最大 1.5Mbps に対応した低速デバイスで主に HID (Human Interface Device) などに使われます。
FS/Full Speed デバイスというのは最大 12Mbps に対応した中速デバイス、 HS/High Speed デバイスは USB2 以降で最大 480Mbps に対応した高速デバイスです。
SS/Super Speed デバイスというのは USB3 以降で最大 5Gbps に対応した超高速デバイスです。

実は USB2 以下と USB3 以上では物理層などの下層のプロトコルが全く別になっていて、 デバイスツリー上では USB2 ルートハブの下には USB2 デバイス、 USB3 ルートハブの下には USB3 デバイスというようにそれぞれ別々のハブに接続されたデバイスとして見えています。そのため USB2 Hub と USB3 Hub の仕様にも細かい違いがいくつかあります。
以降この記事では USB2 Hub を前提に記述します。

USB Hub の初期化

USB Hub は1個のインタラプト転送エンドポイントでポートステータス変更イベントを通知し、コントロール転送の GET_STATUS で各ポートのステータスを取得し、 SET_FEATURE や CLEAR_FEATURE でそれぞれのステータスビットの制御をするのが主な流れになります。

まず最初に GET_DESCRIPTOR コントロール転送で Hub Descriptor を取得します。
Hub Descriptor には USB Hub に関する情報が記述されており、この中で最も重要な情報はハブの提供するポート数 (bNbrPorts) です。
なお、 xHCI の場合は Hub Descriptor をもとにスロットコンテキストの修正が必要です。

f:id:neriring16:20191024102043p:plain

f:id:neriring16:20191024101852p:plain
f:id:neriring16:20191024101904p:plain

bNbrPorts の情報をもとに全てのポートを有効化します。
まず、全てのポートに対して SET_FEATURE で PORT_POWER ビットを1にし、次に CLEAR_FEATURE で C_PORT_CONNECTION ビットをクリアします。この例のように C_ から始まるステータスビットは対応するステータスビットの内容が変化したことを示しています。
これで全てのポートが有効化され、抜き差しが検知できる状態になります。
なお、初期化の時点で既にデバイスが接続されているポートには改めて挿入イベントが発生しませんので後ほど対応します。

ポートの抜き差し検知とポートリセット

Hub の初期化が終わったら、 USB Hub イベントスレッドでインタラプト転送のエンドポイントからデータを読み込みます。
USB HUBのインタラプト転送エンドポイントから読み込まれるデータはステータス変更があったポートのビットマップになっていて、どこかのポートでステータス変更イベントがあると該当するビットが1になります。
注意点としてポート番号は1から始まるので最下位ビット(ビット位置0)は別の意味に使われていることと、一度ステータスチェンジイベントが発生するとイベント要因のステータスビットをクリアしない限り永遠にイベント通知され続ける点に注意が必要です。

f:id:neriring16:20191024102433p:plain

ビット演算をしてイベントの発生したポートがわかったら、 GET_STATUS でポートのステータスを読み込みます。
ポートステータスは32ビットのビットフィールドからなる構造体で、下位16ビットは現在のポートのステータスを表し、上位16ビットは変更のあったステータスビットが1になっています。

f:id:neriring16:20191024101648p:plain

f:id:neriring16:20191024101507p:plain

f:id:neriring16:20191024101520p:plain

ポートの抜き差しをすると C_PORT_CONNECTION ビットが1になり、対応する PORT_CONNECTION ビットの値を見るとデバイスの接続状態がわかります。C_PORT_CONNECTION ビットが1でかつ PORT_CONNECTION ビットが1の場合はデバイスが接続されたことを示しています。
次に SET_FEATURE で PORT_RESET ビットを1にするとポートリセットが行われ、しばらく後に正常終了すると C_PORT_RESET ビットと PORT_ENABLE ビットが1になるので CLEAR_FEATURE で C_PORT_RESET ビットをクリアします。

f:id:neriring16:20191024101456p:plain

f:id:neriring16:20191024101629p:plain

ここまででデバイスは USB バス上に接続され、まだコンフィグレーションされていない状態です。
次は xHC からデバイスにアクセスするための設定が必要になります。

xHCI のデバイススロット割り当て

まず、ルートハブに接続されたデバイスと同じように ENABLE_SLOT コマンドでデバイススロットを割り当てます。
次に ADDRESS_DEVICE コマンドで xHCI のデバイススロットと実際の USB デバイスの紐付けをしますが、この時に指定するスロットコンテキストの内容がルートハブに繋がったデバイスとそれ以外のハブに繋がったデバイスで異なり、どのハブのどのポートに繋がっているかの設定が必要です。

ここで重要となる要素が Route String です。 Route String は4ビットのハブのポート番号を5つ組み合わせた 4 x 5 = 20ビットの値です。

まず、ルートハブ直下のデバイスは全て Route String 0x00000 になります。
ルートハブにハブ0をつないだ場合、ハブ0のポート1に繋がったデバイスは Route String 0x00001 、ポート2に繋がったデバイスは Route String 0x00002 ... ポート15に繋がったデバイスは Route String 0x0000F になります。
ハブ0のポート1にハブ1を繋いだ場合、ハブ2のポート1に繋がったデバイスは Route String 0x00011、ポート2に繋がったデバイスは Route String 0x00021 になります。以降同じようにして5段目まで繋げます。
また、 Route String だけだとルートハブのどのポートに繋がったか判別できません。 Root Hub Port Number にはルートハブに繋がっている最初のハブと同じ値を指定します。

バイスの速度はポートステータスの PORT_LOW_SPEED ビットと PORT_HIGH_SPEED ビットから取得できます。 PORT_LOW_SPEED ビットが1の場合は LS デバイス、 PORT_HIGH_SPEED ビットが1の場合は HS デバイス、それ以外の場合は FS デバイスになります。 USB2 Hub には SS 以上のデバイスを認識する機能はありません。

ADDRESS_DEVICE コマンドが成功したらあとはルートポートに繋がったデバイスと同じように通信できる状態になるので、通常通りコンフィグレーションをして OS からデバイスが使用可能になります。

TL; DR

ここまでで USB Hub に繋がったデバイスが最小限使えるようになりました。

まだ接続処理が不安定で USB Transaction Error がよく出ます。これはある USB Transaction を通信中に別の USB Transaction を始めようとするとよく発生するエラーなので排他制御を頑張らないといけなそうです。
また、筆者の環境だけかもしれませんが、 USB Hub は他の通常のデバイスに比べて動作が不安定でうまくコンフィグレーションできないことがあるようです。

*1:実装上の上限はまた別の話

*2:USB3の場合

USB/xHCI インタラプト転送の罠

USB プロトコルではホストとデバイスの役割が明確に分かれていて、ホスト (PC) から送受信命令を発行しないとデータ転送が始まりません。

ストレージのようなデバイスではこれで問題ありませんが、キーボードやマウスのように人間の気まぐれでいつデータが来るかわからないデバイスはこの方法だとデータを送るタイミングがわかりません。
PS/2 キーボードの場合は割り込みで送信タイミングを通知しますが、 USB で同様のケースはインタラプト転送という転送モードで対応します。

USB プロトコルのインタラプト転送はホストが一定の間隔でデバイスに問い合わせ、データを転送する必要がある場合のみ実際の転送が行われます。名前の予想に反して実際の動作はポーリングです。
一方、 xHCI ではインタラプト転送のエンドポイントに対して読み込み命令を発行するとホストコントローラーが定期的にデバイスに問い合わせ、実際に転送が行われるまで結果を遅延することで擬似的に割り込み動作を実現しています。
そのため、インタラプト転送ではポーリングの間隔値が重要な意味を持ちます。

実際の Interval の設定はエンドポイントの設定時にコンフィグレーションで取得した Endpoint Descriptor にある bInterval フィールドの値を元に Endpoint Context Data Structure の Interval フィールドに値を設定します。
これらのフィールドはどちらも8ビット値ですが、デバイスの速度や転送モードによって意味や取り得る範囲が異なるため、そのまま代入することはできません。

この値の関係は xHCI の仕様書に表が書かれています。

moe では以前からキーボード入力に大幅なラグが発生する事象を確認していて id:uchan_nos 氏に相談してみたところ Interval の設定ミスではないかということでソースコードと仕様書を改めて確認し、値の変換をせずそのまま代入していたのが原因と判明しました。

通常 HID デバイスは 10ms 前後の周期で動作し、 HID でよく使われる LS/FS デバイスのインタラプト転送の Endpoint Descriptor の bInterval の値も 10 前後になりますが、 xHCI Endpoint Context Data Structure の Interval は対数で指定するため、そのままの値を設定すると数百msと解釈されて大幅な遅延が発生します。

インタラプト転送では Interval の設定値も超重要です。

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

UEFI 自作 OS 日記 v0.7 / First Anniversary

moe を公開して一年経ちました。

neriring.hatenablog.jp

meg-os.org

で、そろそろなんかどーんとやりたかったところですが、v0.7が目立った新機能もないのでひっそりします。。。

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がまったく見つからない