Enabling the S3 sleep state (suspend-to-RAM) on the Lenovo Yoga 7 AMD Gen 7 (and possibly, others)

December 6, 2022 -
Tags: hardware, linux, sysadmin, ubuntu

The lack of support for the Suspend-to-RAM functionality (ie. S3 sleep state), and its replacement with the disastrous Connected Standby (ie. S0ix sleep state) is a well-known plague on modern laptops.

In this article I’ll describe how to restore support for it on the Lenovo Yoga 7 AMD Gen 7. This method likely applies to other models; for example, the HP ENVY x360) has the same S3 conditional logic in the DSDT.

Content:

Procedure

Preliminary operations

The procedure is generic, and can be performed on any Linux distribution; the difference should be just in the tools package; on Ubuntu, install the acpica-tools:

$ sudo apt install -y acpica-tools

In order to verify which sleep states the machine supports, run:

# This message comes from the kernel ring buffer, which rotates; if nothing is shown, reboot and
# rerun the command.
#
$ sudo dmesg | grep 'ACPI.*supports S'
[    0.309933] ACPI: PM: (supports S0 S4 S5)

Dumping and patching the DSDT

Dump the DSDT:

# Can also be achieved via `acpidump -b`, which dumps more data (not required in this context).
#
$ sudo cat /sys/firmware/acpi/tables/DSDT > dsdt.dat

Disassemble it:

$ iasl -d dsdt.dat

The resulting disassembly, dsdt.dsl, is human readable. On the Lenovo Yoga 7 AMD Gen 7, one can see that the S3 state is supported, but with conditionals:

    If ((CNSB == Zero))
    {
        If ((DAS3 == One))
        {
            Name (_S3, Package (0x04)  // _S3_: S3 System State
            {
                0x03, 
                0x03, 
                Zero, 
                Zero
            })
        }
    }

I don’t have domain knowledge, however, my educated guess is that this is (primarily) a check whether the option is set in the firmware (the Lenovo Yoga 7 AMD Gen 7 allows the user access only very basic firmware settings, and this is not included).

The fix is simply to remove the conditionals; this can be done with any editor, or with a Perl script:

# A backup file (`dsdt.dsl.bak`) is created.
#
# Regex: remove the four lines before "S3_: S3 System State" and the two lines after; keep the six
# lines in between.
#
perl -0777 -i.bak -pe 's/(.+\n){4}(.+_S3_: S3 System State\n(.+\n){6})(.+\n){2}/$2/m' dsdt.dsl

We also need to bump the DSDT revision, otherwise when booting, the patched DSDT will be overridden (this is not required if patching the kernel):

# Regex: replace the last value of the DSDT table header definition:
#
perl -i -pe 's/^DefinitionBlock.+\K0x00000001/0x00000002/' dsdt.dsl

Now we just reassemble the DSDT:

iasl -tc dsdt.dsl

This will generate multiple files - different override methods require different files.

Overriding the DSDT

There are different approaches to overriding the DSDT. I’ll describe what I’ve tested, and the pros/cons.

Initrd hook

The best method is to add an initrd hook; it’s clean, and it doesn’t require any maintenance:

# Create the initrd image, including the patched DSDT in the approprite directory, which corresponds
# to the `firmware/acpi` subdirectory of the `/sys` virtual filesystem.
#
mkdir -p kernel/firmware/acpi
cp dsdt.aml kernel/firmware/acpi
find kernel | cpio -H newc --create | sudo tee /boot/acpi_override > /dev/null

# Now create the hook. Note that this is not the canonical style for hooks; it's been reduced to the
# simplest form, for clarity.
#
sudo tee /etc/initramfs-tools/hooks/acpi_override << 'SH'
#!/bin/sh

if [ "$1" = prereqs ]; then
  echo
else
  . /usr/share/initramfs-tools/hook-functions
  prepend_earlyinitramfs /boot/acpi_override
fi
SH

sudo chown root: /etc/initramfs-tools/hooks/acpi_override
sudo chmod 755 /etc/initramfs-tools/hooks/acpi_override

