借り初めのひみつきち

仮ブログです。

GPD MicroPC の内蔵キーボードを使う方法

エンジニア向けに人気らしい GPD MicroPC ですが、実はいくつかの OS で内臓キーボードが使えない問題があります。 いくつかの OS には自作 OS も含まれます。

※ リビジョンによってハードウェア詳細が異なる個体があるようなので、この記事が全ての個体に当てはまる保証はできません。

結局、内蔵キーボードは何の規格なの?

答え: PS/2 です。

出た当時は色々言われていましたが、 windows を起動してデバイスマネージャを眺めてみると「PS/2 キーボード」というデバイスが存在します。 そして、 UEFI コンソール上でポート I/O を行うコマンドでテストするとポート 0x60 (PS/2 データポート) からキーコードっぽい値が取れます。

実際に自作 OS で試してみると、 PS/2 コントローラーらしいデバイスが存在していていくつかのコマンドにはちゃんと応答を返しています。

しかし、キーボードをいくら押し込んでもキーのデータは取得できません。

UEFI で直接 PS/2 ポートを叩くプログラムを作ってみるとそれっぽいデータが返ってきますが、 途中で ExitBootServices を呼び出すとそれ以降はやはりデータが返ってきません。

一方、 USB スタックを実装して調べてみると謎の HID デバイスは存在するものの、キーボードらしき応答は返ってきません。

一体何が起こってるんでしょうか?

Linux の事情

インターネットを検索してみると Linux をインストールした人の記事をいくつか見つけることができます。

Linux もバージョンによっては内蔵キーボードがうまく扱えないことがわかります。 一方で、バッテリ管理を有効にするとキーボードが使えるらしいという報告もあります。

バッテリ管理ということは ACPI が怪しいですね。

ACPI、DSDT、AML / ASL

ACPI には AML というバイトコードエンコードされた DSDT というコンピューターの全てのデバイス構成を管理するための巨大なデータベースがあります。

流石にそのまま見てもよくわからないので ASL というテキスト形式に変換して GPD MicroPC の DSDT の一部を見てみましょう。

