最近因为装了台式,加上公司有发笔记本,我自己的笔记本就显得没什么用处了,正好可以用来装 Linux 玩。

其实在去年6月刚买这台华硕灵耀 X13 (Zenbook S13 OLED) 的时候,我就尝试过装 Linux,那个时候整个 Ryzen 6000 系列在 Linux 上的键盘都是不可用的,直到两个月之后 Kernel 6.0 发布。时隔一年多再次尝试,这次遇到的问题倒是没有那么致命,只有指纹和扬声器不可用(插耳机可用),指纹是 Linux 的老大难问题了,一时半会也解决不了,而扬声器这个不仅是我这一台笔记本的问题,几乎所有华硕的笔记本都有,这里记录下如何修复。

Cirrus SmartAMP

第一次在这个笔记本上重装系统的时候我就注意到了,Windows 自动安装的声卡驱动并不能驱动扬声器,必须要到华硕的驱动页面下载一个 Cirrus SmartAMP 的驱动才行。这个 Cirrus SmartAMP 对应的设备是 CS35L41,是一个内置的音频放大器,跟应该跟华硕所谓的哈曼卡顿调音相关吧。

image.png

这也是 Linux 扬声器没有声音的问题来源,从 dmesg 日志中我们可以看到

$ dmesg | grep CSC
[    6.381872] Serial bus multi instantiate pseudo device driver CSC3551:00: Instantiated 2 I2C devices.
[    6.532659] cs35l41-hda i2c-CSC3551:00-cs35l41-hda.0: Error: ACPI _DSD Properties are missing for HID CSC3551.
[    6.532664] cs35l41-hda i2c-CSC3551:00-cs35l41-hda.0: error -EINVAL: Platform not supported
[    6.532696] cs35l41-hda: probe of i2c-CSC3551:00-cs35l41-hda.0 failed with error -22
[    6.541464] cs35l41-hda i2c-CSC3551:00-cs35l41-hda.1: Error: ACPI _DSD Properties are missing for HID CSC3551.
[    6.541468] cs35l41-hda i2c-CSC3551:00-cs35l41-hda.1: error -EINVAL: Platform not supported
[    6.541503] cs35l41-hda: probe of i2c-CSC3551:00-cs35l41-hda.1 failed with error -22

从上面的报错中可以看到,Linux 并不没有这个设备的驱动程序,而是缺少了这个设备在 ACPI Table 上的一些属性。

这其实是华硕的锅,众所周知,ACPI Table 是硬件制造商提供的固件中包含的信息,华硕提供的固件里缺少了一些属性值,据说在 Windows 的驱动中,他们硬编码了相关属性值,而 Linux 这边没有这么做,为了让扬声器重新响起来,我们需要自己给 ACPI Table 打补丁。

修复 ACPI Table

Dump DSDT Table

我们这里用不到完整的 ACPI Table,只需要对 DSDT Table 进行操作即可

$ sudo cat /sys/firmware/acpi/tables/DSDT > dsdt.dat
$ iasl -d dsdt.dat

之后会多出来一个 dsdt.dsl 文件,即反编译的 dsdt table。

了解 CSC3551 在 DSDT 中的定义

在 dsdt.dsl 中搜索 CSC3551,找到和它相关的部分以及它的上一级,在我的设备中,这部分是

