ACPI deep dive

It’s been a while since I blogged, and I thought this was cool enough to share.

After using my ThinkPad T14 for a while, I noticed that the LED above the power button (which doubles as the fingerprint sensor) can change colors - green and orange - in addition to the default white. I observed this during fingerprint registration and authentication on Windows, where it shows the scanner status:

  1. White - default power indicator when the fingerprint scanner is not in use.
  2. Green - sensor is ready and actively waiting for your finger.
  3. 3 orange blinks - if:
    • the fingerprint is not recognized during login, or
    • the finger needs to be repositioned during registration.

None of this happens on Arch Linux. I wanted to understand why, and ideally fix it. I also noticed a second issue: the microphone mute LED stays on permanently, regardless of whether the microphone is actually muted. I suspected both problems might share a common cause.

Discovering thinkpad_acpi Link to heading

After some searching, I came across the ThinkPad ACPI Extras Driver. This is a Linux kernel driver that exposes ThinkPad-specific hardware - including LEDs, hotkeys, and the embedded controller - to userspace. It communicates with the firmware through ACPI (Advanced Configuration and Power Interface) and the ACPI EC (Embedded Controller).

I spent a while reading about ACPI to understand what I was dealing with. In short:

  • ACPI is a standard that lets the OS communicate with firmware for power management and hardware control. It uses a bytecode language called AML that is compiled into tables stored in firmware.
  • The most important table is the DSDT (Differentiated System Description Table), which contains all the AML methods describing how to interact with hardware - including LEDs.
  • The RSDP (Root System Description Pointer) is the entry point - it’s a small structure the OS finds at boot that points to the rest of the ACPI table hierarchy. It’s not where the action is; it’s just the signpost.
  • The EC (Embedded Controller) is a small microcontroller on the motherboard that manages low-level hardware like LEDs, battery, thermal sensors, and keyboard hotkeys. ACPI methods talk to it by reading and writing EC registers.

Dumping and reading the DSDT Link to heading

The DSDT is accessible directly from Linux:

sudo cat /sys/firmware/acpi/tables/DSDT > /tmp/dsdt.bin
cd /tmp
iasl -d dsdt.bin   # decompile AML bytecode to readable DSL

Searching the decompiled output for LED-related methods immediately turned up something useful:

Method (LED, 2, NotSerialized)
{
    Local0 = (Arg0 | Arg1)
    If (H8DR)
    {
        HLCL = Local0
    }
    Else
    {
        WBEC (0x0C, Local0)
    }
}

The LED method takes two arguments - a LED number and a state - ORs them together, and writes the result to the EC register HLCL (at offset 0x0C). The encoding is:

  • Lower nibble = LED selector (which LED)
  • Upper nibble = state (0x00 = off, 0x80 = on, 0xC0 = fast blink, 0xA0 = slow blink)

Searching for all unique LED(...) calls in the DSDT gave a clean picture of every LED the firmware knows about:

LED numberIdentityStates used
0x00Power button0x00 0x80 0xA0 0xC0
0x07Standby0x00 0xC0
0x0ALid logo dot0x00 0x80 0xA0 0xC0
0x0EMic mute0x00 0x80 0xC0

I confirmed each of these by calling the ACPI method directly via acpi_call kernel module:

echo '\_SB.PCI0.LPC0.EC0.LED 0x00 0x80' | sudo tee /proc/acpi/call  # power LED on
echo '\_SB.PCI0.LPC0.EC0.LED 0x0E 0x80' | sudo tee /proc/acpi/call  # mic mute LED on

All four LEDs responded correctly. The power button LED supports four states - but importantly, all of them are white. There is no green or orange state anywhere in the DSDT. The ACPI firmware simply doesn’t know about color on this LED.

The _SST system status method Link to heading

One particularly interesting method in the DSDT is _SST (System Status), which the OS calls during sleep/wake transitions. It reveals exactly how the firmware uses the LED during system state changes:

