2025-05-10

Mobian(Debian)でTOUGHPADのA1/A2ボタンを動くようにした

お久しぶりです。
ブログの更新をずっとサボっていました。ネタがないわけではないんですが遅筆ゆえに書くのが億劫になってしまっていて、お蔵入りしている記事がいくつか・・・。書き途中のものを一ヶ月後とかに書き加えていこうとしてもできないらしいということに最近気づきました。一ヶ月も前の自分なんてほぼ他人ですからね。今月から一ヶ月に一回くらい投稿したいなという、そういう気持ちです。

さて、先日アキバでジャンク品になっていたTOUGHPAD(型番:FZ-G1)を友人から入手しました。購入時には初期化済みでOSが入っていなかったのでDebainにPhosh(スマホ向けのGUI環境)を統合したMobianインストールしてみることにしました(TOUGHPADは友人間で何枚か買ったようで、中にはArch Linuxを入れてGUIにPlasma Mobileを採用している人もいました)。

Mobianのスプラッシュスクリーン

Mobianのパスコード入力画面

設定の DisplaysからScale200%にするとかなり見栄えが良くなりました

MobianのDisplays設定。Scaleが200%になっている

Mobianのホーム画面

Mobianを入れてしばらくブラウザでの動画視聴やDiscord等のソフトウェアが動くことにテンションが上がったりしていたのですが、しばらく触ってみて気づいたことがあります。A1ボタンとA2ボタンが動かないのです。厳密にいうと、物理ボタンのうちA1ボタンとA2ボタンだけが動かないのです。TOUGHPADを購入した友人もみなこのA1/A2ボタンが動かない問題に直面していました。

ここで「なんだ。動かないのか。」で終わることもできたのですが、「他のボタンは完璧に動作するのにA1/A2ボタンだけが動かない」という現象に自分は興味を惹かれたので、「A1/A2ボタンでスクリーンショットを撮影する」ことを目標に調査することにしました。

調査#

入力がOSに認識されているか調べる#

まずはどのデバイスからの入力を特定する必要があると考え、evtestコマンドでの解析を試みました。以下はevtestコマンドを引数なしで実行したときの出力です

mobian@mobian:~$ evtest
No device specified, trying to scan all of /dev/input/event*
Not running as root, no devices may be available.
Available devices:
/dev/input/event0:	AT Translated Set 2 keyboard
/dev/input/event1:	SEM USB Keyboard
/dev/input/event10:	Power Button
/dev/input/event11:	Power Button
/dev/input/event12:	Panasonic Laptop Support
/dev/input/event13:	Intel Virtual Buttons
/dev/input/event14:	HDA Intel HDMI HDMI/DP,pcm=7
/dev/input/event15:	HDA Intel HDMI HDMI/DP,pcm=8
/dev/input/event16:	HDA Intel PCH Headphone
/dev/input/event2:	eGalax Inc. eGalaxTouch EXC3000-0077-22.00.00
/dev/input/event3:	eGalax Inc. eGalaxTouch EXC3000-0077-22.00.00 UNKNOWN
/dev/input/event4:	SEM USB Keyboard Consumer Control
/dev/input/event5:	SEM USB Keyboard System Control
/dev/input/event6:	Wacom ISDv4 EC Pen
/dev/input/event7:	PC Speaker
/dev/input/event8:	HDA Intel HDMI HDMI/DP,pcm=3
/dev/input/event9:	Video Bus
Select the device event number [0-16]:

いくつかのeventデバイスにあたりをつけて調べていくと以下のことがわかりました。

  • /dev/input/event11は電源ボタンを押すとイベントが発火する
  • /dev/input/event12は音量小・音量大ボタンを押すと発火する
  • /dev/input/event13はWindowsボタン・画面回転ボタンを押すと発火する

ただし、いずれのイベントファイルもA1/A2ボタンの押下により発火することはありませんでした。一応libinput debug-eventsacpi_listenを使用したりもしてみましたが、こちらも反応しませんでした。

入力イベントをキャプチャする方法を調べようとしていると以下のブログを友人が共有してくれました。

FZ-G1-Mk3 | Wade Mealing

このブログの「FZ-G1に入れたLinuxでA1/A2ボタンが動かない」というほぼ同じシチュエーションをもとにした考察は自分にとってはかなり有用でした。

