记一次内核之旅--修复板载声卡前置放大器驱动

2023-05-26

问题

笔记本电脑在Linux下一直没有办法正常使用内置音响,初步判断是声卡驱动的问题。

寻找解题

  1. 首先通过Arch Linux Wiki,偶然间看到一个sof-firmware,安装之后可以正常检测到Realtek声卡。耳机孔,蓝牙运作正常。但内置音响声音非常小,且底部低音音响不出声。据此判断,应该是有模拟放大器未激活,导致模拟电路输出功率不足。
  1. alsa-info脚本进行信息搜集:用于解码的声卡应该为Realtek ALC287, 放大器芯片应该为Cirrus cs35l41

  2. 进行广泛搜索后,发现了一篇Gist,作者的amp与我相同。但其解决方案为直接对BIOS进行反编译,然后修改_DSD entry。作者电脑为ASUS的Zenbook UX3402与我不同。且我在对自己的BIOS进行反编译时,发现Lenovo的编码方式无法通过Intel的反编译器完整解析,便作罢。

  3. Arch Linux Bug反馈,求助。按照Debugging Regressions进行相应测试,无果。于是转战kernel bugzilla

  4. 结合对kernel邮件组的潜伏观察,锁定了一个thread。这里有位大佬Cameron Berkenpas自己写了patch并在自己的电脑上进行测试,通过了半年的稳定性测试。而另一位大佬确认了和我相同型号设备也可使用同patch。于是准备自己动手打内核补丁。

思路

  1. 参照经社区验证的补丁,进行自己相应的修改

  2. 下载stable源码,打补丁,编译内核及对应内核模块,安装内核和内核模块,生成initramfs,用dkms安装树外模块(nvidia驱动等),最后更新grub配置,重启,进入新内核。

步骤

