Skip to content

解决无外放声音的问题

xuwd1 edited this page Jul 20, 2023 · 9 revisions

解决无外放声音的问题

目前该机型上无外放声音的问题已经得到解决,对此问题的分析是:

1. 该机型主板上搭载了两个Cirrus Logics公司的cs35l41(这套双放大器的系统称为CSC3551)放大器用于驱动扬声器,每个cs35l41:

  • 通过I2C连接到PCH的I2C_3上(alderlake的I2C控制器是集成在PCH的lpss中的,由intel_lpss_pci驱动负责),地址分别为0x400x41
  • 具有三个引脚
    • 其中第一个引脚是复位脚,两个cs35l41共同连接到一个PCH的GPIO上
    • 第二个引脚是中断脚,两个cs35l41共同连接到APIC上
    • 第三个引脚的功能在其驱动cs35l41_hda.c中称为VSPK_SWITCH,有时也被混用为spkid,应当与放大器的编号和片选有关

2. 想要这个放大器正常工作,需要满足以下的条件:

  • cs35l41首先需要被正常例化成i2c设备(或者spi设备,比如SteamDeck上的cs35l41即是通过spi连接的),这个工作是由内核中的serial-multi-instantiate驱动负责的.
  • 在i2c/spi设备例化后,cs35l41_hda驱动需要能正确的初始化该放大器,具体地,其需要从ACPI表中的CSC3551设备的_DSD方法读出一系列设置,随后对其进行配置和复位
  • 在播放开始前,需要加载正确的固件(是的,这破烂东西居然有固件!),固件的加载是通过cs35l41_hda驱动向hda驱动挂上playback hook,并随后由hda驱动在初始化时执行这个钩子函数实现的

3. 在y9000x 2022(Slim 7i IAH7)上该放大器无法正常工作的原因是上述每一个条件都无法满足:(...)

  • 在旧版本的内核中,serial-multi-instantiate驱动在例化cs35l41设备时总是假设其中断脚是连接到PCH的GPIO上的,然而联想工程师认为连接到APIC上是一个更妙的选择,因此cs35l41对应的i2c/spi设备从一开始就无法例化
  • 自然地,cs35l41_hda根本不会进行probing. 但即使修改了serial-multi-instantiate驱动,由于联想没有在ACPI表中实现CSC3551设备的_DSD方法,cs35l41_hda驱动也无法完成配置
  • 最后,即使改正了前面两个问题,hda驱动也不会加载cs35l41的固件,这是因为hda驱动中没有增加对应y9000x 2022的quirk.

如何解决问题

希望解决该机器上的外放问题,唯一可行的办法只有对内核进行修改,并运行自制内核。内核补丁分为两部分:

1. 对serial-multi-instantiate驱动的修改

请注意这一部分内核补丁已经被接受,不久后应当会出现在内核主线代码中

 .../platform/x86/serial-multi-instantiate.c   | 21 +++++++++++++++----
 1 file changed, 17 insertions(+), 4 deletions(-)

diff --git a/drivers/platform/x86/serial-multi-instantiate.c b/drivers/platform/x86/serial-multi-instantiate.c
index f3dcbdd72fec..2c2abf69f049 100644
--- a/drivers/platform/x86/serial-multi-instantiate.c
+++ b/drivers/platform/x86/serial-multi-instantiate.c
@@ -21,6 +21,7 @@
 #define IRQ_RESOURCE_NONE	0
 #define IRQ_RESOURCE_GPIO	1
 #define IRQ_RESOURCE_APIC	2
