Patching ACPI tables to enable deep sleep on the RedmiBook 16
I recently purchased Xiaomi's RedmiBook 16. For the price, it's an excellent MacBook clone. Being a Ryzen-based laptop, Linux support works great out of the box, with one big caveat: deep sleep does not work. I decided to try and fix this.
Deep sleep?
To clear some confusion about what I mean by deep sleep, I need to explain a bit of how hibernation/suspending works.
There are a number of sleep states on modern machines.
The most basic of these is referred to as S0
. It's implemented purely in software (i.e. the kernel), and doesn't do a very good job at preserving battery. While userland processes are suspended, the machine (and the CPU) is still running and using power. As S0
doesn't rely on hardware compatibility, it's enabled on all devices. Using this mode, my RedmiBook's battery drained to 0% overnight.
S1
, also known as "shallow" sleep, is similar to S0
but enables some additional power saving features such as suspending power to nonboot CPUs. This mode still doesn't provide significant power saving, however.
S3
("suspend-to-RAM") saves the system's state to memory and powers off everything but the memory itself. On boot, this state is restored and the system can resume from suspension. This mode is the one known as "deep sleep" and can provide acceptable levels of power saving. Overnight, this drains only about 3-5% battery on my laptop, which is perfectly fine for my needs.
S4
is known as "suspend-to-disk" and works a lot like S3
, but instead, as you can probably tell by the name, saves the state to disk. This means you can remove power from the device completely and resuming from suspension would still work as the state is not stored in volatile memory.
ACPI
Modes S1
- S4
require hardware compatibility. This compatibility is usually advertised to the operating system's kernel using ACPI definitions. The kernel uses this information to know what suspension methods to provide to the user.
On some systems (such as the RedmiBook), the ACPI definitions declare no or only conditional support for some (or all) modes.
You can see what sleep states your machine supports by looking into /sys/power/mem_sleep
. On my machine, only S0
("s2idle") was supported:
$ cat /sys/power/mem_sleep
[s2idle]
Annoying. I knew deep sleep works on Windows, so it's not a case of missing hardware support. I suspected misconfigured ACPI tables to be at fault here.
Patching ACPI
Luckily, Linux supports loading "patched" ACPI tables during the boot process. It is possible to grab the currently used tables, decompile them, patch out the parts which block S3
from being supported, recompile, and embed the patched table into a cpio
archive.
The specific ACPI component we're interested in is the DSDT table. We can dump this somewhere safe:
# cat /sys/firmware/acpi/tables/DSDT > dsdt.aml
We'll use iasl
from the ACPICA software set to decompile the dumped table:
$ iasl -d dsdt.aml
If you get warnings about unresolved references to external control methods, it might be worth decompiling again, but this time including the SSDT tables. See this post at encryp.ch for more info.
You'll end up with a human-readable dsdt.dsl
file. You'll want to peek into this and search for "S3 System State" to find what you're looking for. In my case, it was nested into two flag checks, which I simply deleted, so as to advertise S3
support even if the flag checks failed:
@@ -18,7 +18,7 @@
* Compiler ID " "
* Compiler Version 0x01000013 (16777235)
*/
-DefinitionBlock ("", "DSDT", 1, "XMCC ", "XMCC1953", 0x00000002)
+DefinitionBlock ("", "DSDT", 1, "XMCC ", "XMCC1953", 0x00000003)
{
/*
* iASL Warning: There were 9 external control methods found during
@@ -769,19 +769,13 @@ DefinitionBlock ("", "DSDT", 1, "XMCC ", "XMCC1953", 0x00000002)
Zero,
Zero
})
- If ((CNSB == Zero))
- {
- If ((DAS3 == One))
- {
- Name (_S3, Package (0x04) // _S3_: S3 System State
- {
- 0x03,
- 0x03,
- Zero,
- Zero
- })
- }
- }
+ Name (_S3, Package (0x04) // _S3_: S3 System State
+ {
+ 0x03,
+ 0x03,
+ Zero,
+ Zero
+ })
Name (_S4, Package (0x04) // _S4_: S4 System State
{
You'll also want to increment the version number by one (as shown above) as the patched table wouldn't be loaded otherwise.
Once this is done, we can recompile it, again using iasl
:
$ iasl dsdt.dsl
If this refuses to compile due to the compiler thinking Zero
is not a valid type, check out the post at encryp.ch, where they shed some light on this.
Compiling using iasl
overwrites the old .aml
file. We'll need to create the proper directory tree in order to archive it in a manner which the kernel accepts:
$ mkdir -p kernel/firmware/acpi
Copy the patched table into place and create the archive using the cpio
tool:
$ cp dsdt.aml kernel/firmware/acpi/.
$ find kernel | cpio -H newc --create > dsdt_patch
Copy the newly created archive into your boot directory:
# cp dsdt_patch /boot/.
You'll need to figure out how to get your bootloader to load this archive on boot. As I use systemd-boot
, I modified my default entry and added the following initrd
line before initramfs
is loaded:
$ grep initrd /boot/loader/entries/arch.conf
initrd /amd-ucode.img
initrd /dsdt_patch
initrd /initramfs-linux.img
For grub
users, you'll need to edit the /boot/grub/grub.cfg
file and add the same line.
I also recommend adding the following kernel parameter, as that makes sure that S3
is used by default instead of S0
:
mem_sleep_default=deep
After rebooting, peek into /sys/power/mem_sleep
once again to make sure deep
is supported and enabled as the current mode:
$ cat /sys/power/mem_sleep
s2idle [deep]
It's also a good idea to check whether the system properly suspends and resumes. In my case, there have been no issues and I get excellent battery life during sleep.
Some readers have tested this method and reported that this method also works for the RedmiBook 14 and the Ryzen edition of the Xiaomi Notebook Pro 15, which have similar hardware.