このブログではまとめると以下のようなことが書かれていました。

  • A1/A2ボタンはlibinput debug-eventsでは反応しない←自分と同様
  • KaarelP2rtel/panasonic-hbtn: Panasonic Toughpad FZ-M1 Tablet Button driver for Linuxに近い実装がある。←このボタンがACPI経由で動作するものであることを示唆している
  • LinuxカーネルのCONFIG_ACPI_DEBUG=yを有効にするとACPIのデバッグログ機能が有効化できるようになる。ACPIのデバッグログを有効にすると、A1/A2ボタン発火時にカーネルログが流れる(dmesgで確認できる)
  • 流れたカーネルログをもとにACPIテーブルを確認し、Device (HKEY)こそが発火元であると推測し、簡単なドライバを書いてみたが効果はなかった

このうち「ACPIのデバッグログを有効にするとボタン押下時にカーネルログが流れる」は試す価値がありそうだったので試すことにしました。

Linuxカーネルのコンパイル〜インストール#

Linuxカーネルのコンパイルの手順は他の方が詳しく書いている記事が転がってると思うのでここでは簡単にやったことを書きます。 スペック的にTOUGHPADでLinuxカーネルをコンパイルするのはかなり時間がかかりそうだと判断し、コンパイルに関しては別のx86_64マシンを用意し、そこで行いました。

1. Linuxカーネルの依存の用意#

依存は以下のコマンドで用意できます

sudo apt build-dep linux

2. Linuxカーネルのソースコードの用意#

TOUGHPADのLinuxカーネルバージョンは6.12.25だったのでlinux-6.12.28.tar.xzを落としてきます。

curl -LO https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.12.28.tar.xz

解凍は以下のコマンドでできます。

tar -xf linux-6.12.25.tar.xz

4. .configの用意#

Linuxカーネルコンパイル時の設定が.configに記述されます。
先のCONFIG_ACPI_DEBUG=yもここで記述します。Mobianのconfigと差異があってはいけないのでMobianのconfigをTOUGHPADから持ってきて、CONFIG_ACPI_DEBUGの項目をyに編集してLinuxカーネルのソースコードのディレクトリの中に.configという名前で配置します(このときついでにいくつか別のも有効化したんですが忘れました)。

CONFIG_ACPI_DEBUG=y

自分のTOUGHPAD上の.config/boot/config-6.12.25にありました。

あとの作業のため、もし以下が有効でなければ有効にしてください。これを有効化しないとブートタイムでのACPIテーブルの上書きが出来ません。

TOUGHPAD上のMobianでは既に有効化されてはいましたが念の為。

CONFIG_ARCH_HAS_ACPI_TABLE_UPGRADE=y
CONFIG_ACPI_TABLE_UPGRADE=y

5. コンパイル#

以下のコマンドを実行して15分くらい待ちます。コンパイルするマシンのスペックによってはもっと待つと思います。
参考までに:自分が使ったCPUはAMD Ryzen 5 7600X 6-Core Processorです

make clean
make -j$(nproc)

6. 成果物をTOUGHPADにコピーする#

TOUGHPAD側でmake installなどを行うのでコンパイル後のLinuxカーネルのディレクトリをそっくりそのままTOUGHPADにコピーしてください。

コンパイル後のLinuxカーネルのディレクトリはめちゃくちゃ重いので移動が大変でしたが、以下のようにrsyncを設定すると7分くらいで終わりました。

コンパイルしたマシンの/etc/rsyncd.confを以下のように編集

uid         = root
gid         = root
log file    = /var/log/rsyncd.log
pid file    = /var/run/rsyncd.pid
 
[linux-kernel]
path = /home/sh1ma/workspace/linux-6.12.25

/home/sh1ma/workspace/linux-6.12.25は各自のLinuxカーネルの置いてあるディレクトリに合わせてください。

コンパイルしたマシンで以下を実行

rsync --daemon

これでrsyncdが起動し、sshではなくrsyncプロトコル(TCP)を介した通信が使えるようになります。

その後、TOUGHPAD側で以下を実行すると高速でファイルがコピーされていきます

rsync -azvh --info=progress2 --whole-file rsync://192.168.100.12/linux-kernel ~/workspace/

7. 成果物のインストール#

コンパイル済みのLinuxカーネルのディレクトリがコピーできればここからはTOUGHPAD側の作業です。

カーネルモジュールは実際には不要なものが多いため、/etc/initramfs-tools/initramfs.confで設定を編集して必要なもののみインストールするよう変更します。

MODULES=dep