+#define IRQ_RESOURCE_AUTO   3
 
 enum smi_bus_type {
 	SMI_I2C,
@@ -52,6 +53,18 @@ static int smi_get_irq(struct platform_device *pdev, struct acpi_device *adev,
 	int ret;
 
 	switch (inst->flags & IRQ_RESOURCE_TYPE) {
+	case IRQ_RESOURCE_AUTO:
+		ret = acpi_dev_gpio_irq_get(adev, inst->irq_idx);
+		if (ret > 0) {
+			dev_dbg(&pdev->dev, "Using gpio irq\n");
+			break;
+		}
+		ret = platform_get_irq(pdev, inst->irq_idx);
+		if (ret > 0) {
+			dev_dbg(&pdev->dev, "Using platform irq\n");
+			break;
+		}
+		break;
 	case IRQ_RESOURCE_GPIO:
 		ret = acpi_dev_gpio_irq_get(adev, inst->irq_idx);
 		break;
@@ -307,10 +320,10 @@ static const struct smi_node int3515_data = {
 
 static const struct smi_node cs35l41_hda = {
 	.instances = {
-		{ "cs35l41-hda", IRQ_RESOURCE_GPIO, 0 },
-		{ "cs35l41-hda", IRQ_RESOURCE_GPIO, 0 },
-		{ "cs35l41-hda", IRQ_RESOURCE_GPIO, 0 },
-		{ "cs35l41-hda", IRQ_RESOURCE_GPIO, 0 },
+		{ "cs35l41-hda", IRQ_RESOURCE_AUTO, 0 },
+		{ "cs35l41-hda", IRQ_RESOURCE_AUTO, 0 },
+		{ "cs35l41-hda", IRQ_RESOURCE_AUTO, 0 },
+		{ "cs35l41-hda", IRQ_RESOURCE_AUTO, 0 },
 		{}
 	},
 	.bus_type = SMI_AUTO_DETECT,

2. 对cs35l41_hda驱动和hda驱动的修改

请注意Cirrus Logics的内核维护者拒绝了这个patch的合并请求. 其称几周内会有一个更干净的解决方法出现在内核代码中,我们可以期待其工作. 希望届时y9000x的声音外放问题可以通过更新内核彻底解决。

 sound/pci/hda/cs35l41_hda.c | 160 ++++++++++++++++++++++++++++++++++++
 1 file changed, 160 insertions(+)

diff --git a/sound/pci/hda/cs35l41_hda.c b/sound/pci/hda/cs35l41_hda.c
index ce5faa620517..d957458dd4e6 100644
--- a/sound/pci/hda/cs35l41_hda.c
+++ b/sound/pci/hda/cs35l41_hda.c
@@ -1211,6 +1211,159 @@ static int cs35l41_get_speaker_id(struct device *dev, int amp_index,
 	return speaker_id;
 }
 
+#define CS35L41_FIXUP_CFG_MAX_DEVICES 4
+
+struct cs35l41_fixup_cfg {
+	unsigned short vender;
+	unsigned short device;
+	unsigned int num_device;  /* The num of cs35l41 instances */
+	/* cs35l41 instance ids, can be i2c index or spi index */
+	int ids[CS35L41_FIXUP_CFG_MAX_DEVICES];
+	unsigned int reset_gpio_idx[CS35L41_FIXUP_CFG_MAX_DEVICES];
+	enum gpiod_flags reset_gpio_flags[CS35L41_FIXUP_CFG_MAX_DEVICES];
+	int spkid_gpio_idx[CS35L41_FIXUP_CFG_MAX_DEVICES];
+	unsigned int spk_pos[CS35L41_FIXUP_CFG_MAX_DEVICES];
+	enum cs35l41_hda_gpio_function gpio1_func[CS35L41_FIXUP_CFG_MAX_DEVICES];
+	enum cs35l41_hda_gpio_function gpio2_func[CS35L41_FIXUP_CFG_MAX_DEVICES];
+	enum cs35l41_boost_type bst_type[CS35L41_FIXUP_CFG_MAX_DEVICES];
+};
+
+static const struct cs35l41_fixup_cfg cs35l41_fixup_cfgtbl[] = {
+	{ // Lenovo Legion Slim 7i 16IAH7
+	.vender = 0x17aa,
+	.device = 0x386e,
+	.num_device = 2,
+	.ids = {0x40, 0x41},
+	.reset_gpio_idx = {0, 0},
+	.reset_gpio_flags = {GPIOD_OUT_LOW, GPIOD_OUT_LOW},
+	.spkid_gpio_idx = {1, 1},
+	.spk_pos = {0, 1},
+	.gpio1_func = {CS35l41_VSPK_SWITCH, CS35l41_VSPK_SWITCH},
+	.gpio2_func = {CS35L41_INTERRUPT, CS35L41_NOT_USED},
+	.bst_type = {CS35L41_EXT_BOOST, CS35L41_EXT_BOOST}
+	},
+	{ // Lenovo Legion Slim 7i 16IAH7 type2
+	.vender = 0x17aa,
+	.device = 0x3803,
+	.num_device = 2,
+	.ids = {0x40, 0x41},
+	.reset_gpio_idx = {0, 0},
+	.reset_gpio_flags = {GPIOD_OUT_LOW, GPIOD_OUT_LOW},
+	.spkid_gpio_idx = {1, 1},
+	.spk_pos = {0, 1},
+	.gpio1_func = {CS35l41_VSPK_SWITCH, CS35l41_VSPK_SWITCH},
+	.gpio2_func = {CS35L41_INTERRUPT, CS35L41_NOT_USED},
+	.bst_type = {CS35L41_EXT_BOOST, CS35L41_EXT_BOOST}
+	},
+	{ // Lenovo Legion 7i 16IAX7
+	.vender = 0x17aa,
+	.device = 0x3874,
+	.num_device = 2,
+	.ids = {0x40, 0x41},
+	.reset_gpio_idx = {0, 0},
+	.reset_gpio_flags = {GPIOD_OUT_LOW, GPIOD_OUT_LOW},
+	.spkid_gpio_idx = {1, 1},
+	.spk_pos = {0, 1},
+	.gpio1_func = {CS35l41_VSPK_SWITCH, CS35l41_VSPK_SWITCH},
+	.gpio2_func = {CS35L41_INTERRUPT, CS35L41_INTERRUPT},
+	.bst_type = {CS35L41_EXT_BOOST, CS35L41_EXT_BOOST}
+	},
+	{ // Lenovo Legion 7i 16IAX7 type 2
+	.vender = 0x17aa,
+	.device = 0x386f,
+	.num_device = 2,
+	.ids = {0x40, 0x41},
+	.reset_gpio_idx = {0, 0},
+	.reset_gpio_flags = {GPIOD_OUT_LOW, GPIOD_OUT_LOW},
+	.spkid_gpio_idx = {1, 1},
+	.spk_pos = {0, 1},
+	.gpio1_func = {CS35l41_VSPK_SWITCH, CS35l41_VSPK_SWITCH},
+	.gpio2_func = {CS35L41_INTERRUPT, CS35L41_INTERRUPT},
+	.bst_type = {CS35L41_EXT_BOOST, CS35L41_EXT_BOOST}
+	},
+	{ // Lenovo Legion Slim 7 16ARHA7
+	.vender = 0x17aa,
+	.device = 0x3877,
+	.num_device = 2,
+	.ids = {0x40, 0x41},
+	.reset_gpio_idx = {0, 0},
+	.reset_gpio_flags = {GPIOD_OUT_LOW, GPIOD_OUT_LOW},
+	.spkid_gpio_idx = {1, 1},
+	.spk_pos = {0, 1},
+	.gpio1_func = {CS35l41_VSPK_SWITCH, CS35l41_VSPK_SWITCH},
+	.gpio2_func = {CS35L41_INTERRUPT, CS35L41_INTERRUPT},
+	.bst_type = {CS35L41_EXT_BOOST, CS35L41_EXT_BOOST}
+	},
+	{} // terminator
+};
+
+static inline int cs35l41_fixup_get_index(const struct cs35l41_fixup_cfg *fixup, int cs35l41_addr)
+{
+	int i;
+
+	for (i = 0; i < fixup->num_device; i++) {
+		if (fixup->ids[i] == cs35l41_addr)
+			return i;
+	}
+	return -ENODEV;
+}
+
+static int apply_cs35l41_fixup_cfg(struct cs35l41_hda *cs35l41,
+				struct device *physdev,
+				int cs35l41_addr,
+				const struct cs35l41_fixup_cfg *fixup_tbl)
+{
+	const char *ssid;
+	unsigned int vendid;
+	unsigned int devid;
+	const struct cs35l41_fixup_cfg *cur_fixup;
+	struct cs35l41_hw_cfg *hw_cfg;
+	int cs35l41_index;
+	int ret;
+	int i;
+
+	ssid = cs35l41->acpi_subsystem_id;
+	ret = sscanf(ssid, "%04x%04x", &vendid, &devid);
+	if (ret != 2)
+		return -EINVAL;
+
+	hw_cfg = &cs35l41->hw_cfg;
+	for (cur_fixup = fixup_tbl; cur_fixup->vender; cur_fixup++) {
+		if (cur_fixup->vender == vendid && cur_fixup->device == devid) {
+			cs35l41_index = cs35l41_fixup_get_index(cur_fixup, cs35l41_addr);
+			if (cs35l41_index == -ENODEV)
+				return -ENODEV;
+			cs35l41->index = cs35l41_index;
+			cs35l41->reset_gpio = gpiod_get_index(
+				physdev,
+				NULL,
+				cur_fixup->reset_gpio_idx[cs35l41_index],
+				cur_fixup->reset_gpio_flags[cs35l41_index]
+				);
+			cs35l41->speaker_id = cs35l41_get_speaker_id(physdev,
+				cs35l41_index,
+				cur_fixup->num_device,
+				cur_fixup->spkid_gpio_idx[cs35l41_index]
+				);
+			hw_cfg->spk_pos = cur_fixup->spk_pos[cs35l41_index];
+			cs35l41->channel_index = 0;
+			for (i = 0; i < cs35l41->index; i++)
+				if (cur_fixup->spk_pos[i] == hw_cfg->spk_pos)
+					cs35l41->channel_index++;
+
+			hw_cfg->gpio1.func = cur_fixup->gpio1_func[cs35l41_index];
+			hw_cfg->gpio1.valid = true;
+			hw_cfg->gpio2.func = cur_fixup->gpio2_func[cs35l41_index];
+			hw_cfg->gpio2.valid = true;
+			hw_cfg->bst_type = cur_fixup->bst_type[cs35l41_index];
+			dev_dbg(physdev, "Fixup applied.\n");
+			break;
+		}
+	}
+	return 0;
+
+}
+
 /*
  * Device CLSA010(0/1) doesn't have _DSD so a gpiod_get by the label reset won't work.
  * And devices created by serial-multi-instantiate don't have their device struct
@@ -1221,6 +1374,7 @@ static int cs35l41_get_speaker_id(struct device *dev, int amp_index,
 static int cs35l41_no_acpi_dsd(struct cs35l41_hda *cs35l41, struct device *physdev, int id,
 			       const char *hid)
 {
+	int ret;
 	struct cs35l41_hw_cfg *hw_cfg = &cs35l41->hw_cfg;
 
 	/* check I2C address to assign the index */
@@ -1243,7 +1397,13 @@ static int cs35l41_no_acpi_dsd(struct cs35l41_hda *cs35l41, struct device *physd
 		/*
 		 * Note: CLSA010(0/1) are special cases which use a slightly different design.
 		 * All other HIDs e.g. CSC3551 require valid ACPI _DSD properties to be supported.
+		 * However many OEMs hardcoded the configurations into their proprietary software
+		 * thus leaving our Linux installation with no speaker sound at all while we see
+		 * no hope those OEMs would fix it. So we apply a ssid specific fixup to fix it.
 		 */
+		if (apply_cs35l41_fixup_cfg(cs35l41, physdev, id, cs35l41_fixup_cfgtbl) == 0)
+			return 0;
+
 		dev_err(cs35l41->dev, "Error: ACPI _DSD Properties are missing for HID %s.\n", hid);
 		hw_cfg->valid = false;
 		hw_cfg->gpio1.valid = false;
 sound/pci/hda/patch_realtek.c | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/sound/pci/hda/patch_realtek.c b/sound/pci/hda/patch_realtek.c
index e2f8b608de82..cc10bb8b75d1 100644
--- a/sound/pci/hda/patch_realtek.c
+++ b/sound/pci/hda/patch_realtek.c
@@ -9831,6 +9831,7 @@ static const struct snd_pci_quirk alc269_fixup_tbl[] = {
 	SND_PCI_QUIRK(0x17aa, 0x31af, "ThinkCentre Station", ALC623_FIXUP_LENOVO_THINKSTATION_P340),
 	SND_PCI_QUIRK(0x17aa, 0x3801, "Lenovo Yoga9 14IAP7", ALC287_FIXUP_YOGA9_14IAP7_BASS_SPK_PIN),
 	SND_PCI_QUIRK(0x17aa, 0x3802, "Lenovo Yoga DuetITL 2021", ALC287_FIXUP_YOGA7_14ITL_SPEAKERS),
+	SND_PCI_QUIRK(0x17aa, 0x3803, "Legion Slim 7i 16IAH7", ALC287_FIXUP_CS35L41_I2C_2),
 	SND_PCI_QUIRK(0x17aa, 0x3813, "Legion 7i 15IMHG05", ALC287_FIXUP_LEGION_15IMHG05_SPEAKERS),
 	SND_PCI_QUIRK(0x17aa, 0x3818, "Lenovo C940 / Yoga Duet 7", ALC298_FIXUP_LENOVO_C940_DUET7),
 	SND_PCI_QUIRK(0x17aa, 0x3819, "Lenovo 13s Gen2 ITL", ALC287_FIXUP_13S_GEN2_SPEAKERS),
@@ -9846,6 +9847,10 @@ 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, 0x386e, "Legion Slim 7i 16IAH7", ALC287_FIXUP_CS35L41_I2C_2),
+	SND_PCI_QUIRK(0x17aa, 0x386f, "Legion 7i 16IAX7", ALC287_FIXUP_CS35L41_I2C_2),
+	SND_PCI_QUIRK(0x17aa, 0x3874, "Legion 7i 16IAX7", ALC287_FIXUP_CS35L41_I2C_2),
+	SND_PCI_QUIRK(0x17aa, 0x3877, "Legion 7 16ARHA7", 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),

琐碎的问题

至此,y9000x的扬声器应当可以正常发出声音了,一些值得注意的点如下:

  1. 如patch中的代码所示,这些patch同样可以用来修复r9000x 2022以及y9000k 2022的外放问题
  2. 打上patch后你可能会发现扬声器的声音比较小,这是因为cs35l41的扬声器驱动方式分为外部驱动和内部驱动两种,patch中给出的是使用外部驱动,实际上就是不对发向扬声器的声音做任何进一步的放大处理,而这就会使得声音比较小. 目前本人没有能力解决cs35l41内部驱动的问题,这是因为cs35l41的内部驱动模式同样需要一系列正确的配置才可以工作,若配置不当,Cirrus Logics的人声称可能会对扬声器造成不可逆的损害,而本人没有能力获得正确的内部驱动配置。