# Now update the initramfs (for all the kernels).
#
sudo update-initramfs -k all -u

Patching the kernel

For those who use a patched kernel, it’s just a matter of setting the related configuration symbol(s):

# Run from the kernel source root.
#
scripts/config --set-val CONFIG_ACPI_CUSTOM_DSDT      y
scripts/config --set-val CONFIG_ACPI_CUSTOM_DSDT_FILE '"/path/to/dsdt.hex"'

Then recompile and boot. Done!

Prepending an initrd image

This is a method that works, but it’s discouraged, since requires repeating the operation every time the initrd image is regenerated (essentially, for any kernel update).

# Create the initrd image, including the patched DSDT in the approprite directory, which corresponds
# to the `firmware/acpi` subdirectory of the `/sys` virtual filesystem.
#
$ mkdir -p kernel/firmware/acpi
$ cp dsdt.aml kernel/firmware/acpi
$ find kernel | cpio -H newc --create > initrd-patched-dsdt.img

# Backup the initrd for the running kernel, and prepend the initrd image just created, to the
# regular kernel one.
#
$ cp /boot/initrd.img-"$(uname -r)" .
$ cat initrd-patched-dsdt.img initrd.img-"$(uname -r)" | sudo tee /boot/initrd.img-"$(uname -r)" > /dev/null

The source, for the generic method, is in the kernel docs.

GRUB setting (not universal)

Another clean and automatic method is to set the custom initrd image via GRUB. Note that this method has been reported to work, but it didn’t on my O/S.

$ sudo cp dsdt.aml /boot/patched-dsdt.aml
$ echo acpi /boot/patched-dsdt.aml | sudo tee -a /boot/grub/custom.cfg
$ sudo update-grub

This should work on systems where the /boot/grub/custom.cfg is included by default; on Ubuntu, this rule is encoded in /etc/grub.d/41_custom:

$ cat /etc/grub.d/41_custom
#!/bin/sh
cat <<EOF
if [ -f  \${config_directory}/custom.cfg ]; then
  source \${config_directory}/custom.cfg
elif [ -z "\${config_directory}" -a -f  \$prefix/custom.cfg ]; then
  source \$prefix/custom.cfg
fi
EOF

In case a given distro doesn’t include /boot/grub/custom.cfg, just add the rule file.

Result

On reboot, support for S3 sleep state will be advertised:

$ sudo dmesg | grep 'ACPI.*supports S'
[    0.648536] ACPI: PM: (supports S0 S3 S4 S5)

# Go to sleep!
#
$ systemctl suspend

Watch out! After suspending, closing the laptop lid will wake up the system!! I don’t know what precisely causes this, but fixing this behavior is outside the scope of the article.

Debugging

If the procedure doesn’t yield the desired effect, my advice is to first rule out problems with the boot override; disable the S4 sleep support (just comment or remove the corresponding block), and if, after boot, the change has been successfully applied:

$ sudo dmesg | grep 'ACPI.*supports S'
[    0.309933] ACPI: PM: (supports S0 S5)

then the problem is in the DSDT patch.

Methods not working

The following methods either didn’t work on my system, or they’re not robust:

I don’t exclude that with appropriate changes, some of the methods above can work.

References

The following are some references on the topic. Watch out: the Microsoft and Anadtech references are biased and/or deceptive, and they’re present only for completeness.

Conclusion

Removal of the S3 sleep state is a terrible state of affairs.

It’s not clear who drove this change; according to some, Microsoft initiated it, in its push for the “Connected/Modern Standby”; according to others, the producers did.

I believe that Microsoft has been the drive, in particular, considering that they’ve been pushing the Modern Standby through a campaign of deceptive half-truths. Don’t forget that Microsoft ships on the near-totality of the (non-Mac) desktop PCs, and they have an enormous leverage on producers.

The future is uncertain. Some hardware producers do make available the S3 option in the firmware; vote with your wallet (and some noise 😁).