粉丝服务
Fan Service

原始链接: https://flak.tedunangst.com/post/fan-service

这位开发者为OpenBSD系统上的华硕笔记本电脑编写了一个自定义的ACPI WMI驱动程序,用于控制风扇速度。通常,这一功能需要通过Fn组合键实现,但在OpenBSD原生环境下无法使用。该项目需要理解ACPI和WMI,解读GUID和ACPI方法调用。过程中,开发者使用了`acpidump`和`iasl`读取系统的ACPI代码,逆向工程厂商特定的ACPI方法,并找出控制风扇所需的正确参数。开发者也参考了Linux系统中已有的华硕支持驱动程序,尤其是在设备ID和用于数据提取的适当位掩码方面。在克服了GUID字节序错误和ACPI事件处理误解等挑战后,成功实现了风扇配置文件的切换。新的驱动程序允许用户通过按键切换“低噪音模式”(低风扇速度,更长的电池续航时间)和“涡轮模式”(最高性能)。

这篇 Hacker News 帖子讨论了笔记本电脑功能(例如亮度控制)日益增长的复杂性。传统上,嵌入式控制器 (EC) 直接处理这些功能,但现在通常需要操作系统驱动程序和 GPU 的参与。用户对此“倒退”表示遗憾,认为这是走向封闭式 PC 生态系统的趋势。 评论者们就这种转变的优缺点展开了辩论。一些人认为这实际上是“欠工程”,源于忽视软件质量,而另一些人则认为这是为了处理现代硬件和多任务操作系统而必要的演变。一位用户分享了反向工程和修补其 EC 固件的故事。 讨论还涉及到诸如 Coreboot 之类的开源 BIOS、ACPI WMI 以及 Linux 驱动程序的挑战,以及在没有操作系统支持的情况下复制经典 Thinkpad 功能的问题。总的来说,许多人似乎怀念更简单的界面,在这些界面中,用户空间代码可以直接控制硬件,同时又需要在现代操作系统环境中平衡安全性和系统稳定性的需求。

原文

ASUS laptops generally have a feature that lets the user toggle the fan speed. Fn-F5 on some models, Fn-F on others. The direct effect is to limit the fan speed, from whisper mode to megablast, and indirectly control performance. But it doesn’t work in OpenBSD, so I needed to write an ASUS ACPI WMI driver.

acpi

ACPI is a hardware abstraction layer that lets the computer tell the operating system how to do things. It provides functions in byte code which the OS interprets to perform certain tasks. So instead of having to write a driver for every CPU, you call the ACPI method _PSS and that tells you what to do.

Of course, vendors want to include functionality that isn’t defined in the standard, so they add some extra methods, and then you end up writing some custom drivers anyway, for the little buttons that turn the microphone on and off, etc. OpenBSD (and other OSs) have lots of little drivers for ThinkPads and whatnot.

ACPI method names are only four letters long, so what do we do if two vendors include a method called WMNB and how do we determine what it does? (Technically, the ACPI nodes are identified by seven letter names, and the methods live under them, so it’s not really a problem, but somebody got paid to make a solution anyway.)

wmi

The solution is an extension to ACPI called WMI, which stands for Windows Multiplies Irritations or something. You ask ACPI for a buffer called _WDG, and this contains a table mapping GUIDs (globally unique, says so right there in the name) to local method names. Looking through this table, we come across DCBA-FE-10-23-456789, which we recognize as a GUID of interest, and this tells us the method to call is WMNB, but now we know it’s the right one.

OpenBSD does not have a WMI driver, which is part of what I need to set the fan profile. There is a WMI driver in Linux, and it includes an ASUS support driver for a variety of functions I’m interested in.

first steps

First, I wrote a little acpi driver that attaches to PNP0C14. Read the _WDG buffer and print out all the GUIDs. Didn’t recognize any of the values, then realized I got the byte order wrong. Fixed that, saw something very close, realized I still didn’t get the byte order quite right. GUIDs are not big endian, they are not little endian, they are Goldilocks endian.

With that sorted out, I was able to enable events and get a printf every time I pushed a hotkey.

events

After receiving an interrupt and landing in our callback, we need to identify which event just happened by calling _WED. I did this, but was only getting 1 in response every time, regardless of key. I was a little stuck here. Very close to getting what I wanted, but without much clue as to what went wrong.

