起因

最近从朋友那搞到了一台松下的“加拉帕戈斯”笔记本——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果然被成功开启和关闭了!这次驱动开发十分成功!

本文及其附件均采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。

添加新评论