松下 Let's Note RZ5 侧边无线物理开关在Linux下的复活指南
起因
最近从朋友那搞到了一台松下的“加拉帕戈斯”笔记本——Let's Note RZ5。这机器哪哪都好,超迷你超轻机身和全功能接口,就是这个侧边的 无线拨动开关在Linux下完全是个摆设。按道理来说,作为一个硬核日系本,这种物理开关应该能在底层直接切断网卡电源,但在 Linux下rfkill没找到一点设备,acpi_listen也是一片死寂。
利用官方的驱动安装指南,发现在Windows下这个开关通过松下专有的ACPI\MAT0028接口工作。但在Linux的世界里,信号似乎被某种神秘的力量拦截了,一点点反应都没有。
寻踪
首先是翻阅DSDT表,找到这个硬接口相关的ACPI实现。
需要利用sysfs导出DSDT表后反编译回ASL源代码
sudo cat /sys/firmware/acpi/tables/DSDT > dsdt.bin
iasl -d dsdt.bin搜索MAT0028,找到了以下代码段
Scope (_SB)
{
Device (WLSW)
{
Mutex (HDMX, 0x00)
Name (_HID, EisaId ("MAT0028")) // _HID: Hardware ID
Name (HINP, Zero)
Name (HOUP, Zero)
Name (HDAT, Buffer (0x20){})
Name (SIFR, Package (0x02)
{
Zero,
One
})
Name (DRVS, Zero)
Name (WNSW, One)
Method (_STA, 0, NotSerialized) // _STA: Status
{
Local0 = Zero
If (CondRefOf (\_OSI, Local6))
{
If (_OSI ("Windows 2012"))
{
Local0 = 0x0F
}
}
Return (Local0)
}
Method (_INI, 0, NotSerialized) // _INI: Initialize
{
}
Method (SQTY, 0, NotSerialized)
{
If ((DRVS == Zero))
{
DRVS = One
WNSW = One
HRES ()
}
Return (0x02)
}
Method (HIND, 1, Serialized)
{
Acquire (HDMX, 0xFFFF)
HDAT [HINP] = Arg0
HINP++
HINP %= 0x20
If ((HINP == HOUP))
{
HOUP++
HOUP %= 0x20
}
Release (HDMX)
}
Method (HINF, 0, Serialized)
{
Acquire (HDMX, 0xFFFF)
If ((HINP == HOUP))
{
Local0 = Zero
}
Else
{
Local0 = DerefOf (HDAT [HOUP])
HOUP++
HOUP %= 0x20
}
Local1 = (HINP != HOUP)
Release (HDMX)
If (Local1)
{
Notify (WLSW, 0x80) // Status Change
}
Return (Local0)
}
Method (HSAV, 0, Serialized)
{
If (DRVS)
{
Acquire (HDMX, 0xFFFF)
Release (HDMX)
}
}
Method (HRES, 0, Serialized)
{
If (DRVS)
{
Acquire (HDMX, 0xFFFF)
SHRF ()
Release (HDMX)
}
}
Method (SINF, 0, Serialized)
{
SIFR [Zero] = SGET (Zero)
SIFR [One] = SGET (One)
Return (SIFR) /* \_SB_.WLSW.SIFR */
}
Method (SGET, 1, Serialized)
{
Local0 = Zero
If ((Arg0 == Zero))
{
Local0 = Zero
If (WSST ())
{
If (CMST ())
{
Local0 = One
}
}
}
If ((Arg0 == One))
{
Local0 = WNSW /* \_SB_.WLSW.WNSW */
}
Return (Local0)
}
Method (SSET, 2, Serialized)
{
If ((Arg0 == Zero)){}
If ((Arg0 == One))
{
WNSW = Arg1
SHRF ()
}
Return (Zero)
}
Method (NOTF, 0, Serialized)
{
HIND (0x50)
Notify (WLSW, 0x80) // Status Change
SHRF ()
}
Method (WSST, 0, Serialized)
{
Local0 = ^^HKEY.SGET (0x0B)
If (((Local0 & 0xC0) == 0xC0))
{
Return (One)
}
Return (Zero)
}
Method (CMST, 0, Serialized)
{
Return (One)
}
Method (SHRF, 0, Serialized)
{
Local0 = Zero
Local1 = Zero
If ((WNSW & One))
{
If ((SGET (Zero) & One))
{
Local0 |= One
Local1 = One
}
}
Local0 |= 0x10
Local0 |= 0x20
Local0 |= 0x0100
Local0 |= 0x0200
Local0 |= 0x00010000
Local0 |= 0x00020000
Local0 |= 0x1000
Local0 |= 0x2000
ASRV (0x0F, 0x03, Local0)
}
}
}通读这段ASL代码后发现这个开关并不是标准的物理断电,而是一个触发Notify(WLSW, 0x80)的ACPI事件设备,通过调用HKEY下的SGET(0x0B)来读取物理状态。
欺骗
在尝试编写驱动之前,遇到了第一个坑:MAT0028设备在Linux下默认居然不启动!查看 _STA 方法发现,这玩意儿居然有系统检测!
If (_OSI ("Windows 2012"))
{
Local0 = 0x0F
}居然需要Windows8以上才初始化这个设备!
按照使用Linux的惯例,直接去grub添加acpi_osi欺骗,添加后重启系统再cat /sys/bus/acpi/devices/MAT0028/status,果然成功返回了 15 (0x0F),活了!
驱动
虽然现在设备是活了,但是没有任何驱动来认领它,对于系统而言,其只是一个莫名其妙的设备,没有信息,没有内容。本来去Linux的主干分支找到了一个名为panasonic-laptop的驱动,但是仔细阅读后发现其过于古早,不如直接自己手搓一个内核模块来的快。
好!说干就干!
驱动的逻辑很简单:绑定ACPI\MAT0028,注册一个标准的rfkill设备。当拨动开关触发ACPI Notify时,驱动去调用DSDT里的WSST方法读取开关位置,并同步给内核的无线电管理子系统。
以下是测试可用的内核模块代码,为了考虑到跟Linux主干的驱动兼容性,故其不叫PanasonicXX驱动,而是叫MatsushitaRadioSwitch,这样以便区分。
#include <linux/module.h>
#include <linux/acpi.h>
#include <linux/rfkill.h>
#define MODULE_NAME "MatRadioSwitch"
struct mat_radio_data {
struct rfkill *rfk;
};
static int mat_rfkill_set_block(void *data, bool blocked) { return 0; }
static const struct rfkill_ops mat_rfkill_ops = { .set_block = mat_rfkill_set_block };
static void mat_update_status(struct acpi_device *device, struct rfkill *rfk) {
unsigned long long result = 0;
if (ACPI_SUCCESS(acpi_evaluate_integer(device->handle, "WSST", NULL, &result)))
rfkill_set_hw_state(rfk, !result);
}
static void mat_acpi_notify(struct acpi_device *device, u32 event) {
struct mat_radio_data *data = acpi_driver_data(device);
if (event == 0x80 && data && data->rfk)
mat_update_status(device, data->rfk);
}
static int mat_acpi_add(struct acpi_device *device) {
struct mat_radio_data *data;
unsigned long long res = 0;
acpi_evaluate_integer(device->handle, "SQTY", NULL, &res);
data = devm_kzalloc(&device->dev, sizeof(*data), GFP_KERNEL);
if (!data) return -ENOMEM;
device->driver_data = data;
data->rfk = rfkill_alloc("matsu_radio_sw", &device->dev,
RFKILL_TYPE_WLAN, &mat_rfkill_ops, data);
if (!data->rfk || rfkill_register(data->rfk)) {
if (data->rfk) rfkill_destroy(data->rfk);
return -ENOMEM;
}
mat_update_status(device, data->rfk);
return 0;
}
static void mat_acpi_remove(struct acpi_device *device) {
struct mat_radio_data *data = acpi_driver_data(device);
if (data && data->rfk) {
rfkill_unregister(data->rfk);
rfkill_destroy(data->rfk);
}
}
static const struct acpi_device_id mat_device_ids[] = { { "MAT0028", 0 }, { "", 0 } };
MODULE_DEVICE_TABLE(acpi, mat_device_ids);
static struct acpi_driver mat_radio_driver = {
.name = "mat_radio_driver",
.class = "pcc",
.ids = mat_device_ids,
.ops = { .add = mat_acpi_add, .remove = mat_acpi_remove, .notify = mat_acpi_notify },
};
module_acpi_driver(mat_radio_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Yuu");
MODULE_DESCRIPTION("Matsushita Radio Switch ACPI Driver");总结
成功编译后,insmod,拨动开关,WIFI果然被成功开启和关闭了!这次驱动开发十分成功!