initramfsを更新します

sudo update-initramfs -u -k 6.12.25

以下を実行します

sudo make modules_install
sudo make install

ここまででインストールは完了です!再起動後に新しいカーネルが適用されているはずです

ACPIのデバッグログを見る#

コンパイルタイムでCONFIG_ACPI_DEBUG=yに設定されたLinuxカーネルを起動すると、/sys/module/acpi/parametersdebug_leveldebug_layerというファイルが追加されていました。

mobian@mobian:~$ ls /sys/module/acpi/parameters
acpica_version    debug_layer  ec_busy_polling  ec_event_clearing  ec_max_queries  ec_polling_guard    immediate_undock   trace_debug_level  trace_state
aml_debug_output  debug_level  ec_delay         ec_freeze_events   ec_no_wakeup    ec_storm_threshold  trace_debug_layer  trace_method_name

これの値を書き換えてACPIのデバッグログが吐かれるようにします。

まずは以下のコマンドでrootに入ります(rootでなければ書き込めないので先にrootになっておきます)。

sudo su

debug_levelパラメータを覗きます

cat /sys/module/acpi/parameters/debug_level

出力↓

Description              	Hex        SET
ACPI_LV_INIT             	0x00000001 [ ]
ACPI_LV_DEBUG_OBJECT     	0x00000002 [ ]
ACPI_LV_INFO             	0x00000004 [ ]
ACPI_LV_REPAIR           	0x00000008 [ ]
ACPI_LV_TRACE_POINT      	0x00000010 [ ]
ACPI_LV_INIT_NAMES       	0x00000020 [ ]
ACPI_LV_PARSE            	0x00000040 [ ]
ACPI_LV_LOAD             	0x00000080 [ ]
ACPI_LV_DISPATCH         	0x00000100 [ ]
ACPI_LV_EXEC             	0x00000200 [ ]
ACPI_LV_NAMES            	0x00000400 [ ]
ACPI_LV_OPREGION         	0x00000800 [ ]
ACPI_LV_BFIELD           	0x00001000 [ ]
ACPI_LV_TABLES           	0x00002000 [ ]
ACPI_LV_VALUES           	0x00004000 [ ]
ACPI_LV_OBJECTS          	0x00008000 [ ]
ACPI_LV_RESOURCES        	0x00010000 [ ]
ACPI_LV_USER_REQUESTS    	0x00020000 [ ]
ACPI_LV_PACKAGE          	0x00040000 [ ]
ACPI_LV_ALLOCATIONS      	0x00100000 [ ]
ACPI_LV_FUNCTIONS        	0x00200000 [ ]
ACPI_LV_OPTIMIZATIONS    	0x00400000 [ ]
ACPI_LV_MUTEX            	0x01000000 [ ]
ACPI_LV_THREADS          	0x02000000 [ ]
ACPI_LV_IO               	0x04000000 [ ]
ACPI_LV_INTERRUPTS       	0x08000000 [ ]
ACPI_LV_AML_DISASSEMBLE  	0x10000000 [ ]
ACPI_LV_VERBOSE_INFO     	0x20000000 [ ]
ACPI_LV_FULL_TABLES      	0x40000000 [ ]
ACPI_LV_EVENTS           	0x80000000 [ ]
--
debug_level = 0x00000000 (* = enabled)

debug_layerパラメータを覗きます

cat /sys/module/acpi/parameters/debug_layer

出力↓

Description              	Hex        SET
ACPI_UTILITIES           	0x00000001 [ ]
ACPI_HARDWARE            	0x00000002 [ ]
ACPI_EVENTS              	0x00000004 [ ]
ACPI_TABLES              	0x00000008 [ ]
ACPI_NAMESPACE           	0x00000010 [ ]
ACPI_PARSER              	0x00000020 [ ]
ACPI_DISPATCHER          	0x00000040 [ ]
ACPI_EXECUTER            	0x00000080 [ ]
ACPI_RESOURCES           	0x00000100 [ ]
ACPI_CA_DEBUGGER         	0x00000200 [ ]
ACPI_OS_SERVICES         	0x00000400 [ ]
ACPI_CA_DISASSEMBLER     	0x00000800 [ ]
ACPI_COMPILER            	0x00001000 [ ]
ACPI_TOOLS               	0x00002000 [ ]
ACPI_ALL_DRIVERS         	0xFFFF0000 [ ]
--
debug_layer = 0x00000000 ( * = enabled)

