This is going to be a long one, and it has taken me a while to write up (I started writing this in 2022, and now we’re almost in 2025!). While this isn’t as polished as I’d liked for it to be, it’s been sitting as a draft for too long, and I almost want to get rid of this laptop, heh.
Four years ago my old 2013 MSI GE40 laptop’s battery finally kicked the bucket, and after swearing off Nvidia and wanting to try out a CPU from team Red, I acquired the only all-AMD laptop I could find at the time against my better judgement: a Dell G5 5505 Special Edition. Honestly, this laptop has been mostly OK (other than having to replace one of its cooling fans under warranty not even two weeks after getting the laptop, great QC there Dell). One of the first things that caught my attention was the full RGB blacklit keyboard on the laptop, and I immediately wanted to see if I could get it working on Linux. This will probably be the subject of another blog post, later, though. However, in the process of reversing what the RGB controller was doing, I figured out that there was a way to reboot the RGB controller into DFU flashing mode, and that in Windows Dell’s software did this via ACPI calls.
For almost all commands that follow, one needs the acpica
or iasl
package, ideally at least the 20240927
or R09_27_24
release.
Dumping and disassembling ACPI tables
On Linux, acpidump
and acpixtract
make quick work of this:
# Extracts all ACPI tables into the tables.acpi file
sudo sh -c "acpidump > tables.acpi"
# Extracts all tables individually into their own .dat binary file
acpixtract -a tables.acpi
At this point, there should be a ton of *.dat files in the working directory. For some reason this laptop has over 20 ssdt
tables.
To then disassemble the files, I do the following (my shell is fish, so some modification may be required to work with bash):
#!/usr/bin/fish
for I in *.dat
echo -----------$I
iasl -e $(ls *.dat | grep -v $I) -d $I
end
In essence we want to pass as many tables to the disassembler as possible so that it can find methods and other objects that the table being disassembled may use. We can’t pass the same table as a reference otherwise iasl
complains about duplicate objects.
Getting WMI information
Great, now we can inspect the tables. From some… poking around Dell files on Windows, I determined that it updates the RGB firmware through some WMI ACPI functions. The Linux kernel has some pretty decent documentation on WMI, including stuff relating to Dell WMI functionality.
We need the bmf2mof
utility to be able to parse some of the binary blobs exposed by the Linux kernel related to WMI methods. On my system, the right BMOF data for the WMAX function can be extracted as follows:
# As root or with root privileges
bmf2mof < /sys/bus/wmi/devices/05901221-D566-11D1-B2F0-00A0C9062910-1/bmof
[WMI, Dynamic, Provider("WmiProv"), Locale("MS\\0x409"), Description("WMI Function"), guid("{A70591CE-A997-11DA-B012-B622A1EF5492}")]
class AWCCWmiMethodFunction {
[key, read] string InstanceName;
[read] boolean Active;
[WmiMethodId(19), Implemented, read, write, Description("Get Fan Sensors.")] void GetFanSensors([in] uint32 arg2, [out] uint32 argr);
[WmiMethodId(20), Implemented, read, write, Description("Thermal Information.")] void Thermal_Information([in] uint32 arg2, [out] uint32 argr);
[WmiMethodId(21), Implemented, read, write, Description("Thermal Control.")] void Thermal_Control([in] uint32 arg2, [out] uint32 argr);
[WmiMethodId(23), Implemented, read, write, Description("MemoryOCControl.")] void MemoryOCControl([in] uint32 arg2, [out] uint32 argr);
[WmiMethodId(26), Implemented, read, write, Description("System Information.")] void SystemInformation([in] uint32 arg2, [out] uint32 argr);
[WmiMethodId(32), Implemented, read, write, Description("FW Update GPIO toggle.")] void FWUpdateGPIOtoggle([in] uint32 arg2, [out] uint32 argr);
[WmiMethodId(33), Implemented, read, write, Description("Read Total of GPIOs.")] void ReadTotalofGPIOs([out] uint32 argr);
[WmiMethodId(34), Implemented, read, write, Description("Read GPIO pin Status.")] void ReadGPIOpPinStatus([in] uint32 arg2, [out] uint32 argr);
[WmiMethodId(36), Implemented, read, write, Description("Read Platform Properties.")] void ReadPlatformProperties([out] uint32 argr);
[WmiMethodId(37), Implemented, read, write, Description("Game Shift Status.")] void GameShiftStatus([in] uint32 arg2, [out] uint32 argr);
};
Oh, that’s so nice. From here we know the GUID of the function A70591CE-A997-11DA-B012-B622A1EF5492
and all of the WMI methods and their IDs. With this information we can go find them in the ACPI tables.
For BIOS version 1.24.0, this is the WMAX
function in ssdt20.dsl
. In there there’s a large switch-case statement with IDs in hexadecimal that match up with the IDs in the BMOF
.
Reimplementing ACPI GPIO methods to avoid screwing up their output
The ReadGPIOpPinStatus
function (ID 34 or 0x22) uses the WISC
method (defined in dsdt.dsl
), which changes the direction of the GPIO pin to read it. This… is not useful if we just want to read the pin without affecting its function (which we can do), as changing its direction makes the GPIO act as thought it was set to output low.
I implemented two new ACPI functions, WISI
and WISO
, to refactor WISC
and to stop screwing up the GPIO output on a GPIO read call. One observation I made is that the “magic” address 0xFED81500
is the address of the gpio-amd-fch
GPIO registers (found in the Linux kernel in drivers/gpio/gpio-amd-fch.c
).
Method (WISO, 2, Serialized)
{
Local0 = (Arg0 << 0x02)
Local0 += 0xFED81500
OperationRegion (GREG, SystemMemory, Local0, 0x04)
Field (GREG, ByteAcc, NoLock, Preserve)
{
Offset (0x02),
, 6,
OPVL, 1,
OPEN, 1
}
OPEN = One
OPVL = Arg1
}
Method (WISI, 1, Serialized)
{
Local0 = (Arg0 << 0x02)
Local0 += 0xFED81500
OperationRegion (GREG, SystemMemory, Local0, 0x04)
Field (GREG, ByteAcc, NoLock, Preserve)
{
Offset (0x02),
PSTS, 1
}
Local2 = PSTS /* \WISI.PSTS */
Return (Local2)
}
I then re-implemented method IDs 0x20 (32) and 0x22 (34) using these new methods. This should let us query the GPIO state without messing with the GPIO direction.
// Write NRST or BOOT0
// Name=FWUpdateGPIOtoggle
Case (0x20)
{
AXBF = Arg2
// BFB0 is pin select (1 == NRST, 0 == BOOT0)
If ((BFB0 == Zero))
{
If ((BFB1 == Zero))
{
WISO(0x05, Zero)
}
Else
{
WISO(0x05, One)
}
}
ElseIf ((BFB0 == One))
{
If ((BFB1 == Zero))
{
WISO(0x0A, Zero)
}
Else
{
WISO(0x0A, One)
}
}
Return (Zero)
}
// Name=ReadTotalofGPIOs
Case (0x21)
{
Return (0x02)
}
// Name=ReadGPIOpPinStatus
Case (0x22)
{
AXBF = Arg2
Local0 = 0x02
// BFB0 is pin select (1 == NRST, 0 == BOOT0)
// WISI does not switch the pin to input
If ((BFB0 == Zero))
{
Local0 = WISI (0x05)
}
ElseIf ((BFB0 == One))
{
Local0 = WISI (0x0A)
}
Return (Local0)
}
Fixing the other ACPI tables (is pain)
If one tries to just recompile the tables, it won’t work. Most of the tables have a ton of issues. At a bare minimum, however, dsdt.dsl
and ssdt20.dsl
(or whichever ssdt has the WMAX
function) need to build. Unfortunately the process for fixing these tables is non-trivial and rather tedious. My best recommendation is to scour online for any help in trying to understand the iasl
error messages.
One critical change that needs to happen is that the version number near the top of the source files needs to be increased, else the Linux ACPI table override system won’t bother trying to override the table.
Compiling the new table is as simple as doing:
# Example rebuilding of the DSDT table
iasl dsdt.dsl
# If successful, it emits dsdt.aml
If there are errors, iasl
will report them. On success a .aml
file is generated, and it contains the compiled table.
Hacking Linux: Overriding the right tables!
Alright, we have the tables re-compiled. Now what? The Linux kernel can override ACPI tables, but the mechanism it uses is by table type… and we’re overriding at least one SSDT out of a bunch of SDDT ones, so the kernel can’t tell them apart.
I’ve come up with what is arguably the worst hack I’ve implemented by far to work around this issue. My hack checks each table in the kernel against the original table size, if they match the kernel prepares to replace the table. It is nasty, but it works, as none of the SSDT tables I’ve been overriding have the same original size.
Here’s the patch, for the 6 SSDT
tables I’m overriding for BIOS version 1.24.0:
-- a/drivers/acpi/tables.c 2022-03-20 13:14:17.000000000 -0700
+++ b/drivers/acpi/tables.c 2022-04-13 16:37:55.389618306 -0700
@@ -688,6 +688,10 @@ acpi_table_initrd_override(struct acpi_t
struct acpi_table_header *table;
u32 table_length;
+ // FIXME Hacks by Gabriel M. to load a specific SSDT on a Dell G5505 SE
+ bool is_ssdt;
+ bool hack;
+
*length = 0;
*address = 0;
if (!acpi_tables_addr)
@@ -705,7 +709,9 @@ acpi_table_initrd_override(struct acpi_t
table_length = table->length;
/* Only override tables matched */
- if (memcmp(existing_table->signature, table->signature, 4) ||
+ is_ssdt = !memcmp(existing_table->signature, "SSDT", 4);
+ hack = is_ssdt && (existing_table->length == 0x517 || existing_table->length == 0x53B || existing_table->length == 0x723C || existing_table->length == 0x28D || existing_table->length == 0x30C8 || existing_table->length == 0xC6C);
+ if ((is_ssdt && !hack) || memcmp(existing_table->signature, table->signature, 4) ||
memcmp(table->oem_id, existing_table->oem_id,
ACPI_OEM_ID_SIZE) ||
memcmp(table->oem_table_id, existing_table->oem_table_id,
@@ -713,6 +719,13 @@ acpi_table_initrd_override(struct acpi_t
acpi_os_unmap_memory(table, ACPI_HEADER_SIZE);
goto next_table;
}
+
+ // FIXME Hack, skip matching all SSDT tables except specific one
+ if (is_ssdt && !hack) {
+ acpi_os_unmap_memory(table, ACPI_HEADER_SIZE);
+ goto next_table;
+ }
+
/*
* Mark the table to avoid being used in
* acpi_table_initrd_scan() and check the revision.
This hack really sucks because it needs to be updated every BIOS update. I wish there were some way to differentiate SSDT
tables already available, but I haven’t found one yet.
To actually have the Linux kernel override the ACPI tables, I’m using dracut
to generate an initramfs image, and its acpi_override
option to actually install the *.aml
files as part of the initramfs. See the man pages for dracut
for more details.
Implementing a debugfs for these newly found ACPI methods
OK, finally, we’re at a point where we can do something interesting. There’s probably a better way to do this, but I’ve implemented some code to allow calling these WMI functions through the Linux kernel, and I’ve exposed them through the debugfs
interface.
I keep the patches that implement this in the kernel in a fork of the kernel on gitlab. Once the module is loaded it exposes the following files:
/sys/kernel/debug/dell_awcc/memory_volt
/sys/kernel/debug/dell_awcc/memory_freq
/sys/kernel/debug/dell_awcc/gameshift
/sys/kernel/debug/dell_awcc/boot0
/sys/kernel/debug/dell_awcc/nrst
I can’t vouch for the complete validity of the memory_*
stuff, but the boot0
and nrst
control the GPIOs connected to the STM32 managing the RGB controller. echo-ing 0 turns the GPIOs low, and echo-ing 1 turns them high. So, to change the RGB controller to its boot DFU mode:
echo 1 > /sys/kernel/debug/dell_awcc/boot0
echo 0 > /sys/kernel/debug/dell_awcc/nrst
echo 1 > /sys/kernel/debug/dell_awcc/nrst
And this is the dmesg
output of doing the above:
[17039.648454] usb 3-3.2: USB disconnect, device number 5
[17043.154318] usb 3-3.2: new full-speed USB device number 6 using xhci_hcd
[17043.242228] usb 3-3.2: New USB device found, idVendor=0483, idProduct=df11, bcdDevice=22.00
[17043.242235] usb 3-3.2: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[17043.242237] usb 3-3.2: Product: STM32 BOOTLOADER
[17043.242239] usb 3-3.2: Manufacturer: STMicroelectronics
[17043.242241] usb 3-3.2: SerialNumber: FFFFFFFEFFFF
Success! We’ve rebooted the RGB controller to its DFU programming mode and can be dumped using dfu-util
!
To turn the RGB chip back to its normal self:
echo 0 > /sys/kernel/debug/dell_awcc/boot0
echo 0 > /sys/kernel/debug/dell_awcc/nrst
echo 1 > /sys/kernel/debug/dell_awcc/nrst
And this is my kernel output:
[17144.938660] usb 3-3.2: reset full-speed USB device number 6 using xhci_hcd
[17145.014269] usb 3-3.2: device firmware changed
[17145.014859] usb 3-3.2: USB disconnect, device number 6
[17145.194627] usb 3-3.2: new full-speed USB device number 7 using xhci_hcd
[17145.285273] usb 3-3.2: New USB device found, idVendor=0483, idProduct=df11, bcdDevice= 2.00
[17145.285283] usb 3-3.2: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[17145.285288] usb 3-3.2: Product: DFU in FS Mode
[17145.285292] usb 3-3.2: Manufacturer: STMicroelectronics
[17145.285295] usb 3-3.2: SerialNumber: 206D335B5353
[17146.912500] usb 3-3.2: USB disconnect, device number 7
[17147.080577] usb 3-3.2: new full-speed USB device number 8 using xhci_hcd
[17147.176269] usb 3-3.2: New USB device found, idVendor=187c, idProduct=0550, bcdDevice= 2.00
[17147.176283] usb 3-3.2: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[17147.176289] usb 3-3.2: Product: AW-ELC
[17147.176295] usb 3-3.2: Manufacturer: Alienware
[17147.176300] usb 3-3.2: SerialNumber: 00.01
[17147.269992] hid-generic 0003:187C:0550.0005: hiddev96,hidraw0: USB HID v1.11 Device [Alienware AW-ELC] on usb-0000:07:00.4-3.2/input0
I do wonder, now that I know what GPIO pins are connected to the RGB controller, if we should just… bypass the ACPI table and talk to the controller directly. Maybe that’s an experiment for another time.