[PATCH v2 3/3] ALSA: hda/realtek: enable AW88399 on Lenovo Legion Pro

Marco Giunta <[email protected]> Wed, 29 Jul 2026 11:33:15 +0200
Newsgroups org.kernel.vger.platform-driver-x86,org.kernel.vger.linux-acpi,org.kernel.vger.linux-kernel,org.kernel.vger.linux-sound
Message-ID <DS7PR19MB7724ACD7C8D1BE71451E1AEEFCCA2@DS7PR19MB7724.namprd19.prod.outlook.com>
Enable audio output through the AW88399 woofer amplifiers on Lenovo
Legion laptops by adding the necessary Realtek ALC287 fixups and
AW88399 per-model quirks.

Realtek fixups (alc269.c):

* ALC287_FIXUP_AW88399_I2C_2: registers the AW88399 as a two-instance
  I2C companion codec using comp_generic_fixup, matching ACPI HID
  "AWDZ8399".

* ALC287_FIXUP_LENOVO_LEGION_AW88399: forces DAC 0x02 for the bass
  speaker pin 0x17, as the default DAC 0x06 lacks volume controls.
  Also applies internal microphone boost calibration via
  alc269_fixup_limit_int_mic_boost and disables unused pin 0x1d
  to match the Windows driver's pin configuration.
  Chained to ALC287_FIXUP_AW88399_I2C_2.

Per-model quirks (aw88399_hda.c):

* Channel swap: the I2C wiring on these Legion models is reversed
  (0x34 is physically the right speaker, 0x35 is the left). The
  quirk swaps the channel assignment to correct L/R audio.

* BSTS status bypass: the AW88399's boost-finished status bit (BSTS,
  SYSST register bit 9) does not reliably assert on this hardware.
  Register dumps during normal playback show both amplifiers
  reporting BSTS=0 on both channels despite clean audio output.
  The quirk sets the bsts_unreliable flag, introduced in
  commit b4530a3e4895 ("ASoC: aw88399: add per-instance BSTS status bypass flag"),
  so the startup status check skips the BSTS requirement
  on these devices.

The R9000P ADR10 entries use HDA_CODEC_QUIRK and are placed before
the existing SND_PCI_QUIRK for 17aa:38bb (Yoga S780-14.5 Air) to
ensure the codec SSID match takes priority over the shared PCI SSID,
following the pattern established by e.g.
commit 0f3a822ae225 ("ALSA: hda/realtek: Fix quirk matching for Legion Pro 7"),
commit dd074f04e046 ("ALSA: hda/realtek: Fix Legion 7 16ITHG6 speaker amp binding").
All other entries also use HDA_CODEC_QUIRK for consistency.

Supported models (Lenovo vendor ID 0x17aa):

* 0x3906: Legion Pro 7i 16IAX10H / Y9000P IAX10 (Intel)
* 0x3907: Legion Pro 7i 16IAX10H / Y9000P IAX10 (Intel)
* 0x3927: Legion R9000P ADR10 (AMD)
* 0x3928: Legion R9000P ADR10 (AMD)
* 0x3938: Legion Pro 7 16AFR10H (AMD)
* 0x3939: Legion Pro 7 16AFR10H (AMD)

Tested-by: Nadim Kobeissi <[email protected]>
Tested-by: Xia Yun'an <[email protected]>
Tested-by: Munzir Taha <[email protected]>
Co-developed-by: Yakov Till <[email protected]>
Signed-off-by: Yakov Till <[email protected]>
Signed-off-by: Marco Giunta <[email protected]>
---
 sound/hda/codecs/realtek/alc269.c          | 50 ++++++++++++++++++++++
 sound/hda/codecs/side-codecs/aw88399_hda.c | 42 ++++++++++++++++++
 2 files changed, 92 insertions(+)

