[RFC PATCH v2 0/3] Add support for Legion 7 16ACHg6 laptop
Hi,
I would like to get some guidance about this solution to support the 16ACHg6 laptop.
Hardware: - The 16ACHg6 laptop has two CS35L41 amplifiers, connected to Realtek ALC287 by an I2S bus and by and direct I2C to the CPU. - The ALC287 codec is connected to the CPU by an HDA bus. - The CS35L41 has a DSP which will require firmware to be loaded.
Architecture: - To load the firmware for CS35L41, this solution will require the wm_adsp library, which requires regmap, header definitions and register tables. - The HDA machine driver will find the registered ASoC by its dai name. - To minimize the duplication of the code, the HDA will call snd_soc_dai_ops from the ASoC driver.
Notes: - This is a work in progress, so the code is not functional, its only intent is to demonstrate the overall solution
Changes from V1:
- Split into multiple patches, separating ACPI, ASoC and HDA patches - Removed CS35L41 exported functions, moved code to snd_soc_dai_ops - Patch realtek uses dai ops functions
Lucas Tanure (3): sound: cs35l41: Allow HDA systems to use CS35l41 ASoC driver ALSA: hda/realtek: Add support for Legion 7 16ACHg6 laptop Speakers ACPI / scan: Create platform device for INT3515 ACPI nodes
drivers/acpi/scan.c | 1 + drivers/platform/x86/i2c-multi-instantiate.c | 7 + include/sound/cs35l41.h | 1 + sound/pci/hda/patch_realtek.c | 109 ++++++++++++++- sound/soc/codecs/cs35l41.c | 139 +++++++++++++++++-- sound/soc/codecs/cs35l41.h | 1 + 6 files changed, 249 insertions(+), 9 deletions(-)
Re-use ASoC driver for supporting for Legion 7 16ACHg6 laptop. HDA machine driver will find the registered dais for the Amp and use snd_soc_dai_ops to configure it.
Signed-off-by: Lucas Tanure tanureal@opensource.cirrus.com --- include/sound/cs35l41.h | 1 + sound/soc/codecs/cs35l41.c | 139 ++++++++++++++++++++++++++++++++++--- sound/soc/codecs/cs35l41.h | 1 + 3 files changed, 133 insertions(+), 8 deletions(-)
diff --git a/include/sound/cs35l41.h b/include/sound/cs35l41.h index 1f1e3c6c9be1..e250d31d4b04 100644 --- a/include/sound/cs35l41.h +++ b/include/sound/cs35l41.h @@ -23,6 +23,7 @@ struct cs35l41_irq_cfg { };
struct cs35l41_platform_data { + bool vspk_always_on; int bst_ind; int bst_ipk; int bst_cap; diff --git a/sound/soc/codecs/cs35l41.c b/sound/soc/codecs/cs35l41.c index b16eb6610c0e..e6bb5c047d89 100644 --- a/sound/soc/codecs/cs35l41.c +++ b/sound/soc/codecs/cs35l41.c @@ -21,6 +21,7 @@ #include <sound/soc.h> #include <sound/soc-dapm.h> #include <sound/tlv.h> +#include <linux/acpi.h>
#include "cs35l41.h"
@@ -1039,9 +1040,7 @@ static int cs35l41_set_pdata(struct cs35l41_private *cs35l41) { int ret;
- /* Set Platform Data */ - /* Required */ - if (cs35l41->pdata.bst_ipk && + if (!cs35l41->pdata.vspk_always_on && cs35l41->pdata.bst_ipk && cs35l41->pdata.bst_ind && cs35l41->pdata.bst_cap) { ret = cs35l41_boost_config(cs35l41, cs35l41->pdata.bst_ind, cs35l41->pdata.bst_cap, @@ -1051,8 +1050,7 @@ static int cs35l41_set_pdata(struct cs35l41_private *cs35l41) return ret; } } else { - dev_err(cs35l41->dev, "Incomplete Boost component DT config\n"); - return -EINVAL; + dev_info(cs35l41->dev, "Boost disabled\n"); }
/* Optional */ @@ -1098,12 +1096,92 @@ static int cs35l41_irq_gpio_config(struct cs35l41_private *cs35l41) return irq_pol; }
+static const struct reg_sequence cs35l41_safe_to_global_enable[] = { + { 0x00000040, 0x00000055 }, + { 0x00000040, 0x000000AA }, + { 0x0000742C, 0x0000000F }, + { 0x0000742C, 0x00000079 }, + { 0x00007438, 0x00585941 }, + { CS35L41_PLL_CLK_CTRL, 0x00000420 }, //3200000Hz ,BCLK Input ,PLL_REFCLK_EN = 0 + { CS35L41_PLL_CLK_CTRL, 0x00000430 }, //3200000Hz ,BCLK Input ,PLL_REFCLK_EN = 1 + { CS35L41_GLOBAL_CLK_CTRL, 0x00000003 }, //GLOBAL_FS = 48 kHz + { CS35L41_SP_ENABLES, 0x00010000 }, //ASP_RX1_EN = 1 + { CS35L41_SP_RATE_CTRL, 0x00000021 }, //ASP_BCLK_FREQ = 3.072 MHz + { CS35L41_SP_FORMAT, 0x18180200 }, //ASP_RX_WIDTH = 24 bits, ASP_TX_WIDTH = 24 bits, ASP_FMT=I2S, BCLK Slave, FSYNC Slave + { CS35L41_DAC_PCM1_SRC, 0x00000008 }, //DACPCM1_SRC = ASPRX1 + { CS35L41_AMP_DIG_VOL_CTRL, 0x00000000 }, //AMP_VOL_PCM 0.0 dB + { CS35L41_AMP_GAIN_CTRL, 0x00000260 }, //AMP_GAIN_PCM 19.5 dB + { CS35L41_PWR_CTRL2, 0x00000001 }, //AMP_EN = 1 + { CS35L41_PWR_CTRL1, 0x00000001 }, //GLOBAL_EN = 1 + { 0x00000040, 0x000000CC }, + { 0x00000040, 0x00000033 }, +}; + +static const struct reg_sequence cs35l41_global_enable_to_active[] = { + { 0x00000040, 0x00000055 }, + { 0x00000040, 0x000000AA }, + { 0x0000742C, 0x000000F9 }, + { 0x00007438, 0x00580941 }, + { 0x00000040, 0x000000CC }, + { 0x00000040, 0x00000033 }, +}; + +static const struct reg_sequence cs35l41_active_to_safe_first[] = { + { 0x00000040, 0x00000055 }, + { 0x00000040, 0x000000AA }, + { 0x00007438, 0x00585941 }, + { CS35L41_AMP_DIG_VOL_CTRL, 0x0000A678 }, //AMP_VOL_PCM Mute + { CS35L41_PWR_CTRL2, 0x00000000 }, //AMP_EN = 0 + { CS35L41_PWR_CTRL1, 0x00000000 }, + { 0x0000742C, 0x00000009 }, + { 0x00000040, 0x000000CC }, + { 0x00000040, 0x00000033 }, +}; + +static const struct reg_sequence cs35l41_active_to_safe_second[] = { + { 0x00000040, 0x00000055 }, + { 0x00000040, 0x000000AA }, + { 0x00007438, 0x00580941 }, + { 0x00000040, 0x000000CC }, + { 0x00000040, 0x00000033 }, +}; + +static void cs35l41_dai_shutdown(struct snd_pcm_substream *sub, struct snd_soc_dai *dai) +{ + struct cs35l41_private *cs35l41 = snd_soc_component_get_drvdata(dai->component); + + if (cs35l41->hda) { + regmap_multi_reg_write(cs35l41->regmap, cs35l41_active_to_safe_first, + ARRAY_SIZE(cs35l41_active_to_safe_first)); + usleep_range(1500, 2000); + regmap_multi_reg_write(cs35l41->regmap, cs35l41_active_to_safe_second, + ARRAY_SIZE(cs35l41_active_to_safe_second)); + } +} + +static int cs35l41_dai_prepare(struct snd_pcm_substream *sub, struct snd_soc_dai *dai) +{ + struct cs35l41_private *cs35l41 = snd_soc_component_get_drvdata(dai->component); + + if (cs35l41->hda) { + regmap_multi_reg_write(cs35l41->regmap, cs35l41_safe_to_global_enable, + ARRAY_SIZE(cs35l41_safe_to_global_enable)); + usleep_range(1500, 2000); + regmap_multi_reg_write(cs35l41->regmap, cs35l41_global_enable_to_active, + ARRAY_SIZE(cs35l41_global_enable_to_active)); + } + + return 0; +} + static const struct snd_soc_dai_ops cs35l41_ops = { .startup = cs35l41_pcm_startup, .set_fmt = cs35l41_set_dai_fmt, .hw_params = cs35l41_pcm_hw_params, .set_sysclk = cs35l41_dai_set_sysclk, .set_channel_map = cs35l41_set_channel_map, + .shutdown = cs35l41_dai_shutdown, + .prepare = cs35l41_dai_prepare, };
static struct snd_soc_dai_driver cs35l41_dai[] = { @@ -1126,6 +1204,7 @@ static struct snd_soc_dai_driver cs35l41_dai[] = { }, .ops = &cs35l41_ops, .symmetric_rate = 1, + .symmetric_sample_bits = 1, }, };
@@ -1148,9 +1227,31 @@ static int cs35l41_handle_pdata(struct device *dev, { struct cs35l41_irq_cfg *irq_gpio1_config = &pdata->irq_config1; struct cs35l41_irq_cfg *irq_gpio2_config = &pdata->irq_config2; + struct acpi_device *adev; + struct device *phys_dev; unsigned int val; int ret;
+ if (memcmp(dev_name(cs35l41->dev), "i2c-CLSA0100", 12) == 0) { + pdata->vspk_always_on = true; + cs35l41->hda = true; + adev = acpi_dev_get_first_match_dev("CLSA0100", "1", -1); + if (!adev) { + dev_err(dev, "Failed to find an ACPI device\n"); + return -ENODEV; + } + + phys_dev = get_device(acpi_get_first_physical_node(adev)); + acpi_dev_put(adev); + + if (!phys_dev) { + dev_err(dev, "Failed to find a physical device\n"); + return -ENODEV; + } + cs35l41->reset_gpio = gpiod_get_index(phys_dev, NULL, 0, GPIOD_ASIS); + return 0; + } + ret = device_property_read_u32(dev, "cirrus,boost-peak-milliamp", &val); if (ret >= 0) pdata->bst_ipk = val; @@ -1237,6 +1338,16 @@ static const struct reg_sequence cs35l41_revb2_errata_patch[] = { { 0x00000040, 0x00003333 }, };
+static const struct reg_sequence cs35l41_reset_to_enabled[] = { + { 0x00000040, 0x00000055 }, + { 0x00000040, 0x000000AA }, + { 0x00007438, 0x00585941 }, + { 0x00007414, 0x08C82222 }, + { 0x0000742C, 0x00000009 }, + { 0x00000040, 0x000000CC }, + { 0x00000040, 0x00000033 }, +}; + int cs35l41_probe(struct cs35l41_private *cs35l41, struct cs35l41_platform_data *pdata) { @@ -1269,8 +1380,8 @@ int cs35l41_probe(struct cs35l41_private *cs35l41, }
/* returning NULL can be an option if in stereo mode */ - cs35l41->reset_gpio = devm_gpiod_get_optional(cs35l41->dev, "reset", - GPIOD_OUT_LOW); + if (!cs35l41->reset_gpio) + cs35l41->reset_gpio = devm_gpiod_get_optional(cs35l41->dev, "reset", GPIOD_OUT_LOW); if (IS_ERR(cs35l41->reset_gpio)) { ret = PTR_ERR(cs35l41->reset_gpio); cs35l41->reset_gpio = NULL; @@ -1365,6 +1476,16 @@ int cs35l41_probe(struct cs35l41_private *cs35l41, break; }
+ if (cs35l41->pdata.vspk_always_on) { + ret = regmap_multi_reg_write(cs35l41->regmap, cs35l41_reset_to_enabled, + ARRAY_SIZE(cs35l41_reset_to_enabled)); + if (ret < 0) { + dev_err(cs35l41->dev, "Failed to apply reset to enabled patch: %d\n", ret); + goto err; + } + dev_info(cs35l41->dev, "Safe mode enabled\n"); + } + irq_pol = cs35l41_irq_gpio_config(cs35l41);
/* Set interrupt masks for critical errors */ @@ -1437,7 +1558,9 @@ int cs35l41_remove(struct cs35l41_private *cs35l41) { regmap_write(cs35l41->regmap, CS35L41_IRQ1_MASK1, 0xFFFFFFFF); regulator_bulk_disable(CS35L41_NUM_SUPPLIES, cs35l41->supplies); - gpiod_set_value_cansleep(cs35l41->reset_gpio, 0); + + if (cs35l41->reset_gpio && !cs35l41->pdata.vspk_always_on) + gpiod_set_value_cansleep(cs35l41->reset_gpio, 0);
return 0; } diff --git a/sound/soc/codecs/cs35l41.h b/sound/soc/codecs/cs35l41.h index 0e2639d6ef19..bb1f08e36c04 100644 --- a/sound/soc/codecs/cs35l41.h +++ b/sound/soc/codecs/cs35l41.h @@ -762,6 +762,7 @@ struct cs35l41_private { struct regmap *regmap; struct regulator_bulk_data supplies[CS35L41_NUM_SUPPLIES]; int irq; + bool hda; /* GPIO for /RST */ struct gpio_desc *reset_gpio; void (*otp_setup)(struct cs35l41_private *cs35l41, bool is_pre_setup,
On Wed, Oct 20, 2021 at 09:59:42AM +0100, Lucas Tanure wrote:
Please submit patches using subject lines reflecting the style for the subsystem, this makes it easier for people to identify relevant patches. Look at what existing commits in the area you're changing are doing and make sure your subject lines visually resemble what they're doing.
Re-use ASoC driver for supporting for Legion 7 16ACHg6 laptop. HDA machine driver will find the registered dais for the Amp and use snd_soc_dai_ops to configure it.
It will find the registered DAIs in what way exactly?
This patch is doing a whole bunch of stuff which isn't described at all in the changelog which makes it very hard to review, I can't tell what it's supposed to do.
Find the associated Amps by dai name, and use dai ops to configure it. Disable support for Amps if ASoC not built.
Signed-off-by: Lucas Tanure tanureal@opensource.cirrus.com --- sound/pci/hda/patch_realtek.c | 109 +++++++++++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 1 deletion(-)
diff --git a/sound/pci/hda/patch_realtek.c b/sound/pci/hda/patch_realtek.c index 4407f7da57c4..9a6fe6048b60 100644 --- a/sound/pci/hda/patch_realtek.c +++ b/sound/pci/hda/patch_realtek.c @@ -21,6 +21,7 @@ #include <sound/core.h> #include <sound/jack.h> #include <sound/hda_codec.h> +#include <sound/soc.h> #include "hda_local.h" #include "hda_auto_parser.h" #include "hda_jack.h" @@ -74,6 +75,11 @@ struct alc_coef_led { unsigned int off; };
+struct alc_soc_dai_node { + struct list_head node; + struct snd_soc_dai *dai; +}; + struct alc_spec { struct hda_gen_spec gen; /* must be at head */
@@ -126,6 +132,9 @@ struct alc_spec { unsigned int coef0; struct input_dev *kb_dev; u8 alc_mute_keycode_map[1]; + + /* ASoC DAIs used to init ASoC components in this sound card */ + struct list_head soc_dais; };
/* @@ -6443,6 +6452,96 @@ static void alc287_fixup_legion_15imhg05_speakers(struct hda_codec *codec, } }
+static void alc_soc_dai_clear_list(struct alc_spec *spec) +{ + struct alc_soc_dai_node *n, *tmp; + + if (list_empty(&spec->soc_dais)) + return; + + list_for_each_entry_safe(n, tmp, &spec->soc_dais, node) { + list_del(&n->node); + kfree(n); + } +} + +static int alc_add_soc_dai_list(struct alc_spec *spec, const char *dai_name) +{ +#if IS_ENABLED(CONFIG_SND_SOC) + struct snd_soc_dai_link_component dlc; + struct alc_soc_dai_node *dai_node; + //struct snd_soc_component *comp; + struct snd_soc_dai *dai; + + dlc.dai_name = dai_name; + dlc.of_node = NULL; + dlc.name = NULL; + + dai = snd_soc_find_dai(&dlc); + if (!dai) + return -EPROBE_DEFER; + + dai_node = kmalloc(sizeof(*dai_node), GFP_KERNEL); + if (!dai_node) + return -ENOMEM; + dai_node->dai = dai; + + list_add(&dai_node->node, &spec->soc_dais); + + return 0; +#else + return -EPERM; +#endif +} + +void lenovo_y760_pcm_playback_hook(struct hda_pcm_stream *hinfo, struct hda_codec *cdc, + struct snd_pcm_substream *sub, int act) +{ + struct alc_spec *spec = cdc->spec; + struct alc_soc_dai_node *dai_node; + struct snd_soc_dai *dai; + unsigned int rx_slots[2] = {1, 0}; + int i = 0; + + list_for_each_entry(dai_node, &spec->soc_dais, node) { + dai = dai_node->dai; + if (!dai->driver->ops) + continue; + switch (act) { + case HDA_GEN_PCM_ACT_PREPARE: + if (dai->driver->ops->set_channel_map) + dai->driver->ops->set_channel_map(dai, 0, NULL, 1, &rx_slots[i++]); + if (dai->driver->ops->prepare) + dai->driver->ops->prepare(sub, dai); + break; + case HDA_GEN_PCM_ACT_CLOSE: + if (dai->driver->ops->shutdown) + dai->driver->ops->shutdown(sub, dai); + break; + } + } + +} + +static void alc287_fixup_lenovo_y760(struct hda_codec *codec, const struct hda_fixup *fix, int act) +{ + struct alc_spec *spec = codec->spec; + int ret; + + INIT_LIST_HEAD(&spec->soc_dais); + + ret = alc_add_soc_dai_list(spec, "i2c-CLSA0100:00-cs35l41.0"); + if (ret) + return; + + ret = alc_add_soc_dai_list(spec, "i2c-CLSA0100:00-cs35l41.1"); + if (ret) { + alc_soc_dai_clear_list(spec); + return; + } + spec->gen.pcm_playback_hook = lenovo_y760_pcm_playback_hook; +} + /* for alc295_fixup_hp_top_speakers */ #include "hp_x360_helper.c"
@@ -6663,7 +6762,8 @@ enum { ALC287_FIXUP_LEGION_15IMHG05_SPEAKERS, ALC287_FIXUP_LEGION_15IMHG05_AUTOMUTE, ALC287_FIXUP_YOGA7_14ITL_SPEAKERS, - ALC287_FIXUP_13S_GEN2_SPEAKERS + ALC287_FIXUP_13S_GEN2_SPEAKERS, + ALC287_FIXUP_LENOVO_Y760 };
static const struct hda_fixup alc269_fixups[] = { @@ -8361,6 +8461,10 @@ static const struct hda_fixup alc269_fixups[] = { .chained = true, .chain_id = ALC269_FIXUP_HEADSET_MODE, }, + [ALC287_FIXUP_LENOVO_Y760] = { + .type = HDA_FIXUP_FUNC, + .v.func = alc287_fixup_lenovo_y760, + }, };
static const struct snd_pci_quirk alc269_fixup_tbl[] = { @@ -8755,6 +8859,7 @@ static const struct snd_pci_quirk alc269_fixup_tbl[] = { SND_PCI_QUIRK(0x17aa, 0x3818, "Lenovo C940", ALC298_FIXUP_LENOVO_SPK_VOLUME), SND_PCI_QUIRK(0x17aa, 0x3827, "Ideapad S740", ALC285_FIXUP_IDEAPAD_S740_COEF), SND_PCI_QUIRK(0x17aa, 0x3843, "Yoga 9i", ALC287_FIXUP_IDEAPAD_BASS_SPK_AMP), + SND_PCI_QUIRK(0x17aa, 0x3847, "Legion Y760", ALC287_FIXUP_LENOVO_Y760), SND_PCI_QUIRK(0x17aa, 0x3813, "Legion 7i 15IMHG05", ALC287_FIXUP_LEGION_15IMHG05_SPEAKERS), SND_PCI_QUIRK(0x17aa, 0x3852, "Lenovo Yoga 7 14ITL5", ALC287_FIXUP_YOGA7_14ITL_SPEAKERS), SND_PCI_QUIRK(0x17aa, 0x3853, "Lenovo Yoga 7 15ITL5", ALC287_FIXUP_YOGA7_14ITL_SPEAKERS), @@ -9499,6 +9604,8 @@ static int patch_alc269(struct hda_codec *codec) spec->shutup = alc_default_shutup; spec->init_hook = alc_default_init;
+ INIT_LIST_HEAD(&spec->soc_dais); + switch (codec->core.vendor_id) { case 0x10ec0269: spec->codec_variant = ALC269_TYPE_ALC269VA;
On Wed, 20 Oct 2021 10:59:43 +0200, Lucas Tanure wrote:
Find the associated Amps by dai name, and use dai ops to configure it. Disable support for Amps if ASoC not built.
Hrm, it's the question whether such a sneaking into DAI access in open code is a good idea. If any, it could be done by some helper function instead.
And some more details:
+static int alc_add_soc_dai_list(struct alc_spec *spec, const char *dai_name) +{ +#if IS_ENABLED(CONFIG_SND_SOC)
- struct snd_soc_dai_link_component dlc;
- struct alc_soc_dai_node *dai_node;
- //struct snd_soc_component *comp;
- struct snd_soc_dai *dai;
- dlc.dai_name = dai_name;
- dlc.of_node = NULL;
- dlc.name = NULL;
- dai = snd_soc_find_dai(&dlc);
- if (!dai)
return -EPROBE_DEFER;
The deferred probe won't work at this stage for HD-audio codecs unlike many ASoC codec drivers. And moreover, the fixup action doesn't handle the error at all...
Second, this way may lead to use-after-free if the ASoC stuff is unbound while the usage from HD-audio codec side.
Also, the dependency mess is still there. Even if we allow the hard binding to ASoC core here, IS_ENABLED() wouldn't work properly. It must be IS_REACHABLE().
thanks,
Takashi
The ACPI device with CLSA0100 is a sound card with multiple instances of CS35L41.
We add an ID to the I2C multi instantiate list to enumerate all I2C slaves correctly.
Signed-off-by: Lucas Tanure tanureal@opensource.cirrus.com --- drivers/acpi/scan.c | 1 + drivers/platform/x86/i2c-multi-instantiate.c | 7 +++++++ 2 files changed, 8 insertions(+)
diff --git a/drivers/acpi/scan.c b/drivers/acpi/scan.c index 5b54c80b9d32..c1c27a408420 100644 --- a/drivers/acpi/scan.c +++ b/drivers/acpi/scan.c @@ -1703,6 +1703,7 @@ static bool acpi_device_enumeration_by_parent(struct acpi_device *device) {"BSG2150", }, {"INT33FE", }, {"INT3515", }, + {"CLSA0100", }, {} };
diff --git a/drivers/platform/x86/i2c-multi-instantiate.c b/drivers/platform/x86/i2c-multi-instantiate.c index a50153ecd560..b61f7e30d42a 100644 --- a/drivers/platform/x86/i2c-multi-instantiate.c +++ b/drivers/platform/x86/i2c-multi-instantiate.c @@ -139,6 +139,12 @@ static const struct i2c_inst_data bsg2150_data[] = { {} };
+static const struct i2c_inst_data clsa0100_data[] = { + { "cs35l41", IRQ_RESOURCE_GPIO, 0 }, + { "cs35l41", IRQ_RESOURCE_GPIO, 0 }, + {} +}; + /* * Device with _HID INT3515 (TI PD controllers) has some unresolved interrupt * issues. The most common problem seen is interrupt flood. @@ -170,6 +176,7 @@ static const struct i2c_inst_data bsg2150_data[] = { static const struct acpi_device_id i2c_multi_inst_acpi_ids[] = { { "BSG1160", (unsigned long)bsg1160_data }, { "BSG2150", (unsigned long)bsg2150_data }, + { "CLSA0100", (unsigned long)clsa0100_data }, { } }; MODULE_DEVICE_TABLE(acpi, i2c_multi_inst_acpi_ids);
participants (3)
-
Lucas Tanure
-
Mark Brown
-
Takashi Iwai