Turns out you can just look at your system’s ACPI code. OpenBSD runs acpidump at boot and saves all the files. Copy them to a work directory, run iasl -d, and have a look. A quick grep for _WED reveals the source.

            Method (_WED, 1, NotSerialized)
            {   
                If ((Arg0 == 0xFF))
                {
                    Return (GANQ ())
                }
     
                Return (One)
            }
            Method (GANQ, 0, Serialized)
            {
                P8XH (Zero, 0xF2)
                If (AQNO)
                {
                    AQNO--
                    Local0 = DerefOf (ATKQ [AQHI])
                    AQHI++ 
                    AQHI &= 0x0F
                    Return (Local0)
                }

                Return (Ones)
            }

Even without knowing anything at all about ASL, this looks like a function that dequeues an event, but only if called with an argument of 255. That was the bug! Once I called it with the correct argument, I started receiving different event codes for backlight toggle, fan toggle, AC attach, etc.

devices

Wrote some more code to handle getting and setting device state. There’s a magic incantation to call an ACPI method and pass it a device id, which could be the keyboard backlight, or the wifi led, or the fan profile, etc. Started with the keyboard backlight, since this was pretty easy to observe.

After fixing a few simple bugs like reversing the device ids for fnlock and backlight, I was able to blink the keyboard light by pressing F7. It’s all coming together.

Toggling the fan profile didn’t seem to accomplish anything, though. Under Windows, there is an audible difference switching to whisper mode, but I didn’t observe any change.

The Linux driver includes code for at least three different fan and performance profile settings. The same driver is used for a wide range of laptops, from thin and lights to gaming fraggers. According to the Linux driver, you can detect which devices are present by reading their state, then checking for a high bit, and then getting the value by masking off some low bits. This seemed to indicate I had a fan boost device, but it wasn’t responding to my changes.

After printing out the raw values, I noticed they were all -2. That doesn’t seem very likely. But it definitely has a high bit set, and masking off the low bits made it look reasonable. I think I missed something in the Linux driver? There’s lots of shifts and masks in lots of places.

Back to the ACPI dump, the WMNB method is a giant switch statement that ends with Return (0xFFFFFFFE). So sure enough, there’s the -2 for a missing device, but I still don’t know which device id I should be using.

Back to the Linux driver, there’s an epic norse define for ASUS_WMI_DEVID_THROTTLE_THERMAL_POLICY_VIVO which I hadn’t yet integrated because I was testing on a Zenbook, not a Vivobook. Added a check anyway, rebooted, and that’s a bingo! The Linux define name is a lie. Confirmed by inspecting the ASL code.

            If ((IIA0 == 0x00110019))
            {
                Return (FANL (IIA1))
            }

Now I had all the pieces. After a bit of cleanup and refactoring, my driver is complete. Kinda. There’s still a bunch of sensors that would be interesting to read, but my immediate concern has been addressed.

results

Pressing Fn-F after boot drops the fan speed noticeably, from a medium whir to barely a whisper. This has only a moderate effect on ordinary performance. The processor’s power budget is reduced, but not its (single core) clock speeds, so everything still runs smoothly. Lengthy compiles are slower, but I can also switch back to megablast turbo mode with the tap of a button.

Battery life is also much better. The fan itself is obviously eating less power. I think the CPU also enters a somewhat lower power state in whisper mode, or it’s willing to sleep a little deeper. It’s not clear everything that changes, and it varies by machine, but that’s the beauty of it. I wrote the driver on an AMD Zenbook, but it works without changes on an Intel Vivobook. Very convenient.

misc

I also looked at the FreeBSD driver. FreeBSD directly uses the ACPICA code interfaces, calling function like AcpiEvaluateObject which personally look kinda out of place in BSD driver code. Linux does too, but all the functions have been renamed to acpi_evaluate_object, so they resemble the local style. I’m not entirely sure of the history here, but I think the generic ACPICA was at some pointer extracted and generified from the Linux code. OpenBSD uses a custom AML interpreter, so we have functions like aml_evalname.

In OpenBSD, nearly all header files are kept with their C files. So both dsdt.c and dsdt.h live in dev/acpi. (Notable exception is core C files in kern and headers in sys.) As a first time acpi driver writer, it was very convenient that all the OpenBSD code I needed to reference, from sample drivers to data structures to helper functions, was all in one spot.

Linux separates things such that I was looking at C files in drivers/platform/x86 and header files in include/linux/platform_data/x86. And the ACPI code lives other places as well. It’s all very orderly, but at times it felt like navigating a grocery store that arranges products in alphabetical order. Logical, but not exactly cozy.

联系我们 contact @ memedata.com