Scope (_SB.I2CA)
{

    Device (SPKR)
    {
        Name (_HID, "CSC3551")  // _HID: Hardware ID
        Name (_SUB, "10431F12")  // _SUB: Subsystem ID
        Name (_UID, One)  // _UID: Unique ID
        Method (_CRS, 0, NotSerialized)  // _CRS: Current Resource Settings
        {
            Name (RBUF, ResourceTemplate ()
            {
                I2cSerialBusV2 (0x0040, ControllerInitiated, 0x000F4240,
                    AddressingMode7Bit, "\\_SB.I2CA",
                    0x00, ResourceConsumer, , Exclusive,
                    )
                I2cSerialBusV2 (0x0041, ControllerInitiated, 0x000F4240,
                    AddressingMode7Bit, "\\_SB.I2CA",
                    0x00, ResourceConsumer, , Exclusive,
                    )
                GpioIo (Exclusive, PullDown, 0x0000, 0x0000, IoRestrictionOutputOnly,
                    "\\_SB.GPIO", 0x00, ResourceConsumer, ,
                    )
                    {   // Pin list
                        0x0004
                    }

这里是要了解 CSC3551 这条设备挂在哪个总线下面,可以看到,我这个设备挂在 _SB.I2CA 这条线下,设备号是 SPKR,设备硬件ID是 10431F12

但是除了 I2C 总线,这个设备还可能挂载在 SPI 总线之下,下面是一个 SPI 总线下的 DSDT 部分示例

Scope (_SB.PC00.SPI3)
{
    Device (SPK1)
    {
        Name (_HID, "CSC3551")  // _HID: Hardware ID
        Name (_SUB, "10431CAF")  // _SUB: Subsystem ID
        Name (_UID, One)  // _UID: Unique ID
        Method (_CRS, 0, NotSerialized)  // _CRS: Current Resource Settings
        {
            Name (SBUF, ResourceTemplate ()
            {
                SpiSerialBusV2 (0x0000, PolarityLow, FourWireMode, 0x08,
                    ControllerInitiated, 0x003D0900, ClockPolarityLow,
                    ClockPhaseFirst, "\\_SB.PC00.SPI3",
                    0x00, ResourceConsumer, , Exclusive,
                    )
                SpiSerialBusV2 (0x0001, PolarityLow, FourWireMode, 0x08,
                    ControllerInitiated, 0x003D0900, ClockPolarityLow,
                    ClockPhaseFirst, "\\_SB.PC00.SPI3",
                    0x00, ResourceConsumer, , Exclusive,
                    )

编写 Patch

下面是示例 patch

DefinitionBlock ("", "SSDT", 1, "CUSTOM", "CSC3551", 0x00000001)
{
    External (<EXTERNAL>, DeviceObj)
    External (<EXTERNAL>.<SPK>, DeviceObj)

    Scope (<SCOPE>.<SPK>)
    {
        Name (_DSD, Package ()   // _DSD: Device-Specific Data
        {
            ToUUID ("daffd814-6eba-4d8c-8a91-bc9bbf4aa301"),
            Package ()
            {
                Package () { "cirrus,dev-index", Package () { <ADDRESS_PAIR> }},
                Package () { "reset-gpios", Package () {
                    <SPK>, Zero, Zero, Zero,
                    <SPK>, Zero, Zero, Zero,
                } },
                Package () { "spk-id-gpios", Package () {
                    <SPK>, 0x02, Zero, Zero,
                    <SPK>, 0x02, Zero, Zero,
                } },
                Package () { "cirrus,speaker-position",     Package () { Zero, One } },
                // gpioX-func: 0 not used, 1 VPSK_SWITCH, 2: INTERRUPT, 3: SYNC
                Package () { "cirrus,gpio1-func",           Package () { One, One } },
                Package () { "cirrus,gpio2-func",           Package () { 0x02, 0x02 } },
                // boost-type: 0 internal, 1 external
                Package () { "cirrus,boost-type",           Package () { One, One } },
            },
        })
    }
}

其中

  • <EXTERNAL> 一般为 _SB_
  • <SPK> 为前面提到的哪条总线,如 I2CA
  • <SCOPE> 和前面提到 Scope() 里的内容相同,注意这里的 _SB 少一个后置的下划线
  • <ADDRESS_PAIR> 在 I2C 设备上是 0x0040, 0x0041,在 SPI 设备上是 Zero, One

对于我这台笔记本来说,patch 如下

DefinitionBlock ("", "SSDT", 1, "CUSTOM", "CSC3551", 0x000000001)
{
    External (_SB_, DeviceObj)
    External (_SB_.I2CA, DeviceObj)

    Scope (_SB.I2CA.SPKR)
    {
        Name (_DSD, Package ()
        {
            ToUUID ("daffd814-6eba-4d8c-8a91-bc9bbf4aa301"),
            Package ()
            {
                Package () { "cirrus,dev-index", Package () { 0x0040, 0x0041 }},
                Package () { "reset-gpios", Package () {
                    SPKR, Zero, Zero, Zero,
                    SPKR, Zero, Zero, Zero,
                } },
                Package () { "spk-id-gpios", Package () {
                    SPKR, 0x02, Zero, Zero,
                    SPKR, 0x02, Zero, Zero,
                } },
                Package () { "cirrus,speaker-position",     Package () { Zero, One } },
                // gpioX-func: 0 not used, 1 VPSK_SWITCH, 2: INTERRUPT, 3: SYNC
                Package () { "cirrus,gpio1-func",           Package () { One, One } },
                Package () { "cirrus,gpio2-func",           Package () { 0x02, 0x02 } },
                // boost-type: 0 internal, 1 external
                Package () { "cirrus,boost-type",           Package () { One, One } },
            }
        })
    }
}

编译与使用 patch

使用命令编译 iasl -tc ssdt_csc3551.dsl, 获得文件 ssdt_csc3551.aml,将文件复制到 /boot

/etc/grub.d/01_acpi 中写入,并 chmod +x /etc/grub.d/01_acpi 添加权限

#!/bin/sh -e

#sudo cp -f 01_acpi /etc/grub.d/01_acpi
# Uncomment to load custom ACPI tables
GRUB_CUSTOM_ACPI_DIR="/boot"

# DON'T MODIFY ANYTHING BELOW THIS LINE!

prefix=/usr
exec_prefix=${prefix}
libdir=${exec_prefix}/lib

. "$pkgdatadir/grub-mkconfig_lib"

# Load custom ACPI tables
if [ x${GRUB_CUSTOM_ACPI_DIR} != x ] && [ -d ${GRUB_CUSTOM_ACPI_DIR} ]; then
    echo "Searching for custom ACPI tables in ${GRUB_CUSTOM_ACPI_DIR}" >&2
    for file in ${GRUB_CUSTOM_ACPI_DIR}/ssdt*.aml; do
        if [ -f "${file}" ] && is_path_readable_by_grub "${file}"; then
            echo "Found custom ACPI table: ${file}" >&2
            prepare_grub_to_access_device `${grub_probe} --target=device ${file}` | sed -e "s/^/ /"
            cat << EOF
acpi (\$root)`make_system_path_relative_to_its_root ${file}`
EOF
        fi
    done
fi

使用命令 grub2-mkconfig -o /boot/efi/EFI/fedora/grub.cfg 生成新的 grub 配置,并重启电脑。
(这里的配置文件路径和发行版相关,如果是 debian 系的直接 update-grub2 就行)

重启后测试扬声器可用,查看日志

dmesg | grep CSC
[    0.004184] ACPI: SSDT 0x00000000A8AA53DE 000143 (v01 CUSTOM CSC3551  00000001 INTL 20220331)
[    6.393627] Serial bus multi instantiate pseudo device driver CSC3551:00: Instantiated 2 I2C devices.
[    6.585168] cs35l41-hda i2c-CSC3551:00-cs35l41-hda.0: Cirrus Logic CS35L41 (35a40), Revision: B2
[    6.585683] cs35l41-hda i2c-CSC3551:00-cs35l41-hda.1: Reset line busy, assuming shared reset
[    6.642721] cs35l41-hda i2c-CSC3551:00-cs35l41-hda.1: Cirrus Logic CS35L41 (35a40), Revision: B2
[    6.754917] cs35l41-hda i2c-CSC3551:00-cs35l41-hda.0: DSP1: Firmware version: 3
[    6.754927] cs35l41-hda i2c-CSC3551:00-cs35l41-hda.0: DSP1: cirrus/cs35l41-dsp1-spk-prot-10431f12.wmfw: Fri 27 Aug 2021 14:58:19 W. Europe Daylight Time
[    7.213773] cs35l41-hda i2c-CSC3551:00-cs35l41-hda.0: DSP1: Firmware: 400a4 vendor: 0x2 v0.43.1, 2 algorithms
[    7.214860] cs35l41-hda i2c-CSC3551:00-cs35l41-hda.0: DSP1: 0: ID cd v29.63.1 XM@94 YM@e
[    7.214868] cs35l41-hda i2c-CSC3551:00-cs35l41-hda.0: DSP1: 1: ID f20b v0.1.0 XM@176 YM@0
[    7.214876] cs35l41-hda i2c-CSC3551:00-cs35l41-hda.0: DSP1: spk-prot: C:\Users\tyang\Desktop\Product Setting\SmartAMP\ASUS\ASUS_Zenbook\UM5302\Tuning_release\220210\UM5302_L_with Rtrace_Tuning_YC_Rattle.bin
[    7.299115] snd_hda_codec_realtek hdaudioC1D0: bound i2c-CSC3551:00-cs35l41-hda.0 (ops cs35l41_hda_comp_ops [snd_hda_scodec_cs35l41])
[    7.302803] cs35l41-hda i2c-CSC3551:00-cs35l41-hda.1: DSP1: Firmware version: 3
[    7.302809] cs35l41-hda i2c-CSC3551:00-cs35l41-hda.1: DSP1: cirrus/cs35l41-dsp1-spk-prot-10431f12.wmfw: Fri 27 Aug 2021 14:58:19 W. Europe Daylight Time
[    7.761640] cs35l41-hda i2c-CSC3551:00-cs35l41-hda.1: DSP1: Firmware: 400a4 vendor: 0x2 v0.43.1, 2 algorithms
[    7.762715] cs35l41-hda i2c-CSC3551:00-cs35l41-hda.1: DSP1: 0: ID cd v29.63.1 XM@94 YM@e
[    7.762724] cs35l41-hda i2c-CSC3551:00-cs35l41-hda.1: DSP1: 1: ID f20b v0.1.0 XM@176 YM@0
[    7.762731] cs35l41-hda i2c-CSC3551:00-cs35l41-hda.1: DSP1: spk-prot: C:\Users\tyang\Desktop\Product Setting\SmartAMP\ASUS\ASUS_Zenbook\UM5302\Tuning_release\220210\UM5302_R_Tuning_YC_Rattle.bin
[    7.846106] snd_hda_codec_realtek hdaudioC1D0: bound i2c-CSC3551:00-cs35l41-hda.1 (ops cs35l41_hda_comp_ops [snd_hda_scodec_cs35l41])

(可选) 下载 cirrus 固件

如果你的发行版没有提供 cirrus 驱动的固件,那你需要手动下载

git clone --depth=1 https://gitlab.com/asus-linux/firmware.git
cp -r firmware/cirrus /lib/firmware

(可选) 内核版本

华硕这个不仅需要 dsdt, 固件,还要有一个内核的补丁,这里我的建议是升到最新的内核,如果没有的话开摆等着别人修吧。

Reference