実際にA1/A2ボタンが押されたときに流れるログの設定としては、以下のように書き換えるとよかったです。

echo 0x200 > /sys/module/acpi/parameters/debug_level
echo 0x82 > /sys/module/acpi/parameters/debug_layer
  • debug_level0x200に設定するとACPI_LV_EXECのみ有効な状態になる
  • debug_layer0x82に設定するとACPI_HARDWAREACPI_EXECUTERが有効な状態になる

上記が書き換えが成功しているとデバッグログは有効になります。再起動すると設定は失われます。

デバッグログを確認します。デバッグログはカーネルログとして出るのでdmesg -wで確認ができます。

ログは大量に流れますが、以下のようなコマンドで実行するとわかりやすい流速で流れると思います。

Wade Mealing

dmesg -w | grep Notify -A5 -B5

注目すべきはNotify付近の以下のようなログです。A1/A2ボタンのどちらを何度押してもこのログは必ず流れます。恐らくこれがA1/A2ボタンを押したときのログとみて間違いなさそうです。特に、ログ中のTBTNはACPIテーブルのDSDTのシンボルに見えます。

[31002.859886]   exresop-0126 ex_resolve_operands   : Opcode 86 [Notify] RequiredOperandTypes=000001A6
[31002.859890]  exresolv-0084 ex_resolve_to_value   : Resolved object 00000000c7a02e9c
[31002.859892]    exdump-0880 ex_dump_operands      : **** Start operand dump for opcode [Notify], 2 operands
[31002.859895]    exdump-0603 ex_dump_operand       : 000000009e1948ee Namespace Node:  0  TBTN Device       000000009e1948ee 001 Notify Object: 00000000dfc93cab

ACPIテーブルの逆コンパイル#

ここまでの状況を整理します。

  • libinput debug-eventsevtestacpi_listenといった入力分析ツールではボタン押下イベントは出力されなかった
    • このことから、以下のことがわかりました
      • Linux Input SubSystemには入力イベントが報告されていない
      • ACPIの通知がLinuxのカーネルからユーザランドに届いていない
  • Linuxカーネルのログ(dmesg -w)ではボタンの押下を検知できた

以上のことから「ACPIイベントがLinuxカーネルに無視されている、または握りつぶされている」ことがわかりました。

なので次はFZ-G1-Mk3 | Wade Mealingのブログの操作と同様に、ACPIテーブルをダンプしDSDTを逆コンパイルします。そして先ほど見つけたTBTNの定義について探索します。逆コンパイル→DSDTを上書きする作業については以下のページにより詳しく書かれています。

DSDT - ArchWiki

まずは調査のためのツールacpica-toolをインストールします。

sudo apt install acpica-tool

acpidumpコマンドでACPIテーブルをダンプします(カレントディレクトリに複数のファイルが生成されるので作業用ディレクトリで行うことをおすすめします)。

sudo acpidump -b

実行後、カレントディレクトリにダンプされたACPIのバイナリファイルが出力されます

mobian@mobian:~/tmp$ ls
 apic.dat    dsdt.dat   hpet.dat   pcct.dat     ssdt12.dat   ssdt2.dat   ssdt6.dat   tcpa.dat
'asf!.dat'   facp.dat   lpit.dat   slic.dat     ssdt13.dat   ssdt3.dat   ssdt7.dat
 bgrt.dat    facs.dat   mcfg.dat   ssdt10.dat   ssdt14.dat   ssdt4.dat   ssdt8.dat
 dmar.dat    fpdt.dat   msdm.dat   ssdt11.dat   ssdt1.dat    ssdt5.dat   ssdt9.dat

使用するのはこのうちdsdt.datのみです。 以下のコマンドdsdt.datを逆コンパイルしたファイルdsdt.dslが生成されます。

iasl -d dsdt.dat

生成されたdsdt.dslを以下のgistに保存しておきました。
TOUGHPAD FZ-G1 ACPI DSDT Table