diff --git a/sound/hda/codecs/realtek/alc269.c b/sound/hda/codecs/realtek/alc269.c
index 6b3c1d529bd1..6384620a2062 100644
--- a/sound/hda/codecs/realtek/alc269.c
+++ b/sound/hda/codecs/realtek/alc269.c
@@ -3262,6 +3262,34 @@ static void find_cirrus_companion_amps(struct hda_codec *cdc)
 	comp_generic_fixup(cdc, HDA_FIXUP_ACT_PRE_PROBE, bus, acpi_ids[i].hid, match, count);
 }
 
+static void aw88399_fixup_i2c_two(struct hda_codec *cdc, const struct hda_fixup *fix, int action)
+{
+	comp_generic_fixup(cdc, action, "i2c", "AWDZ8399", "-%s:00-aw88399-hda.%d", 2);
+}
+
+static void alc287_fixup_legion_16iax10h_aw88399(struct hda_codec *codec,
+						 const struct hda_fixup *fix, int action)
+{
+	static const struct hda_pintbl pincfgs[] = {
+		{ 0x1d, 0x411111f0 }, /* unused bogus pin */
+		{ }
+	};
+
+	/*
+	 * Force DAC 0x02 for the bass speaker 0x17, as the default 0x06 lacks volume controls.
+	 */
+	static const hda_nid_t conn[] = { 0x02 };
+
+	alc269_fixup_limit_int_mic_boost(codec, fix, action);
+
+	switch (action) {
+	case HDA_FIXUP_ACT_PRE_PROBE:
+		snd_hda_apply_pincfgs(codec, pincfgs);
+		snd_hda_override_conn_list(codec, 0x17, ARRAY_SIZE(conn), conn);
+		break;
+	}
+}
+
 static void cs35l41_fixup_i2c_two(struct hda_codec *cdc, const struct hda_fixup *fix, int action)
 {
 	comp_generic_fixup(cdc, action, "i2c", "CSC3551", "-%s:00-cs35l41-hda.%d", 2);
@@ -4231,6 +4259,8 @@ enum {
 	ALC236_FIXUP_DELL_HP_POP_NOISE,
 	ALC274_FIXUP_HP_89E9_GPIO,
 	ALC274_FIXUP_HP_VERBS,
+	ALC287_FIXUP_AW88399_I2C_2,
+	ALC287_FIXUP_LENOVO_LEGION_AW88399,
 };
 
 /* A special fixup for Lenovo C940 and Yoga Duet 7;
@@ -6917,6 +6947,16 @@ static const struct hda_fixup alc269_fixups[] = {
 		.chained = true,
 		.chain_id = ALC274_FIXUP_HP_89E9_GPIO,
 	},
+	[ALC287_FIXUP_AW88399_I2C_2] = {
+		.type = HDA_FIXUP_FUNC,
+		.v.func = aw88399_fixup_i2c_two,
+	},
+	[ALC287_FIXUP_LENOVO_LEGION_AW88399] = {
+		.type = HDA_FIXUP_FUNC,
+		.v.func = alc287_fixup_legion_16iax10h_aw88399,
+		.chained = true,
+		.chain_id = ALC287_FIXUP_AW88399_I2C_2,
+	},
 };
 
 static const struct hda_quirk alc269_fixup_tbl[] = {
@@ -7975,6 +8015,11 @@ static const struct hda_quirk alc269_fixup_tbl[] = {
 	SND_PCI_QUIRK(0x17aa, 0x38b8, "Yoga S780-14.5 proX AMD YC Dual", ALC287_FIXUP_TAS2781_I2C),
 	SND_PCI_QUIRK(0x17aa, 0x38b9, "Yoga S780-14.5 proX AMD LX Dual", ALC287_FIXUP_TAS2781_I2C),
 	SND_PCI_QUIRK(0x17aa, 0x38ba, "Yoga S780-14.5 Air AMD quad YC", ALC287_FIXUP_TAS2781_I2C),
+	/* Legion R9000P ADR10 shares PCI SSID 17aa:38bb with Yoga S780-14.5 Air AMD quad AAC;
+	 * use codec SSID to distinguish them
+	 */
+	HDA_CODEC_QUIRK(0x17aa, 0x3927, "Legion R9000P ADR10", ALC287_FIXUP_LENOVO_LEGION_AW88399),
+	HDA_CODEC_QUIRK(0x17aa, 0x3928, "Legion R9000P ADR10", ALC287_FIXUP_LENOVO_LEGION_AW88399),
 	SND_PCI_QUIRK(0x17aa, 0x38bb, "Yoga S780-14.5 Air AMD quad AAC", ALC287_FIXUP_TAS2781_I2C),
 	SND_PCI_QUIRK(0x17aa, 0x38be, "Yoga S980-14.5 proX YC Dual", ALC287_FIXUP_TAS2781_I2C),
 	SND_PCI_QUIRK(0x17aa, 0x38bf, "Yoga S980-14.5 proX LX Dual", ALC287_FIXUP_TAS2781_I2C),
@@ -8002,6 +8047,8 @@ static const struct hda_quirk alc269_fixup_tbl[] = {
 	SND_PCI_QUIRK(0x17aa, 0x38fc, "Lenovo Yoga Pro 7 15ASH11", ALC287_FIXUP_LENOVO_YOGA_PRO7),
 	SND_PCI_QUIRK(0x17aa, 0x38fd, "ThinkBook plus Gen5 Hybrid", ALC287_FIXUP_TAS2781_I2C),
 	SND_PCI_QUIRK(0x17aa, 0x3902, "Lenovo E50-80", ALC269_FIXUP_DMIC_THINKPAD_ACPI),
+	HDA_CODEC_QUIRK(0x17aa, 0x3906, "Legion Pro 7i 16IAX10H / Y9000P IAX10", ALC287_FIXUP_LENOVO_LEGION_AW88399),
+	HDA_CODEC_QUIRK(0x17aa, 0x3907, "Legion Pro 7i 16IAX10H / Y9000P IAX10", ALC287_FIXUP_LENOVO_LEGION_AW88399),
 	SND_PCI_QUIRK(0x17aa, 0x390d, "Lenovo Yoga Pro 7 14ASP10", ALC287_FIXUP_YOGA9_14IAP7_BASS_SPK_PIN),
 	SND_PCI_QUIRK(0x17aa, 0x3911, "Lenovo Yoga Pro 7 14IAH10", ALC287_FIXUP_YOGA9_14IAP7_BASS_SPK_PIN),
 	SND_PCI_QUIRK(0x17aa, 0x3912, "Lenovo Xiaoxin 14 GT", ALC287_FIXUP_YOGA9_14IAP7_BASS_SPK_PIN),
@@ -8011,6 +8058,8 @@ static const struct hda_quirk alc269_fixup_tbl[] = {
 	SND_PCI_QUIRK(0x17aa, 0x3920, "Yoga S990-16 pro Quad VECO Quad", ALC287_FIXUP_TXNW2781_I2C),
 	SND_PCI_QUIRK(0x17aa, 0x3929, "Thinkbook 13x Gen 5", ALC287_FIXUP_MG_RTKC_CSAMP_CS35L41_I2C_THINKPAD),
 	SND_PCI_QUIRK(0x17aa, 0x392b, "Thinkbook 13x Gen 5", ALC287_FIXUP_MG_RTKC_CSAMP_CS35L41_I2C_THINKPAD),
+	HDA_CODEC_QUIRK(0x17aa, 0x3938, "Legion Pro 7 16AFR10H", ALC287_FIXUP_LENOVO_LEGION_AW88399),
+	HDA_CODEC_QUIRK(0x17aa, 0x3939, "Legion Pro 7 16AFR10H", ALC287_FIXUP_LENOVO_LEGION_AW88399),
 	HDA_CODEC_QUIRK(0x17aa, 0x394c, "Lenovo Yoga Slim 7 14AGP11", ALC287_FIXUP_YOGA9_14IAP7_BASS_SPK_PIN),
 	SND_PCI_QUIRK(0x17aa, 0x3977, "IdeaPad S210", ALC283_FIXUP_INT_MIC),
 	SND_PCI_QUIRK(0x17aa, 0x3978, "Lenovo B50-70", ALC269_FIXUP_DMIC_THINKPAD_ACPI),
@@ -8314,6 +8363,7 @@ static const struct hda_model_fixup alc269_fixup_models[] = {
 	{.id = ALC2XX_FIXUP_HEADSET_MIC, .name = "alc2xx-fixup-headset-mic"},
 	{.id = ALC245_FIXUP_BASS_HP_DAC, .name = "alc245-fixup-bass-hp-dac"},
 	{.id = ALC256_FIXUP_HONOR_MRB_XXX_M1020_AUDIO, .name = "alc256-honor-mrb-xxx-m1020-audio"},
+	{.id = ALC287_FIXUP_LENOVO_LEGION_AW88399, .name = "alc287-lenovo-legion-aw88399"},
 	{}
 };
 #define ALC225_STANDARD_PINS \
diff --git a/sound/hda/codecs/side-codecs/aw88399_hda.c b/sound/hda/codecs/side-codecs/aw88399_hda.c
index ed57d9a4a39d..42de68faddbc 100644
--- a/sound/hda/codecs/side-codecs/aw88399_hda.c
+++ b/sound/hda/codecs/side-codecs/aw88399_hda.c
@@ -140,12 +140,54 @@ static int aw88399_hda_init(struct aw88399_hda *aw88399)
 	return 0;
 }
 
+static int aw88399_swap_channels(struct aw88399_hda *aw88399)
+{
+	/*
+	 * Certain Lenovo Legion laptops have their
+	 * I2C wiring reversed: 0x34 is physically the right speaker,
+	 * 0x35 is the left. Swap channels to correct L/R assignment.
+	 * This is a model-specific hardware wiring issue, not a driver bug.
+	 */
+	aw88399->channel = 1 - aw88399->channel;
+	dev_dbg(aw88399->dev,
+		"Channel swap applied: index %d -> channel %d\n",
+		aw88399->index, aw88399->channel);
+	return 0;
+}
+
+static int aw88399_skip_bsts_check(struct aw88399_hda *aw88399)
+{
+	/*
+	 * BSTS (boost-finished) status bit does not reliably report on
+	 * some hardware. On certain Lenovo Legion laptops, both amps
+	 * report BSTS=0 (boost not finished) during normal playback
+	 * despite clean audio output. Skip BSTS in the startup status
+	 * check to avoid false init failures.
+	 */
+	aw88399->bsts_unreliable = true;
+	dev_dbg(aw88399->dev, "BSTS status check disabled\n");
+	return 0;
+}
+
+static int aw88399_apply_legion_quirks(struct aw88399_hda *aw88399)
+{
+	aw88399_swap_channels(aw88399);
+	aw88399_skip_bsts_check(aw88399);
+	return 0;
+}
+
 struct aw88399_prop_model {
 	const char *ssid;
 	int (*apply_prop)(struct aw88399_hda *aw88399);
 };
 
 static const struct aw88399_prop_model aw88399_prop_model_table[] = {
+	{ "17AA3906", aw88399_apply_legion_quirks },
+	{ "17AA3907", aw88399_apply_legion_quirks },
+	{ "17AA3927", aw88399_apply_legion_quirks },
+	{ "17AA3928", aw88399_apply_legion_quirks },
+	{ "17AA3938", aw88399_apply_legion_quirks },
+	{ "17AA3939", aw88399_apply_legion_quirks },
 	{ }
 };
 
-- 
2.55.0