_SST(0x00) → LED(0x00, 0x00) + LED(0x0A, 0x00)  // all off
_SST(0x01) → LED(0x00, 0x80) + LED(0x0A, 0x80)  // running (white)
_SST(0x02) → LED(0x00, 0x80) + LED(0x0A, 0x80) + LED(0x07, 0xC0)  // sleeping
_SST(0x03) → LED(0x00, 0xC0) + LED(0x0A, 0xC0)  // waking (blink)

Interestingly, LED(0x00, ...) and LED(0x0A, ...) are always called together. This suggests they are two channels of the same physical LED - but testing both in isolation confirmed that 0x0A is the lid logo dot, not a color channel of the power button. Both channels being white confirmed that ACPI has no knowledge of green or orange on the power button LED at all.

The mic mute LED - a different problem Link to heading

The mic mute LED issue turned out to be unrelated to ACPI LED control. The DSDT has a method called MMTS for mic mute:

Method (MMTS, 1, NotSerialized)
{
    If (HDMC)
    {
        Noop         // ← the culprit
    }
    ElseIf ((Arg0 == 0x02))
    {
        LED (0x0E, 0x80)
    }
    ...
}

The HDMC flag (bit field at EC offset 0xD79) means “Hardware/Driver Mic Controlled” - when set, it signals that the OS audio stack is responsible for the LED, so MMTS steps aside. On AMD ThinkPads, HDMC is always set. The kernel driver calls MMTS, which silently does nothing, and the LED stays stuck on from firmware initialization.

The fix, however, turned out to be entirely in userspace. The audio-micmute LED trigger in the kernel is supposed to fire whenever the mic mute state changes, which then drives platform::micmute brightness and ultimately calls the LED. But this whole chain depends on PipeWire’s ALSA ACP device loading correctly - and it wasn’t, because alsa-plugins wasn’t installed.

sudo pacman -S alsa-plugins

After installing and rebooting, the mic mute LED started tracking mute state correctly. The WirePlumber log had been quietly warning about it the whole time:

spa.alsa: Path Mic ACP LED is not a volume or mute control
wp-device: SPA handle 'api.alsa.acp.device' could not be loaded

The fingerprint LED - confirmed USB Link to heading

Since the DSDT contains no color information for the power button LED, and the Synaptics fingerprint sensor (06cb:00f9) sits on the USB bus, the color control had to be coming from somewhere else. I set up a Windows VM in VirtualBox with USB passthrough for the sensor and captured the USB traffic with Wireshark and USBPcap.

During fingerprint registration in Windows, I could visually confirm the LED changing colors - green when ready, orange on error - while watching the USB traffic. Unfortunately, the traffic was encrypted with TLSv1.2. I confirmed this by digging through the strings in the Synaptics fingerprint driver DLLs on Windows, which revealed TLS references in the host-to-device communication.

This is actually consistent with how match-on-chip sensors work. The Synaptics 06cb:00f9 is a match-on-chip device - unlike older match-on-host sensors where fingerprint processing happens on the CPU, this sensor has its own MCU and flash storage on-chip. The fingerprint templates are enrolled directly onto the chip, matching happens on the chip, and the host only receives a pass/fail result. The TLS channel protects the enrollment and authentication commands between the host driver and the chip firmware.

This also means fingerprints are not automatically shared between Windows and Linux even though they physically live on the chip - Windows Hello tags its enrollment data with its own credentials, and fprintd on Linux maintains a separate enrollment. Both use chip slots independently.

The LED color commands are buried inside this encrypted channel, which means reverse engineering them requires either finding the session key or finding another approach. That’s the next step.

Where things stand Link to heading

IssueStatus
Mic mute LED always on✅ Fixed - install alsa-plugins
Fingerprint LED green/orange🔍 In progress - LED control is via encrypted USB
Power button LED ACPI states✅ Fully mapped from DSDT

The mic mute fix was surprisingly simple once the root cause was found. The fingerprint LED is a harder problem - the commands exist, the hardware supports them, but they’re hidden behind TLS. The next step is identifying the key exchange in the TLS handshake and finding a way to decrypt the traffic, or finding another path into the LED control that doesn’t require decryption.


References Link to heading