dsdt.dslからTBTNについて調べていきます。
TBTNで検索すると以下のような記述が現れます。

    Scope (\_SB)
    {
        Device (TBTN)
        {
            Mutex (HDMX, 0x00)
            Method (_HID, 0, NotSerialized)  // _HID: Hardware ID
            {
                If ((\_SB.PCI0.LPCB.GSGP (0x10, 0x01) == 0x01))
                {
                    If ((\_SB.PCI0.LPCB.GSGP (0x16, 0x01) != \_SB.PCI0.LPCB.GSGP (0x17, 0x01)))
                    {
                        Return (0x2B003434)
                    }
                }
 
                Return (0x2A003434)
            }

gistの該当箇所

Device (TBTN)にはいくつかのメソッドが紐づいています。 そのうち_HIDはハードウェア固有のIDを返す参照です。

ACPIの最新のSpecの6.1.5によると_HIDは以下のような説明されています。

6.1.5 _HID (Hardware ID)

This object is used to supply OSPM with the device’s PNP ID or ACPI ID.
(和訳:このオブジェクトは、デバイスの PNP ID または ACPI ID を OSPM に提供するために使用されます。) )

OSPMとは「OS主導の構成と電源管理(原語:Operating System-directed configuration and Power Management)」の略で、ACPIと言い換えて問題ないはずです。つまりOSにHIDを伝えるメソッドと理解して差し支えないでしょう。

ここまではわかったのですが気になったのは他のHIDと様相が異なることです。具体的には、以下の2つの点が異なります。

  • dsdt.dslの他のHIDはName (_HID, EisaId ("PNP0C02")のような形式です。これは_HIDのメソッドが叩かれたときには常にEisaId ("PNP0C02")が返るような定数関数として定義されているのに対し、TBTN_HIDメソッドには条件分岐があります。
  • 他の_HIDEisaId()に包まれた文字列を返しているのに対し、TBTN_HIDはプリミティブな数値を返します。

_HIDが数値型も許容するのか疑問だったのでACPI Specを読み進めたところ、以下のことがわかりました。

  • EisaId()は文字列のEISA IDを数値型に変換するマクロ
    • ACPI Spec19.6.37 EISAID (EISA ID String To Integer Conversion Macro) で言及がありました
  • _HIDの返り値は文字列か数値のどちらか
    • 文字列の場合は英数で構成されたPNP IDかACPI ID
    • 数値の場合は32ビット圧縮EISAタイプID(原語:32-bit compressed EISA type ID)
      • この数値は文字列のHIDに変換可能です。ACPI Spec19.3.4 ASL Macros で以下のように言及されています

        Converts and compresses the 7-character text argument into its corresponding 4-byte numeric EISA ID encoding (Integer). This can be used when declaring IDs for devices that are EISA IDs.

      • また、ここでは32ビット圧縮EISAタイプIDから文字列に変換する手順も紹介されていました

ここまででTBTN_HIDの返り値は32ビット圧縮EISAタイプIDと分かったので先ほどのTBTN_HIDの2つの返り値をデコードするスクリプトを書きます。

def decode_eisa_id(eisa_id: int):
    eisa_id = int.from_bytes(eisa_id.to_bytes(4, "little"), "big")
    vendor = ""
    vendor_bits = (eisa_id >> 16) & 0xFFFF
 
    char1_val = (vendor_bits >> 10) & 0x1F
    char2_val = (vendor_bits >> 5) & 0x1F
    char3_val = vendor_bits & 0x1F
 
    vendor += chr(char1_val + 0x40)
    vendor += chr(char2_val + 0x40)
    vendor += chr(char3_val + 0x40)
 
    product_id = eisa_id & 0xFFFF
    return f"{vendor}{product_id:04X}"
 
 
id1 = 0x2A003434
id2 = 0x2B003434
 
print(f"0x{id1:08X} -> {decode_eisa_id(id1)}")
print(f"0x{id2:08X} -> {decode_eisa_id(id2)}")

gist

出力は以下のようになりました。

❯ python decode.py
0x2A003434 -> MAT002A
0x2B003434 -> MAT002B

いくつか定義された他のデバイスのHIDを見ましたが、接頭辞がMATのものがいくつかありました。よってデコード結果はこれで正しそうです。

HIDが分かればあとはドライバを書いていく作業になります。

以下に成果物を載せておきます

sh1ma/tbtn-driver: TOUGPAD TBTN (A1, A2 buttons) Linux kernel driver

ここからA1/A2ボタンが動いている様子の動画を確認できます

まとめ#

本当は本物のドライバを書く前にDSDTのTBTNのHIDを固定値PNP0C40に書き換えた上でACPIテーブルを上書きし、それのドライバを書いたりして動くところまで持っていったりしたんですが記事では端折りました。(書くのがめんどくなってしまった)
DSDTの上書きのやり方とかは備忘録的に別記事で書けたらなーと思ってます。