借り初めのひみつきち

仮ブログです。

ACPI で電源を切る

いまどきの PC はすべて ACPI に対応しています。

ACPI について学習する大きなモチベーションのひとつが、 ACPI による電源切断だと思います。
これを覚えれば ACPI に対応した全てのコンピューターで電源を切断することができるようになり、自作 OS に shutdown コマンドが実装できるようになります!

長くなったので補足の記事を追加しておきます。

ACPI で電源を切るの補足 - 借り初めのひみつきち

※ この記事の方法では AML の処理を端折っているので、実機でそのまま実行するとハングアップしたりうまく動作しない機種があるようです。

ふたつの OUT 命令

その方法はこちらです。

out dx, al
out dx, ax

なんと、ACPI ではたった2つの OUT 命令で電源を切断することができます。
もちろん、実際には前処理があるので本当に OUT 命令だけで電源を切ることはできませんが。。

では、このふたつの OUT 命令は一体何なのでしょうか。

ひとつめの OUT 命令

ひとつめの OUT 命令はレガシーモードから ACPI モードに移行するために ACPI ENABLE というコマンドを SMI_CMD というポートに出力しています。
SMI_CMD という名前の通り SMM で動作しているファームウェアに対して通知するコマンドです。このコマンドが正常に終了すると ハードウェアがレガシーモードから ACPI モードに移行し、すべての ACPI 機能が OS の制御下になります。
HW reduced ACPI の機種は常に ACPI モードで動作しているためこの操作は定義されていません。

よくあるわかりやすい変化が、レガシーモードのときは電源ボタンを軽く押してすぐに電源切れたのが ACPI モードになると長押ししないと電源が切れなくなります。これは、 ACPI の電源ボタンが実は単なるボタンで、 ACPI モードでは押下時の処理がファームウェアから OS に移譲されるためです。
ACPI aware OS では起動時に実行されるコマンドですが、そうでない場合は電源を切る直前に実行した方が良いでしょう。
ACPI ENABLEを実行した後実際に有効になるまで PM1 Control Register の SCI_EN ビットが1かどうか調べます。

実際の SMI_CMD のアドレスや ACPI ENABLE コマンドの値は FADT テーブルに記述されています。

つまり、実際にはひとつめの OUT 命令は以下のようなシーケンスになります(これでも端折っていますが)

    mov dx, [FADT+SMI_CMD]
    mov al, [FADT+ACPI_ENABLE]
    out dx, al

    mov dx, [FADT+PM1a_CNT_BLK]
.loop:
    in ax, dx
    and ax, 0x0001 ; SCI_EN
    jz .loop

なお、 SMM で実装されている USB HID の PS/2 エミュレーションなども ACPI ENABLE を実行すると終了してしまいます。

ふたつめの OUT 命令

ふたつめの OUT 命令は、 PM1 Control Register に対してこれから S5 状態に遷移することを指示しています。
ACPI ではいろいろな電源状態が定義されていて例えば S0 は通常動作している状態で S5 は Soft Off 、つまり実際に電源を切断するということになります。

PM1 Control Register の実際のポートアドレスは FADT の PM1a_CNT_BLK から取得できます。 PM1a と PM1b の2種類定義するようになっていますが通常は PM1a の方を使います。 PM1b の存在理由はよくわかりません。

PM1 Control Register のフィールドは以下のように仕様書に定義されています。(抜粋)

f:id:neriring16:20190307235813p:plain

SLP_EN ビットを1にし、 SLP_TYPx を S5 ステートに相当する値にして PM1 Control Register に出力すれば S5 ステートに移行できることがわかります。
HW reduced ACPI の機種には PM Control Register が存在しない代わりに SLP_EN ビットとSLP_TYPx ビットのみ独立した Sleep Control Register というものがありますが、基本的な考え方はほとんど同じです。

しかし、ここまで細かく決まっているのに S5 のときに SLP_TYPx に具体的に何の値を出力すればいいのかは仕様では決まっていません。FADT にもこれ以上の情報はありません。
SLP_TYPx はたったの3ビットで取り得る値も8種類しかないので総当りしてみたくなるところですが、 S5 以外の電源状態でもこのコントロールポートを使用するため、よくわからない値を適当に出力すると誤動作の原因となります。

じゃあどこから情報を探せばいいのかというと、ここで AML が登場します。

AML というのは ACPI Machine Language の略で、 ACPI で管理された各種デバイスの構成情報や ACPI で決められた操作に対するメソッドの定義などが収められた規格です。
主に仕様書にテキストで表記されている ASL に対し、 AML は ASL をバイナリに翻訳して ACPI テーブルに実際に格納されているデータになります。

AML はツリー構造で PC に接続されているあらゆるデバイスの情報が定義された巨大なデータ構造です。
真面目に自力でパースすると大変すぎて死んでしまいますが、今は電源を切断したいだけなので「S5」という名前のオブジェクトを単純にスキャンするだけで十分です。

実際の AML では「S5」オブジェクトは「_S5_」という名前で記述されています。 S5 オブジェクトが見つかったら、それに続く中身は4つのバイト値のパッケージになっていることが規格で決まっています。
1つ目の値が PM1a に出力するべき値、2つめの値が PM1b に出力するべき値となっているのでこれをデコードします。

S5 を探すコードはこんな感じになります。

uint8_t *p = (uint8_t*)&dsdt->Structure;
size_t maxlen = (dsdt->Header.length - 0x24);
for (size_t i = 0; i < maxlen; i++) {
    if (!is_equal_signature(p+i, "_S5_")) continue;
    if ((p[i+4] != 0x12) || ((p[i-1] != 0x08) && (p[i-2] != 0x08 || p[i-1] != '\\'))) continue;
    i += 5;
    i += ((p[i] & 0xC0) >> 6) + 2;

    if (p[i] == 0x0A) i++;
    SLP_TYP5a = 0x07 & p[i++];

    if (p[i] == 0x0A) i++;
    SLP_TYP5b = 0x07 & p[i++];

    break;
}

S5 に切り替えるための値がわかったら、実際に OUT 命令で出力すると電源が落ちます。

mov dx, [FADT+PM1a_CNT_BLK]
mov ax, [SLP_TYP5a]
shl ax, 10
or ax, 0x2000
out dx, ax

なお、PM1b が存在する機種では同じことを PM1b に対しても実行する必要があるようです。

値を求めるまでが長いだけで、実際の処理はたったひとつの OUT 命令で電源が落ちました。