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

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

This 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.

I’ve been informed that this method also works for the RedmiBook 14, which has similar hardware.

Author | nns

Ethical Hacking and Cybersecurity professional with a special interest for hardware hacking, IoT and Linux/GNU.