// 修改社区的补丁lenovo-7i-gen7-sound-6.2.0-rc3-0.0.5b-002.patch (https://bugzilla.kernel.org/attachment.cgi?id=303828)
// (https://gist.github.com/levihuayuzhang/6137ae4ae46301a355fd37c63e0d876a)
diff --git a/sound/pci/hda/cs35l41_hda.c b/sound/pci/hda/cs35l41_hda.c
index f7815ee24f83..93d86c5a9d53 100644
--- a/sound/pci/hda/cs35l41_hda.c
+++ b/sound/pci/hda/cs35l41_hda.c
@@ -1270,6 +1270,8 @@ static int cs35l41_hda_read_acpi(struct cs35l41_hda *cs35l41, const char *hid, i
 	size_t nval;
 	int i, ret;
 
+	printk("CSC3551: probing %s\n", hid);
+
 	adev = acpi_dev_get_first_match_dev(hid, NULL, -1);
 	if (!adev) {
 		dev_err(cs35l41->dev, "Failed to find an ACPI device for %s\n", hid);
@@ -1287,8 +1289,9 @@ static int cs35l41_hda_read_acpi(struct cs35l41_hda *cs35l41, const char *hid, i
 	property = "cirrus,dev-index";
 	ret = device_property_count_u32(physdev, property);
 	if (ret <= 0) {
-		ret = cs35l41_no_acpi_dsd(cs35l41, physdev, id, hid);
-		goto err_put_physdev;
+	    //ret = cs35l41_no_acpi_dsd(cs35l41, physdev, id, hid);
+	    //goto err_put_physdev;
+	    goto no_acpi_dsd;
 	}
 	if (ret > ARRAY_SIZE(values)) {
 		ret = -EINVAL;
@@ -1383,6 +1386,92 @@ static int cs35l41_hda_read_acpi(struct cs35l41_hda *cs35l41, const char *hid, i
 	put_device(physdev);
 
 	return ret;
+
+no_acpi_dsd:
+	/*
+	 * Device CLSA0100 doesn't have _DSD so a gpiod_get by the label reset won't work.
+	 * And devices created by i2c-multi-instantiate don't have their device struct pointing to
+	 * the correct fwnode, so acpi_dev must be used here.
+	 * And devm functions expect that the device requesting the resource has the correct
+	 * fwnode.
+	 */
+
+	printk("CSC3551: no_acpi_dsd: %s\n", hid);
+
+	/* TODO: This is a hack. */
+	if (strncmp(hid, "CSC3551", 7) == 0) {
+	    goto csc3551;
+	}
+
+	if (strncmp(hid, "CLSA0100", 8) != 0)
+		return -EINVAL;
+
+	/* check I2C address to assign the index */
+	cs35l41->index = id == 0x40 ? 0 : 1;
+	cs35l41->hw_cfg.spk_pos = cs35l41->index;
+	cs35l41->channel_index = 0;
+	cs35l41->reset_gpio = gpiod_get_index(physdev, NULL, 0, GPIOD_OUT_HIGH);
+	cs35l41->hw_cfg.bst_type = CS35L41_EXT_BOOST_NO_VSPK_SWITCH;
+	hw_cfg->gpio2.func = CS35L41_GPIO2_INT_OPEN_DRAIN;
+	hw_cfg->gpio2.valid = true;
+	cs35l41->hw_cfg.valid = true;
+	put_device(physdev);
+
+	return 0;
+
+ csc3551:
+
+	printk("CSC3551: id == 0x%x\n", id);
+
+	// cirrus,dev-index
+	if(id == 0x40)
+	    cs35l41->index = 0;
+	else
+	    cs35l41->index = 1;
+
+	cs35l41->channel_index = 0;
+
+	cs35l41->reset_gpio = gpiod_get_index(physdev, NULL, cs35l41->index, GPIOD_OUT_LOW);
+
+	printk("CS3551: reset_gpio == 0x%x\n", cs35l41->reset_gpio);
+
+	// cirrus,speaker-position
+	if(cs35l41->index == 0)
+	    hw_cfg->spk_pos = 0;
+	else
+	    hw_cfg->spk_pos = 1;
+
+	// cirrus,gpio1-func
+	hw_cfg->gpio1.func = 1;
+        hw_cfg->gpio1.valid = true;
+
+	// cirrus,gpio2-func
+	hw_cfg->gpio2.func = 0x02;
+        hw_cfg->gpio2.valid = true;
+
+	// cirrus,boost-peak-milliamp
+	hw_cfg->bst_ipk = -1;
+
+	// cirrus,boost-ind-nanohenry
+	hw_cfg->bst_ind = -1;
+
+	// cirrus,boost-cap-microfarad
+	hw_cfg->bst_cap = -1;
+
+	cs35l41->speaker_id = cs35l41_get_speaker_id(physdev, cs35l41->index, nval, -1);
+
+        if (hw_cfg->bst_ind > 0 || hw_cfg->bst_cap > 0 || hw_cfg->bst_ipk > 0)
+                hw_cfg->bst_type = CS35L41_INT_BOOST;
+        else
+                hw_cfg->bst_type = CS35L41_EXT_BOOST;
+
+	hw_cfg->valid = true;
+
+	put_device(physdev);
+
+	printk("CSC3551: Done.\n");
+
+	return 0;
 }
 
 int cs35l41_hda_probe(struct device *dev, const char *device_name, int id, int irq,
diff --git a/sound/pci/hda/patch_realtek.c b/sound/pci/hda/patch_realtek.c
index e103bb3693c0..8ec2b0f99d8c 100644
--- a/sound/pci/hda/patch_realtek.c
+++ b/sound/pci/hda/patch_realtek.c
@@ -9734,6 +9734,7 @@ static const struct snd_pci_quirk alc269_fixup_tbl[] = {
 	SND_PCI_QUIRK(0x17aa, 0x3853, "Lenovo Yoga 7 15ITL5", ALC287_FIXUP_YOGA7_14ITL_SPEAKERS),
 	SND_PCI_QUIRK(0x17aa, 0x3855, "Legion 7 16ITHG6", ALC287_FIXUP_LEGION_16ITHG6),
 	SND_PCI_QUIRK(0x17aa, 0x3869, "Lenovo Yoga7 14IAL7", ALC287_FIXUP_YOGA9_14IAP7_BASS_SPK_PIN),
+	SND_PCI_QUIRK(0x17aa, 0x38a9, "Lenovo ThinkBook 16p Gen4 IRH", ALC287_FIXUP_CS35L41_I2C_2),
 	SND_PCI_QUIRK(0x17aa, 0x3902, "Lenovo E50-80", ALC269_FIXUP_DMIC_THINKPAD_ACPI),
 	SND_PCI_QUIRK(0x17aa, 0x3977, "IdeaPad S210", ALC283_FIXUP_INT_MIC),
 	SND_PCI_QUIRK(0x17aa, 0x3978, "Lenovo B50-70", ALC269_FIXUP_DMIC_THINKPAD_ACPI),
# https://wiki.archlinux.org/title/Kernel/Traditional_compilation

# download, verifym and extract source code
# modify the patch, then
make mrproper # clean up

zcat /proc/config.gz  > .config # use running kernel config

patch -p1 < ./lenovo-7i-gen7-sound-6.2.0-rc3-0.0.5b-002.patch # applying patch

make -j$(nproc) # max parallel compile

make modules # compile in tree modules

sudo make INSTALL_MOD_STRIP=1 modules_install # install corresponding module

make bzImage # compile kernle image

# copy image to /boot directory and rename it
cp -v arch/x86/boot/bzImage /boot/vmlinuz-linux6.3.4
# make a new mkinitcpio preset for new kernel
cp /etc/mkinitcpio.d/linux.preset /etc/mkinitcpio.d/linux6.3.4.preset

# edit preset of mkinitcpio
vim /etc/mkinitcpio.d/linux6.3.4.preset
-------------------------------------
...
ALL_kver="/boot/vmlinuz-linux6.3.4"
...
default_image="/boot/initramfs-linux6.3.4.img"
...
fallback_image="/boot/initramfs-linux6.3.4-fallback.img"
# generate new initramfs for new kernel
mkinitcpio -p linux6.3.4

# update grub config
sudo grub-mkconfig  -o /boot/grub/grub.cfg 

# out of tree moudle install using dkms
sudo dkms autoinstall -k 6.3.4

# reboot the system and select new kernel at grub entry during booting
reboot now

总结

  1. 这是一个非常危险的patch(仅仅时一个来自社区的workaround而不是fix),未经过Cirrus(硬件制造商)和Lenovo(设备制造商)的官方确认。由社区人员经猜测在另一Levono型号上测试成功,并经猜想应该可以使用到有着相同芯片和类似设计的电脑上。

  2. 声卡电路唯一的保护是BIOS中的ACPI控制程序,但恰恰Lenovo在出厂时未在其中合理配置。(Windows中应该是硬编码在了驱动程序中,而Linux社区则还未得到支持)

后记

  1. 在Cirrus或Lenovo把quirk投入内核树前,如果还想继续使用内置音响,必须自己打patch编译内核。(中低音量低音频音响发声,高音量高音频音响发声,应该是存在分频问题)(这导致音响分频不正确,且无法正确调节音量,至少是Gnome中)

  2. 或者买个usb音响先凑合

  3. 这个问题只能由芯片制造商或者设备制造商解决。因为只有他们才知道芯片的具体设计和相应的接口参数。已在mail list看到Cirrus的工程师了,希望社区能早日得到支持。

更新

2024-04-11:

由笔者报告给Linux mailing list,并与Cirrus的工程师商讨的第一版fix已由Stefan Binding <sbinding@opensource.cirrus.com>提交给Linux Sound team,并预计由V6.8发布。

  1. merge commit: https://github.com/tiwai/sound/commit/37d9d5ff5216df1908a41e6ddd72460c5d938b8a
  2. 邮件历史:https://lore.kernel.org/linux-sound/?q=Huayu+Zhang

2024-04-13:

感谢Stefan大佬的帮助修复了音量控制问题。原因是之前的patch使用了解码器和amp之间的错误配置。

2024-04-18:

提交的修复patch进入Linux Sound子系统https://github.com/tiwai/sound/commit/dca5f4dfa925b51becee65031869e917e6229620

2024-04-22:

patch进入Linux main line tree v6.9-rc5.

2024-04-28:

正式发布(v6.8.8)https://git.kernel.org/stable/ds/v6.8.8/v6.8.7

Ref:

  1. Linux kernel关于Cirrus的文档:https://www.kernel.org/doc/Documentation/devicetree/bindings/sound/cirrus%2Ccs35l41.yaml
  2. alsa-info输出:http://alsa-project.org/db/?f=1ad9f2709886f0ec5d2d87d5d8e59a0ac05384be
  3. Arch Linux kernel编译:https://wiki.archlinux.org/title/Kernel/Traditional_compilation
  4. 参考的fix:https://bugzilla.kernel.org/attachment.cgi?id=303828&action=diff
  5. 发布在Gists的个人补丁与社区讨论:https://gist.github.com/levihuayuzhang/6137ae4ae46301a355fd37c63e0d876a