Device (PS2K)
{
    Name (_HID, EisaId ("PNP0303") /* IBM Enhanced Keyboard (101/102-key, PS/2 Mouse) */)  // _HID: Hardware ID
    Name (_UID, Zero)  // _UID: Unique ID
    Name (LDN, 0x06)
    Name (_CID, EisaId ("PNP030B"))  // _CID: Compatible ID

...

Scope (_SB.PCI0)
{
    Device (PS2K)
    {
        Name (_HID, "MSFT0001")  // _HID: Hardware ID
        Name (_CID, EisaId ("PNP0303") /* IBM Enhanced Keyboard (101/102-key, PS/2 Mouse) */)  // _CID: Compatible ID

PS2K というデバイスを見つけることができ、これは PS/2 接続のキーボードということがわかります。

PS2K デバイスの ASL をさらに眺めていると、以下のように我々のよく知っている PS/2 キーボードのリソース情報が記述されています。

IO (Decode16,
    0x0060,             // Range Minimum
    0x0060,             // Range Maximum
    0x00,               // Alignment
    0x01,               // Length
    )
IO (Decode16,
    0x0064,             // Range Minimum
    0x0064,             // Range Maximum
    0x00,               // Alignment
    0x01,               // Length
    )
IRQNoFlags ()
    {1}

PS/2 キーボードの存在は確からしいことがわかりました。 問題は、なぜ ExitBootServices を呼び出すと、キーボードが反応なくなるのでしょうか?

ACPI Embedded Controller

ACPI には Embedded Controller (EC) という、 ACPI に関するコマンドやイベントのやりとりを行うコントローラーがあります。 EC は PS/2 コントローラーによく似ていて、一部の機種では PS/2 コントローラーと兼用している例もあるようです。

f:id:neriring16:20210523145757p:plain

コントローラーチップの謎

分解記事を見るとコントローラーチップらしき型番 (IT8987E) を見つけることができるのでそれを元に検索すると、 キーボードマトリックスPS/2 キーボードとして使えるコントローラーと Embedded Controller を内蔵しているブロック図を見つけることができます。 しかし、他に手がかりになりそうな情報を見つけることはできませんでした。

あえてこのようなチップを内蔵しているということは、おそらく PS/2 接続キーボードなんでしょう。

チップ本体の情報は見つかりませんが、似ている別の型番のチップでは EC のコマンドでキーボードなどの有効化を制御することができます。

つまり、 EC に何らかのコマンドを送ると内蔵キーボードの ON/OFF が制御できるのではないか、ということが予想できます。

全ての材料は揃った

ここまでの情報を整理すると、 ACPI を有効化する過程で EC になんらかのコマンドを送ればキーボードが使えるようになると予想できます。

あとは AML/ASL からそれらしい情報を探してくるだけです。

ここが一番大変ですけどね

おぼろげながら浮かんできた 0x11 という数値

ここまでは比較的早い段階で辿り着くことができましたが、 AML/ASL はしらみつぶしに探すには巨大すぎるし、名前が4文字制限でよく分からない略語がいくつも出てきて何をやってるかさっぱり分からなくなるので、進捗はほとんどありませんでした。

そして、当初の目的も忘れてきたある日ふと ASL を眺めていたところ、 EmbeddedControl に KBCD という怪しいフィールドを見つけました。

OperationRegion (ECF2, EmbeddedControl, Zero, 0xFF)
Field (ECF2, ByteAcc, Lock, Preserve)
{
    XXX0,   8, 
    XXX1,   8, 
    XXX2,   8, 
    Offset (0x11), 
    KBCD,   8, 

いかにもキーボード関係ありそうな怪しい名前のこのフィールドを ASL から検索してみると、バッテリー関連のメソッドで 0 を書き込んでいました。 なお、別のところでは 3 を書き込むケースもありました。

というわけで、実機で試してみました。

f:id:neriring16:20210523142033j:plain

キーボードが動きました。

ヤッタネ

実装の仕方

EC は PS/2 コントローラーによく似ています。 GPD MicroPC の場合はポート番号 0x62 がデータレジスタ、ポート番号 0x66 がコマンド・ステータスレジスタとなり、レジスタにデータを書き込む際はステータスレジスタIBF フラグの値を調べます。

f:id:neriring16:20210523145933p:plain

EC は 256 バイトの空間を持っていて、書き込む際は 0x81 Write Embedded Controller (WR_EC) をコマンドレジスタに書き込んだあと、アドレスと書き込むデータをデータレジスタに書き込みます。

f:id:neriring16:20210523150013p:plain

WR_EC は以下のようなコードになります。

unsafe fn wr_ec(addr: u8, data: u8) {
    Self::ec_wait_for_ibf();
    asm!("out 0x66, al", in("al") 0x81u8);
    Self::ec_wait_for_ibf();
    asm!("out 0x62, al", in("al") addr);
    Self::ec_wait_for_ibf();
    asm!("out 0x62, al", in("al") data);
}

unsafe fn ec_wait_for_ibf() {
    let mut al: u8;
    loop {
        asm!("in al, 0x66", out("al") al);
        if (al & 0x02) == 0 {
            break;
        }
        asm!("pause");
    }
}

件の問題では EmbeddedControl 0x11 に 0 を書き込みたいので、

Self::wr_ec(0x11, 0x00);

のように呼び出せばいいです。

しかし、このままだと GPD MicroPC 以外では何が起きるかわかりません。

EC が 62/66 にない機種ではステータス待ちでおそらくハングアップしますし、 EC があっても内容は機種依存なのでデバイスに予期せぬダメージを与えてしまうかもしれません。 というわけで、 SMBIOS で機種判別をして「GPD MicroPC」の時だけ処理するようにします。*1

let device = System::current_device();
match device.manufacturer_name() {
    Some("GPD") => {
        match device.model_name() {
            Some("MicroPC") => {
                Self::wr_ec(0x11, 0x00);
            }
            _ => (),
        }
    }
    _ => (),
}

これで GPD MicroPC の時だけキーボード有効化の EC コマンドを書き込みするようにできました。

本来は ACPI をちゃんと実装すればバッテリ関連のメソッドで自動的に実行されます。 現在の実装はあくまでワークアラウンドです。 そもそもキーボードが勝手に無効化されてしまうのはファームウェアのバグだと思うのでいずれ修正されるかもしれません。

いかがだったでしょうか?

自分の所有しているデバイスでキーボードがちゃんと扱えないのは悲しいものです。 意外と簡単な修正でキーボードが使えることがわかって嬉しいですね。

*1:実は多くの中華端末では SMBIOS の内容が適当すぎて機種が判別できません。手元にある他の GPD も判別できませんでした。 MicroPC だけはちゃんと判別できてよかったです