この記事について
Linuxの中にACPI SSDT overlayという仕組みがあることに気が付いたので、
この仕組みで遊んでみてLinuxにおけるACPIの扱いに対する理解を少しだけ深めてみようという試みです。
※ よって実用的な内容ではありません
なお本内容は以下の環境で試しています。
- x86_64 VM (fedora 39/kernel 6.6.7)
ACPI SSDT overlayとは
そもそもACPIが何かという話ですが、ざっくりと言えばACPIはOSとファーウエアのインタフェースであり、コンピュータの電力制御等を実現する仕組みです。
(ACPIの説明は難しいので詳細は割愛します。正確な情報が知りたい方はACPICAのサイトにあるintroductionのドキュメントをお勧めします。
https://www.intel.com/content/www/us/en/developer/topic-technology/open/acpica/overview.html)
ACPIの役割の一つはハードウェアの構成をOSに伝えることであり、
この(階層的な)構成情報をACPI namespaceと呼びます。
ACPI namespaceはDSDT/SSDTと呼ばれるテーブルとして表現され、
このテーブルをファームウェアからOSへ渡すことでOSがハードウェアの構成を認識します。
テーブルの情報はファームウェアが作成します。製品ではハードの構成が決まっているためこのことに何も問題がりませんが、開発ボードなどでハード構成を変更する場合はそのたびにファームウェアの変更も必要になり手間です。この問題を解決する仕組みがSSDT overlayです。
名前から推測されるように、SSDT overlayの仕組みを利用することでファームウェアを修正しなくとも動的に(Linux起動中に)ACPI namespaceの変更(≒ Linuxが認識するデバイスの追加)を行うことができます。
SSDT overalyの詳細については以下のkernelドキュメントを参照してください。
https://docs.kernel.org/admin-guide/acpi/ssdt-overlays.html
上述のようにoverlayの仕組みは開発者向けですが、
今回はこの仕組みを利用(というより濫用)して擬似的なデバイスを作成し、
そのデバイスを認識するACPIドライバを書いてみます。
試してみる
debug kernelで起動する
まず後述の理由によりdebug kernelが必要になるのでインストールします。
debug kernelは名前の通りlinuxにある様々なdebugオプションを有効にしたkernelです。
$ sudo dnf install kernel-debug kernel-debug-devel
再起動後、grubメニューで"+debug"のついたエントリを選択するとdebug kernelが起動します。
overlayしてみる
早速overlayで(擬似的な)デバイスを作成してみます。
ドキュメントを参考に何もしないデバイスを作成してみます。
DefinitionBlock ("my-test.aml", "SSDT", 1, "XXX", "YYY", 0x1)
{
External(\_SB, DeviceObj)
Scope(\_SB)
{
Device(MYDV)
{
Name (_HID, "ABCD1234")
Name (_UID, 0x00)
}
}
}
実質的にデバイスの識別子(HID/UID)だけを定義しました。
"_SB"というのはSystem Busのことで、ACPI namespaceの起点となる場所です。
上の例では_SBの直下にMYDVという疑似デバイスを作成しています。
※ なお識別子(HID)に適当な文字列を振っていますが、
実際はACPI仕様で定められているので勝手な文字列は使えません
https://uefi.org/PNP_ACPI_Registry
次に作成したコード(ASL, ACPI Source Language)をOSが読み込むバイトコードにコンパイルします。
このためにはacpica-toolsで提供されるiaslコマンドを利用します。
$ sudo dnf install acpica-tools
$ iasl my-test.asl
Compilation successful. 0 Errors, 0 Warnings, 0 Remarks, 1 Optimizations
構文が間違えていなければ無事にコンパイルされるはずです。
後はこのバイトコードをkernelにロードします。ドキュメントを見ると3つの方法
が記載されていますが、一番下のconfigfsを使用するやり方が簡単なのでこれを使います
(configfsを利用するためにdebug kernelが必要です)。
ドキュメントの通りに以下のようにしてロードします。
$ sudo sudo modprobe acpi_configfs
$ sudo ls /sys/kernel/config/
acpi
$ sudo mkdir /sys/kernel/config/acpi/table/my_ssdt
$ sudo sh -c "cat my-test.aml > /sys/kernel/config/acpi/table/my_ssdt/aml"
正しくデバイスが作られればsyslogにログがでます。
またsysfsにもデバイスの情報がてきます。
$ dmesg
ACPI: Host-directed Dynamic ACPI Table Load:
ACPI: SSDT 0xFFFF88818746D880 00005C (v01 XXX YYY 00000001 INTL 20220331)
$ ls /sys/bus/acpi/devices/ABCD1234\:00/
hid modalias path physical_node power subsystem uevent uid
以上でoverlayによりACPIデバイスを作成することができました。
なおoverlayで作成するデバイスがi2cデバイスなどであれば作成したsysfsのファイルを
削除することでデバイスも削除することができます。しかし今回はシステムバスに直接
デバイスが接続されているように見せている(LinuxでいうところのPlatform Device)
ため削除はできません。overlayする情報を更新したい時はVMを再起動して再度overlayします。
ドライバを作る
次にLinuxのドライバの動きを見ていきます。
LinuxのACPIサブシステムではデバイスの識別子(HID)を利用して
対応するドライバの処理を呼び出す仕組みがあります。
最低限の処理をするドライバを作成してみると以下のようになります。
// SPDX-License-Identifier: GPL-2.0
#include <linux/acpi.h>
#include <linux/module.h>
static const struct acpi_device_id my_acpi_ids[] = {
{"ABCD1234", 0},
{"", 0},
};
static int my_add(struct acpi_device *device)
{
pr_info("my_module add: %lld\n", data);
return 0;
}
static struct acpi_driver my_acpi_driver =
{
.name = "my_acpi_driver",
.class = "my_class",
.ids = my_acpi_ids,
.ops = {
.add = my_add,
},
};
static int __init my_init(void)
{
pr_info("my_module init\n");
return acpi_bus_register_driver(&my_acpi_driver);
}
static void __exit my_exit(void)
{
acpi_bus_unregister_driver(&my_acpi_driver);
pr_info("my_module exit\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
ポイントをまとめると以下になります。
- ACPIドライバを作るためにstruct acpi_driverの情報を用意する
- idsにこのドライバで処理するデバイスのIDを記載する
- opsにACPIサブシステムから実行されるコールバック関数をしていする。ここではデバイスを認識(add)したときに呼び出されるadd関数のみを作成
- モジュールの初期化関数でacpi_bus_regsiter_driverを利用してこのドライバをACPIバスに登録する
つまりLinuxがACPIデバイスを認識すると、そのHIDに対応するドライバのadd コールバックが呼び出されます (ちなみにLinuxではデバイスの認識とドライバのロードはどちらが先でも大丈夫な作りになっています)。
実際に試してみます。
$ make -C /lib/modules/$(uname -r)/build -C $(PWD) modules
$ sudo insmod my_module.ko
$ dmesg | tail
my_module init
my_module add
モジュールの初期化関数とaddコールバックが呼ばれていることが確認できます。つまり想定どおりにドライバがロードされていることが確認できました。
methodを追加する
このままだと何もインタラクションが無いのでACPI methodを追加してみます。
ACPI methodはファームウェアに定義される処理で、Linuxのドライバから
名前を指定して実行させる事ができます。methodを利用することで
特定の処理をkernelからファームウェアに依頼することができ、またLinux
は処理の詳細(実際にはファームウェアがハードウェアと具体的にどういうやりとりをするか)を気にする必要がなります。
ということでファームウェアがmethodの中で何をするかが実際は重要ですが、
ここではダミーの空のmethodを作ります。ASLのコードに以下を追加してみます
Method(_ZZZ, 0, Serialized)
{
Return (0x42)
}
再度コンパイルします
...
$ iasl my-test.asl
...
my-test.asl 11: Method(_ZZZ, 0, Serialized)
Warning 3133 - Unknown reserved name ^ (_ZZZ)
...
Compilation successful. 0 Errors, 1 Warnings, 0 Remarks, 1 Optimizations
デバイスに対するmethodの種類もACPI仕様で定められています。
適当にダミーのメソッドを追加したのでwarningが出ていることがわかります。
さてLinuxドライバではACPI methodの実行するためにacpi_evaluate_XXXという
関数が用意されています。今回はint型をファームウェアが返す想定なので
acpi_evaluate_integerを使います。この関数をaddコールバックの中で呼び出すようにしてみます。
コードを以下のように変更します。
static int my_add(struct acpi_device *device)
{
acpi_status status;
unsigned long long data;
status = acpi_evaluate_integer(device->handle, "_ZZZ",
NULL, &data);
if (ACPI_FAILURE(status))
return -ENODEV;
pr_info("my_module add: %lld\x", data);
return 0;
}
ドライバをロードすると以下のsyslogが表示されます。
my_module add: 42
定義したmethodが呼び出されていることが確認できます。
notifyを追加する (未完)
methodとは逆に、ファームウェアの処理の中で何らかの対応が必要であることをLinuxに伝える仕組みもあり、これをnotifyと呼びます。
※ notifyを試すコードも追加しようと思いましたが間に合わなかっため未完です。
おわりに(コメント)
冒頭で説明しましたがSSDT overlayは開発時に有用な仕組みです。今回の例は説明のための濫用なのでその点はご留意ください。
ACPIは説明しにくい存在だなと常々思っているので、今回のような内容を通じて少しずつ理解を整理できないかと考えています。本当はnotifyに関係して割り込みの扱いについても説明できないかと思ったのですが間に合いませんでした。今後の課題にします。
以上