summaryrefslogtreecommitdiff
path: root/sound/hda/codecs/side-codecs
diff options
context:
space:
mode:
Diffstat (limited to 'sound/hda/codecs/side-codecs')
-rw-r--r--sound/hda/codecs/side-codecs/Kconfig128
-rw-r--r--sound/hda/codecs/side-codecs/Makefile28
-rw-r--r--sound/hda/codecs/side-codecs/cirrus_scodec.c73
-rw-r--r--sound/hda/codecs/side-codecs/cirrus_scodec.h13
-rw-r--r--sound/hda/codecs/side-codecs/cirrus_scodec_test.c332
-rw-r--r--sound/hda/codecs/side-codecs/cs35l41_hda.c2112
-rw-r--r--sound/hda/codecs/side-codecs/cs35l41_hda.h110
-rw-r--r--sound/hda/codecs/side-codecs/cs35l41_hda_i2c.c69
-rw-r--r--sound/hda/codecs/side-codecs/cs35l41_hda_property.c578
-rw-r--r--sound/hda/codecs/side-codecs/cs35l41_hda_property.h18
-rw-r--r--sound/hda/codecs/side-codecs/cs35l41_hda_spi.c64
-rw-r--r--sound/hda/codecs/side-codecs/cs35l56_hda.c1181
-rw-r--r--sound/hda/codecs/side-codecs/cs35l56_hda.h50
-rw-r--r--sound/hda/codecs/side-codecs/cs35l56_hda_i2c.c87
-rw-r--r--sound/hda/codecs/side-codecs/cs35l56_hda_spi.c90
-rw-r--r--sound/hda/codecs/side-codecs/hda_component.c212
-rw-r--r--sound/hda/codecs/side-codecs/hda_component.h103
-rw-r--r--sound/hda/codecs/side-codecs/tas2781_hda.c379
-rw-r--r--sound/hda/codecs/side-codecs/tas2781_hda.h90
-rw-r--r--sound/hda/codecs/side-codecs/tas2781_hda_i2c.c798
-rw-r--r--sound/hda/codecs/side-codecs/tas2781_hda_spi.c954
21 files changed, 7469 insertions, 0 deletions
diff --git a/sound/hda/codecs/side-codecs/Kconfig b/sound/hda/codecs/side-codecs/Kconfig
new file mode 100644
index 000000000000..cbf1847896bc
--- /dev/null
+++ b/sound/hda/codecs/side-codecs/Kconfig
@@ -0,0 +1,128 @@
+config SND_HDA_CIRRUS_SCODEC
+ tristate
+
+config SND_HDA_CIRRUS_SCODEC_KUNIT_TEST
+ tristate "KUnit test for Cirrus side-codec library" if !KUNIT_ALL_TESTS
+ depends on SND_HDA_CIRRUS_SCODEC && GPIOLIB && KUNIT
+ default KUNIT_ALL_TESTS
+ help
+ This builds KUnit tests for the cirrus side-codec library.
+ For more information on KUnit and unit tests in general,
+ please refer to the KUnit documentation in
+ Documentation/dev-tools/kunit/.
+ If in doubt, say "N".
+
+config SND_HDA_SCODEC_CS35L41
+ tristate
+ select SND_HDA_GENERIC
+ select REGMAP_IRQ
+ select FW_CS_DSP
+
+config SND_HDA_SCODEC_COMPONENT
+ tristate
+
+config SND_HDA_SCODEC_CS35L41_I2C
+ tristate "Build CS35L41 HD-audio side codec support for I2C Bus"
+ depends on I2C
+ depends on ACPI
+ depends on EFI
+ depends on SND_SOC
+ select SND_SOC_CS35L41_LIB
+ select SND_HDA_SCODEC_CS35L41
+ select SND_SOC_CS_AMP_LIB
+ help
+ Say Y or M here to include CS35L41 I2C HD-audio side codec support
+ in snd-hda-intel driver, such as ALC287.
+
+comment "Set to Y if you want auto-loading the side codec driver"
+ depends on SND_HDA=y && SND_HDA_SCODEC_CS35L41_I2C=m
+
+config SND_HDA_SCODEC_CS35L41_SPI
+ tristate "Build CS35L41 HD-audio codec support for SPI Bus"
+ depends on SPI_MASTER
+ depends on ACPI
+ depends on EFI
+ depends on SND_SOC
+ select SND_SOC_CS35L41_LIB
+ select SND_HDA_SCODEC_CS35L41
+ select SND_SOC_CS_AMP_LIB
+ help
+ Say Y or M here to include CS35L41 SPI HD-audio side codec support
+ in snd-hda-intel driver, such as ALC287.
+
+comment "Set to Y if you want auto-loading the side codec driver"
+ depends on SND_HDA=y && SND_HDA_SCODEC_CS35L41_SPI=m
+
+config SND_HDA_SCODEC_CS35L56
+ tristate
+
+config SND_HDA_SCODEC_CS35L56_I2C
+ tristate "Build CS35L56 HD-audio side codec support for I2C Bus"
+ depends on I2C
+ depends on ACPI
+ depends on SND_SOC
+ select FW_CS_DSP
+ imply SERIAL_MULTI_INSTANTIATE
+ select SND_HDA_GENERIC
+ select SND_SOC_CS35L56_SHARED
+ select SND_HDA_SCODEC_CS35L56
+ select SND_HDA_CIRRUS_SCODEC
+ select SND_SOC_CS_AMP_LIB
+ help
+ Say Y or M here to include CS35L56 amplifier support with
+ I2C control.
+
+config SND_HDA_SCODEC_CS35L56_SPI
+ tristate "Build CS35L56 HD-audio side codec support for SPI Bus"
+ depends on SPI_MASTER
+ depends on ACPI
+ depends on SND_SOC
+ select FW_CS_DSP
+ imply SERIAL_MULTI_INSTANTIATE
+ select SND_HDA_GENERIC
+ select SND_SOC_CS35L56_SHARED
+ select SND_HDA_SCODEC_CS35L56
+ select SND_HDA_CIRRUS_SCODEC
+ select SND_SOC_CS_AMP_LIB
+ help
+ Say Y or M here to include CS35L56 amplifier support with
+ SPI control.
+
+config SND_HDA_SCODEC_TAS2781
+ tristate
+ select SND_HDA_GENERIC
+
+config SND_HDA_SCODEC_TAS2781_I2C
+ tristate "Build TAS2781 HD-audio side codec support for I2C Bus"
+ depends on I2C
+ depends on ACPI
+ depends on EFI
+ depends on SND_SOC
+ select SND_HDA_SCODEC_TAS2781
+ select SND_SOC_TAS2781_COMLIB_I2C
+ select SND_SOC_TAS2781_FMWLIB
+ select CRC32
+ help
+ Say Y or M here to include TAS2781 I2C HD-audio side codec support
+ in snd-hda-intel driver, such as ALC287.
+
+comment "Set to Y if you want auto-loading the side codec driver"
+ depends on SND_HDA=y && SND_HDA_SCODEC_TAS2781_I2C=m
+
+config SND_HDA_SCODEC_TAS2781_SPI
+ tristate "Build TAS2781 HD-audio side codec support for SPI Bus"
+ depends on SPI_MASTER
+ depends on ACPI
+ depends on EFI
+ depends on SND_SOC
+ select SND_HDA_SCODEC_TAS2781
+ select SND_SOC_TAS2781_COMLIB
+ select SND_SOC_TAS2781_FMWLIB
+ select CRC8
+ select CRC32
+ help
+ Say Y or M here to include TAS2781 SPI HD-audio side codec support
+ in snd-hda-intel driver, such as ALC287.
+
+comment "Set to Y if you want auto-loading the side codec driver"
+ depends on SND_HDA=y && SND_HDA_SCODEC_TAS2781_SPI=m
diff --git a/sound/hda/codecs/side-codecs/Makefile b/sound/hda/codecs/side-codecs/Makefile
new file mode 100644
index 000000000000..245e84f6a121
--- /dev/null
+++ b/sound/hda/codecs/side-codecs/Makefile
@@ -0,0 +1,28 @@
+# SPDX-License-Identifier: GPL-2.0
+subdir-ccflags-y += -I$(src)/../../common
+
+snd-hda-cirrus-scodec-y := cirrus_scodec.o
+snd-hda-cirrus-scodec-test-y := cirrus_scodec_test.o
+snd-hda-scodec-cs35l41-y := cs35l41_hda.o cs35l41_hda_property.o
+snd-hda-scodec-cs35l41-i2c-y := cs35l41_hda_i2c.o
+snd-hda-scodec-cs35l41-spi-y := cs35l41_hda_spi.o
+snd-hda-scodec-cs35l56-y := cs35l56_hda.o
+snd-hda-scodec-cs35l56-i2c-y := cs35l56_hda_i2c.o
+snd-hda-scodec-cs35l56-spi-y := cs35l56_hda_spi.o
+snd-hda-scodec-component-y := hda_component.o
+snd-hda-scodec-tas2781-y := tas2781_hda.o
+snd-hda-scodec-tas2781-i2c-y := tas2781_hda_i2c.o
+snd-hda-scodec-tas2781-spi-y := tas2781_hda_spi.o
+
+obj-$(CONFIG_SND_HDA_CIRRUS_SCODEC) += snd-hda-cirrus-scodec.o
+obj-$(CONFIG_SND_HDA_CIRRUS_SCODEC_KUNIT_TEST) += snd-hda-cirrus-scodec-test.o
+obj-$(CONFIG_SND_HDA_SCODEC_CS35L41) += snd-hda-scodec-cs35l41.o
+obj-$(CONFIG_SND_HDA_SCODEC_CS35L41_I2C) += snd-hda-scodec-cs35l41-i2c.o
+obj-$(CONFIG_SND_HDA_SCODEC_CS35L41_SPI) += snd-hda-scodec-cs35l41-spi.o
+obj-$(CONFIG_SND_HDA_SCODEC_CS35L56) += snd-hda-scodec-cs35l56.o
+obj-$(CONFIG_SND_HDA_SCODEC_CS35L56_I2C) += snd-hda-scodec-cs35l56-i2c.o
+obj-$(CONFIG_SND_HDA_SCODEC_CS35L56_SPI) += snd-hda-scodec-cs35l56-spi.o
+obj-$(CONFIG_SND_HDA_SCODEC_COMPONENT) += snd-hda-scodec-component.o
+obj-$(CONFIG_SND_HDA_SCODEC_TAS2781) += snd-hda-scodec-tas2781.o
+obj-$(CONFIG_SND_HDA_SCODEC_TAS2781_I2C) += snd-hda-scodec-tas2781-i2c.o
+obj-$(CONFIG_SND_HDA_SCODEC_TAS2781_SPI) += snd-hda-scodec-tas2781-spi.o
diff --git a/sound/hda/codecs/side-codecs/cirrus_scodec.c b/sound/hda/codecs/side-codecs/cirrus_scodec.c
new file mode 100644
index 000000000000..3c670207ba30
--- /dev/null
+++ b/sound/hda/codecs/side-codecs/cirrus_scodec.c
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: GPL-2.0-only
+//
+// Common code for Cirrus side-codecs.
+//
+// Copyright (C) 2021, 2023 Cirrus Logic, Inc. and
+// Cirrus Logic International Semiconductor Ltd.
+
+#include <linux/dev_printk.h>
+#include <linux/gpio/consumer.h>
+#include <linux/module.h>
+
+#include "cirrus_scodec.h"
+
+int cirrus_scodec_get_speaker_id(struct device *dev, int amp_index,
+ int num_amps, int fixed_gpio_id)
+{
+ struct gpio_desc *speaker_id_desc;
+ int speaker_id = -ENOENT;
+
+ if (fixed_gpio_id >= 0) {
+ dev_dbg(dev, "Found Fixed Speaker ID GPIO (index = %d)\n", fixed_gpio_id);
+ speaker_id_desc = gpiod_get_index(dev, NULL, fixed_gpio_id, GPIOD_IN);
+ if (IS_ERR(speaker_id_desc)) {
+ speaker_id = PTR_ERR(speaker_id_desc);
+ return speaker_id;
+ }
+ speaker_id = gpiod_get_value_cansleep(speaker_id_desc);
+ gpiod_put(speaker_id_desc);
+ } else {
+ int base_index;
+ int gpios_per_amp;
+ int count;
+ int tmp;
+ int i;
+
+ count = gpiod_count(dev, "spk-id");
+ if (count > 0) {
+ speaker_id = 0;
+ gpios_per_amp = count / num_amps;
+ base_index = gpios_per_amp * amp_index;
+
+ if (count % num_amps)
+ return -EINVAL;
+
+ dev_dbg(dev, "Found %d Speaker ID GPIOs per Amp\n", gpios_per_amp);
+
+ for (i = 0; i < gpios_per_amp; i++) {
+ speaker_id_desc = gpiod_get_index(dev, "spk-id", i + base_index,
+ GPIOD_IN);
+ if (IS_ERR(speaker_id_desc)) {
+ speaker_id = PTR_ERR(speaker_id_desc);
+ break;
+ }
+ tmp = gpiod_get_value_cansleep(speaker_id_desc);
+ gpiod_put(speaker_id_desc);
+ if (tmp < 0) {
+ speaker_id = tmp;
+ break;
+ }
+ speaker_id |= tmp << i;
+ }
+ }
+ }
+
+ dev_dbg(dev, "Speaker ID = %d\n", speaker_id);
+
+ return speaker_id;
+}
+EXPORT_SYMBOL_NS_GPL(cirrus_scodec_get_speaker_id, "SND_HDA_CIRRUS_SCODEC");
+
+MODULE_DESCRIPTION("HDA Cirrus side-codec library");
+MODULE_AUTHOR("Richard Fitzgerald <rf@opensource.cirrus.com>");
+MODULE_LICENSE("GPL");
diff --git a/sound/hda/codecs/side-codecs/cirrus_scodec.h b/sound/hda/codecs/side-codecs/cirrus_scodec.h
new file mode 100644
index 000000000000..ba2041d8ef24
--- /dev/null
+++ b/sound/hda/codecs/side-codecs/cirrus_scodec.h
@@ -0,0 +1,13 @@
+/* SPDX-License-Identifier: GPL-2.0
+ *
+ * Copyright (C) 2023 Cirrus Logic, Inc. and
+ * Cirrus Logic International Semiconductor Ltd.
+ */
+
+#ifndef CIRRUS_SCODEC_H
+#define CIRRUS_SCODEC_H
+
+int cirrus_scodec_get_speaker_id(struct device *dev, int amp_index,
+ int num_amps, int fixed_gpio_id);
+
+#endif /* CIRRUS_SCODEC_H */
diff --git a/sound/hda/codecs/side-codecs/cirrus_scodec_test.c b/sound/hda/codecs/side-codecs/cirrus_scodec_test.c
new file mode 100644
index 000000000000..93b9cbf1f08a
--- /dev/null
+++ b/sound/hda/codecs/side-codecs/cirrus_scodec_test.c
@@ -0,0 +1,332 @@
+// SPDX-License-Identifier: GPL-2.0-only
+//
+// KUnit test for the Cirrus side-codec library.
+//
+// Copyright (C) 2023 Cirrus Logic, Inc. and
+// Cirrus Logic International Semiconductor Ltd.
+
+#include <kunit/platform_device.h>
+#include <kunit/resource.h>
+#include <kunit/test.h>
+#include <linux/device.h>
+#include <linux/device/faux.h>
+#include <linux/gpio/driver.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+
+#include "cirrus_scodec.h"
+
+KUNIT_DEFINE_ACTION_WRAPPER(faux_device_destroy_wrapper, faux_device_destroy,
+ struct faux_device *)
+KUNIT_DEFINE_ACTION_WRAPPER(device_remove_software_node_wrapper,
+ device_remove_software_node,
+ struct device *)
+
+struct cirrus_scodec_test_gpio {
+ unsigned int pin_state;
+ struct gpio_chip chip;
+};
+
+struct cirrus_scodec_test_priv {
+ struct faux_device *amp_dev;
+ struct platform_device *gpio_pdev;
+ struct cirrus_scodec_test_gpio *gpio_priv;
+};
+
+static int cirrus_scodec_test_gpio_get_direction(struct gpio_chip *chip,
+ unsigned int offset)
+{
+ return GPIO_LINE_DIRECTION_IN;
+}
+
+static int cirrus_scodec_test_gpio_direction_in(struct gpio_chip *chip,
+ unsigned int offset)
+{
+ return 0;
+}
+
+static int cirrus_scodec_test_gpio_get(struct gpio_chip *chip, unsigned int offset)
+{
+ struct cirrus_scodec_test_gpio *gpio_priv = gpiochip_get_data(chip);
+
+ return !!(gpio_priv->pin_state & BIT(offset));
+}
+
+static int cirrus_scodec_test_gpio_direction_out(struct gpio_chip *chip,
+ unsigned int offset, int value)
+{
+ return -EOPNOTSUPP;
+}
+
+static int cirrus_scodec_test_gpio_set(struct gpio_chip *chip,
+ unsigned int offset, int value)
+{
+ return -EOPNOTSUPP;
+}
+
+static int cirrus_scodec_test_gpio_set_config(struct gpio_chip *gc,
+ unsigned int offset,
+ unsigned long config)
+{
+ switch (pinconf_to_config_param(config)) {
+ case PIN_CONFIG_OUTPUT:
+ case PIN_CONFIG_OUTPUT_ENABLE:
+ return -EOPNOTSUPP;
+ default:
+ return 0;
+ }
+}
+
+static const struct gpio_chip cirrus_scodec_test_gpio_chip = {
+ .label = "cirrus_scodec_test_gpio",
+ .owner = THIS_MODULE,
+ .request = gpiochip_generic_request,
+ .free = gpiochip_generic_free,
+ .get_direction = cirrus_scodec_test_gpio_get_direction,
+ .direction_input = cirrus_scodec_test_gpio_direction_in,
+ .get = cirrus_scodec_test_gpio_get,
+ .direction_output = cirrus_scodec_test_gpio_direction_out,
+ .set_rv = cirrus_scodec_test_gpio_set,
+ .set_config = cirrus_scodec_test_gpio_set_config,
+ .base = -1,
+ .ngpio = 32,
+};
+
+static int cirrus_scodec_test_gpio_probe(struct platform_device *pdev)
+{
+ struct cirrus_scodec_test_gpio *gpio_priv;
+ int ret;
+
+ gpio_priv = devm_kzalloc(&pdev->dev, sizeof(*gpio_priv), GFP_KERNEL);
+ if (!gpio_priv)
+ return -ENOMEM;
+
+ /* GPIO core modifies our struct gpio_chip so use a copy */
+ gpio_priv->chip = cirrus_scodec_test_gpio_chip;
+ ret = devm_gpiochip_add_data(&pdev->dev, &gpio_priv->chip, gpio_priv);
+ if (ret)
+ return dev_err_probe(&pdev->dev, ret, "Failed to add gpiochip\n");
+
+ dev_set_drvdata(&pdev->dev, gpio_priv);
+
+ return 0;
+}
+
+static struct platform_driver cirrus_scodec_test_gpio_driver = {
+ .driver.name = "cirrus_scodec_test_gpio_drv",
+ .driver.owner = THIS_MODULE,
+ .probe = cirrus_scodec_test_gpio_probe,
+};
+
+/* software_node referencing the gpio driver */
+static const struct software_node cirrus_scodec_test_gpio_swnode = {
+ .name = "cirrus_scodec_test_gpio",
+};
+
+static void cirrus_scodec_test_create_gpio(struct kunit *test)
+{
+ struct cirrus_scodec_test_priv *priv = test->priv;
+
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_platform_driver_register(test, &cirrus_scodec_test_gpio_driver));
+
+ priv->gpio_pdev = kunit_platform_device_alloc(test,
+ cirrus_scodec_test_gpio_driver.driver.name,
+ PLATFORM_DEVID_NONE);
+ KUNIT_ASSERT_NOT_NULL(test, priv->gpio_pdev);
+
+ KUNIT_ASSERT_EQ(test, 0, device_add_software_node(&priv->gpio_pdev->dev,
+ &cirrus_scodec_test_gpio_swnode));
+ KUNIT_ASSERT_EQ(test, 0, kunit_add_action_or_reset(test,
+ device_remove_software_node_wrapper,
+ &priv->gpio_pdev->dev));
+
+ KUNIT_ASSERT_EQ(test, 0, kunit_platform_device_add(test, priv->gpio_pdev));
+
+ priv->gpio_priv = dev_get_drvdata(&priv->gpio_pdev->dev);
+ KUNIT_ASSERT_NOT_NULL(test, priv->gpio_priv);
+}
+
+static void cirrus_scodec_test_set_gpio_ref_arg(struct software_node_ref_args *arg,
+ int gpio_num)
+{
+ struct software_node_ref_args template =
+ SOFTWARE_NODE_REFERENCE(&cirrus_scodec_test_gpio_swnode, gpio_num, 0);
+
+ *arg = template;
+}
+
+static int cirrus_scodec_test_set_spkid_swnode(struct kunit *test,
+ struct device *dev,
+ struct software_node_ref_args *args,
+ int num_args)
+{
+ const struct property_entry props_template[] = {
+ PROPERTY_ENTRY_REF_ARRAY_LEN("spk-id-gpios", args, num_args),
+ { }
+ };
+ struct property_entry *props;
+ struct software_node *node;
+
+ node = kunit_kzalloc(test, sizeof(*node), GFP_KERNEL);
+ if (!node)
+ return -ENOMEM;
+
+ props = kunit_kzalloc(test, sizeof(props_template), GFP_KERNEL);
+ if (!props)
+ return -ENOMEM;
+
+ memcpy(props, props_template, sizeof(props_template));
+ node->properties = props;
+
+ return device_add_software_node(dev, node);
+}
+
+struct cirrus_scodec_test_spkid_param {
+ int num_amps;
+ int gpios_per_amp;
+ int num_amps_sharing;
+};
+
+static void cirrus_scodec_test_spkid_parse(struct kunit *test)
+{
+ struct cirrus_scodec_test_priv *priv = test->priv;
+ const struct cirrus_scodec_test_spkid_param *param = test->param_value;
+ int num_spk_id_refs = param->num_amps * param->gpios_per_amp;
+ struct software_node_ref_args *refs;
+ struct device *dev = &priv->amp_dev->dev;
+ unsigned int v;
+ int i, ret;
+
+ refs = kunit_kcalloc(test, num_spk_id_refs, sizeof(*refs), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, refs);
+
+ for (i = 0, v = 0; i < num_spk_id_refs; ) {
+ cirrus_scodec_test_set_gpio_ref_arg(&refs[i++], v++);
+
+ /*
+ * If amps are sharing GPIOs repeat the last set of
+ * GPIOs until we've done that number of amps.
+ * We have done all GPIOs for an amp when i is a multiple
+ * of gpios_per_amp.
+ * We have done all amps sharing the same GPIOs when i is
+ * a multiple of (gpios_per_amp * num_amps_sharing).
+ */
+ if (!(i % param->gpios_per_amp) &&
+ (i % (param->gpios_per_amp * param->num_amps_sharing)))
+ v -= param->gpios_per_amp;
+ }
+
+ ret = cirrus_scodec_test_set_spkid_swnode(test, dev, refs, num_spk_id_refs);
+ KUNIT_EXPECT_EQ_MSG(test, ret, 0, "Failed to add swnode\n");
+
+ for (i = 0; i < param->num_amps; ++i) {
+ for (v = 0; v < (1 << param->gpios_per_amp); ++v) {
+ /* Set only the GPIO bits used by this amp */
+ priv->gpio_priv->pin_state =
+ v << (param->gpios_per_amp * (i / param->num_amps_sharing));
+
+ ret = cirrus_scodec_get_speaker_id(dev, i, param->num_amps, -1);
+ KUNIT_EXPECT_EQ_MSG(test, ret, v,
+ "get_speaker_id failed amp:%d pin_state:%#x\n",
+ i, priv->gpio_priv->pin_state);
+ }
+ }
+}
+
+static void cirrus_scodec_test_no_spkid(struct kunit *test)
+{
+ struct cirrus_scodec_test_priv *priv = test->priv;
+ struct device *dev = &priv->amp_dev->dev;
+ int ret;
+
+ ret = cirrus_scodec_get_speaker_id(dev, 0, 4, -1);
+ KUNIT_EXPECT_EQ(test, ret, -ENOENT);
+}
+
+static int cirrus_scodec_test_case_init(struct kunit *test)
+{
+ struct cirrus_scodec_test_priv *priv;
+
+ priv = kunit_kzalloc(test, sizeof(*priv), GFP_KERNEL);
+ if (!priv)
+ return -ENOMEM;
+
+ test->priv = priv;
+
+ /* Create dummy GPIO */
+ cirrus_scodec_test_create_gpio(test);
+
+ /* Create dummy amp driver dev */
+ priv->amp_dev = faux_device_create("cirrus_scodec_test_amp_drv", NULL, NULL);
+ KUNIT_ASSERT_NOT_NULL(test, priv->amp_dev);
+ KUNIT_ASSERT_EQ(test, 0, kunit_add_action_or_reset(test,
+ faux_device_destroy_wrapper,
+ priv->amp_dev));
+
+ return 0;
+}
+
+static const struct cirrus_scodec_test_spkid_param cirrus_scodec_test_spkid_param_cases[] = {
+ { .num_amps = 2, .gpios_per_amp = 1, .num_amps_sharing = 1 },
+ { .num_amps = 2, .gpios_per_amp = 2, .num_amps_sharing = 1 },
+ { .num_amps = 2, .gpios_per_amp = 3, .num_amps_sharing = 1 },
+ { .num_amps = 2, .gpios_per_amp = 4, .num_amps_sharing = 1 },
+ { .num_amps = 3, .gpios_per_amp = 1, .num_amps_sharing = 1 },
+ { .num_amps = 3, .gpios_per_amp = 2, .num_amps_sharing = 1 },
+ { .num_amps = 3, .gpios_per_amp = 3, .num_amps_sharing = 1 },
+ { .num_amps = 3, .gpios_per_amp = 4, .num_amps_sharing = 1 },
+ { .num_amps = 4, .gpios_per_amp = 1, .num_amps_sharing = 1 },
+ { .num_amps = 4, .gpios_per_amp = 2, .num_amps_sharing = 1 },
+ { .num_amps = 4, .gpios_per_amp = 3, .num_amps_sharing = 1 },
+ { .num_amps = 4, .gpios_per_amp = 4, .num_amps_sharing = 1 },
+
+ /* Same GPIO shared by all amps */
+ { .num_amps = 2, .gpios_per_amp = 1, .num_amps_sharing = 2 },
+ { .num_amps = 2, .gpios_per_amp = 2, .num_amps_sharing = 2 },
+ { .num_amps = 2, .gpios_per_amp = 3, .num_amps_sharing = 2 },
+ { .num_amps = 2, .gpios_per_amp = 4, .num_amps_sharing = 2 },
+ { .num_amps = 3, .gpios_per_amp = 1, .num_amps_sharing = 3 },
+ { .num_amps = 3, .gpios_per_amp = 2, .num_amps_sharing = 3 },
+ { .num_amps = 3, .gpios_per_amp = 3, .num_amps_sharing = 3 },
+ { .num_amps = 3, .gpios_per_amp = 4, .num_amps_sharing = 3 },
+ { .num_amps = 4, .gpios_per_amp = 1, .num_amps_sharing = 4 },
+ { .num_amps = 4, .gpios_per_amp = 2, .num_amps_sharing = 4 },
+ { .num_amps = 4, .gpios_per_amp = 3, .num_amps_sharing = 4 },
+ { .num_amps = 4, .gpios_per_amp = 4, .num_amps_sharing = 4 },
+
+ /* Two sets of shared GPIOs */
+ { .num_amps = 4, .gpios_per_amp = 1, .num_amps_sharing = 2 },
+ { .num_amps = 4, .gpios_per_amp = 2, .num_amps_sharing = 2 },
+ { .num_amps = 4, .gpios_per_amp = 3, .num_amps_sharing = 2 },
+ { .num_amps = 4, .gpios_per_amp = 4, .num_amps_sharing = 2 },
+};
+
+static void cirrus_scodec_test_spkid_param_desc(const struct cirrus_scodec_test_spkid_param *param,
+ char *desc)
+{
+ snprintf(desc, KUNIT_PARAM_DESC_SIZE, "amps:%d gpios_per_amp:%d num_amps_sharing:%d",
+ param->num_amps, param->gpios_per_amp, param->num_amps_sharing);
+}
+
+KUNIT_ARRAY_PARAM(cirrus_scodec_test_spkid, cirrus_scodec_test_spkid_param_cases,
+ cirrus_scodec_test_spkid_param_desc);
+
+static struct kunit_case cirrus_scodec_test_cases[] = {
+ KUNIT_CASE_PARAM(cirrus_scodec_test_spkid_parse, cirrus_scodec_test_spkid_gen_params),
+ KUNIT_CASE(cirrus_scodec_test_no_spkid),
+ { } /* terminator */
+};
+
+static struct kunit_suite cirrus_scodec_test_suite = {
+ .name = "snd-hda-scodec-cs35l56-test",
+ .init = cirrus_scodec_test_case_init,
+ .test_cases = cirrus_scodec_test_cases,
+};
+
+kunit_test_suite(cirrus_scodec_test_suite);
+
+MODULE_IMPORT_NS("SND_HDA_CIRRUS_SCODEC");
+MODULE_DESCRIPTION("KUnit test for the Cirrus side-codec library");
+MODULE_AUTHOR("Richard Fitzgerald <rf@opensource.cirrus.com>");
+MODULE_LICENSE("GPL");
diff --git a/sound/hda/codecs/side-codecs/cs35l41_hda.c b/sound/hda/codecs/side-codecs/cs35l41_hda.c
new file mode 100644
index 000000000000..37f2cdc8ce82
--- /dev/null
+++ b/sound/hda/codecs/side-codecs/cs35l41_hda.c
@@ -0,0 +1,2112 @@
+// SPDX-License-Identifier: GPL-2.0
+//
+// CS35l41 ALSA HDA audio driver
+//
+// Copyright 2021 Cirrus Logic, Inc.
+//
+// Author: Lucas Tanure <tanureal@opensource.cirrus.com>
+
+#include <linux/acpi.h>
+#include <linux/module.h>
+#include <linux/moduleparam.h>
+#include <sound/hda_codec.h>
+#include <sound/soc.h>
+#include <linux/pm_runtime.h>
+#include <linux/spi/spi.h>
+#include <linux/vmalloc.h>
+#include "hda_local.h"
+#include "hda_auto_parser.h"
+#include "hda_jack.h"
+#include "../generic.h"
+#include "hda_component.h"
+#include "cs35l41_hda.h"
+#include "cs35l41_hda_property.h"
+
+#define CS35L41_PART "cs35l41"
+
+#define HALO_STATE_DSP_CTL_NAME "HALO_STATE"
+#define HALO_STATE_DSP_CTL_TYPE 5
+#define HALO_STATE_DSP_CTL_ALG 262308
+#define CAL_R_DSP_CTL_NAME "CAL_R"
+#define CAL_STATUS_DSP_CTL_NAME "CAL_STATUS"
+#define CAL_CHECKSUM_DSP_CTL_NAME "CAL_CHECKSUM"
+#define CAL_AMBIENT_DSP_CTL_NAME "CAL_AMBIENT"
+#define CAL_DSP_CTL_TYPE 5
+#define CAL_DSP_CTL_ALG 205
+#define CS35L41_UUID "50d90cdc-3de4-4f18-b528-c7fe3b71f40d"
+#define CS35L41_DSM_GET_MUTE 5
+#define CS35L41_NOTIFY_EVENT 0x91
+#define CS35L41_TUNING_SIG 0x109A4A35
+
+enum cs35l41_tuning_param_types {
+ TUNING_PARAM_GAIN,
+};
+
+struct cs35l41_tuning_param_hdr {
+ __le32 tuning_index;
+ __le32 type;
+ __le32 size;
+} __packed;
+
+struct cs35l41_tuning_param {
+ struct cs35l41_tuning_param_hdr hdr;
+ union {
+ __le32 gain;
+ };
+} __packed;
+
+struct cs35l41_tuning_params {
+ __le32 signature;
+ __le32 version;
+ __le32 size;
+ __le32 num_entries;
+ u8 data[];
+} __packed;
+
+/* Firmware calibration controls */
+static const struct cirrus_amp_cal_controls cs35l41_calibration_controls = {
+ .alg_id = CAL_DSP_CTL_ALG,
+ .mem_region = CAL_DSP_CTL_TYPE,
+ .ambient = CAL_AMBIENT_DSP_CTL_NAME,
+ .calr = CAL_R_DSP_CTL_NAME,
+ .status = CAL_STATUS_DSP_CTL_NAME,
+ .checksum = CAL_CHECKSUM_DSP_CTL_NAME,
+};
+
+enum cs35l41_hda_fw_id {
+ CS35L41_HDA_FW_SPK_PROT,
+ CS35L41_HDA_FW_SPK_CALI,
+ CS35L41_HDA_FW_SPK_DIAG,
+ CS35L41_HDA_FW_MISC,
+ CS35L41_HDA_NUM_FW
+};
+
+static const char * const cs35l41_hda_fw_ids[CS35L41_HDA_NUM_FW] = {
+ [CS35L41_HDA_FW_SPK_PROT] = "spk-prot",
+ [CS35L41_HDA_FW_SPK_CALI] = "spk-cali",
+ [CS35L41_HDA_FW_SPK_DIAG] = "spk-diag",
+ [CS35L41_HDA_FW_MISC] = "misc",
+};
+
+static bool firmware_autostart = 1;
+module_param(firmware_autostart, bool, 0444);
+MODULE_PARM_DESC(firmware_autostart, "Allow automatic firmware download on boot"
+ "(0=Disable, 1=Enable) (default=1); ");
+
+static const char channel_name[3] = { 'L', 'R', 'C' };
+
+static const struct reg_sequence cs35l41_hda_config[] = {
+ { CS35L41_PLL_CLK_CTRL, 0x00000430 }, // 3072000Hz, BCLK Input, PLL_REFCLK_EN = 1
+ { CS35L41_DSP_CLK_CTRL, 0x00000003 }, // DSP CLK EN
+ { CS35L41_GLOBAL_CLK_CTRL, 0x00000003 }, // GLOBAL_FS = 48 kHz
+ { CS35L41_SP_RATE_CTRL, 0x00000021 }, // ASP_BCLK_FREQ = 3.072 MHz
+ { CS35L41_SP_FORMAT, 0x20200200 }, // 32 bits RX/TX slots, I2S, clk consumer
+ { CS35L41_SP_TX_WL, 0x00000018 }, // 24 cycles/slot
+ { CS35L41_SP_RX_WL, 0x00000018 }, // 24 cycles/slot
+ { CS35L41_ASP_TX1_SRC, 0x00000018 }, // ASPTX1 SRC = VMON
+ { CS35L41_ASP_TX2_SRC, 0x00000019 }, // ASPTX2 SRC = IMON
+ { CS35L41_DSP1_RX3_SRC, 0x00000018 }, // DSP1RX3 SRC = VMON
+ { CS35L41_DSP1_RX4_SRC, 0x00000019 }, // DSP1RX4 SRC = IMON
+};
+
+static const struct reg_sequence cs35l41_hda_config_no_dsp[] = {
+ { CS35L41_SP_HIZ_CTRL, 0x00000002 }, // Hi-Z unused
+ { CS35L41_DAC_PCM1_SRC, 0x00000008 }, // DACPCM1_SRC = ASPRX1
+ { CS35L41_ASP_TX3_SRC, 0x00000000 }, // ASPTX3 SRC = ZERO FILL
+ { CS35L41_ASP_TX4_SRC, 0x00000000 }, // ASPTX4 SRC = ZERO FILL
+ { CS35L41_DSP1_RX5_SRC, 0x00000020 }, // DSP1RX5 SRC = ERRVOL
+ { CS35L41_DSP1_RX6_SRC, 0x00000021 }, // DSP1RX6 SRC = CLASSH_TGT
+};
+
+static const struct reg_sequence cs35l41_hda_config_dsp[] = {
+ { CS35L41_SP_HIZ_CTRL, 0x00000003 }, // Hi-Z unused/disabled
+ { CS35L41_DAC_PCM1_SRC, 0x00000032 }, // DACPCM1_SRC = DSP1TX1
+ { CS35L41_ASP_TX3_SRC, 0x00000028 }, // ASPTX3 SRC = VPMON
+ { CS35L41_ASP_TX4_SRC, 0x00000029 }, // ASPTX4 SRC = VBSTMON
+ { CS35L41_DSP1_RX6_SRC, 0x00000029 }, // DSP1RX6 SRC = VBSTMON
+};
+
+static const struct reg_sequence cs35l41_hda_unmute[] = {
+ { CS35L41_AMP_DIG_VOL_CTRL, 0x00008000 }, // AMP_HPF_PCM_EN = 1, AMP_VOL_PCM 0.0 dB
+ { CS35L41_AMP_GAIN_CTRL, 0x00000084 }, // AMP_GAIN_PCM 4.5 dB
+};
+
+static const struct reg_sequence cs35l41_hda_mute[] = {
+ { CS35L41_AMP_GAIN_CTRL, 0x00000000 }, // AMP_GAIN_PCM 0.5 dB
+ { CS35L41_AMP_DIG_VOL_CTRL, 0x0000A678 }, // AMP_HPF_PCM_EN = 1, AMP_VOL_PCM Mute
+};
+
+static const struct cs_dsp_client_ops client_ops = {
+ /* cs_dsp requires the client to provide this even if it is empty */
+};
+
+static int cs35l41_request_tuning_param_file(struct cs35l41_hda *cs35l41, char *tuning_filename,
+ const struct firmware **firmware, char **filename,
+ const char *ssid)
+{
+ int ret = 0;
+
+ /* Filename is the same as the tuning file with "cfg" suffix */
+ *filename = kasprintf(GFP_KERNEL, "%scfg", tuning_filename);
+ if (*filename == NULL)
+ return -ENOMEM;
+
+ ret = firmware_request_nowarn(firmware, *filename, cs35l41->dev);
+ if (ret != 0) {
+ dev_dbg(cs35l41->dev, "Failed to request '%s'\n", *filename);
+ kfree(*filename);
+ *filename = NULL;
+ }
+
+ return ret;
+}
+
+static int cs35l41_request_firmware_file(struct cs35l41_hda *cs35l41,
+ const struct firmware **firmware, char **filename,
+ const char *ssid, const char *amp_name,
+ int spkid, const char *filetype)
+{
+ const char * const dsp_name = cs35l41->cs_dsp.name;
+ char *s, c;
+ int ret = 0;
+
+ if (spkid > -1 && ssid && amp_name)
+ *filename = kasprintf(GFP_KERNEL, "cirrus/%s-%s-%s-%s-spkid%d-%s.%s", CS35L41_PART,
+ dsp_name, cs35l41_hda_fw_ids[cs35l41->firmware_type],
+ ssid, spkid, amp_name, filetype);
+ else if (spkid > -1 && ssid)
+ *filename = kasprintf(GFP_KERNEL, "cirrus/%s-%s-%s-%s-spkid%d.%s", CS35L41_PART,
+ dsp_name, cs35l41_hda_fw_ids[cs35l41->firmware_type],
+ ssid, spkid, filetype);
+ else if (ssid && amp_name)
+ *filename = kasprintf(GFP_KERNEL, "cirrus/%s-%s-%s-%s-%s.%s", CS35L41_PART,
+ dsp_name, cs35l41_hda_fw_ids[cs35l41->firmware_type],
+ ssid, amp_name, filetype);
+ else if (ssid)
+ *filename = kasprintf(GFP_KERNEL, "cirrus/%s-%s-%s-%s.%s", CS35L41_PART,
+ dsp_name, cs35l41_hda_fw_ids[cs35l41->firmware_type],
+ ssid, filetype);
+ else
+ *filename = kasprintf(GFP_KERNEL, "cirrus/%s-%s-%s.%s", CS35L41_PART,
+ dsp_name, cs35l41_hda_fw_ids[cs35l41->firmware_type],
+ filetype);
+
+ if (*filename == NULL)
+ return -ENOMEM;
+
+ /*
+ * Make sure that filename is lower-case and any non alpha-numeric
+ * characters except full stop and '/' are replaced with hyphens.
+ */
+ s = *filename;
+ while (*s) {
+ c = *s;
+ if (isalnum(c))
+ *s = tolower(c);
+ else if (c != '.' && c != '/')
+ *s = '-';
+ s++;
+ }
+
+ ret = firmware_request_nowarn(firmware, *filename, cs35l41->dev);
+ if (ret != 0) {
+ dev_dbg(cs35l41->dev, "Failed to request '%s'\n", *filename);
+ kfree(*filename);
+ *filename = NULL;
+ }
+
+ return ret;
+}
+
+static int cs35l41_request_firmware_files_spkid(struct cs35l41_hda *cs35l41,
+ const struct firmware **wmfw_firmware,
+ char **wmfw_filename,
+ const struct firmware **coeff_firmware,
+ char **coeff_filename)
+{
+ int ret;
+
+ /* try cirrus/part-dspN-fwtype-sub<-spkidN><-ampname>.wmfw */
+ ret = cs35l41_request_firmware_file(cs35l41, wmfw_firmware, wmfw_filename,
+ cs35l41->acpi_subsystem_id, cs35l41->amp_name,
+ cs35l41->speaker_id, "wmfw");
+ if (!ret) {
+ /* try cirrus/part-dspN-fwtype-sub<-spkidN><-ampname>.bin */
+ ret = cs35l41_request_firmware_file(cs35l41, coeff_firmware, coeff_filename,
+ cs35l41->acpi_subsystem_id, cs35l41->amp_name,
+ cs35l41->speaker_id, "bin");
+ if (ret)
+ goto coeff_err;
+
+ return 0;
+ }
+
+ /* try cirrus/part-dspN-fwtype-sub<-ampname>.wmfw */
+ ret = cs35l41_request_firmware_file(cs35l41, wmfw_firmware, wmfw_filename,
+ cs35l41->acpi_subsystem_id,
+ cs35l41->amp_name, -1, "wmfw");
+ if (!ret) {
+ /* try cirrus/part-dspN-fwtype-sub<-spkidN><-ampname>.bin */
+ ret = cs35l41_request_firmware_file(cs35l41, coeff_firmware, coeff_filename,
+ cs35l41->acpi_subsystem_id, cs35l41->amp_name,
+ cs35l41->speaker_id, "bin");
+ if (ret)
+ goto coeff_err;
+
+ return 0;
+ }
+
+ /* try cirrus/part-dspN-fwtype-sub<-spkidN>.wmfw */
+ ret = cs35l41_request_firmware_file(cs35l41, wmfw_firmware, wmfw_filename,
+ cs35l41->acpi_subsystem_id,
+ NULL, cs35l41->speaker_id, "wmfw");
+ if (!ret) {
+ /* try cirrus/part-dspN-fwtype-sub<-spkidN><-ampname>.bin */
+ ret = cs35l41_request_firmware_file(cs35l41, coeff_firmware, coeff_filename,
+ cs35l41->acpi_subsystem_id,
+ cs35l41->amp_name, cs35l41->speaker_id, "bin");
+ if (ret)
+ /* try cirrus/part-dspN-fwtype-sub<-spkidN>.bin */
+ ret = cs35l41_request_firmware_file(cs35l41, coeff_firmware,
+ coeff_filename,
+ cs35l41->acpi_subsystem_id, NULL,
+ cs35l41->speaker_id, "bin");
+ if (ret)
+ goto coeff_err;
+
+ return 0;
+ }
+
+ /* try cirrus/part-dspN-fwtype-sub.wmfw */
+ ret = cs35l41_request_firmware_file(cs35l41, wmfw_firmware, wmfw_filename,
+ cs35l41->acpi_subsystem_id,
+ NULL, -1, "wmfw");
+ if (!ret) {
+ /* try cirrus/part-dspN-fwtype-sub<-spkidN><-ampname>.bin */
+ ret = cs35l41_request_firmware_file(cs35l41, coeff_firmware, coeff_filename,
+ cs35l41->acpi_subsystem_id, cs35l41->amp_name,
+ cs35l41->speaker_id, "bin");
+ if (ret)
+ /* try cirrus/part-dspN-fwtype-sub<-spkidN>.bin */
+ ret = cs35l41_request_firmware_file(cs35l41, coeff_firmware,
+ coeff_filename,
+ cs35l41->acpi_subsystem_id, NULL,
+ cs35l41->speaker_id, "bin");
+ if (ret)
+ goto coeff_err;
+ }
+
+ return ret;
+coeff_err:
+ release_firmware(*wmfw_firmware);
+ kfree(*wmfw_filename);
+ return ret;
+}
+
+static int cs35l41_fallback_firmware_file(struct cs35l41_hda *cs35l41,
+ const struct firmware **wmfw_firmware,
+ char **wmfw_filename,
+ const struct firmware **coeff_firmware,
+ char **coeff_filename)
+{
+ int ret;
+
+ /* Handle fallback */
+ dev_warn(cs35l41->dev, "Falling back to default firmware.\n");
+
+ /* fallback try cirrus/part-dspN-fwtype.wmfw */
+ ret = cs35l41_request_firmware_file(cs35l41, wmfw_firmware, wmfw_filename,
+ NULL, NULL, -1, "wmfw");
+ if (ret)
+ goto err;
+
+ /* fallback try cirrus/part-dspN-fwtype.bin */
+ ret = cs35l41_request_firmware_file(cs35l41, coeff_firmware, coeff_filename,
+ NULL, NULL, -1, "bin");
+ if (ret) {
+ release_firmware(*wmfw_firmware);
+ kfree(*wmfw_filename);
+ goto err;
+ }
+ return 0;
+
+err:
+ dev_warn(cs35l41->dev, "Unable to find firmware and tuning\n");
+ return ret;
+}
+
+static int cs35l41_request_firmware_files(struct cs35l41_hda *cs35l41,
+ const struct firmware **wmfw_firmware,
+ char **wmfw_filename,
+ const struct firmware **coeff_firmware,
+ char **coeff_filename)
+{
+ int ret;
+
+ if (cs35l41->speaker_id > -1) {
+ ret = cs35l41_request_firmware_files_spkid(cs35l41, wmfw_firmware, wmfw_filename,
+ coeff_firmware, coeff_filename);
+ goto out;
+ }
+
+ /* try cirrus/part-dspN-fwtype-sub<-ampname>.wmfw */
+ ret = cs35l41_request_firmware_file(cs35l41, wmfw_firmware, wmfw_filename,
+ cs35l41->acpi_subsystem_id,
+ cs35l41->amp_name, -1, "wmfw");
+ if (!ret) {
+ /* try cirrus/part-dspN-fwtype-sub<-ampname>.bin */
+ ret = cs35l41_request_firmware_file(cs35l41, coeff_firmware, coeff_filename,
+ cs35l41->acpi_subsystem_id, cs35l41->amp_name,
+ -1, "bin");
+ if (ret)
+ goto coeff_err;
+
+ goto out;
+ }
+
+ /* try cirrus/part-dspN-fwtype-sub.wmfw */
+ ret = cs35l41_request_firmware_file(cs35l41, wmfw_firmware, wmfw_filename,
+ cs35l41->acpi_subsystem_id,
+ NULL, -1, "wmfw");
+ if (!ret) {
+ /* try cirrus/part-dspN-fwtype-sub<-ampname>.bin */
+ ret = cs35l41_request_firmware_file(cs35l41, coeff_firmware, coeff_filename,
+ cs35l41->acpi_subsystem_id,
+ cs35l41->amp_name, -1, "bin");
+ if (ret)
+ /* try cirrus/part-dspN-fwtype-sub.bin */
+ ret = cs35l41_request_firmware_file(cs35l41, coeff_firmware, coeff_filename,
+ cs35l41->acpi_subsystem_id, NULL, -1,
+ "bin");
+ if (ret)
+ goto coeff_err;
+ }
+
+out:
+ if (ret)
+ /* if all attempts at finding firmware fail, try fallback */
+ goto fallback;
+
+ return 0;
+
+coeff_err:
+ release_firmware(*wmfw_firmware);
+ kfree(*wmfw_filename);
+fallback:
+ return cs35l41_fallback_firmware_file(cs35l41, wmfw_firmware, wmfw_filename,
+ coeff_firmware, coeff_filename);
+}
+
+
+static void cs35l41_hda_apply_calibration(struct cs35l41_hda *cs35l41)
+{
+ int ret;
+
+ if (!cs35l41->cal_data_valid)
+ return;
+
+ ret = cs_amp_write_cal_coeffs(&cs35l41->cs_dsp, &cs35l41_calibration_controls,
+ &cs35l41->cal_data);
+ if (ret < 0)
+ dev_warn(cs35l41->dev, "Failed to apply calibration: %d\n", ret);
+ else
+ dev_info(cs35l41->dev, "Calibration applied: R0=%d\n", cs35l41->cal_data.calR);
+}
+
+static int cs35l41_read_silicon_uid(struct cs35l41_hda *cs35l41, u64 *uid)
+{
+ u32 tmp;
+ int ret;
+
+ ret = regmap_read(cs35l41->regmap, CS35L41_DIE_STS2, &tmp);
+ if (ret) {
+ dev_err(cs35l41->dev, "Cannot obtain CS35L41_DIE_STS2: %d\n", ret);
+ return ret;
+ }
+
+ *uid = tmp;
+ *uid <<= 32;
+
+ ret = regmap_read(cs35l41->regmap, CS35L41_DIE_STS1, &tmp);
+ if (ret) {
+ dev_err(cs35l41->dev, "Cannot obtain CS35L41_DIE_STS1: %d\n", ret);
+ return ret;
+ }
+
+ *uid |= tmp;
+
+ dev_dbg(cs35l41->dev, "UniqueID = %#llx\n", *uid);
+
+ return 0;
+}
+
+static int cs35l41_get_calibration(struct cs35l41_hda *cs35l41)
+{
+ u64 silicon_uid;
+ int ret;
+
+ ret = cs35l41_read_silicon_uid(cs35l41, &silicon_uid);
+ if (ret < 0)
+ return ret;
+
+ ret = cs_amp_get_efi_calibration_data(cs35l41->dev, silicon_uid,
+ cs35l41->index,
+ &cs35l41->cal_data);
+
+ /* Only return an error status if probe should be aborted */
+ if ((ret == -ENOENT) || (ret == -EOVERFLOW))
+ return 0;
+
+ if (ret < 0)
+ return ret;
+
+ cs35l41->cal_data_valid = true;
+
+ return 0;
+}
+
+
+static void cs35l41_set_default_tuning_params(struct cs35l41_hda *cs35l41)
+{
+ cs35l41->tuning_gain = DEFAULT_AMP_GAIN_PCM;
+}
+
+static int cs35l41_read_tuning_params(struct cs35l41_hda *cs35l41, const struct firmware *firmware)
+{
+ struct cs35l41_tuning_params *params;
+ unsigned int offset = 0;
+ unsigned int end;
+ int i;
+
+ params = (void *)&firmware->data[0];
+
+ if (le32_to_cpu(params->size) != firmware->size) {
+ dev_err(cs35l41->dev, "Wrong Size for Tuning Param file. Expected %d got %zu\n",
+ le32_to_cpu(params->size), firmware->size);
+ return -EINVAL;
+ }
+
+ if (le32_to_cpu(params->version) != 1) {
+ dev_err(cs35l41->dev, "Unsupported Tuning Param Version: %d\n",
+ le32_to_cpu(params->version));
+ return -EINVAL;
+ }
+
+ if (le32_to_cpu(params->signature) != CS35L41_TUNING_SIG) {
+ dev_err(cs35l41->dev,
+ "Mismatched Signature for Tuning Param file. Expected %#x got %#x\n",
+ CS35L41_TUNING_SIG, le32_to_cpu(params->signature));
+ return -EINVAL;
+ }
+
+ end = firmware->size - sizeof(struct cs35l41_tuning_params);
+
+ for (i = 0; i < le32_to_cpu(params->num_entries); i++) {
+ struct cs35l41_tuning_param *param;
+
+ if ((offset >= end) || ((offset + sizeof(struct cs35l41_tuning_param_hdr)) >= end))
+ return -EFAULT;
+
+ param = (void *)&params->data[offset];
+ offset += le32_to_cpu(param->hdr.size);
+
+ if (offset > end)
+ return -EFAULT;
+
+ switch (le32_to_cpu(param->hdr.type)) {
+ case TUNING_PARAM_GAIN:
+ cs35l41->tuning_gain = le32_to_cpu(param->gain);
+ dev_dbg(cs35l41->dev, "Applying Gain: %d\n", cs35l41->tuning_gain);
+ break;
+ default:
+ break;
+ }
+ }
+
+ return 0;
+}
+
+static int cs35l41_load_tuning_params(struct cs35l41_hda *cs35l41, char *tuning_filename)
+{
+ const struct firmware *tuning_param_file = NULL;
+ char *tuning_param_filename = NULL;
+ int ret;
+
+ ret = cs35l41_request_tuning_param_file(cs35l41, tuning_filename, &tuning_param_file,
+ &tuning_param_filename, cs35l41->acpi_subsystem_id);
+ if (ret) {
+ dev_dbg(cs35l41->dev, "Missing Tuning Param for file: %s: %d\n", tuning_filename,
+ ret);
+ return 0;
+ }
+
+ ret = cs35l41_read_tuning_params(cs35l41, tuning_param_file);
+ if (ret) {
+ dev_err(cs35l41->dev, "Error reading Tuning Params from file: %s: %d\n",
+ tuning_param_filename, ret);
+ /* Reset to default Tuning Parameters */
+ cs35l41_set_default_tuning_params(cs35l41);
+ }
+
+ release_firmware(tuning_param_file);
+ kfree(tuning_param_filename);
+
+ return ret;
+}
+
+static int cs35l41_init_dsp(struct cs35l41_hda *cs35l41)
+{
+ const struct firmware *coeff_firmware = NULL;
+ const struct firmware *wmfw_firmware = NULL;
+ struct cs_dsp *dsp = &cs35l41->cs_dsp;
+ char *coeff_filename = NULL;
+ char *wmfw_filename = NULL;
+ int ret;
+
+ if (!cs35l41->halo_initialized) {
+ cs35l41_configure_cs_dsp(cs35l41->dev, cs35l41->regmap, dsp);
+ dsp->client_ops = &client_ops;
+
+ ret = cs_dsp_halo_init(&cs35l41->cs_dsp);
+ if (ret)
+ return ret;
+ cs35l41->halo_initialized = true;
+ }
+
+ cs35l41_set_default_tuning_params(cs35l41);
+
+ ret = cs35l41_request_firmware_files(cs35l41, &wmfw_firmware, &wmfw_filename,
+ &coeff_firmware, &coeff_filename);
+ if (ret < 0)
+ return ret;
+
+ dev_dbg(cs35l41->dev, "Loading WMFW Firmware: %s\n", wmfw_filename);
+ if (coeff_filename) {
+ dev_dbg(cs35l41->dev, "Loading Coefficient File: %s\n", coeff_filename);
+ ret = cs35l41_load_tuning_params(cs35l41, coeff_filename);
+ if (ret)
+ dev_warn(cs35l41->dev, "Unable to load Tuning Parameters: %d\n", ret);
+ } else {
+ dev_warn(cs35l41->dev, "No Coefficient File available.\n");
+ }
+
+ ret = cs_dsp_power_up(dsp, wmfw_firmware, wmfw_filename, coeff_firmware, coeff_filename,
+ cs35l41_hda_fw_ids[cs35l41->firmware_type]);
+ if (ret)
+ goto err;
+
+ cs35l41_hda_apply_calibration(cs35l41);
+
+err:
+ if (ret)
+ cs35l41_set_default_tuning_params(cs35l41);
+ release_firmware(wmfw_firmware);
+ release_firmware(coeff_firmware);
+ kfree(wmfw_filename);
+ kfree(coeff_filename);
+
+ return ret;
+}
+
+static void cs35l41_shutdown_dsp(struct cs35l41_hda *cs35l41)
+{
+ struct cs_dsp *dsp = &cs35l41->cs_dsp;
+
+ cs35l41_set_default_tuning_params(cs35l41);
+ cs_dsp_stop(dsp);
+ cs_dsp_power_down(dsp);
+ dev_dbg(cs35l41->dev, "Unloaded Firmware\n");
+}
+
+static void cs35l41_remove_dsp(struct cs35l41_hda *cs35l41)
+{
+ struct cs_dsp *dsp = &cs35l41->cs_dsp;
+
+ cancel_work_sync(&cs35l41->fw_load_work);
+
+ mutex_lock(&cs35l41->fw_mutex);
+ cs35l41_shutdown_dsp(cs35l41);
+ cs_dsp_remove(dsp);
+ cs35l41->halo_initialized = false;
+ mutex_unlock(&cs35l41->fw_mutex);
+}
+
+/* Protection release cycle to get the speaker out of Safe-Mode */
+static void cs35l41_error_release(struct device *dev, struct regmap *regmap, unsigned int mask)
+{
+ regmap_write(regmap, CS35L41_PROTECT_REL_ERR_IGN, 0);
+ regmap_set_bits(regmap, CS35L41_PROTECT_REL_ERR_IGN, mask);
+ regmap_clear_bits(regmap, CS35L41_PROTECT_REL_ERR_IGN, mask);
+}
+
+/* Clear all errors to release safe mode. Global Enable must be cleared first. */
+static void cs35l41_irq_release(struct cs35l41_hda *cs35l41)
+{
+ cs35l41_error_release(cs35l41->dev, cs35l41->regmap, cs35l41->irq_errors);
+ cs35l41->irq_errors = 0;
+}
+
+static void cs35l41_update_mixer(struct cs35l41_hda *cs35l41)
+{
+ struct regmap *reg = cs35l41->regmap;
+ unsigned int asp_en = 0;
+ unsigned int dsp1rx2_src = 0;
+
+ regmap_multi_reg_write(reg, cs35l41_hda_config, ARRAY_SIZE(cs35l41_hda_config));
+
+ if (cs35l41->cs_dsp.running) {
+ asp_en |= CS35L41_ASP_TX1_EN_MASK; // ASP_TX1_EN = 1
+ regmap_multi_reg_write(reg, cs35l41_hda_config_dsp,
+ ARRAY_SIZE(cs35l41_hda_config_dsp));
+ if (cs35l41->hw_cfg.bst_type == CS35L41_INT_BOOST)
+ regmap_write(reg, CS35L41_DSP1_RX5_SRC, CS35L41_INPUT_SRC_VPMON);
+ else
+ regmap_write(reg, CS35L41_DSP1_RX5_SRC, CS35L41_INPUT_SRC_VBSTMON);
+ } else {
+ regmap_multi_reg_write(reg, cs35l41_hda_config_no_dsp,
+ ARRAY_SIZE(cs35l41_hda_config_no_dsp));
+ }
+
+ if (cs35l41->hw_cfg.spk_pos == CS35L41_CENTER) {
+ asp_en |= CS35L41_ASP_RX2_EN_MASK; // ASP_RX2_EN = 1
+ dsp1rx2_src = 0x00000009; // DSP1RX2 SRC = ASPRX2
+ } else {
+ dsp1rx2_src = 0x00000008; // DSP1RX2 SRC = ASPRX1
+ }
+
+ asp_en |= CS35L41_ASP_RX1_EN_MASK; // ASP_RX1_EN = 1
+
+ regmap_write(reg, CS35L41_SP_ENABLES, asp_en);
+ regmap_write(reg, CS35L41_DSP1_RX1_SRC, 0x00000008); // DSP1RX1 SRC = ASPRX1
+ regmap_write(reg, CS35L41_DSP1_RX2_SRC, dsp1rx2_src);
+}
+
+static void cs35l41_hda_play_start(struct device *dev)
+{
+ struct cs35l41_hda *cs35l41 = dev_get_drvdata(dev);
+ struct regmap *reg = cs35l41->regmap;
+
+ dev_dbg(dev, "Play (Start)\n");
+
+ if (cs35l41->playback_started) {
+ dev_dbg(dev, "Playback already started.");
+ return;
+ }
+
+ cs35l41->playback_started = true;
+
+ cs35l41_update_mixer(cs35l41);
+
+ if (cs35l41->cs_dsp.running) {
+ regmap_update_bits(reg, CS35L41_PWR_CTRL2,
+ CS35L41_VMON_EN_MASK | CS35L41_IMON_EN_MASK,
+ 1 << CS35L41_VMON_EN_SHIFT | 1 << CS35L41_IMON_EN_SHIFT);
+ cs35l41_set_cspl_mbox_cmd(cs35l41->dev, reg, CSPL_MBOX_CMD_RESUME);
+ }
+ regmap_update_bits(reg, CS35L41_PWR_CTRL2, CS35L41_AMP_EN_MASK, 1 << CS35L41_AMP_EN_SHIFT);
+ if (cs35l41->hw_cfg.bst_type == CS35L41_EXT_BOOST)
+ regmap_write(reg, CS35L41_GPIO1_CTRL1, 0x00008001);
+
+}
+
+static void cs35l41_mute(struct device *dev, bool mute)
+{
+ struct cs35l41_hda *cs35l41 = dev_get_drvdata(dev);
+ struct regmap *reg = cs35l41->regmap;
+ unsigned int amp_gain;
+
+ dev_dbg(dev, "Mute(%d:%d) Playback Started: %d\n", mute, cs35l41->mute_override,
+ cs35l41->playback_started);
+
+ if (cs35l41->playback_started) {
+ if (mute || cs35l41->mute_override) {
+ dev_dbg(dev, "Muting\n");
+ regmap_multi_reg_write(reg, cs35l41_hda_mute, ARRAY_SIZE(cs35l41_hda_mute));
+ } else {
+ dev_dbg(dev, "Unmuting\n");
+ if (cs35l41->cs_dsp.running) {
+ dev_dbg(dev, "Using Tuned Gain: %d\n", cs35l41->tuning_gain);
+ amp_gain = (cs35l41->tuning_gain << CS35L41_AMP_GAIN_PCM_SHIFT) |
+ (DEFAULT_AMP_GAIN_PDM << CS35L41_AMP_GAIN_PDM_SHIFT);
+
+ /* AMP_HPF_PCM_EN = 1, AMP_VOL_PCM 0.0 dB */
+ regmap_write(reg, CS35L41_AMP_DIG_VOL_CTRL, 0x00008000);
+ regmap_write(reg, CS35L41_AMP_GAIN_CTRL, amp_gain);
+ } else {
+ regmap_multi_reg_write(reg, cs35l41_hda_unmute,
+ ARRAY_SIZE(cs35l41_hda_unmute));
+ }
+ }
+ }
+}
+
+static void cs35l41_hda_play_done(struct device *dev)
+{
+ struct cs35l41_hda *cs35l41 = dev_get_drvdata(dev);
+ struct regmap *reg = cs35l41->regmap;
+
+ dev_dbg(dev, "Play (Complete)\n");
+
+ cs35l41_global_enable(dev, reg, cs35l41->hw_cfg.bst_type, 1,
+ &cs35l41->cs_dsp);
+ cs35l41_mute(dev, false);
+}
+
+static void cs35l41_hda_pause_start(struct device *dev)
+{
+ struct cs35l41_hda *cs35l41 = dev_get_drvdata(dev);
+ struct regmap *reg = cs35l41->regmap;
+
+ dev_dbg(dev, "Pause (Start)\n");
+
+ cs35l41_mute(dev, true);
+ cs35l41_global_enable(dev, reg, cs35l41->hw_cfg.bst_type, 0,
+ &cs35l41->cs_dsp);
+}
+
+static void cs35l41_hda_pause_done(struct device *dev)
+{
+ struct cs35l41_hda *cs35l41 = dev_get_drvdata(dev);
+ struct regmap *reg = cs35l41->regmap;
+
+ dev_dbg(dev, "Pause (Complete)\n");
+
+ regmap_update_bits(reg, CS35L41_PWR_CTRL2, CS35L41_AMP_EN_MASK, 0 << CS35L41_AMP_EN_SHIFT);
+ if (cs35l41->hw_cfg.bst_type == CS35L41_EXT_BOOST)
+ regmap_write(reg, CS35L41_GPIO1_CTRL1, 0x00000001);
+ if (cs35l41->cs_dsp.running) {
+ cs35l41_set_cspl_mbox_cmd(dev, reg, CSPL_MBOX_CMD_PAUSE);
+ regmap_update_bits(reg, CS35L41_PWR_CTRL2,
+ CS35L41_VMON_EN_MASK | CS35L41_IMON_EN_MASK,
+ 0 << CS35L41_VMON_EN_SHIFT | 0 << CS35L41_IMON_EN_SHIFT);
+ }
+ cs35l41_irq_release(cs35l41);
+ cs35l41->playback_started = false;
+}
+
+static void cs35l41_hda_pre_playback_hook(struct device *dev, int action)
+{
+ struct cs35l41_hda *cs35l41 = dev_get_drvdata(dev);
+
+ switch (action) {
+ case HDA_GEN_PCM_ACT_CLEANUP:
+ mutex_lock(&cs35l41->fw_mutex);
+ cs35l41_hda_pause_start(dev);
+ mutex_unlock(&cs35l41->fw_mutex);
+ break;
+ default:
+ break;
+ }
+}
+static void cs35l41_hda_playback_hook(struct device *dev, int action)
+{
+ struct cs35l41_hda *cs35l41 = dev_get_drvdata(dev);
+
+ switch (action) {
+ case HDA_GEN_PCM_ACT_OPEN:
+ /*
+ * All amps must be resumed before we can start playing back.
+ * This ensures, for external boost, that all amps are in AMP_SAFE mode.
+ * Do this in HDA_GEN_PCM_ACT_OPEN, since this is run prior to any of the
+ * other actions.
+ */
+ pm_runtime_get_sync(dev);
+ break;
+ case HDA_GEN_PCM_ACT_PREPARE:
+ mutex_lock(&cs35l41->fw_mutex);
+ cs35l41_hda_play_start(dev);
+ mutex_unlock(&cs35l41->fw_mutex);
+ break;
+ case HDA_GEN_PCM_ACT_CLEANUP:
+ mutex_lock(&cs35l41->fw_mutex);
+ cs35l41_hda_pause_done(dev);
+ mutex_unlock(&cs35l41->fw_mutex);
+ break;
+ case HDA_GEN_PCM_ACT_CLOSE:
+ mutex_lock(&cs35l41->fw_mutex);
+ if (!cs35l41->cs_dsp.running && cs35l41->request_fw_load &&
+ !cs35l41->fw_request_ongoing) {
+ dev_info(dev, "Requesting Firmware Load after HDA_GEN_PCM_ACT_CLOSE\n");
+ cs35l41->fw_request_ongoing = true;
+ schedule_work(&cs35l41->fw_load_work);
+ }
+ mutex_unlock(&cs35l41->fw_mutex);
+
+ /*
+ * Playback must be finished for all amps before we start runtime suspend.
+ * This ensures no amps are playing back when we start putting them to sleep.
+ */
+ pm_runtime_put_autosuspend(dev);
+ break;
+ default:
+ break;
+ }
+}
+
+static void cs35l41_hda_post_playback_hook(struct device *dev, int action)
+{
+ struct cs35l41_hda *cs35l41 = dev_get_drvdata(dev);
+
+ switch (action) {
+ case HDA_GEN_PCM_ACT_PREPARE:
+ mutex_lock(&cs35l41->fw_mutex);
+ cs35l41_hda_play_done(dev);
+ mutex_unlock(&cs35l41->fw_mutex);
+ break;
+ default:
+ break;
+ }
+}
+
+static int cs35l41_hda_channel_map(struct cs35l41_hda *cs35l41)
+{
+ unsigned int tx_num = 0;
+ unsigned int *tx_slot = NULL;
+ unsigned int rx_num;
+ unsigned int *rx_slot;
+ unsigned int mono = 0;
+
+ if (!cs35l41->amp_name) {
+ if (cs35l41->hw_cfg.spk_pos >= ARRAY_SIZE(channel_name))
+ return -EINVAL;
+
+ cs35l41->amp_name = devm_kasprintf(cs35l41->dev, GFP_KERNEL, "%c%d",
+ channel_name[cs35l41->hw_cfg.spk_pos],
+ cs35l41->channel_index);
+ if (!cs35l41->amp_name)
+ return -ENOMEM;
+ }
+
+ rx_num = 1;
+ if (cs35l41->hw_cfg.spk_pos == CS35L41_CENTER)
+ rx_slot = &mono;
+ else
+ rx_slot = &cs35l41->hw_cfg.spk_pos;
+
+ return cs35l41_set_channels(cs35l41->dev, cs35l41->regmap, tx_num, tx_slot, rx_num,
+ rx_slot);
+}
+
+static int cs35l41_verify_id(struct cs35l41_hda *cs35l41, unsigned int *regid, unsigned int *reg_revid)
+{
+ unsigned int mtl_revid, chipid;
+ int ret;
+
+ ret = regmap_read(cs35l41->regmap, CS35L41_DEVID, regid);
+ if (ret) {
+ dev_err_probe(cs35l41->dev, ret, "Get Device ID failed\n");
+ return ret;
+ }
+
+ ret = regmap_read(cs35l41->regmap, CS35L41_REVID, reg_revid);
+ if (ret) {
+ dev_err_probe(cs35l41->dev, ret, "Get Revision ID failed\n");
+ return ret;
+ }
+
+ mtl_revid = *reg_revid & CS35L41_MTLREVID_MASK;
+
+ chipid = (mtl_revid % 2) ? CS35L41R_CHIP_ID : CS35L41_CHIP_ID;
+ if (*regid != chipid) {
+ dev_err(cs35l41->dev, "CS35L41 Device ID (%X). Expected ID %X\n", *regid, chipid);
+ return -ENODEV;
+ }
+
+ return 0;
+}
+
+static int cs35l41_ready_for_reset(struct cs35l41_hda *cs35l41)
+{
+ mutex_lock(&cs35l41->fw_mutex);
+ if (cs35l41->cs_dsp.running) {
+ cs35l41->cs_dsp.running = false;
+ cs35l41->cs_dsp.booted = false;
+ }
+ regcache_mark_dirty(cs35l41->regmap);
+ mutex_unlock(&cs35l41->fw_mutex);
+
+ return 0;
+}
+
+static int cs35l41_system_suspend_prep(struct device *dev)
+{
+ struct cs35l41_hda *cs35l41 = dev_get_drvdata(dev);
+
+ dev_dbg(cs35l41->dev, "System Suspend Prepare\n");
+
+ if (cs35l41->hw_cfg.bst_type == CS35L41_EXT_BOOST_NO_VSPK_SWITCH) {
+ dev_err_once(cs35l41->dev, "System Suspend not supported\n");
+ return 0; /* don't block the whole system suspend */
+ }
+
+ mutex_lock(&cs35l41->fw_mutex);
+ if (cs35l41->playback_started)
+ cs35l41_hda_pause_start(dev);
+ mutex_unlock(&cs35l41->fw_mutex);
+
+ return 0;
+}
+
+static int cs35l41_system_suspend(struct device *dev)
+{
+ struct cs35l41_hda *cs35l41 = dev_get_drvdata(dev);
+ int ret;
+
+ dev_dbg(cs35l41->dev, "System Suspend\n");
+
+ if (cs35l41->hw_cfg.bst_type == CS35L41_EXT_BOOST_NO_VSPK_SWITCH) {
+ dev_err_once(cs35l41->dev, "System Suspend not supported\n");
+ return 0; /* don't block the whole system suspend */
+ }
+
+ mutex_lock(&cs35l41->fw_mutex);
+ if (cs35l41->playback_started)
+ cs35l41_hda_pause_done(dev);
+ mutex_unlock(&cs35l41->fw_mutex);
+
+ ret = pm_runtime_force_suspend(dev);
+ if (ret) {
+ dev_err(dev, "System Suspend Failed, unable to runtime suspend: %d\n", ret);
+ return ret;
+ }
+
+ /* Shutdown DSP before system suspend */
+ ret = cs35l41_ready_for_reset(cs35l41);
+ if (ret)
+ dev_err(dev, "System Suspend Failed, not ready for Reset: %d\n", ret);
+
+ if (cs35l41->reset_gpio) {
+ dev_info(cs35l41->dev, "Asserting Reset\n");
+ gpiod_set_value_cansleep(cs35l41->reset_gpio, 0);
+ usleep_range(2000, 2100);
+ }
+
+ dev_dbg(cs35l41->dev, "System Suspended\n");
+
+ return ret;
+}
+
+static int cs35l41_wait_boot_done(struct cs35l41_hda *cs35l41)
+{
+ unsigned int int_status;
+ int ret;
+
+ ret = regmap_read_poll_timeout(cs35l41->regmap, CS35L41_IRQ1_STATUS4, int_status,
+ int_status & CS35L41_OTP_BOOT_DONE, 1000, 100000);
+ if (ret) {
+ dev_err(cs35l41->dev, "Failed waiting for OTP_BOOT_DONE\n");
+ return ret;
+ }
+
+ ret = regmap_read(cs35l41->regmap, CS35L41_IRQ1_STATUS3, &int_status);
+ if (ret || (int_status & CS35L41_OTP_BOOT_ERR)) {
+ dev_err(cs35l41->dev, "OTP Boot status %x error\n",
+ int_status & CS35L41_OTP_BOOT_ERR);
+ if (!ret)
+ ret = -EIO;
+ return ret;
+ }
+
+ return 0;
+}
+
+static int cs35l41_system_resume(struct device *dev)
+{
+ struct cs35l41_hda *cs35l41 = dev_get_drvdata(dev);
+ int ret;
+
+ dev_dbg(cs35l41->dev, "System Resume\n");
+
+ if (cs35l41->hw_cfg.bst_type == CS35L41_EXT_BOOST_NO_VSPK_SWITCH) {
+ dev_err_once(cs35l41->dev, "System Resume not supported\n");
+ return 0; /* don't block the whole system resume */
+ }
+
+ if (cs35l41->reset_gpio) {
+ gpiod_set_value_cansleep(cs35l41->reset_gpio, 0);
+ usleep_range(2000, 2100);
+ gpiod_set_value_cansleep(cs35l41->reset_gpio, 1);
+ }
+
+ usleep_range(2000, 2100);
+
+ regcache_cache_only(cs35l41->regmap, false);
+
+ regmap_write(cs35l41->regmap, CS35L41_SFT_RESET, CS35L41_SOFTWARE_RESET);
+ usleep_range(2000, 2100);
+
+ ret = cs35l41_wait_boot_done(cs35l41);
+ if (ret)
+ return ret;
+
+ regcache_cache_only(cs35l41->regmap, true);
+
+ ret = pm_runtime_force_resume(dev);
+ if (ret) {
+ dev_err(dev, "System Resume Failed: Unable to runtime resume: %d\n", ret);
+ return ret;
+ }
+
+ mutex_lock(&cs35l41->fw_mutex);
+
+ if (cs35l41->request_fw_load && !cs35l41->fw_request_ongoing) {
+ cs35l41->fw_request_ongoing = true;
+ schedule_work(&cs35l41->fw_load_work);
+ }
+ mutex_unlock(&cs35l41->fw_mutex);
+
+ return ret;
+}
+
+static int cs35l41_runtime_idle(struct device *dev)
+{
+ struct cs35l41_hda *cs35l41 = dev_get_drvdata(dev);
+
+ if (cs35l41->hw_cfg.bst_type == CS35L41_EXT_BOOST_NO_VSPK_SWITCH)
+ return -EBUSY; /* suspend not supported yet on this model */
+ return 0;
+}
+
+static int cs35l41_runtime_suspend(struct device *dev)
+{
+ struct cs35l41_hda *cs35l41 = dev_get_drvdata(dev);
+ int ret = 0;
+
+ dev_dbg(cs35l41->dev, "Runtime Suspend\n");
+
+ if (cs35l41->hw_cfg.bst_type == CS35L41_EXT_BOOST_NO_VSPK_SWITCH) {
+ dev_dbg(cs35l41->dev, "Runtime Suspend not supported\n");
+ return 0;
+ }
+
+ mutex_lock(&cs35l41->fw_mutex);
+
+ if (cs35l41->cs_dsp.running) {
+ ret = cs35l41_enter_hibernate(cs35l41->dev, cs35l41->regmap,
+ cs35l41->hw_cfg.bst_type);
+ if (ret)
+ goto err;
+ } else {
+ cs35l41_safe_reset(cs35l41->regmap, cs35l41->hw_cfg.bst_type);
+ }
+
+ regcache_cache_only(cs35l41->regmap, true);
+ regcache_mark_dirty(cs35l41->regmap);
+
+err:
+ mutex_unlock(&cs35l41->fw_mutex);
+
+ return ret;
+}
+
+static int cs35l41_runtime_resume(struct device *dev)
+{
+ struct cs35l41_hda *cs35l41 = dev_get_drvdata(dev);
+ unsigned int regid, reg_revid;
+ int ret = 0;
+
+ dev_dbg(cs35l41->dev, "Runtime Resume\n");
+
+ if (cs35l41->hw_cfg.bst_type == CS35L41_EXT_BOOST_NO_VSPK_SWITCH) {
+ dev_dbg(cs35l41->dev, "Runtime Resume not supported\n");
+ return 0;
+ }
+
+ mutex_lock(&cs35l41->fw_mutex);
+
+ regcache_cache_only(cs35l41->regmap, false);
+
+ if (cs35l41->cs_dsp.running) {
+ ret = cs35l41_exit_hibernate(cs35l41->dev, cs35l41->regmap);
+ if (ret) {
+ dev_warn(cs35l41->dev, "Unable to exit Hibernate.");
+ goto err;
+ }
+ }
+
+ ret = cs35l41_verify_id(cs35l41, &regid, &reg_revid);
+ if (ret)
+ goto err;
+
+ /* Test key needs to be unlocked to allow the OTP settings to re-apply */
+ cs35l41_test_key_unlock(cs35l41->dev, cs35l41->regmap);
+ ret = regcache_sync(cs35l41->regmap);
+ cs35l41_test_key_lock(cs35l41->dev, cs35l41->regmap);
+ if (ret) {
+ dev_err(cs35l41->dev, "Failed to restore register cache: %d\n", ret);
+ goto err;
+ }
+
+ if (cs35l41->hw_cfg.bst_type == CS35L41_EXT_BOOST)
+ cs35l41_init_boost(cs35l41->dev, cs35l41->regmap, &cs35l41->hw_cfg);
+
+ dev_dbg(cs35l41->dev, "CS35L41 Resumed (%x), Revision: %02X\n", regid, reg_revid);
+
+err:
+ mutex_unlock(&cs35l41->fw_mutex);
+
+ return ret;
+}
+
+static int cs35l41_hda_read_ctl(struct cs_dsp *dsp, const char *name, int type,
+ unsigned int alg, void *buf, size_t len)
+{
+ int ret;
+
+ mutex_lock(&dsp->pwr_lock);
+ ret = cs_dsp_coeff_read_ctrl(cs_dsp_get_ctl(dsp, name, type, alg), 0, buf, len);
+ mutex_unlock(&dsp->pwr_lock);
+
+ return ret;
+}
+
+static int cs35l41_smart_amp(struct cs35l41_hda *cs35l41)
+{
+ unsigned int fw_status;
+ __be32 halo_sts;
+ int ret;
+
+ if (cs35l41->bypass_fw) {
+ dev_warn(cs35l41->dev, "Bypassing Firmware.\n");
+ return 0;
+ }
+
+ ret = cs35l41_init_dsp(cs35l41);
+ if (ret) {
+ dev_warn(cs35l41->dev, "Cannot Initialize Firmware. Error: %d\n", ret);
+ goto clean_dsp;
+ }
+
+ ret = cs35l41_write_fs_errata(cs35l41->dev, cs35l41->regmap);
+ if (ret) {
+ dev_err(cs35l41->dev, "Cannot Write FS Errata: %d\n", ret);
+ goto clean_dsp;
+ }
+
+ ret = cs_dsp_run(&cs35l41->cs_dsp);
+ if (ret) {
+ dev_err(cs35l41->dev, "Fail to start dsp: %d\n", ret);
+ goto clean_dsp;
+ }
+
+ ret = read_poll_timeout(cs35l41_hda_read_ctl, ret,
+ be32_to_cpu(halo_sts) == HALO_STATE_CODE_RUN,
+ 1000, 15000, false, &cs35l41->cs_dsp, HALO_STATE_DSP_CTL_NAME,
+ HALO_STATE_DSP_CTL_TYPE, HALO_STATE_DSP_CTL_ALG,
+ &halo_sts, sizeof(halo_sts));
+
+ if (ret) {
+ dev_err(cs35l41->dev, "Timeout waiting for HALO Core to start. State: %u\n",
+ halo_sts);
+ goto clean_dsp;
+ }
+
+ ret = regmap_read(cs35l41->regmap, CS35L41_DSP_MBOX_2, &fw_status);
+ if (ret < 0) {
+ dev_err(cs35l41->dev,
+ "Failed to read firmware status: %d\n", ret);
+ goto clean_dsp;
+ }
+
+ switch (fw_status) {
+ case CSPL_MBOX_STS_RUNNING:
+ case CSPL_MBOX_STS_PAUSED:
+ break;
+ default:
+ dev_err(cs35l41->dev, "Firmware status is invalid: %u\n",
+ fw_status);
+ ret = -EINVAL;
+ goto clean_dsp;
+ }
+
+ ret = cs35l41_set_cspl_mbox_cmd(cs35l41->dev, cs35l41->regmap, CSPL_MBOX_CMD_PAUSE);
+ if (ret) {
+ dev_err(cs35l41->dev, "Error waiting for DSP to pause: %u\n", ret);
+ goto clean_dsp;
+ }
+
+ dev_info(cs35l41->dev, "Firmware Loaded - Type: %s, Gain: %d\n",
+ cs35l41_hda_fw_ids[cs35l41->firmware_type], cs35l41->tuning_gain);
+
+ return 0;
+
+clean_dsp:
+ cs35l41_shutdown_dsp(cs35l41);
+ return ret;
+}
+
+static void cs35l41_load_firmware(struct cs35l41_hda *cs35l41, bool load)
+{
+ if (cs35l41->cs_dsp.running && !load) {
+ dev_dbg(cs35l41->dev, "Unloading Firmware\n");
+ cs35l41_shutdown_dsp(cs35l41);
+ } else if (!cs35l41->cs_dsp.running && load) {
+ dev_dbg(cs35l41->dev, "Loading Firmware\n");
+ cs35l41_smart_amp(cs35l41);
+ } else {
+ dev_dbg(cs35l41->dev, "Unable to Load firmware.\n");
+ }
+}
+
+static int cs35l41_fw_load_ctl_get(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct cs35l41_hda *cs35l41 = snd_kcontrol_chip(kcontrol);
+
+ ucontrol->value.integer.value[0] = cs35l41->request_fw_load;
+ return 0;
+}
+
+static int cs35l41_mute_override_ctl_get(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct cs35l41_hda *cs35l41 = snd_kcontrol_chip(kcontrol);
+
+ ucontrol->value.integer.value[0] = cs35l41->mute_override;
+ return 0;
+}
+
+static void cs35l41_fw_load_work(struct work_struct *work)
+{
+ struct cs35l41_hda *cs35l41 = container_of(work, struct cs35l41_hda, fw_load_work);
+
+ pm_runtime_get_sync(cs35l41->dev);
+
+ mutex_lock(&cs35l41->fw_mutex);
+
+ /* Recheck if playback is ongoing, mutex will block playback during firmware loading */
+ if (cs35l41->playback_started)
+ dev_err(cs35l41->dev, "Cannot Load/Unload firmware during Playback. Retrying...\n");
+ else
+ cs35l41_load_firmware(cs35l41, cs35l41->request_fw_load);
+
+ cs35l41->fw_request_ongoing = false;
+ mutex_unlock(&cs35l41->fw_mutex);
+
+ pm_runtime_put_autosuspend(cs35l41->dev);
+}
+
+static int cs35l41_fw_load_ctl_put(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct cs35l41_hda *cs35l41 = snd_kcontrol_chip(kcontrol);
+
+ if (cs35l41->request_fw_load == ucontrol->value.integer.value[0])
+ return 0;
+
+ if (cs35l41->fw_request_ongoing) {
+ dev_dbg(cs35l41->dev, "Existing request not complete\n");
+ return -EBUSY;
+ }
+
+ /* Check if playback is ongoing when initial request is made */
+ if (cs35l41->playback_started) {
+ dev_err(cs35l41->dev, "Cannot Load/Unload firmware during Playback\n");
+ return -EBUSY;
+ }
+
+ cs35l41->fw_request_ongoing = true;
+ cs35l41->request_fw_load = ucontrol->value.integer.value[0];
+ schedule_work(&cs35l41->fw_load_work);
+
+ return 1;
+}
+
+static int cs35l41_fw_type_ctl_get(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct cs35l41_hda *cs35l41 = snd_kcontrol_chip(kcontrol);
+
+ ucontrol->value.enumerated.item[0] = cs35l41->firmware_type;
+
+ return 0;
+}
+
+static int cs35l41_fw_type_ctl_put(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct cs35l41_hda *cs35l41 = snd_kcontrol_chip(kcontrol);
+
+ if (ucontrol->value.enumerated.item[0] < CS35L41_HDA_NUM_FW) {
+ if (cs35l41->firmware_type != ucontrol->value.enumerated.item[0]) {
+ cs35l41->firmware_type = ucontrol->value.enumerated.item[0];
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+
+ return -EINVAL;
+}
+
+static int cs35l41_fw_type_ctl_info(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_info *uinfo)
+{
+ return snd_ctl_enum_info(uinfo, 1, ARRAY_SIZE(cs35l41_hda_fw_ids), cs35l41_hda_fw_ids);
+}
+
+static int cs35l41_create_controls(struct cs35l41_hda *cs35l41)
+{
+ char fw_type_ctl_name[SNDRV_CTL_ELEM_ID_NAME_MAXLEN];
+ char fw_load_ctl_name[SNDRV_CTL_ELEM_ID_NAME_MAXLEN];
+ char mute_override_ctl_name[SNDRV_CTL_ELEM_ID_NAME_MAXLEN];
+ struct snd_kcontrol_new fw_type_ctl = {
+ .name = fw_type_ctl_name,
+ .iface = SNDRV_CTL_ELEM_IFACE_CARD,
+ .info = cs35l41_fw_type_ctl_info,
+ .get = cs35l41_fw_type_ctl_get,
+ .put = cs35l41_fw_type_ctl_put,
+ };
+ struct snd_kcontrol_new fw_load_ctl = {
+ .name = fw_load_ctl_name,
+ .iface = SNDRV_CTL_ELEM_IFACE_CARD,
+ .info = snd_ctl_boolean_mono_info,
+ .get = cs35l41_fw_load_ctl_get,
+ .put = cs35l41_fw_load_ctl_put,
+ };
+ struct snd_kcontrol_new mute_override_ctl = {
+ .name = mute_override_ctl_name,
+ .iface = SNDRV_CTL_ELEM_IFACE_CARD,
+ .info = snd_ctl_boolean_mono_info,
+ .access = SNDRV_CTL_ELEM_ACCESS_READ | SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+ .get = cs35l41_mute_override_ctl_get,
+ };
+ int ret;
+
+ scnprintf(fw_type_ctl_name, SNDRV_CTL_ELEM_ID_NAME_MAXLEN, "%s DSP1 Firmware Type",
+ cs35l41->amp_name);
+ scnprintf(fw_load_ctl_name, SNDRV_CTL_ELEM_ID_NAME_MAXLEN, "%s DSP1 Firmware Load",
+ cs35l41->amp_name);
+ scnprintf(mute_override_ctl_name, SNDRV_CTL_ELEM_ID_NAME_MAXLEN, "%s Forced Mute Status",
+ cs35l41->amp_name);
+
+ ret = snd_ctl_add(cs35l41->codec->card, snd_ctl_new1(&fw_type_ctl, cs35l41));
+ if (ret) {
+ dev_err(cs35l41->dev, "Failed to add KControl %s = %d\n", fw_type_ctl.name, ret);
+ return ret;
+ }
+
+ dev_dbg(cs35l41->dev, "Added Control %s\n", fw_type_ctl.name);
+
+ ret = snd_ctl_add(cs35l41->codec->card, snd_ctl_new1(&fw_load_ctl, cs35l41));
+ if (ret) {
+ dev_err(cs35l41->dev, "Failed to add KControl %s = %d\n", fw_load_ctl.name, ret);
+ return ret;
+ }
+
+ dev_dbg(cs35l41->dev, "Added Control %s\n", fw_load_ctl.name);
+
+ ret = snd_ctl_add(cs35l41->codec->card, snd_ctl_new1(&mute_override_ctl, cs35l41));
+ if (ret) {
+ dev_err(cs35l41->dev, "Failed to add KControl %s = %d\n", mute_override_ctl.name,
+ ret);
+ return ret;
+ }
+
+ dev_dbg(cs35l41->dev, "Added Control %s\n", mute_override_ctl.name);
+
+ return 0;
+}
+
+static bool cs35l41_dsm_supported(acpi_handle handle, unsigned int commands)
+{
+ guid_t guid;
+
+ guid_parse(CS35L41_UUID, &guid);
+
+ return acpi_check_dsm(handle, &guid, 0, BIT(commands));
+}
+
+static int cs35l41_get_acpi_mute_state(struct cs35l41_hda *cs35l41, acpi_handle handle)
+{
+ guid_t guid;
+ union acpi_object *ret;
+ int mute = -ENODEV;
+
+ guid_parse(CS35L41_UUID, &guid);
+
+ if (cs35l41_dsm_supported(handle, CS35L41_DSM_GET_MUTE)) {
+ ret = acpi_evaluate_dsm(handle, &guid, 0, CS35L41_DSM_GET_MUTE, NULL);
+ mute = *ret->buffer.pointer;
+ dev_dbg(cs35l41->dev, "CS35L41_DSM_GET_MUTE: %d\n", mute);
+ }
+
+ dev_dbg(cs35l41->dev, "%s: %d\n", __func__, mute);
+
+ return mute;
+}
+
+static void cs35l41_acpi_device_notify(acpi_handle handle, u32 event, struct device *dev)
+{
+ struct cs35l41_hda *cs35l41 = dev_get_drvdata(dev);
+ int mute;
+
+ if (event != CS35L41_NOTIFY_EVENT)
+ return;
+
+ mute = cs35l41_get_acpi_mute_state(cs35l41, handle);
+ if (mute < 0) {
+ dev_warn(cs35l41->dev, "Unable to retrieve mute state: %d\n", mute);
+ return;
+ }
+
+ dev_dbg(cs35l41->dev, "Requesting mute value: %d\n", mute);
+ cs35l41->mute_override = (mute > 0);
+ cs35l41_mute(cs35l41->dev, cs35l41->mute_override);
+}
+
+static int cs35l41_hda_bind(struct device *dev, struct device *master, void *master_data)
+{
+ struct cs35l41_hda *cs35l41 = dev_get_drvdata(dev);
+ struct hda_component_parent *parent = master_data;
+ struct hda_component *comp;
+ unsigned int sleep_flags;
+ int ret = 0;
+
+ comp = hda_component_from_index(parent, cs35l41->index);
+ if (!comp)
+ return -EINVAL;
+
+ if (comp->dev)
+ return -EBUSY;
+
+ pm_runtime_get_sync(dev);
+
+ mutex_lock(&cs35l41->fw_mutex);
+
+ comp->dev = dev;
+ cs35l41->codec = parent->codec;
+ if (!cs35l41->acpi_subsystem_id)
+ cs35l41->acpi_subsystem_id = kasprintf(GFP_KERNEL, "%.8x",
+ cs35l41->codec->core.subsystem_id);
+
+ strscpy(comp->name, dev_name(dev), sizeof(comp->name));
+
+ cs35l41->firmware_type = CS35L41_HDA_FW_SPK_PROT;
+
+ if (firmware_autostart) {
+ dev_dbg(cs35l41->dev, "Firmware Autostart.\n");
+ cs35l41->request_fw_load = true;
+ if (cs35l41_smart_amp(cs35l41) < 0)
+ dev_warn(cs35l41->dev, "Cannot Run Firmware, reverting to dsp bypass...\n");
+ } else {
+ dev_dbg(cs35l41->dev, "Firmware Autostart is disabled.\n");
+ }
+
+ ret = cs35l41_create_controls(cs35l41);
+
+ comp->playback_hook = cs35l41_hda_playback_hook;
+ comp->pre_playback_hook = cs35l41_hda_pre_playback_hook;
+ comp->post_playback_hook = cs35l41_hda_post_playback_hook;
+ comp->acpi_notify = cs35l41_acpi_device_notify;
+ comp->adev = cs35l41->dacpi;
+
+ comp->acpi_notifications_supported = cs35l41_dsm_supported(acpi_device_handle(comp->adev),
+ CS35L41_DSM_GET_MUTE);
+
+ cs35l41->mute_override = cs35l41_get_acpi_mute_state(cs35l41,
+ acpi_device_handle(cs35l41->dacpi)) > 0;
+
+ mutex_unlock(&cs35l41->fw_mutex);
+
+ sleep_flags = lock_system_sleep();
+ if (!device_link_add(&cs35l41->codec->core.dev, cs35l41->dev, DL_FLAG_STATELESS))
+ dev_warn(dev, "Unable to create device link\n");
+ unlock_system_sleep(sleep_flags);
+
+ pm_runtime_put_autosuspend(dev);
+
+ dev_info(cs35l41->dev,
+ "CS35L41 Bound - SSID: %s, BST: %d, VSPK: %d, CH: %c, FW EN: %d, SPKID: %d\n",
+ cs35l41->acpi_subsystem_id, cs35l41->hw_cfg.bst_type,
+ cs35l41->hw_cfg.gpio1.func == CS35l41_VSPK_SWITCH,
+ channel_name[cs35l41->hw_cfg.spk_pos],
+ cs35l41->cs_dsp.running, cs35l41->speaker_id);
+
+ return ret;
+}
+
+static void cs35l41_hda_unbind(struct device *dev, struct device *master, void *master_data)
+{
+ struct cs35l41_hda *cs35l41 = dev_get_drvdata(dev);
+ struct hda_component_parent *parent = master_data;
+ struct hda_component *comp;
+ unsigned int sleep_flags;
+
+ comp = hda_component_from_index(parent, cs35l41->index);
+ if (!comp)
+ return;
+
+ if (comp->dev == dev) {
+ sleep_flags = lock_system_sleep();
+ device_link_remove(&cs35l41->codec->core.dev, cs35l41->dev);
+ unlock_system_sleep(sleep_flags);
+ memset(comp, 0, sizeof(*comp));
+ }
+}
+
+static const struct component_ops cs35l41_hda_comp_ops = {
+ .bind = cs35l41_hda_bind,
+ .unbind = cs35l41_hda_unbind,
+};
+
+static irqreturn_t cs35l41_bst_short_err(int irq, void *data)
+{
+ struct cs35l41_hda *cs35l41 = data;
+
+ dev_crit_ratelimited(cs35l41->dev, "LBST Error\n");
+ set_bit(CS35L41_BST_SHORT_ERR_RLS_SHIFT, &cs35l41->irq_errors);
+
+ return IRQ_HANDLED;
+}
+
+static irqreturn_t cs35l41_bst_dcm_uvp_err(int irq, void *data)
+{
+ struct cs35l41_hda *cs35l41 = data;
+
+ dev_crit_ratelimited(cs35l41->dev, "DCM VBST Under Voltage Error\n");
+ set_bit(CS35L41_BST_UVP_ERR_RLS_SHIFT, &cs35l41->irq_errors);
+
+ return IRQ_HANDLED;
+}
+
+static irqreturn_t cs35l41_bst_ovp_err(int irq, void *data)
+{
+ struct cs35l41_hda *cs35l41 = data;
+
+ dev_crit_ratelimited(cs35l41->dev, "VBST Over Voltage error\n");
+ set_bit(CS35L41_BST_OVP_ERR_RLS_SHIFT, &cs35l41->irq_errors);
+
+ return IRQ_HANDLED;
+}
+
+static irqreturn_t cs35l41_temp_err(int irq, void *data)
+{
+ struct cs35l41_hda *cs35l41 = data;
+
+ dev_crit_ratelimited(cs35l41->dev, "Over temperature error\n");
+ set_bit(CS35L41_TEMP_ERR_RLS_SHIFT, &cs35l41->irq_errors);
+
+ return IRQ_HANDLED;
+}
+
+static irqreturn_t cs35l41_temp_warn(int irq, void *data)
+{
+ struct cs35l41_hda *cs35l41 = data;
+
+ dev_crit_ratelimited(cs35l41->dev, "Over temperature warning\n");
+ set_bit(CS35L41_TEMP_WARN_ERR_RLS_SHIFT, &cs35l41->irq_errors);
+
+ return IRQ_HANDLED;
+}
+
+static irqreturn_t cs35l41_amp_short(int irq, void *data)
+{
+ struct cs35l41_hda *cs35l41 = data;
+
+ dev_crit_ratelimited(cs35l41->dev, "Amp short error\n");
+ set_bit(CS35L41_AMP_SHORT_ERR_RLS_SHIFT, &cs35l41->irq_errors);
+
+ return IRQ_HANDLED;
+}
+
+static const struct cs35l41_irq cs35l41_irqs[] = {
+ CS35L41_IRQ(BST_OVP_ERR, "Boost Overvoltage Error", cs35l41_bst_ovp_err),
+ CS35L41_IRQ(BST_DCM_UVP_ERR, "Boost Undervoltage Error", cs35l41_bst_dcm_uvp_err),
+ CS35L41_IRQ(BST_SHORT_ERR, "Boost Inductor Short Error", cs35l41_bst_short_err),
+ CS35L41_IRQ(TEMP_WARN, "Temperature Warning", cs35l41_temp_warn),
+ CS35L41_IRQ(TEMP_ERR, "Temperature Error", cs35l41_temp_err),
+ CS35L41_IRQ(AMP_SHORT_ERR, "Amp Short", cs35l41_amp_short),
+};
+
+static const struct regmap_irq cs35l41_reg_irqs[] = {
+ CS35L41_REG_IRQ(IRQ1_STATUS1, BST_OVP_ERR),
+ CS35L41_REG_IRQ(IRQ1_STATUS1, BST_DCM_UVP_ERR),
+ CS35L41_REG_IRQ(IRQ1_STATUS1, BST_SHORT_ERR),
+ CS35L41_REG_IRQ(IRQ1_STATUS1, TEMP_WARN),
+ CS35L41_REG_IRQ(IRQ1_STATUS1, TEMP_ERR),
+ CS35L41_REG_IRQ(IRQ1_STATUS1, AMP_SHORT_ERR),
+};
+
+static const struct regmap_irq_chip cs35l41_regmap_irq_chip = {
+ .name = "cs35l41 IRQ1 Controller",
+ .status_base = CS35L41_IRQ1_STATUS1,
+ .mask_base = CS35L41_IRQ1_MASK1,
+ .ack_base = CS35L41_IRQ1_STATUS1,
+ .num_regs = 4,
+ .irqs = cs35l41_reg_irqs,
+ .num_irqs = ARRAY_SIZE(cs35l41_reg_irqs),
+ .runtime_pm = true,
+};
+
+static void cs35l41_configure_interrupt(struct cs35l41_hda *cs35l41, int irq_pol)
+{
+ int irq;
+ int ret;
+ int i;
+
+ if (!cs35l41->irq) {
+ dev_warn(cs35l41->dev, "No Interrupt Found");
+ goto err;
+ }
+
+ ret = devm_regmap_add_irq_chip(cs35l41->dev, cs35l41->regmap, cs35l41->irq,
+ IRQF_ONESHOT | IRQF_SHARED | irq_pol,
+ 0, &cs35l41_regmap_irq_chip, &cs35l41->irq_data);
+ if (ret) {
+ dev_dbg(cs35l41->dev, "Unable to add IRQ Chip: %d.", ret);
+ goto err;
+ }
+
+ for (i = 0; i < ARRAY_SIZE(cs35l41_irqs); i++) {
+ irq = regmap_irq_get_virq(cs35l41->irq_data, cs35l41_irqs[i].irq);
+ if (irq < 0) {
+ ret = irq;
+ dev_dbg(cs35l41->dev, "Unable to map IRQ %s: %d.", cs35l41_irqs[i].name,
+ ret);
+ goto err;
+ }
+
+ ret = devm_request_threaded_irq(cs35l41->dev, irq, NULL,
+ cs35l41_irqs[i].handler,
+ IRQF_ONESHOT | IRQF_SHARED | irq_pol,
+ cs35l41_irqs[i].name, cs35l41);
+ if (ret) {
+ dev_dbg(cs35l41->dev, "Unable to allocate IRQ %s:: %d.",
+ cs35l41_irqs[i].name, ret);
+ goto err;
+ }
+ }
+ return;
+err:
+ dev_warn(cs35l41->dev,
+ "IRQ Config Failed. Amp errors may not be recoverable without reboot.");
+}
+
+static int cs35l41_hda_apply_properties(struct cs35l41_hda *cs35l41)
+{
+ struct cs35l41_hw_cfg *hw_cfg = &cs35l41->hw_cfg;
+ bool using_irq = false;
+ int irq_pol;
+ int ret;
+
+ if (!cs35l41->hw_cfg.valid)
+ return -EINVAL;
+
+ ret = cs35l41_init_boost(cs35l41->dev, cs35l41->regmap, hw_cfg);
+ if (ret)
+ return ret;
+
+ if (hw_cfg->gpio1.valid) {
+ switch (hw_cfg->gpio1.func) {
+ case CS35L41_NOT_USED:
+ break;
+ case CS35l41_VSPK_SWITCH:
+ hw_cfg->gpio1.func = CS35L41_GPIO1_GPIO;
+ hw_cfg->gpio1.out_en = true;
+ break;
+ case CS35l41_SYNC:
+ hw_cfg->gpio1.func = CS35L41_GPIO1_MDSYNC;
+ break;
+ default:
+ dev_err(cs35l41->dev, "Invalid function %d for GPIO1\n",
+ hw_cfg->gpio1.func);
+ return -EINVAL;
+ }
+ }
+
+ if (hw_cfg->gpio2.valid) {
+ switch (hw_cfg->gpio2.func) {
+ case CS35L41_NOT_USED:
+ break;
+ case CS35L41_INTERRUPT:
+ using_irq = true;
+ hw_cfg->gpio2.func = CS35L41_GPIO2_INT_OPEN_DRAIN;
+ break;
+ default:
+ dev_err(cs35l41->dev, "Invalid GPIO2 function %d\n", hw_cfg->gpio2.func);
+ return -EINVAL;
+ }
+ }
+
+ irq_pol = cs35l41_gpio_config(cs35l41->regmap, hw_cfg);
+
+ if (using_irq)
+ cs35l41_configure_interrupt(cs35l41, irq_pol);
+
+ return cs35l41_hda_channel_map(cs35l41);
+}
+
+int cs35l41_get_speaker_id(struct device *dev, int amp_index, int num_amps, int fixed_gpio_id)
+{
+ struct gpio_desc *speaker_id_desc;
+ int speaker_id = -ENODEV;
+
+ if (fixed_gpio_id >= 0) {
+ dev_dbg(dev, "Found Fixed Speaker ID GPIO (index = %d)\n", fixed_gpio_id);
+ speaker_id_desc = gpiod_get_index(dev, NULL, fixed_gpio_id, GPIOD_IN);
+ if (IS_ERR(speaker_id_desc)) {
+ speaker_id = PTR_ERR(speaker_id_desc);
+ return speaker_id;
+ }
+ speaker_id = gpiod_get_value_cansleep(speaker_id_desc);
+ gpiod_put(speaker_id_desc);
+ dev_dbg(dev, "Speaker ID = %d\n", speaker_id);
+ } else {
+ int base_index;
+ int gpios_per_amp;
+ int count;
+ int tmp;
+ int i;
+
+ count = gpiod_count(dev, "spk-id");
+ if (count > 0) {
+ speaker_id = 0;
+ gpios_per_amp = count / num_amps;
+ base_index = gpios_per_amp * amp_index;
+
+ if (count % num_amps)
+ return -EINVAL;
+
+ dev_dbg(dev, "Found %d Speaker ID GPIOs per Amp\n", gpios_per_amp);
+
+ for (i = 0; i < gpios_per_amp; i++) {
+ speaker_id_desc = gpiod_get_index(dev, "spk-id", i + base_index,
+ GPIOD_IN);
+ if (IS_ERR(speaker_id_desc)) {
+ speaker_id = PTR_ERR(speaker_id_desc);
+ break;
+ }
+ tmp = gpiod_get_value_cansleep(speaker_id_desc);
+ gpiod_put(speaker_id_desc);
+ if (tmp < 0) {
+ speaker_id = tmp;
+ break;
+ }
+ speaker_id |= tmp << i;
+ }
+ dev_dbg(dev, "Speaker ID = %d\n", speaker_id);
+ }
+ }
+ return speaker_id;
+}
+
+int cs35l41_hda_parse_acpi(struct cs35l41_hda *cs35l41, struct device *physdev, int id)
+{
+ struct cs35l41_hw_cfg *hw_cfg = &cs35l41->hw_cfg;
+ u32 values[HDA_MAX_COMPONENTS];
+ char *property;
+ size_t nval;
+ int i, ret;
+
+ property = "cirrus,dev-index";
+ ret = device_property_count_u32(physdev, property);
+ if (ret <= 0)
+ goto err;
+
+ if (ret > ARRAY_SIZE(values)) {
+ ret = -EINVAL;
+ goto err;
+ }
+ nval = ret;
+
+ ret = device_property_read_u32_array(physdev, property, values, nval);
+ if (ret)
+ goto err;
+
+ cs35l41->index = -1;
+ for (i = 0; i < nval; i++) {
+ if (values[i] == id) {
+ cs35l41->index = i;
+ break;
+ }
+ }
+ if (cs35l41->index == -1) {
+ dev_err(cs35l41->dev, "No index found in %s\n", property);
+ ret = -ENODEV;
+ goto err;
+ }
+
+ /* To use the same release code for all laptop variants we can't use devm_ version of
+ * gpiod_get here, as CLSA010* don't have a fully functional bios with an _DSD node
+ */
+ cs35l41->reset_gpio = fwnode_gpiod_get_index(acpi_fwnode_handle(cs35l41->dacpi), "reset",
+ cs35l41->index, GPIOD_OUT_LOW,
+ "cs35l41-reset");
+
+ property = "cirrus,speaker-position";
+ ret = device_property_read_u32_array(physdev, property, values, nval);
+ if (ret)
+ goto err;
+ hw_cfg->spk_pos = values[cs35l41->index];
+
+ cs35l41->channel_index = 0;
+ for (i = 0; i < cs35l41->index; i++)
+ if (values[i] == hw_cfg->spk_pos)
+ cs35l41->channel_index++;
+
+ property = "cirrus,gpio1-func";
+ ret = device_property_read_u32_array(physdev, property, values, nval);
+ if (ret)
+ goto err;
+ hw_cfg->gpio1.func = values[cs35l41->index];
+ hw_cfg->gpio1.valid = true;
+
+ property = "cirrus,gpio2-func";
+ ret = device_property_read_u32_array(physdev, property, values, nval);
+ if (ret)
+ goto err;
+ hw_cfg->gpio2.func = values[cs35l41->index];
+ hw_cfg->gpio2.valid = true;
+
+ property = "cirrus,boost-peak-milliamp";
+ ret = device_property_read_u32_array(physdev, property, values, nval);
+ if (ret == 0)
+ hw_cfg->bst_ipk = values[cs35l41->index];
+ else
+ hw_cfg->bst_ipk = -1;
+
+ property = "cirrus,boost-ind-nanohenry";
+ ret = device_property_read_u32_array(physdev, property, values, nval);
+ if (ret == 0)
+ hw_cfg->bst_ind = values[cs35l41->index];
+ else
+ hw_cfg->bst_ind = -1;
+
+ property = "cirrus,boost-cap-microfarad";
+ ret = device_property_read_u32_array(physdev, property, values, nval);
+ if (ret == 0)
+ hw_cfg->bst_cap = values[cs35l41->index];
+ else
+ 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;
+
+ return 0;
+err:
+ dev_err(cs35l41->dev, "Failed property %s: %d\n", property, ret);
+ hw_cfg->valid = false;
+ hw_cfg->gpio1.valid = false;
+ hw_cfg->gpio2.valid = false;
+ acpi_dev_put(cs35l41->dacpi);
+
+ return ret;
+}
+
+static int cs35l41_hda_read_acpi(struct cs35l41_hda *cs35l41, const char *hid, int id)
+{
+ struct acpi_device *adev;
+ struct device *physdev;
+ struct spi_device *spi;
+ const char *sub;
+ int ret;
+
+ 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);
+ return -ENODEV;
+ }
+
+ cs35l41->dacpi = adev;
+ physdev = get_device(acpi_get_first_physical_node(adev));
+
+ sub = acpi_get_subsystem_id(ACPI_HANDLE(physdev));
+ if (IS_ERR(sub))
+ sub = NULL;
+ cs35l41->acpi_subsystem_id = sub;
+
+ ret = cs35l41_add_dsd_properties(cs35l41, physdev, id, hid);
+ if (!ret) {
+ dev_info(cs35l41->dev, "Using extra _DSD properties, bypassing _DSD in ACPI\n");
+ goto out;
+ }
+
+ ret = cs35l41_hda_parse_acpi(cs35l41, physdev, id);
+ if (ret) {
+ put_device(physdev);
+ return ret;
+ }
+out:
+ put_device(physdev);
+
+ cs35l41->bypass_fw = false;
+ if (cs35l41->control_bus == SPI) {
+ spi = to_spi_device(cs35l41->dev);
+ if (spi->max_speed_hz < CS35L41_MAX_ACCEPTABLE_SPI_SPEED_HZ) {
+ dev_warn(cs35l41->dev,
+ "SPI speed is too slow to support firmware download: %d Hz.\n",
+ spi->max_speed_hz);
+ cs35l41->bypass_fw = true;
+ }
+ }
+
+ return 0;
+}
+
+int cs35l41_hda_probe(struct device *dev, const char *device_name, int id, int irq,
+ struct regmap *regmap, enum control_bus control_bus)
+{
+ unsigned int regid, reg_revid;
+ struct cs35l41_hda *cs35l41;
+ int ret;
+
+ BUILD_BUG_ON(ARRAY_SIZE(cs35l41_irqs) != ARRAY_SIZE(cs35l41_reg_irqs));
+ BUILD_BUG_ON(ARRAY_SIZE(cs35l41_irqs) != CS35L41_NUM_IRQ);
+
+ if (IS_ERR(regmap))
+ return PTR_ERR(regmap);
+
+ cs35l41 = devm_kzalloc(dev, sizeof(*cs35l41), GFP_KERNEL);
+ if (!cs35l41)
+ return -ENOMEM;
+
+ cs35l41->dev = dev;
+ cs35l41->irq = irq;
+ cs35l41->regmap = regmap;
+ cs35l41->control_bus = control_bus;
+ dev_set_drvdata(dev, cs35l41);
+
+ ret = cs35l41_hda_read_acpi(cs35l41, device_name, id);
+ if (ret)
+ return dev_err_probe(cs35l41->dev, ret, "Platform not supported\n");
+
+ if (IS_ERR(cs35l41->reset_gpio)) {
+ ret = PTR_ERR(cs35l41->reset_gpio);
+ cs35l41->reset_gpio = NULL;
+ if (ret == -EBUSY) {
+ dev_info(cs35l41->dev, "Reset line busy, assuming shared reset\n");
+ } else {
+ dev_err_probe(cs35l41->dev, ret, "Failed to get reset GPIO\n");
+ goto err;
+ }
+ }
+ if (cs35l41->reset_gpio) {
+ gpiod_set_value_cansleep(cs35l41->reset_gpio, 0);
+ usleep_range(2000, 2100);
+ gpiod_set_value_cansleep(cs35l41->reset_gpio, 1);
+ }
+
+ usleep_range(2000, 2100);
+ regmap_write(cs35l41->regmap, CS35L41_SFT_RESET, CS35L41_SOFTWARE_RESET);
+ usleep_range(2000, 2100);
+
+ ret = cs35l41_wait_boot_done(cs35l41);
+ if (ret)
+ goto err;
+
+ ret = cs35l41_verify_id(cs35l41, &regid, &reg_revid);
+ if (ret)
+ goto err;
+
+ ret = cs35l41_test_key_unlock(cs35l41->dev, cs35l41->regmap);
+ if (ret)
+ goto err;
+
+ ret = cs35l41_register_errata_patch(cs35l41->dev, cs35l41->regmap, reg_revid);
+ if (ret)
+ goto err;
+
+ ret = cs35l41_otp_unpack(cs35l41->dev, cs35l41->regmap);
+ if (ret) {
+ dev_err_probe(cs35l41->dev, ret, "OTP Unpack failed\n");
+ goto err;
+ }
+
+ ret = cs35l41_test_key_lock(cs35l41->dev, cs35l41->regmap);
+ if (ret)
+ goto err;
+
+ ret = cs35l41_get_calibration(cs35l41);
+ if (ret && ret != -ENOENT)
+ goto err;
+
+ cs35l41_mute(cs35l41->dev, true);
+
+ INIT_WORK(&cs35l41->fw_load_work, cs35l41_fw_load_work);
+ mutex_init(&cs35l41->fw_mutex);
+
+ pm_runtime_set_autosuspend_delay(cs35l41->dev, 3000);
+ pm_runtime_use_autosuspend(cs35l41->dev);
+ pm_runtime_set_active(cs35l41->dev);
+ pm_runtime_get_noresume(cs35l41->dev);
+ pm_runtime_enable(cs35l41->dev);
+
+ ret = cs35l41_hda_apply_properties(cs35l41);
+ if (ret)
+ goto err_pm;
+
+ pm_runtime_put_autosuspend(cs35l41->dev);
+
+ ret = component_add(cs35l41->dev, &cs35l41_hda_comp_ops);
+ if (ret) {
+ dev_err_probe(cs35l41->dev, ret, "Register component failed\n");
+ goto err_pm;
+ }
+
+ dev_info(cs35l41->dev, "Cirrus Logic CS35L41 (%x), Revision: %02X\n", regid, reg_revid);
+
+ return 0;
+
+err_pm:
+ pm_runtime_dont_use_autosuspend(cs35l41->dev);
+ pm_runtime_disable(cs35l41->dev);
+ pm_runtime_put_noidle(cs35l41->dev);
+
+err:
+ if (cs35l41_safe_reset(cs35l41->regmap, cs35l41->hw_cfg.bst_type))
+ gpiod_set_value_cansleep(cs35l41->reset_gpio, 0);
+ gpiod_put(cs35l41->reset_gpio);
+ gpiod_put(cs35l41->cs_gpio);
+ acpi_dev_put(cs35l41->dacpi);
+ kfree(cs35l41->acpi_subsystem_id);
+
+ return ret;
+}
+EXPORT_SYMBOL_NS_GPL(cs35l41_hda_probe, "SND_HDA_SCODEC_CS35L41");
+
+void cs35l41_hda_remove(struct device *dev)
+{
+ struct cs35l41_hda *cs35l41 = dev_get_drvdata(dev);
+
+ component_del(cs35l41->dev, &cs35l41_hda_comp_ops);
+
+ pm_runtime_get_sync(cs35l41->dev);
+ pm_runtime_dont_use_autosuspend(cs35l41->dev);
+ pm_runtime_disable(cs35l41->dev);
+
+ if (cs35l41->halo_initialized)
+ cs35l41_remove_dsp(cs35l41);
+
+ acpi_dev_put(cs35l41->dacpi);
+
+ pm_runtime_put_noidle(cs35l41->dev);
+
+ if (cs35l41_safe_reset(cs35l41->regmap, cs35l41->hw_cfg.bst_type))
+ gpiod_set_value_cansleep(cs35l41->reset_gpio, 0);
+ gpiod_put(cs35l41->reset_gpio);
+ gpiod_put(cs35l41->cs_gpio);
+ kfree(cs35l41->acpi_subsystem_id);
+}
+EXPORT_SYMBOL_NS_GPL(cs35l41_hda_remove, "SND_HDA_SCODEC_CS35L41");
+
+const struct dev_pm_ops cs35l41_hda_pm_ops = {
+ RUNTIME_PM_OPS(cs35l41_runtime_suspend, cs35l41_runtime_resume,
+ cs35l41_runtime_idle)
+ .prepare = cs35l41_system_suspend_prep,
+ SYSTEM_SLEEP_PM_OPS(cs35l41_system_suspend, cs35l41_system_resume)
+};
+EXPORT_SYMBOL_NS_GPL(cs35l41_hda_pm_ops, "SND_HDA_SCODEC_CS35L41");
+
+MODULE_DESCRIPTION("CS35L41 HDA Driver");
+MODULE_IMPORT_NS("SND_SOC_CS_AMP_LIB");
+MODULE_AUTHOR("Lucas Tanure, Cirrus Logic Inc, <tanureal@opensource.cirrus.com>");
+MODULE_LICENSE("GPL");
+MODULE_IMPORT_NS("FW_CS_DSP");
+MODULE_FIRMWARE("cirrus/cs35l41-*.wmfw");
+MODULE_FIRMWARE("cirrus/cs35l41-*.bin");
diff --git a/sound/hda/codecs/side-codecs/cs35l41_hda.h b/sound/hda/codecs/side-codecs/cs35l41_hda.h
new file mode 100644
index 000000000000..7d003c598e93
--- /dev/null
+++ b/sound/hda/codecs/side-codecs/cs35l41_hda.h
@@ -0,0 +1,110 @@
+/* SPDX-License-Identifier: GPL-2.0
+ *
+ * CS35L41 ALSA HDA audio driver
+ *
+ * Copyright 2021 Cirrus Logic, Inc.
+ *
+ * Author: Lucas Tanure <tanureal@opensource.cirrus.com>
+ */
+
+#ifndef __CS35L41_HDA_H__
+#define __CS35L41_HDA_H__
+
+#include <linux/acpi.h>
+#include <linux/efi.h>
+#include <linux/regulator/consumer.h>
+#include <linux/gpio/consumer.h>
+#include <linux/device.h>
+#include <sound/cs35l41.h>
+#include <sound/cs-amp-lib.h>
+
+#include <linux/firmware/cirrus/cs_dsp.h>
+#include <linux/firmware/cirrus/wmfw.h>
+
+#define CS35L41_MAX_ACCEPTABLE_SPI_SPEED_HZ 1000000
+#define DEFAULT_AMP_GAIN_PCM 17 /* 17.5dB Gain */
+#define DEFAULT_AMP_GAIN_PDM 19 /* 19.5dB Gain */
+
+struct cs35l41_amp_cal_data {
+ u32 calTarget[2];
+ u32 calTime[2];
+ s8 calAmbient;
+ u8 calStatus;
+ u16 calR;
+} __packed;
+
+struct cs35l41_amp_efi_data {
+ u32 size;
+ u32 count;
+ struct cs35l41_amp_cal_data data[];
+} __packed;
+
+enum cs35l41_hda_spk_pos {
+ CS35L41_LEFT,
+ CS35L41_RIGHT,
+ CS35L41_CENTER,
+};
+
+enum cs35l41_hda_gpio_function {
+ CS35L41_NOT_USED,
+ CS35l41_VSPK_SWITCH,
+ CS35L41_INTERRUPT,
+ CS35l41_SYNC,
+};
+
+enum control_bus {
+ I2C,
+ SPI
+};
+
+struct cs35l41_hda {
+ struct device *dev;
+ struct regmap *regmap;
+ struct gpio_desc *reset_gpio;
+ struct gpio_desc *cs_gpio;
+ struct cs35l41_hw_cfg hw_cfg;
+ struct hda_codec *codec;
+
+ int irq;
+ int index;
+ int channel_index;
+ unsigned volatile long irq_errors;
+ const char *amp_name;
+ const char *acpi_subsystem_id;
+ int firmware_type;
+ int speaker_id;
+ struct mutex fw_mutex;
+ struct work_struct fw_load_work;
+
+ struct regmap_irq_chip_data *irq_data;
+ bool firmware_running;
+ bool request_fw_load;
+ bool fw_request_ongoing;
+ bool halo_initialized;
+ bool playback_started;
+ struct cs_dsp cs_dsp;
+ struct acpi_device *dacpi;
+ bool mute_override;
+ enum control_bus control_bus;
+ bool bypass_fw;
+ unsigned int tuning_gain;
+ struct cirrus_amp_cal_data cal_data;
+ bool cal_data_valid;
+
+};
+
+enum halo_state {
+ HALO_STATE_CODE_INIT_DOWNLOAD = 0,
+ HALO_STATE_CODE_START,
+ HALO_STATE_CODE_RUN
+};
+
+extern const struct dev_pm_ops cs35l41_hda_pm_ops;
+
+int cs35l41_hda_probe(struct device *dev, const char *device_name, int id, int irq,
+ struct regmap *regmap, enum control_bus control_bus);
+void cs35l41_hda_remove(struct device *dev);
+int cs35l41_get_speaker_id(struct device *dev, int amp_index, int num_amps, int fixed_gpio_id);
+int cs35l41_hda_parse_acpi(struct cs35l41_hda *cs35l41, struct device *physdev, int id);
+
+#endif /*__CS35L41_HDA_H__*/
diff --git a/sound/hda/codecs/side-codecs/cs35l41_hda_i2c.c b/sound/hda/codecs/side-codecs/cs35l41_hda_i2c.c
new file mode 100644
index 000000000000..e77495413c21
--- /dev/null
+++ b/sound/hda/codecs/side-codecs/cs35l41_hda_i2c.c
@@ -0,0 +1,69 @@
+// SPDX-License-Identifier: GPL-2.0
+//
+// CS35l41 HDA I2C driver
+//
+// Copyright 2021 Cirrus Logic, Inc.
+//
+// Author: Lucas Tanure <tanureal@opensource.cirrus.com>
+
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/i2c.h>
+
+#include "cs35l41_hda.h"
+
+static int cs35l41_hda_i2c_probe(struct i2c_client *clt)
+{
+ const char *device_name;
+
+ /*
+ * Compare against the device name so it works for SPI, normal ACPI
+ * and for ACPI by serial-multi-instantiate matching cases.
+ */
+ if (strstr(dev_name(&clt->dev), "CLSA0100"))
+ device_name = "CLSA0100";
+ else if (strstr(dev_name(&clt->dev), "CLSA0101"))
+ device_name = "CLSA0101";
+ else if (strstr(dev_name(&clt->dev), "CSC3551"))
+ device_name = "CSC3551";
+ else
+ return -ENODEV;
+
+ return cs35l41_hda_probe(&clt->dev, device_name, clt->addr, clt->irq,
+ devm_regmap_init_i2c(clt, &cs35l41_regmap_i2c), I2C);
+}
+
+static void cs35l41_hda_i2c_remove(struct i2c_client *clt)
+{
+ cs35l41_hda_remove(&clt->dev);
+}
+
+static const struct i2c_device_id cs35l41_hda_i2c_id[] = {
+ { "cs35l41-hda" },
+ {}
+};
+
+static const struct acpi_device_id cs35l41_acpi_hda_match[] = {
+ {"CLSA0100", 0 },
+ {"CLSA0101", 0 },
+ {"CSC3551", 0 },
+ {}
+};
+MODULE_DEVICE_TABLE(acpi, cs35l41_acpi_hda_match);
+
+static struct i2c_driver cs35l41_i2c_driver = {
+ .driver = {
+ .name = "cs35l41-hda",
+ .acpi_match_table = cs35l41_acpi_hda_match,
+ .pm = &cs35l41_hda_pm_ops,
+ },
+ .id_table = cs35l41_hda_i2c_id,
+ .probe = cs35l41_hda_i2c_probe,
+ .remove = cs35l41_hda_i2c_remove,
+};
+module_i2c_driver(cs35l41_i2c_driver);
+
+MODULE_DESCRIPTION("HDA CS35L41 driver");
+MODULE_IMPORT_NS("SND_HDA_SCODEC_CS35L41");
+MODULE_AUTHOR("Lucas Tanure <tanureal@opensource.cirrus.com>");
+MODULE_LICENSE("GPL");
diff --git a/sound/hda/codecs/side-codecs/cs35l41_hda_property.c b/sound/hda/codecs/side-codecs/cs35l41_hda_property.c
new file mode 100644
index 000000000000..d8249d997c2a
--- /dev/null
+++ b/sound/hda/codecs/side-codecs/cs35l41_hda_property.c
@@ -0,0 +1,578 @@
+// SPDX-License-Identifier: GPL-2.0
+//
+// CS35L41 ALSA HDA Property driver
+//
+// Copyright 2023 Cirrus Logic, Inc.
+//
+// Author: Stefan Binding <sbinding@opensource.cirrus.com>
+
+#include <linux/acpi.h>
+#include <linux/gpio/consumer.h>
+#include <linux/string.h>
+#include "cs35l41_hda_property.h"
+#include <linux/spi/spi.h>
+
+#define MAX_AMPS 4
+
+struct cs35l41_config {
+ const char *ssid;
+ int num_amps;
+ enum {
+ INTERNAL,
+ EXTERNAL
+ } boost_type;
+ u8 channel[MAX_AMPS];
+ int reset_gpio_index; /* -1 if no reset gpio */
+ int spkid_gpio_index; /* -1 if no spkid gpio */
+ int cs_gpio_index; /* -1 if no cs gpio, or cs-gpios already exists, max num amps == 2 */
+ int boost_ind_nanohenry; /* Required if boost_type == Internal */
+ int boost_peak_milliamp; /* Required if boost_type == Internal */
+ int boost_cap_microfarad; /* Required if boost_type == Internal */
+};
+
+static const struct cs35l41_config cs35l41_config_table[] = {
+ { "10251826", 2, EXTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, -1, -1, 0, 0, 0 },
+ { "1025182C", 2, EXTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, -1, -1, 0, 0, 0 },
+ { "10251844", 2, EXTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, -1, -1, 0, 0, 0 },
+ { "10280B27", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 1, 2, 0, 1000, 4500, 24 },
+ { "10280B28", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 1, 2, 0, 1000, 4500, 24 },
+ { "10280BEB", 2, EXTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 1, -1, 0, 0, 0, 0 },
+ { "10280C4D", 4, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, CS35L41_LEFT, CS35L41_RIGHT }, 0, 1, -1, 1000, 4500, 24 },
+/*
+ * Device 103C89C6 does have _DSD, however it is setup to use the wrong boost type.
+ * We can override the _DSD to correct the boost type here.
+ * Since this laptop has valid ACPI, we do not need to handle cs-gpios, since that already exists
+ * in the ACPI. The Reset GPIO is also valid, so we can use the Reset defined in _DSD.
+ */
+ { "103C89C6", 2, INTERNAL, { CS35L41_RIGHT, CS35L41_LEFT, 0, 0 }, -1, -1, -1, 1000, 4500, 24 },
+ { "103C8A28", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4100, 24 },
+ { "103C8A29", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4100, 24 },
+ { "103C8A2A", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4100, 24 },
+ { "103C8A2B", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4100, 24 },
+ { "103C8A2C", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4100, 24 },
+ { "103C8A2D", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4100, 24 },
+ { "103C8A2E", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4100, 24 },
+ { "103C8A30", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4100, 24 },
+ { "103C8A31", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4100, 24 },
+ { "103C8A6E", 4, EXTERNAL, { CS35L41_LEFT, CS35L41_LEFT, CS35L41_RIGHT, CS35L41_RIGHT }, 0, -1, -1, 0, 0, 0 },
+ { "103C8BB3", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4100, 24 },
+ { "103C8BB4", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4100, 24 },
+ { "103C8BDD", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4100, 24 },
+ { "103C8BDE", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4100, 24 },
+ { "103C8BDF", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4100, 24 },
+ { "103C8BE0", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4100, 24 },
+ { "103C8BE1", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4100, 24 },
+ { "103C8BE2", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4100, 24 },
+ { "103C8BE3", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4100, 24 },
+ { "103C8BE5", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4100, 24 },
+ { "103C8BE6", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4100, 24 },
+ { "103C8BE7", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4100, 24 },
+ { "103C8BE8", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4100, 24 },
+ { "103C8BE9", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4100, 24 },
+ { "103C8B3A", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4100, 24 },
+ { "103C8C15", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4000, 24 },
+ { "103C8C16", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4000, 24 },
+ { "103C8C17", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4000, 24 },
+ { "103C8C4D", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4100, 24 },
+ { "103C8C4E", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4100, 24 },
+ { "103C8C4F", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4100, 24 },
+ { "103C8C50", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4100, 24 },
+ { "103C8C51", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4100, 24 },
+ { "103C8CDD", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4100, 24 },
+ { "103C8CDE", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 3900, 24 },
+ { "104312AF", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 1, 2, 0, 1000, 4500, 24 },
+ { "10431433", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4500, 24 },
+ { "10431463", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4500, 24 },
+ { "10431473", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 1, -1, 0, 1000, 4500, 24 },
+ { "10431483", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 1, -1, 0, 1000, 4500, 24 },
+ { "10431493", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 1, 2, 0, 1000, 4500, 24 },
+ { "104314D3", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 1, 2, 0, 1000, 4500, 24 },
+ { "104314E3", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4500, 24 },
+ { "10431503", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4500, 24 },
+ { "10431533", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4500, 24 },
+ { "10431573", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 1, 2, 0, 1000, 4500, 24 },
+ { "10431663", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 1, -1, 0, 1000, 4500, 24 },
+ { "10431683", 2, EXTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 0, 0, 0 },
+ { "104316A3", 2, EXTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 1, 2, 0, 0, 0, 0 },
+ { "104316D3", 2, EXTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 1, 2, 0, 0, 0, 0 },
+ { "104316F3", 2, EXTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 1, 2, 0, 0, 0, 0 },
+ { "104317F3", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4500, 24 },
+ { "10431863", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 1, 2, 0, 1000, 4500, 24 },
+ { "104318D3", 2, EXTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 0, 0, 0 },
+ { "10431A83", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4500, 24 },
+ { "10431B93", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 1, 2, 0, 1000, 4500, 24 },
+ { "10431C9F", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 1, 2, 0, 1000, 4500, 24 },
+ { "10431CAF", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 1, 2, 0, 1000, 4500, 24 },
+ { "10431CCF", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 1, 2, 0, 1000, 4500, 24 },
+ { "10431CDF", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 1, 2, 0, 1000, 4500, 24 },
+ { "10431CEF", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 1, 2, 0, 1000, 4500, 24 },
+ { "10431D1F", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4500, 24 },
+ { "10431DA2", 2, EXTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 1, 2, 0, 0, 0, 0 },
+ { "10431E02", 2, EXTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 1, 2, 0, 0, 0, 0 },
+ { "10431E12", 2, EXTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 0, 0, 0 },
+ { "10431EE2", 2, EXTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, -1, -1, 0, 0, 0 },
+ { "10431F12", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 1000, 4500, 24 },
+ { "10431F1F", 2, EXTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 1, -1, 0, 0, 0, 0 },
+ { "10431F62", 2, EXTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 1, 2, 0, 0, 0, 0 },
+ { "10433A20", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 1, 2, 0, 1000, 4500, 24 },
+ { "10433A30", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 1, 2, 0, 1000, 4500, 24 },
+ { "10433A40", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 1, 2, 0, 1000, 4500, 24 },
+ { "10433A50", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 1, 2, 0, 1000, 4500, 24 },
+ { "10433A60", 2, INTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 1, 2, 0, 1000, 4500, 24 },
+ { "17AA3865", 2, EXTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, -1, -1, 0, 0, 0 },
+ { "17AA3866", 2, EXTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, -1, -1, 0, 0, 0 },
+ { "17AA386E", 2, EXTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 2, -1, 0, 0, 0 },
+ { "17AA386F", 2, EXTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, -1, -1, 0, 0, 0 },
+ { "17AA3877", 2, EXTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, -1, -1, 0, 0, 0 },
+ { "17AA3878", 2, EXTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, -1, -1, 0, 0, 0 },
+ { "17AA38A9", 2, EXTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 2, -1, 0, 0, 0 },
+ { "17AA38AB", 2, EXTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 2, -1, 0, 0, 0 },
+ { "17AA38B4", 2, EXTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 0, 0, 0 },
+ { "17AA38B5", 2, EXTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 0, 0, 0 },
+ { "17AA38B6", 2, EXTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 0, 0, 0 },
+ { "17AA38B7", 2, EXTERNAL, { CS35L41_LEFT, CS35L41_RIGHT, 0, 0 }, 0, 1, -1, 0, 0, 0 },
+ { "17AA38C7", 4, INTERNAL, { CS35L41_RIGHT, CS35L41_LEFT, CS35L41_RIGHT, CS35L41_LEFT }, 0, 2, -1, 1000, 4500, 24 },
+ { "17AA38C8", 4, INTERNAL, { CS35L41_RIGHT, CS35L41_LEFT, CS35L41_RIGHT, CS35L41_LEFT }, 0, 2, -1, 1000, 4500, 24 },
+ { "17AA38F9", 2, EXTERNAL, { CS35L41_RIGHT, CS35L41_LEFT, 0, 0 }, 0, 2, -1, 0, 0, 0 },
+ { "17AA38FA", 2, EXTERNAL, { CS35L41_RIGHT, CS35L41_LEFT, 0, 0 }, 0, 2, -1, 0, 0, 0 },
+ {}
+};
+
+static int cs35l41_add_gpios(struct cs35l41_hda *cs35l41, struct device *physdev, int reset_gpio,
+ int spkid_gpio, int cs_gpio_index, int num_amps)
+{
+ struct acpi_gpio_mapping *gpio_mapping = NULL;
+ struct acpi_gpio_params *reset_gpio_params = NULL;
+ struct acpi_gpio_params *spkid_gpio_params = NULL;
+ struct acpi_gpio_params *cs_gpio_params = NULL;
+ unsigned int num_entries = 0;
+ unsigned int reset_index, spkid_index, csgpio_index;
+ int i;
+
+ /*
+ * GPIO Mapping only needs to be done once, since it would be available for subsequent amps
+ */
+ if (cs35l41->dacpi->driver_gpios)
+ return 0;
+
+ if (reset_gpio >= 0) {
+ reset_index = num_entries;
+ num_entries++;
+ }
+
+ if (spkid_gpio >= 0) {
+ spkid_index = num_entries;
+ num_entries++;
+ }
+
+ if ((cs_gpio_index >= 0) && (num_amps == 2)) {
+ csgpio_index = num_entries;
+ num_entries++;
+ }
+
+ if (!num_entries)
+ return 0;
+
+ /* must include termination entry */
+ num_entries++;
+
+ gpio_mapping = devm_kcalloc(physdev, num_entries, sizeof(struct acpi_gpio_mapping),
+ GFP_KERNEL);
+
+ if (!gpio_mapping)
+ goto err;
+
+ if (reset_gpio >= 0) {
+ gpio_mapping[reset_index].name = "reset-gpios";
+ reset_gpio_params = devm_kcalloc(physdev, num_amps, sizeof(struct acpi_gpio_params),
+ GFP_KERNEL);
+ if (!reset_gpio_params)
+ goto err;
+
+ for (i = 0; i < num_amps; i++)
+ reset_gpio_params[i].crs_entry_index = reset_gpio;
+
+ gpio_mapping[reset_index].data = reset_gpio_params;
+ gpio_mapping[reset_index].size = num_amps;
+ }
+
+ if (spkid_gpio >= 0) {
+ gpio_mapping[spkid_index].name = "spk-id-gpios";
+ spkid_gpio_params = devm_kcalloc(physdev, num_amps, sizeof(struct acpi_gpio_params),
+ GFP_KERNEL);
+ if (!spkid_gpio_params)
+ goto err;
+
+ for (i = 0; i < num_amps; i++)
+ spkid_gpio_params[i].crs_entry_index = spkid_gpio;
+
+ gpio_mapping[spkid_index].data = spkid_gpio_params;
+ gpio_mapping[spkid_index].size = num_amps;
+ }
+
+ if ((cs_gpio_index >= 0) && (num_amps == 2)) {
+ gpio_mapping[csgpio_index].name = "cs-gpios";
+ /* only one GPIO CS is supported without using _DSD, obtained using index 0 */
+ cs_gpio_params = devm_kzalloc(physdev, sizeof(struct acpi_gpio_params), GFP_KERNEL);
+ if (!cs_gpio_params)
+ goto err;
+
+ cs_gpio_params->crs_entry_index = cs_gpio_index;
+
+ gpio_mapping[csgpio_index].data = cs_gpio_params;
+ gpio_mapping[csgpio_index].size = 1;
+ }
+
+ return devm_acpi_dev_add_driver_gpios(physdev, gpio_mapping);
+err:
+ devm_kfree(physdev, gpio_mapping);
+ devm_kfree(physdev, reset_gpio_params);
+ devm_kfree(physdev, spkid_gpio_params);
+ devm_kfree(physdev, cs_gpio_params);
+ return -ENOMEM;
+}
+
+static int generic_dsd_config(struct cs35l41_hda *cs35l41, struct device *physdev, int id,
+ const char *hid)
+{
+ struct cs35l41_hw_cfg *hw_cfg = &cs35l41->hw_cfg;
+ const struct cs35l41_config *cfg;
+ struct gpio_desc *cs_gpiod;
+ struct spi_device *spi;
+ bool dsd_found;
+ int ret;
+ int i;
+
+ for (cfg = cs35l41_config_table; cfg->ssid; cfg++) {
+ if (!strcasecmp(cfg->ssid, cs35l41->acpi_subsystem_id))
+ break;
+ }
+
+ if (!cfg->ssid)
+ return -ENOENT;
+
+ if (!cs35l41->dacpi || cs35l41->dacpi != ACPI_COMPANION(physdev)) {
+ dev_err(cs35l41->dev, "ACPI Device does not match, cannot override _DSD.\n");
+ return -ENODEV;
+ }
+
+ dev_info(cs35l41->dev, "Adding DSD properties for %s\n", cs35l41->acpi_subsystem_id);
+
+ dsd_found = acpi_dev_has_props(cs35l41->dacpi);
+
+ if (!dsd_found) {
+ ret = cs35l41_add_gpios(cs35l41, physdev, cfg->reset_gpio_index,
+ cfg->spkid_gpio_index, cfg->cs_gpio_index,
+ cfg->num_amps);
+ if (ret) {
+ dev_err(cs35l41->dev, "Error adding GPIO mapping: %d\n", ret);
+ return ret;
+ }
+ } else if (cfg->reset_gpio_index >= 0 || cfg->spkid_gpio_index >= 0) {
+ dev_warn(cs35l41->dev, "Cannot add Reset/Speaker ID/SPI CS GPIO Mapping, "
+ "_DSD already exists.\n");
+ }
+
+ if (cs35l41->control_bus == SPI) {
+ cs35l41->index = id;
+
+ /*
+ * Manually set the Chip Select for the second amp <cs_gpio_index> in the node.
+ * This is only supported for systems with 2 amps, since we cannot expand the
+ * default number of chip selects without using cs-gpios
+ * The CS GPIO must be set high prior to communicating with the first amp (which
+ * uses a native chip select), to ensure the second amp does not clash with the
+ * first.
+ */
+ if (IS_ENABLED(CONFIG_SPI) && cfg->cs_gpio_index >= 0) {
+ spi = to_spi_device(cs35l41->dev);
+
+ if (cfg->num_amps != 2) {
+ dev_warn(cs35l41->dev,
+ "Cannot update SPI CS, Number of Amps (%d) != 2\n",
+ cfg->num_amps);
+ } else if (dsd_found) {
+ dev_warn(cs35l41->dev,
+ "Cannot update SPI CS, _DSD already exists.\n");
+ } else {
+ /*
+ * This is obtained using driver_gpios, since only one GPIO for CS
+ * exists, this can be obtained using index 0.
+ */
+ cs_gpiod = gpiod_get_index(physdev, "cs", 0, GPIOD_OUT_LOW);
+ if (IS_ERR(cs_gpiod)) {
+ dev_err(cs35l41->dev,
+ "Unable to get Chip Select GPIO descriptor\n");
+ return PTR_ERR(cs_gpiod);
+ }
+ if (id == 1) {
+ spi_set_csgpiod(spi, 0, cs_gpiod);
+ cs35l41->cs_gpio = cs_gpiod;
+ } else {
+ gpiod_set_value_cansleep(cs_gpiod, true);
+ gpiod_put(cs_gpiod);
+ }
+ spi_setup(spi);
+ }
+ }
+ } else {
+ if (cfg->num_amps > 2)
+ /*
+ * i2c addresses for 3/4 amps are used in order: 0x40, 0x41, 0x42, 0x43,
+ * subtracting 0x40 would give zero-based index
+ */
+ cs35l41->index = id - 0x40;
+ else
+ /* i2c addr 0x40 for first amp (always), 0x41/0x42 for 2nd amp */
+ cs35l41->index = id == 0x40 ? 0 : 1;
+ }
+
+ cs35l41->reset_gpio = fwnode_gpiod_get_index(acpi_fwnode_handle(cs35l41->dacpi), "reset",
+ cs35l41->index, GPIOD_OUT_LOW,
+ "cs35l41-reset");
+ cs35l41->speaker_id = cs35l41_get_speaker_id(physdev, cs35l41->index, cfg->num_amps, -1);
+
+ hw_cfg->spk_pos = cfg->channel[cs35l41->index];
+
+ cs35l41->channel_index = 0;
+ for (i = 0; i < cs35l41->index; i++)
+ if (cfg->channel[i] == hw_cfg->spk_pos)
+ cs35l41->channel_index++;
+
+ if (cfg->boost_type == INTERNAL) {
+ hw_cfg->bst_type = CS35L41_INT_BOOST;
+ hw_cfg->bst_ind = cfg->boost_ind_nanohenry;
+ hw_cfg->bst_ipk = cfg->boost_peak_milliamp;
+ hw_cfg->bst_cap = cfg->boost_cap_microfarad;
+ hw_cfg->gpio1.func = CS35L41_NOT_USED;
+ hw_cfg->gpio1.valid = true;
+ } else {
+ hw_cfg->bst_type = CS35L41_EXT_BOOST;
+ hw_cfg->bst_ind = -1;
+ hw_cfg->bst_ipk = -1;
+ hw_cfg->bst_cap = -1;
+ hw_cfg->gpio1.func = CS35l41_VSPK_SWITCH;
+ hw_cfg->gpio1.valid = true;
+ }
+
+ hw_cfg->gpio2.func = CS35L41_INTERRUPT;
+ hw_cfg->gpio2.valid = true;
+ hw_cfg->valid = true;
+
+ return 0;
+}
+
+/*
+ * Systems 103C8C66, 103C8C67, 103C8C68, 103C8C6A use a dual speaker id system - each speaker has
+ * its own speaker id.
+ */
+static int hp_i2c_int_2amp_dual_spkid(struct cs35l41_hda *cs35l41, struct device *physdev, int id,
+ const char *hid)
+{
+ struct cs35l41_hw_cfg *hw_cfg = &cs35l41->hw_cfg;
+
+ /* If _DSD exists for this laptop, we cannot support it through here */
+ if (acpi_dev_has_props(cs35l41->dacpi))
+ return -ENOENT;
+
+ /* check I2C address to assign the index */
+ cs35l41->index = id == 0x40 ? 0 : 1;
+ cs35l41->channel_index = 0;
+ cs35l41->reset_gpio = gpiod_get_index(physdev, NULL, 0, GPIOD_OUT_HIGH);
+ if (cs35l41->index == 0)
+ cs35l41->speaker_id = cs35l41_get_speaker_id(physdev, 0, 0, 1);
+ else
+ cs35l41->speaker_id = cs35l41_get_speaker_id(physdev, 0, 0, 2);
+ hw_cfg->spk_pos = cs35l41->index;
+ hw_cfg->gpio2.func = CS35L41_INTERRUPT;
+ hw_cfg->gpio2.valid = true;
+ hw_cfg->valid = true;
+
+ hw_cfg->bst_type = CS35L41_INT_BOOST;
+ hw_cfg->bst_ind = 1000;
+ hw_cfg->bst_ipk = 4100;
+ hw_cfg->bst_cap = 24;
+ hw_cfg->gpio1.func = CS35L41_NOT_USED;
+ hw_cfg->gpio1.valid = true;
+
+ 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
+ * 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.
+ */
+static int lenovo_legion_no_acpi(struct cs35l41_hda *cs35l41, struct device *physdev, int id,
+ const char *hid)
+{
+ struct cs35l41_hw_cfg *hw_cfg = &cs35l41->hw_cfg;
+
+ /* check I2C address to assign the index */
+ cs35l41->index = id == 0x40 ? 0 : 1;
+ cs35l41->channel_index = 0;
+ cs35l41->reset_gpio = gpiod_get_index(physdev, NULL, 0, GPIOD_OUT_HIGH);
+ cs35l41->speaker_id = cs35l41_get_speaker_id(physdev, 0, 0, 2);
+ hw_cfg->spk_pos = cs35l41->index;
+ hw_cfg->gpio2.func = CS35L41_INTERRUPT;
+ hw_cfg->gpio2.valid = true;
+ hw_cfg->valid = true;
+
+ if (strcmp(hid, "CLSA0100") == 0) {
+ hw_cfg->bst_type = CS35L41_EXT_BOOST_NO_VSPK_SWITCH;
+ } else if (strcmp(hid, "CLSA0101") == 0) {
+ hw_cfg->bst_type = CS35L41_EXT_BOOST;
+ hw_cfg->gpio1.func = CS35l41_VSPK_SWITCH;
+ hw_cfg->gpio1.valid = true;
+ }
+
+ return 0;
+}
+
+static int missing_speaker_id_gpio2(struct cs35l41_hda *cs35l41, struct device *physdev, int id,
+ const char *hid)
+{
+ int ret;
+
+ ret = cs35l41_add_gpios(cs35l41, physdev, -1, 2, -1, 2);
+ if (ret) {
+ dev_err(cs35l41->dev, "Error adding GPIO mapping: %d\n", ret);
+ return ret;
+ }
+
+ return cs35l41_hda_parse_acpi(cs35l41, physdev, id);
+}
+
+struct cs35l41_prop_model {
+ const char *hid;
+ const char *ssid;
+ int (*add_prop)(struct cs35l41_hda *cs35l41, struct device *physdev, int id,
+ const char *hid);
+};
+
+static const struct cs35l41_prop_model cs35l41_prop_model_table[] = {
+ { "CLSA0100", NULL, lenovo_legion_no_acpi },
+ { "CLSA0101", NULL, lenovo_legion_no_acpi },
+ { "CSC3551", "10251826", generic_dsd_config },
+ { "CSC3551", "1025182C", generic_dsd_config },
+ { "CSC3551", "10251844", generic_dsd_config },
+ { "CSC3551", "10280B27", generic_dsd_config },
+ { "CSC3551", "10280B28", generic_dsd_config },
+ { "CSC3551", "10280BEB", generic_dsd_config },
+ { "CSC3551", "10280C4D", generic_dsd_config },
+ { "CSC3551", "103C89C6", generic_dsd_config },
+ { "CSC3551", "103C8A28", generic_dsd_config },
+ { "CSC3551", "103C8A29", generic_dsd_config },
+ { "CSC3551", "103C8A2A", generic_dsd_config },
+ { "CSC3551", "103C8A2B", generic_dsd_config },
+ { "CSC3551", "103C8A2C", generic_dsd_config },
+ { "CSC3551", "103C8A2D", generic_dsd_config },
+ { "CSC3551", "103C8A2E", generic_dsd_config },
+ { "CSC3551", "103C8A30", generic_dsd_config },
+ { "CSC3551", "103C8A31", generic_dsd_config },
+ { "CSC3551", "103C8A6E", generic_dsd_config },
+ { "CSC3551", "103C8BB3", generic_dsd_config },
+ { "CSC3551", "103C8BB4", generic_dsd_config },
+ { "CSC3551", "103C8BDD", generic_dsd_config },
+ { "CSC3551", "103C8BDE", generic_dsd_config },
+ { "CSC3551", "103C8BDF", generic_dsd_config },
+ { "CSC3551", "103C8BE0", generic_dsd_config },
+ { "CSC3551", "103C8BE1", generic_dsd_config },
+ { "CSC3551", "103C8BE2", generic_dsd_config },
+ { "CSC3551", "103C8BE3", generic_dsd_config },
+ { "CSC3551", "103C8BE5", generic_dsd_config },
+ { "CSC3551", "103C8BE6", generic_dsd_config },
+ { "CSC3551", "103C8BE7", generic_dsd_config },
+ { "CSC3551", "103C8BE8", generic_dsd_config },
+ { "CSC3551", "103C8BE9", generic_dsd_config },
+ { "CSC3551", "103C8B3A", generic_dsd_config },
+ { "CSC3551", "103C8C15", generic_dsd_config },
+ { "CSC3551", "103C8C16", generic_dsd_config },
+ { "CSC3551", "103C8C17", generic_dsd_config },
+ { "CSC3551", "103C8C4D", generic_dsd_config },
+ { "CSC3551", "103C8C4E", generic_dsd_config },
+ { "CSC3551", "103C8C4F", generic_dsd_config },
+ { "CSC3551", "103C8C50", generic_dsd_config },
+ { "CSC3551", "103C8C51", generic_dsd_config },
+ { "CSC3551", "103C8C66", hp_i2c_int_2amp_dual_spkid },
+ { "CSC3551", "103C8C67", hp_i2c_int_2amp_dual_spkid },
+ { "CSC3551", "103C8C68", hp_i2c_int_2amp_dual_spkid },
+ { "CSC3551", "103C8C6A", hp_i2c_int_2amp_dual_spkid },
+ { "CSC3551", "103C8CDD", generic_dsd_config },
+ { "CSC3551", "103C8CDE", generic_dsd_config },
+ { "CSC3551", "104312AF", generic_dsd_config },
+ { "CSC3551", "10431433", generic_dsd_config },
+ { "CSC3551", "10431463", generic_dsd_config },
+ { "CSC3551", "10431473", generic_dsd_config },
+ { "CSC3551", "10431483", generic_dsd_config },
+ { "CSC3551", "10431493", generic_dsd_config },
+ { "CSC3551", "104314D3", generic_dsd_config },
+ { "CSC3551", "104314E3", generic_dsd_config },
+ { "CSC3551", "10431503", generic_dsd_config },
+ { "CSC3551", "10431533", generic_dsd_config },
+ { "CSC3551", "10431573", generic_dsd_config },
+ { "CSC3551", "10431663", generic_dsd_config },
+ { "CSC3551", "10431683", generic_dsd_config },
+ { "CSC3551", "104316A3", generic_dsd_config },
+ { "CSC3551", "104316D3", generic_dsd_config },
+ { "CSC3551", "104316F3", generic_dsd_config },
+ { "CSC3551", "104317F3", generic_dsd_config },
+ { "CSC3551", "10431863", generic_dsd_config },
+ { "CSC3551", "104318D3", generic_dsd_config },
+ { "CSC3551", "10431A63", missing_speaker_id_gpio2 },
+ { "CSC3551", "10431A83", generic_dsd_config },
+ { "CSC3551", "10431B93", generic_dsd_config },
+ { "CSC3551", "10431C9F", generic_dsd_config },
+ { "CSC3551", "10431CAF", generic_dsd_config },
+ { "CSC3551", "10431CCF", generic_dsd_config },
+ { "CSC3551", "10431CDF", generic_dsd_config },
+ { "CSC3551", "10431CEF", generic_dsd_config },
+ { "CSC3551", "10431D1F", generic_dsd_config },
+ { "CSC3551", "10431DA2", generic_dsd_config },
+ { "CSC3551", "10431E02", generic_dsd_config },
+ { "CSC3551", "10431E12", generic_dsd_config },
+ { "CSC3551", "10431EE2", generic_dsd_config },
+ { "CSC3551", "10431F12", generic_dsd_config },
+ { "CSC3551", "10431F1F", generic_dsd_config },
+ { "CSC3551", "10431F62", generic_dsd_config },
+ { "CSC3551", "10433A20", generic_dsd_config },
+ { "CSC3551", "10433A30", generic_dsd_config },
+ { "CSC3551", "10433A40", generic_dsd_config },
+ { "CSC3551", "10433A50", generic_dsd_config },
+ { "CSC3551", "10433A60", generic_dsd_config },
+ { "CSC3551", "17AA3865", generic_dsd_config },
+ { "CSC3551", "17AA3866", generic_dsd_config },
+ { "CSC3551", "17AA386E", generic_dsd_config },
+ { "CSC3551", "17AA386F", generic_dsd_config },
+ { "CSC3551", "17AA3877", generic_dsd_config },
+ { "CSC3551", "17AA3878", generic_dsd_config },
+ { "CSC3551", "17AA38A9", generic_dsd_config },
+ { "CSC3551", "17AA38AB", generic_dsd_config },
+ { "CSC3551", "17AA38B4", generic_dsd_config },
+ { "CSC3551", "17AA38B5", generic_dsd_config },
+ { "CSC3551", "17AA38B6", generic_dsd_config },
+ { "CSC3551", "17AA38B7", generic_dsd_config },
+ { "CSC3551", "17AA38C7", generic_dsd_config },
+ { "CSC3551", "17AA38C8", generic_dsd_config },
+ { "CSC3551", "17AA38F9", generic_dsd_config },
+ { "CSC3551", "17AA38FA", generic_dsd_config },
+ {}
+};
+
+int cs35l41_add_dsd_properties(struct cs35l41_hda *cs35l41, struct device *physdev, int id,
+ const char *hid)
+{
+ const struct cs35l41_prop_model *model;
+
+ for (model = cs35l41_prop_model_table; model->hid; model++) {
+ if (!strcmp(model->hid, hid) &&
+ (!model->ssid ||
+ (cs35l41->acpi_subsystem_id &&
+ !strcasecmp(model->ssid, cs35l41->acpi_subsystem_id))))
+ return model->add_prop(cs35l41, physdev, id, hid);
+ }
+
+ return -ENOENT;
+}
diff --git a/sound/hda/codecs/side-codecs/cs35l41_hda_property.h b/sound/hda/codecs/side-codecs/cs35l41_hda_property.h
new file mode 100644
index 000000000000..fd834042e2fd
--- /dev/null
+++ b/sound/hda/codecs/side-codecs/cs35l41_hda_property.h
@@ -0,0 +1,18 @@
+/* SPDX-License-Identifier: GPL-2.0
+ *
+ * CS35L41 ALSA HDA Property driver
+ *
+ * Copyright 2023 Cirrus Logic, Inc.
+ *
+ * Author: Stefan Binding <sbinding@opensource.cirrus.com>
+ */
+
+#ifndef CS35L41_HDA_PROP_H
+#define CS35L41_HDA_PROP_H
+
+#include <linux/device.h>
+#include "cs35l41_hda.h"
+
+int cs35l41_add_dsd_properties(struct cs35l41_hda *cs35l41, struct device *physdev, int id,
+ const char *hid);
+#endif /* CS35L41_HDA_PROP_H */
diff --git a/sound/hda/codecs/side-codecs/cs35l41_hda_spi.c b/sound/hda/codecs/side-codecs/cs35l41_hda_spi.c
new file mode 100644
index 000000000000..2acbaf8467a0
--- /dev/null
+++ b/sound/hda/codecs/side-codecs/cs35l41_hda_spi.c
@@ -0,0 +1,64 @@
+// SPDX-License-Identifier: GPL-2.0
+//
+// CS35l41 HDA SPI driver
+//
+// Copyright 2021 Cirrus Logic, Inc.
+//
+// Author: Lucas Tanure <tanureal@opensource.cirrus.com>
+
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/spi/spi.h>
+
+#include "cs35l41_hda.h"
+
+static int cs35l41_hda_spi_probe(struct spi_device *spi)
+{
+ const char *device_name;
+
+ /*
+ * Compare against the device name so it works for SPI, normal ACPI
+ * and for ACPI by serial-multi-instantiate matching cases.
+ */
+ if (strstr(dev_name(&spi->dev), "CSC3551"))
+ device_name = "CSC3551";
+ else
+ return -ENODEV;
+
+ return cs35l41_hda_probe(&spi->dev, device_name, spi_get_chipselect(spi, 0), spi->irq,
+ devm_regmap_init_spi(spi, &cs35l41_regmap_spi), SPI);
+}
+
+static void cs35l41_hda_spi_remove(struct spi_device *spi)
+{
+ cs35l41_hda_remove(&spi->dev);
+}
+
+static const struct spi_device_id cs35l41_hda_spi_id[] = {
+ { "cs35l41-hda", 0 },
+ {}
+};
+MODULE_DEVICE_TABLE(spi, cs35l41_hda_spi_id);
+
+static const struct acpi_device_id cs35l41_acpi_hda_match[] = {
+ { "CSC3551", 0 },
+ {}
+};
+MODULE_DEVICE_TABLE(acpi, cs35l41_acpi_hda_match);
+
+static struct spi_driver cs35l41_spi_driver = {
+ .driver = {
+ .name = "cs35l41-hda",
+ .acpi_match_table = cs35l41_acpi_hda_match,
+ .pm = &cs35l41_hda_pm_ops,
+ },
+ .id_table = cs35l41_hda_spi_id,
+ .probe = cs35l41_hda_spi_probe,
+ .remove = cs35l41_hda_spi_remove,
+};
+module_spi_driver(cs35l41_spi_driver);
+
+MODULE_DESCRIPTION("HDA CS35L41 driver");
+MODULE_IMPORT_NS("SND_HDA_SCODEC_CS35L41");
+MODULE_AUTHOR("Lucas Tanure <tanureal@opensource.cirrus.com>");
+MODULE_LICENSE("GPL");
diff --git a/sound/hda/codecs/side-codecs/cs35l56_hda.c b/sound/hda/codecs/side-codecs/cs35l56_hda.c
new file mode 100644
index 000000000000..36fa62a41984
--- /dev/null
+++ b/sound/hda/codecs/side-codecs/cs35l56_hda.c
@@ -0,0 +1,1181 @@
+// SPDX-License-Identifier: GPL-2.0-only
+//
+// HDA audio driver for Cirrus Logic CS35L56 smart amp
+//
+// Copyright (C) 2023 Cirrus Logic, Inc. and
+// Cirrus Logic International Semiconductor Ltd.
+//
+
+#include <linux/acpi.h>
+#include <linux/debugfs.h>
+#include <linux/gpio/consumer.h>
+#include <linux/module.h>
+#include <linux/pm_runtime.h>
+#include <linux/regmap.h>
+#include <linux/slab.h>
+#include <sound/core.h>
+#include <sound/cs-amp-lib.h>
+#include <sound/hda_codec.h>
+#include <sound/tlv.h>
+#include "cirrus_scodec.h"
+#include "cs35l56_hda.h"
+#include "hda_component.h"
+#include "../generic.h"
+
+ /*
+ * The cs35l56_hda_dai_config[] reg sequence configures the device as
+ * ASP1_BCLK_FREQ = 3.072 MHz
+ * ASP1_RX_WIDTH = 32 cycles per slot, ASP1_TX_WIDTH = 32 cycles per slot, ASP1_FMT = I2S
+ * ASP1_DOUT_HIZ_CONTROL = Hi-Z during unused timeslots
+ * ASP1_RX_WL = 24 bits per sample
+ * ASP1_TX_WL = 24 bits per sample
+ * ASP1_RXn_EN 1..3 and ASP1_TXn_EN 1..4 disabled
+ *
+ * Override any Windows-specific mixer settings applied by the firmware.
+ */
+static const struct reg_sequence cs35l56_hda_dai_config[] = {
+ { CS35L56_ASP1_CONTROL1, 0x00000021 },
+ { CS35L56_ASP1_CONTROL2, 0x20200200 },
+ { CS35L56_ASP1_CONTROL3, 0x00000003 },
+ { CS35L56_ASP1_FRAME_CONTROL1, 0x03020100 },
+ { CS35L56_ASP1_FRAME_CONTROL5, 0x00020100 },
+ { CS35L56_ASP1_DATA_CONTROL5, 0x00000018 },
+ { CS35L56_ASP1_DATA_CONTROL1, 0x00000018 },
+ { CS35L56_ASP1_ENABLES1, 0x00000000 },
+ { CS35L56_ASP1TX1_INPUT, 0x00000018 },
+ { CS35L56_ASP1TX2_INPUT, 0x00000019 },
+ { CS35L56_ASP1TX3_INPUT, 0x00000020 },
+ { CS35L56_ASP1TX4_INPUT, 0x00000028 },
+
+};
+
+static void cs35l56_hda_wait_dsp_ready(struct cs35l56_hda *cs35l56)
+{
+ /* Wait for patching to complete */
+ flush_work(&cs35l56->dsp_work);
+}
+
+static void cs35l56_hda_play(struct cs35l56_hda *cs35l56)
+{
+ unsigned int val;
+ int ret;
+
+ cs35l56_hda_wait_dsp_ready(cs35l56);
+
+ pm_runtime_get_sync(cs35l56->base.dev);
+ ret = cs35l56_mbox_send(&cs35l56->base, CS35L56_MBOX_CMD_AUDIO_PLAY);
+ if (ret == 0) {
+ /* Wait for firmware to enter PS0 power state */
+ ret = regmap_read_poll_timeout(cs35l56->base.regmap,
+ cs35l56->base.fw_reg->transducer_actual_ps,
+ val, (val == CS35L56_PS0),
+ CS35L56_PS0_POLL_US,
+ CS35L56_PS0_TIMEOUT_US);
+ if (ret)
+ dev_warn(cs35l56->base.dev, "PS0 wait failed: %d\n", ret);
+ }
+ regmap_set_bits(cs35l56->base.regmap, CS35L56_ASP1_ENABLES1,
+ BIT(CS35L56_ASP_RX1_EN_SHIFT) | BIT(CS35L56_ASP_RX2_EN_SHIFT) |
+ cs35l56->asp_tx_mask);
+ cs35l56->playing = true;
+}
+
+static void cs35l56_hda_pause(struct cs35l56_hda *cs35l56)
+{
+ cs35l56->playing = false;
+ cs35l56_mbox_send(&cs35l56->base, CS35L56_MBOX_CMD_AUDIO_PAUSE);
+ regmap_clear_bits(cs35l56->base.regmap, CS35L56_ASP1_ENABLES1,
+ BIT(CS35L56_ASP_RX1_EN_SHIFT) | BIT(CS35L56_ASP_RX2_EN_SHIFT) |
+ BIT(CS35L56_ASP_TX1_EN_SHIFT) | BIT(CS35L56_ASP_TX2_EN_SHIFT) |
+ BIT(CS35L56_ASP_TX3_EN_SHIFT) | BIT(CS35L56_ASP_TX4_EN_SHIFT));
+
+ pm_runtime_put_autosuspend(cs35l56->base.dev);
+}
+
+static void cs35l56_hda_playback_hook(struct device *dev, int action)
+{
+ struct cs35l56_hda *cs35l56 = dev_get_drvdata(dev);
+
+ dev_dbg(cs35l56->base.dev, "%s()%d: action: %d\n", __func__, __LINE__, action);
+
+ switch (action) {
+ case HDA_GEN_PCM_ACT_PREPARE:
+ if (cs35l56->playing)
+ break;
+
+ /* If we're suspended: flag that resume should start playback */
+ if (cs35l56->suspended) {
+ cs35l56->playing = true;
+ break;
+ }
+
+ cs35l56_hda_play(cs35l56);
+ break;
+ case HDA_GEN_PCM_ACT_CLEANUP:
+ if (!cs35l56->playing)
+ break;
+
+ cs35l56_hda_pause(cs35l56);
+ break;
+ default:
+ break;
+ }
+}
+
+static int cs35l56_hda_runtime_suspend(struct device *dev)
+{
+ struct cs35l56_hda *cs35l56 = dev_get_drvdata(dev);
+
+ if (cs35l56->cs_dsp.booted)
+ cs_dsp_stop(&cs35l56->cs_dsp);
+
+ return cs35l56_runtime_suspend_common(&cs35l56->base);
+}
+
+static int cs35l56_hda_runtime_resume(struct device *dev)
+{
+ struct cs35l56_hda *cs35l56 = dev_get_drvdata(dev);
+ int ret;
+
+ ret = cs35l56_runtime_resume_common(&cs35l56->base, false);
+ if (ret < 0)
+ return ret;
+
+ if (cs35l56->cs_dsp.booted) {
+ ret = cs_dsp_run(&cs35l56->cs_dsp);
+ if (ret) {
+ dev_dbg(cs35l56->base.dev, "%s: cs_dsp_run ret %d\n", __func__, ret);
+ goto err;
+ }
+ }
+
+ return 0;
+
+err:
+ cs35l56_mbox_send(&cs35l56->base, CS35L56_MBOX_CMD_ALLOW_AUTO_HIBERNATE);
+ regmap_write(cs35l56->base.regmap, CS35L56_DSP_VIRTUAL1_MBOX_1,
+ CS35L56_MBOX_CMD_HIBERNATE_NOW);
+
+ regcache_cache_only(cs35l56->base.regmap, true);
+
+ return ret;
+}
+
+static int cs35l56_hda_mixer_info(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_ENUMERATED;
+ uinfo->count = 1;
+ uinfo->value.enumerated.items = CS35L56_NUM_INPUT_SRC;
+ if (uinfo->value.enumerated.item >= CS35L56_NUM_INPUT_SRC)
+ uinfo->value.enumerated.item = CS35L56_NUM_INPUT_SRC - 1;
+ strscpy(uinfo->value.enumerated.name, cs35l56_tx_input_texts[uinfo->value.enumerated.item],
+ sizeof(uinfo->value.enumerated.name));
+
+ return 0;
+}
+
+static int cs35l56_hda_mixer_get(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct cs35l56_hda *cs35l56 = snd_kcontrol_chip(kcontrol);
+ unsigned int reg_val;
+ int i;
+
+ cs35l56_hda_wait_dsp_ready(cs35l56);
+
+ regmap_read(cs35l56->base.regmap, kcontrol->private_value, &reg_val);
+ reg_val &= CS35L56_ASP_TXn_SRC_MASK;
+
+ for (i = 0; i < CS35L56_NUM_INPUT_SRC; ++i) {
+ if (cs35l56_tx_input_values[i] == reg_val) {
+ ucontrol->value.enumerated.item[0] = i;
+ break;
+ }
+ }
+
+ return 0;
+}
+
+static int cs35l56_hda_mixer_put(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct cs35l56_hda *cs35l56 = snd_kcontrol_chip(kcontrol);
+ unsigned int item = ucontrol->value.enumerated.item[0];
+ bool changed;
+
+ if (item >= CS35L56_NUM_INPUT_SRC)
+ return -EINVAL;
+
+ cs35l56_hda_wait_dsp_ready(cs35l56);
+
+ regmap_update_bits_check(cs35l56->base.regmap, kcontrol->private_value,
+ CS35L56_INPUT_MASK, cs35l56_tx_input_values[item],
+ &changed);
+
+ return changed;
+}
+
+static int cs35l56_hda_posture_info(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = CS35L56_MAIN_POSTURE_MIN;
+ uinfo->value.integer.max = CS35L56_MAIN_POSTURE_MAX;
+ return 0;
+}
+
+static int cs35l56_hda_posture_get(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct cs35l56_hda *cs35l56 = snd_kcontrol_chip(kcontrol);
+ unsigned int pos;
+ int ret;
+
+ cs35l56_hda_wait_dsp_ready(cs35l56);
+
+ ret = regmap_read(cs35l56->base.regmap,
+ cs35l56->base.fw_reg->posture_number, &pos);
+ if (ret)
+ return ret;
+
+ ucontrol->value.integer.value[0] = pos;
+
+ return 0;
+}
+
+static int cs35l56_hda_posture_put(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct cs35l56_hda *cs35l56 = snd_kcontrol_chip(kcontrol);
+ unsigned long pos = ucontrol->value.integer.value[0];
+ bool changed;
+ int ret;
+
+ if ((pos < CS35L56_MAIN_POSTURE_MIN) ||
+ (pos > CS35L56_MAIN_POSTURE_MAX))
+ return -EINVAL;
+
+ cs35l56_hda_wait_dsp_ready(cs35l56);
+
+ ret = regmap_update_bits_check(cs35l56->base.regmap, cs35l56->base.fw_reg->posture_number,
+ CS35L56_MAIN_POSTURE_MASK, pos, &changed);
+ if (ret)
+ return ret;
+
+ return changed;
+}
+
+static const struct {
+ const char *name;
+ unsigned int reg;
+} cs35l56_hda_mixer_controls[] = {
+ { "ASP1 TX1 Source", CS35L56_ASP1TX1_INPUT },
+ { "ASP1 TX2 Source", CS35L56_ASP1TX2_INPUT },
+ { "ASP1 TX3 Source", CS35L56_ASP1TX3_INPUT },
+ { "ASP1 TX4 Source", CS35L56_ASP1TX4_INPUT },
+};
+
+static const DECLARE_TLV_DB_SCALE(cs35l56_hda_vol_tlv, -10000, 25, 0);
+
+static int cs35l56_hda_vol_info(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.step = 1;
+ uinfo->value.integer.min = 0;
+ uinfo->value.integer.max = CS35L56_MAIN_RENDER_USER_VOLUME_MAX -
+ CS35L56_MAIN_RENDER_USER_VOLUME_MIN;
+
+ return 0;
+}
+
+static int cs35l56_hda_vol_get(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct cs35l56_hda *cs35l56 = snd_kcontrol_chip(kcontrol);
+ unsigned int raw_vol;
+ int vol;
+ int ret;
+
+ cs35l56_hda_wait_dsp_ready(cs35l56);
+
+ ret = regmap_read(cs35l56->base.regmap, cs35l56->base.fw_reg->user_volume, &raw_vol);
+
+ if (ret)
+ return ret;
+
+ vol = (s16)(raw_vol & 0xFFFF);
+ vol >>= CS35L56_MAIN_RENDER_USER_VOLUME_SHIFT;
+
+ if (vol & BIT(CS35L56_MAIN_RENDER_USER_VOLUME_SIGNBIT))
+ vol |= ~((int)(BIT(CS35L56_MAIN_RENDER_USER_VOLUME_SIGNBIT) - 1));
+
+ ucontrol->value.integer.value[0] = vol - CS35L56_MAIN_RENDER_USER_VOLUME_MIN;
+
+ return 0;
+}
+
+static int cs35l56_hda_vol_put(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct cs35l56_hda *cs35l56 = snd_kcontrol_chip(kcontrol);
+ long vol = ucontrol->value.integer.value[0];
+ unsigned int raw_vol;
+ bool changed;
+ int ret;
+
+ if ((vol < 0) || (vol > (CS35L56_MAIN_RENDER_USER_VOLUME_MAX -
+ CS35L56_MAIN_RENDER_USER_VOLUME_MIN)))
+ return -EINVAL;
+
+ raw_vol = (vol + CS35L56_MAIN_RENDER_USER_VOLUME_MIN) <<
+ CS35L56_MAIN_RENDER_USER_VOLUME_SHIFT;
+
+ cs35l56_hda_wait_dsp_ready(cs35l56);
+
+ ret = regmap_update_bits_check(cs35l56->base.regmap, cs35l56->base.fw_reg->user_volume,
+ CS35L56_MAIN_RENDER_USER_VOLUME_MASK, raw_vol, &changed);
+ if (ret)
+ return ret;
+
+ return changed;
+}
+
+static void cs35l56_hda_create_controls(struct cs35l56_hda *cs35l56)
+{
+ struct snd_kcontrol_new ctl_template = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE,
+ .info = cs35l56_hda_posture_info,
+ .get = cs35l56_hda_posture_get,
+ .put = cs35l56_hda_posture_put,
+ };
+ char name[64];
+ int i;
+
+ snprintf(name, sizeof(name), "%s Posture Number", cs35l56->amp_name);
+ ctl_template.name = name;
+ cs35l56->posture_ctl = snd_ctl_new1(&ctl_template, cs35l56);
+ if (snd_ctl_add(cs35l56->codec->card, cs35l56->posture_ctl))
+ dev_err(cs35l56->base.dev, "Failed to add KControl: %s\n", ctl_template.name);
+
+ /* Mixer controls */
+ ctl_template.info = cs35l56_hda_mixer_info;
+ ctl_template.get = cs35l56_hda_mixer_get;
+ ctl_template.put = cs35l56_hda_mixer_put;
+
+ BUILD_BUG_ON(ARRAY_SIZE(cs35l56->mixer_ctl) != ARRAY_SIZE(cs35l56_hda_mixer_controls));
+
+ for (i = 0; i < ARRAY_SIZE(cs35l56_hda_mixer_controls); ++i) {
+ snprintf(name, sizeof(name), "%s %s", cs35l56->amp_name,
+ cs35l56_hda_mixer_controls[i].name);
+ ctl_template.private_value = cs35l56_hda_mixer_controls[i].reg;
+ cs35l56->mixer_ctl[i] = snd_ctl_new1(&ctl_template, cs35l56);
+ if (snd_ctl_add(cs35l56->codec->card, cs35l56->mixer_ctl[i])) {
+ dev_err(cs35l56->base.dev, "Failed to add KControl: %s\n",
+ ctl_template.name);
+ }
+ }
+
+ ctl_template.info = cs35l56_hda_vol_info;
+ ctl_template.get = cs35l56_hda_vol_get;
+ ctl_template.put = cs35l56_hda_vol_put;
+ ctl_template.access = (SNDRV_CTL_ELEM_ACCESS_READWRITE | SNDRV_CTL_ELEM_ACCESS_TLV_READ);
+ ctl_template.tlv.p = cs35l56_hda_vol_tlv;
+ snprintf(name, sizeof(name), "%s Speaker Playback Volume", cs35l56->amp_name);
+ ctl_template.name = name;
+ cs35l56->volume_ctl = snd_ctl_new1(&ctl_template, cs35l56);
+ if (snd_ctl_add(cs35l56->codec->card, cs35l56->volume_ctl))
+ dev_err(cs35l56->base.dev, "Failed to add KControl: %s\n", ctl_template.name);
+}
+
+static void cs35l56_hda_remove_controls(struct cs35l56_hda *cs35l56)
+{
+ int i;
+
+ for (i = ARRAY_SIZE(cs35l56->mixer_ctl) - 1; i >= 0; i--)
+ snd_ctl_remove(cs35l56->codec->card, cs35l56->mixer_ctl[i]);
+
+ snd_ctl_remove(cs35l56->codec->card, cs35l56->posture_ctl);
+ snd_ctl_remove(cs35l56->codec->card, cs35l56->volume_ctl);
+}
+
+static const struct cs_dsp_client_ops cs35l56_hda_client_ops = {
+ /* cs_dsp requires the client to provide this even if it is empty */
+};
+
+static int cs35l56_hda_request_firmware_file(struct cs35l56_hda *cs35l56,
+ const struct firmware **firmware, char **filename,
+ const char *base_name, const char *system_name,
+ const char *amp_name,
+ const char *filetype)
+{
+ char *s, c;
+ int ret = 0;
+
+ if (system_name && amp_name)
+ *filename = kasprintf(GFP_KERNEL, "%s-%s-%s.%s", base_name,
+ system_name, amp_name, filetype);
+ else if (system_name)
+ *filename = kasprintf(GFP_KERNEL, "%s-%s.%s", base_name,
+ system_name, filetype);
+ else
+ *filename = kasprintf(GFP_KERNEL, "%s.%s", base_name, filetype);
+
+ if (!*filename)
+ return -ENOMEM;
+
+ /*
+ * Make sure that filename is lower-case and any non alpha-numeric
+ * characters except full stop and forward slash are replaced with
+ * hyphens.
+ */
+ s = *filename;
+ while (*s) {
+ c = *s;
+ if (isalnum(c))
+ *s = tolower(c);
+ else if (c != '.' && c != '/')
+ *s = '-';
+ s++;
+ }
+
+ ret = firmware_request_nowarn(firmware, *filename, cs35l56->base.dev);
+ if (ret) {
+ dev_dbg(cs35l56->base.dev, "Failed to request '%s'\n", *filename);
+ kfree(*filename);
+ *filename = NULL;
+ return ret;
+ }
+
+ dev_dbg(cs35l56->base.dev, "Found '%s'\n", *filename);
+
+ return 0;
+}
+
+static void cs35l56_hda_request_firmware_files(struct cs35l56_hda *cs35l56,
+ unsigned int preloaded_fw_ver,
+ const struct firmware **wmfw_firmware,
+ char **wmfw_filename,
+ const struct firmware **coeff_firmware,
+ char **coeff_filename)
+{
+ const char *system_name = cs35l56->system_name;
+ const char *amp_name = cs35l56->amp_name;
+ char base_name[37];
+ int ret;
+
+ if (preloaded_fw_ver) {
+ snprintf(base_name, sizeof(base_name),
+ "cirrus/cs35l%02x-%02x%s-%06x-dsp1-misc",
+ cs35l56->base.type,
+ cs35l56->base.rev,
+ cs35l56->base.secured ? "-s" : "",
+ preloaded_fw_ver & 0xffffff);
+ } else {
+ snprintf(base_name, sizeof(base_name),
+ "cirrus/cs35l%02x-%02x%s-dsp1-misc",
+ cs35l56->base.type,
+ cs35l56->base.rev,
+ cs35l56->base.secured ? "-s" : "");
+ }
+
+ if (system_name && amp_name) {
+ if (!cs35l56_hda_request_firmware_file(cs35l56, wmfw_firmware, wmfw_filename,
+ base_name, system_name, amp_name, "wmfw")) {
+ cs35l56_hda_request_firmware_file(cs35l56, coeff_firmware, coeff_filename,
+ base_name, system_name, amp_name, "bin");
+ return;
+ }
+ }
+
+ if (system_name) {
+ if (!cs35l56_hda_request_firmware_file(cs35l56, wmfw_firmware, wmfw_filename,
+ base_name, system_name, NULL, "wmfw")) {
+ if (amp_name)
+ cs35l56_hda_request_firmware_file(cs35l56,
+ coeff_firmware, coeff_filename,
+ base_name, system_name,
+ amp_name, "bin");
+ if (!*coeff_firmware)
+ cs35l56_hda_request_firmware_file(cs35l56,
+ coeff_firmware, coeff_filename,
+ base_name, system_name,
+ NULL, "bin");
+ return;
+ }
+
+ /*
+ * Check for system-specific bin files without wmfw before
+ * falling back to generic firmware
+ */
+ if (amp_name)
+ cs35l56_hda_request_firmware_file(cs35l56, coeff_firmware, coeff_filename,
+ base_name, system_name, amp_name, "bin");
+ if (!*coeff_firmware)
+ cs35l56_hda_request_firmware_file(cs35l56, coeff_firmware, coeff_filename,
+ base_name, system_name, NULL, "bin");
+
+ if (*coeff_firmware)
+ return;
+ }
+
+ ret = cs35l56_hda_request_firmware_file(cs35l56, wmfw_firmware, wmfw_filename,
+ base_name, NULL, NULL, "wmfw");
+ if (!ret) {
+ cs35l56_hda_request_firmware_file(cs35l56, coeff_firmware, coeff_filename,
+ base_name, NULL, NULL, "bin");
+ return;
+ }
+
+ if (!*coeff_firmware)
+ cs35l56_hda_request_firmware_file(cs35l56, coeff_firmware, coeff_filename,
+ base_name, NULL, NULL, "bin");
+}
+
+static void cs35l56_hda_release_firmware_files(const struct firmware *wmfw_firmware,
+ char *wmfw_filename,
+ const struct firmware *coeff_firmware,
+ char *coeff_filename)
+{
+ release_firmware(wmfw_firmware);
+ kfree(wmfw_filename);
+
+ release_firmware(coeff_firmware);
+ kfree(coeff_filename);
+}
+
+static void cs35l56_hda_apply_calibration(struct cs35l56_hda *cs35l56)
+{
+ int ret;
+
+ if (!cs35l56->base.cal_data_valid || cs35l56->base.secured)
+ return;
+
+ ret = cs_amp_write_cal_coeffs(&cs35l56->cs_dsp,
+ &cs35l56_calibration_controls,
+ &cs35l56->base.cal_data);
+ if (ret < 0)
+ dev_warn(cs35l56->base.dev, "Failed to write calibration: %d\n", ret);
+ else
+ dev_info(cs35l56->base.dev, "Calibration applied\n");
+}
+
+static void cs35l56_hda_fw_load(struct cs35l56_hda *cs35l56)
+{
+ const struct firmware *coeff_firmware = NULL;
+ const struct firmware *wmfw_firmware = NULL;
+ char *coeff_filename = NULL;
+ char *wmfw_filename = NULL;
+ unsigned int preloaded_fw_ver;
+ bool firmware_missing;
+ int ret;
+
+ /*
+ * Prepare for a new DSP power-up. If the DSP has had firmware
+ * downloaded previously then it needs to be powered down so that it
+ * can be updated.
+ */
+ if (cs35l56->base.fw_patched)
+ cs_dsp_power_down(&cs35l56->cs_dsp);
+
+ cs35l56->base.fw_patched = false;
+
+ ret = pm_runtime_resume_and_get(cs35l56->base.dev);
+ if (ret < 0) {
+ dev_err(cs35l56->base.dev, "Failed to resume and get %d\n", ret);
+ return;
+ }
+
+ /*
+ * The firmware can only be upgraded if it is currently running
+ * from the built-in ROM. If not, the wmfw/bin must be for the
+ * version of firmware that is running on the chip.
+ */
+ ret = cs35l56_read_prot_status(&cs35l56->base, &firmware_missing, &preloaded_fw_ver);
+ if (ret)
+ goto err_pm_put;
+
+ if (firmware_missing)
+ preloaded_fw_ver = 0;
+
+ cs35l56_hda_request_firmware_files(cs35l56, preloaded_fw_ver,
+ &wmfw_firmware, &wmfw_filename,
+ &coeff_firmware, &coeff_filename);
+
+ /*
+ * If the BIOS didn't patch the firmware a bin file is mandatory to
+ * enable the ASP·
+ */
+ if (!coeff_firmware && firmware_missing) {
+ dev_err(cs35l56->base.dev, ".bin file required but not found\n");
+ goto err_fw_release;
+ }
+
+ mutex_lock(&cs35l56->base.irq_lock);
+
+ /*
+ * If the firmware hasn't been patched it must be shutdown before
+ * doing a full patch and reset afterwards. If it is already
+ * running a patched version the firmware files only contain
+ * tunings and we can use the lower cost reinit sequence instead.
+ */
+ if (firmware_missing && (wmfw_firmware || coeff_firmware)) {
+ ret = cs35l56_firmware_shutdown(&cs35l56->base);
+ if (ret)
+ goto err;
+ }
+
+ ret = cs_dsp_power_up(&cs35l56->cs_dsp, wmfw_firmware, wmfw_filename,
+ coeff_firmware, coeff_filename, "misc");
+ if (ret) {
+ dev_dbg(cs35l56->base.dev, "%s: cs_dsp_power_up ret %d\n", __func__, ret);
+ goto err;
+ }
+
+ if (wmfw_filename)
+ dev_dbg(cs35l56->base.dev, "Loaded WMFW Firmware: %s\n", wmfw_filename);
+
+ if (coeff_filename)
+ dev_dbg(cs35l56->base.dev, "Loaded Coefficients: %s\n", coeff_filename);
+
+ /* If we downloaded firmware, reset the device and wait for it to boot */
+ if (firmware_missing && (wmfw_firmware || coeff_firmware)) {
+ cs35l56_system_reset(&cs35l56->base, false);
+ regcache_mark_dirty(cs35l56->base.regmap);
+ ret = cs35l56_wait_for_firmware_boot(&cs35l56->base);
+ if (ret)
+ goto err_powered_up;
+
+ regcache_cache_only(cs35l56->base.regmap, false);
+ }
+
+ /* Disable auto-hibernate so that runtime_pm has control */
+ ret = cs35l56_mbox_send(&cs35l56->base, CS35L56_MBOX_CMD_PREVENT_AUTO_HIBERNATE);
+ if (ret)
+ goto err_powered_up;
+
+ regcache_sync(cs35l56->base.regmap);
+
+ regmap_clear_bits(cs35l56->base.regmap,
+ cs35l56->base.fw_reg->prot_sts,
+ CS35L56_FIRMWARE_MISSING);
+ cs35l56->base.fw_patched = true;
+
+ ret = cs_dsp_run(&cs35l56->cs_dsp);
+ if (ret)
+ dev_dbg(cs35l56->base.dev, "%s: cs_dsp_run ret %d\n", __func__, ret);
+
+ cs35l56_hda_apply_calibration(cs35l56);
+ ret = cs35l56_mbox_send(&cs35l56->base, CS35L56_MBOX_CMD_AUDIO_REINIT);
+ if (ret)
+ cs_dsp_stop(&cs35l56->cs_dsp);
+
+ cs35l56_log_tuning(&cs35l56->base, &cs35l56->cs_dsp);
+
+err_powered_up:
+ if (!cs35l56->base.fw_patched)
+ cs_dsp_power_down(&cs35l56->cs_dsp);
+err:
+ mutex_unlock(&cs35l56->base.irq_lock);
+err_fw_release:
+ cs35l56_hda_release_firmware_files(wmfw_firmware, wmfw_filename,
+ coeff_firmware, coeff_filename);
+err_pm_put:
+ pm_runtime_put(cs35l56->base.dev);
+}
+
+static void cs35l56_hda_dsp_work(struct work_struct *work)
+{
+ struct cs35l56_hda *cs35l56 = container_of(work, struct cs35l56_hda, dsp_work);
+
+ cs35l56_hda_fw_load(cs35l56);
+}
+
+static int cs35l56_hda_bind(struct device *dev, struct device *master, void *master_data)
+{
+ struct cs35l56_hda *cs35l56 = dev_get_drvdata(dev);
+ struct hda_component_parent *parent = master_data;
+ struct hda_component *comp;
+
+ comp = hda_component_from_index(parent, cs35l56->index);
+ if (!comp)
+ return -EINVAL;
+
+ if (comp->dev)
+ return -EBUSY;
+
+ comp->dev = dev;
+ cs35l56->codec = parent->codec;
+ strscpy(comp->name, dev_name(dev), sizeof(comp->name));
+ comp->playback_hook = cs35l56_hda_playback_hook;
+
+ queue_work(system_long_wq, &cs35l56->dsp_work);
+
+ cs35l56_hda_create_controls(cs35l56);
+
+#if IS_ENABLED(CONFIG_SND_DEBUG)
+ cs35l56->debugfs_root = debugfs_create_dir(dev_name(cs35l56->base.dev), sound_debugfs_root);
+ cs_dsp_init_debugfs(&cs35l56->cs_dsp, cs35l56->debugfs_root);
+#endif
+
+ dev_dbg(cs35l56->base.dev, "Bound\n");
+
+ return 0;
+}
+
+static void cs35l56_hda_unbind(struct device *dev, struct device *master, void *master_data)
+{
+ struct cs35l56_hda *cs35l56 = dev_get_drvdata(dev);
+ struct hda_component_parent *parent = master_data;
+ struct hda_component *comp;
+
+ cancel_work_sync(&cs35l56->dsp_work);
+
+ cs35l56_hda_remove_controls(cs35l56);
+
+#if IS_ENABLED(CONFIG_SND_DEBUG)
+ cs_dsp_cleanup_debugfs(&cs35l56->cs_dsp);
+ debugfs_remove_recursive(cs35l56->debugfs_root);
+#endif
+
+ if (cs35l56->base.fw_patched)
+ cs_dsp_power_down(&cs35l56->cs_dsp);
+
+ comp = hda_component_from_index(parent, cs35l56->index);
+ if (comp && (comp->dev == dev))
+ memset(comp, 0, sizeof(*comp));
+
+ cs35l56->codec = NULL;
+
+ dev_dbg(cs35l56->base.dev, "Unbound\n");
+}
+
+static const struct component_ops cs35l56_hda_comp_ops = {
+ .bind = cs35l56_hda_bind,
+ .unbind = cs35l56_hda_unbind,
+};
+
+static int cs35l56_hda_system_suspend(struct device *dev)
+{
+ struct cs35l56_hda *cs35l56 = dev_get_drvdata(dev);
+
+ cs35l56_hda_wait_dsp_ready(cs35l56);
+
+ if (cs35l56->playing)
+ cs35l56_hda_pause(cs35l56);
+
+ cs35l56->suspended = true;
+
+ /*
+ * The interrupt line is normally shared, but after we start suspending
+ * we can't check if our device is the source of an interrupt, and can't
+ * clear it. Prevent this race by temporarily disabling the parent irq
+ * until we reach _no_irq.
+ */
+ if (cs35l56->base.irq)
+ disable_irq(cs35l56->base.irq);
+
+ return pm_runtime_force_suspend(dev);
+}
+
+static int cs35l56_hda_system_suspend_late(struct device *dev)
+{
+ struct cs35l56_hda *cs35l56 = dev_get_drvdata(dev);
+
+ /*
+ * RESET is usually shared by all amps so it must not be asserted until
+ * all driver instances have done their suspend() stage.
+ */
+ if (cs35l56->base.reset_gpio) {
+ gpiod_set_value_cansleep(cs35l56->base.reset_gpio, 0);
+ cs35l56_wait_min_reset_pulse();
+ }
+
+ return 0;
+}
+
+static int cs35l56_hda_system_suspend_no_irq(struct device *dev)
+{
+ struct cs35l56_hda *cs35l56 = dev_get_drvdata(dev);
+
+ /* Handlers are now disabled so the parent IRQ can safely be re-enabled. */
+ if (cs35l56->base.irq)
+ enable_irq(cs35l56->base.irq);
+
+ return 0;
+}
+
+static int cs35l56_hda_system_resume_no_irq(struct device *dev)
+{
+ struct cs35l56_hda *cs35l56 = dev_get_drvdata(dev);
+
+ /*
+ * WAKE interrupts unmask if the CS35L56 hibernates, which can cause
+ * spurious interrupts, and the interrupt line is normally shared.
+ * We can't check if our device is the source of an interrupt, and can't
+ * clear it, until it has fully resumed. Prevent this race by temporarily
+ * disabling the parent irq until we complete resume().
+ */
+ if (cs35l56->base.irq)
+ disable_irq(cs35l56->base.irq);
+
+ return 0;
+}
+
+static int cs35l56_hda_system_resume_early(struct device *dev)
+{
+ struct cs35l56_hda *cs35l56 = dev_get_drvdata(dev);
+
+ /* Ensure a spec-compliant RESET pulse. */
+ if (cs35l56->base.reset_gpio) {
+ gpiod_set_value_cansleep(cs35l56->base.reset_gpio, 0);
+ cs35l56_wait_min_reset_pulse();
+
+ /* Release shared RESET before drivers start resume(). */
+ gpiod_set_value_cansleep(cs35l56->base.reset_gpio, 1);
+ cs35l56_wait_control_port_ready();
+ }
+
+ return 0;
+}
+
+static int cs35l56_hda_system_resume(struct device *dev)
+{
+ struct cs35l56_hda *cs35l56 = dev_get_drvdata(dev);
+ int ret;
+
+ /* Undo pm_runtime_force_suspend() before re-enabling the irq */
+ ret = pm_runtime_force_resume(dev);
+ if (cs35l56->base.irq)
+ enable_irq(cs35l56->base.irq);
+
+ if (ret)
+ return ret;
+
+ cs35l56->suspended = false;
+
+ if (!cs35l56->codec)
+ return 0;
+
+ ret = cs35l56_is_fw_reload_needed(&cs35l56->base);
+ dev_dbg(cs35l56->base.dev, "fw_reload_needed: %d\n", ret);
+ if (ret > 0)
+ queue_work(system_long_wq, &cs35l56->dsp_work);
+
+ if (cs35l56->playing)
+ cs35l56_hda_play(cs35l56);
+
+ return 0;
+}
+
+static int cs35l56_hda_fixup_yoga9(struct cs35l56_hda *cs35l56, int *bus_addr)
+{
+ /* The cirrus,dev-index property has the wrong values */
+ switch (*bus_addr) {
+ case 0x30:
+ cs35l56->index = 1;
+ return 0;
+ case 0x31:
+ cs35l56->index = 0;
+ return 0;
+ default:
+ /* There is a pseudo-address for broadcast to both amps - ignore it */
+ dev_dbg(cs35l56->base.dev, "Ignoring I2C address %#x\n", *bus_addr);
+ return 0;
+ }
+}
+
+static const struct {
+ const char *sub;
+ int (*fixup_fn)(struct cs35l56_hda *cs35l56, int *bus_addr);
+} cs35l56_hda_fixups[] = {
+ {
+ .sub = "17AA390B", /* Lenovo Yoga Book 9i GenX */
+ .fixup_fn = cs35l56_hda_fixup_yoga9,
+ },
+};
+
+static int cs35l56_hda_apply_platform_fixups(struct cs35l56_hda *cs35l56, const char *sub,
+ int *bus_addr)
+{
+ int i;
+
+ if (IS_ERR(sub))
+ return 0;
+
+ for (i = 0; i < ARRAY_SIZE(cs35l56_hda_fixups); i++) {
+ if (strcasecmp(cs35l56_hda_fixups[i].sub, sub) == 0) {
+ dev_dbg(cs35l56->base.dev, "Applying fixup for %s\n",
+ cs35l56_hda_fixups[i].sub);
+ return (cs35l56_hda_fixups[i].fixup_fn)(cs35l56, bus_addr);
+ }
+ }
+
+ return 0;
+}
+
+static int cs35l56_hda_read_acpi(struct cs35l56_hda *cs35l56, int hid, int id)
+{
+ u32 values[HDA_MAX_COMPONENTS];
+ char hid_string[8];
+ struct acpi_device *adev;
+ const char *property, *sub;
+ size_t nval;
+ int i, ret;
+
+ /*
+ * ACPI_COMPANION isn't available when this driver was instantiated by
+ * the serial-multi-instantiate driver, so lookup the node by HID
+ */
+ if (!ACPI_COMPANION(cs35l56->base.dev)) {
+ snprintf(hid_string, sizeof(hid_string), "CSC%04X", hid);
+ adev = acpi_dev_get_first_match_dev(hid_string, NULL, -1);
+ if (!adev) {
+ dev_err(cs35l56->base.dev, "Failed to find an ACPI device for %s\n",
+ dev_name(cs35l56->base.dev));
+ return -ENODEV;
+ }
+ ACPI_COMPANION_SET(cs35l56->base.dev, adev);
+ }
+
+ /* Initialize things that could be overwritten by a fixup */
+ cs35l56->index = -1;
+
+ sub = acpi_get_subsystem_id(ACPI_HANDLE(cs35l56->base.dev));
+ ret = cs35l56_hda_apply_platform_fixups(cs35l56, sub, &id);
+ if (ret)
+ return ret;
+
+ if (cs35l56->index == -1) {
+ property = "cirrus,dev-index";
+ ret = device_property_count_u32(cs35l56->base.dev, property);
+ if (ret <= 0)
+ goto err;
+
+ if (ret > ARRAY_SIZE(values)) {
+ ret = -EINVAL;
+ goto err;
+ }
+ nval = ret;
+
+ ret = device_property_read_u32_array(cs35l56->base.dev, property, values, nval);
+ if (ret)
+ goto err;
+
+ for (i = 0; i < nval; i++) {
+ if (values[i] == id) {
+ cs35l56->index = i;
+ break;
+ }
+ }
+
+ /*
+ * It's not an error for the ID to be missing: for I2C there can be
+ * an alias address that is not a real device. So reject silently.
+ */
+ if (cs35l56->index == -1) {
+ dev_dbg(cs35l56->base.dev, "No index found in %s\n", property);
+ ret = -ENODEV;
+ goto err;
+ }
+ }
+
+ if (IS_ERR(sub)) {
+ dev_info(cs35l56->base.dev,
+ "Read ACPI _SUB failed(%ld): fallback to generic firmware\n",
+ PTR_ERR(sub));
+ } else {
+ ret = cirrus_scodec_get_speaker_id(cs35l56->base.dev, cs35l56->index, nval, -1);
+ if (ret == -ENOENT) {
+ cs35l56->system_name = sub;
+ } else if (ret >= 0) {
+ cs35l56->system_name = kasprintf(GFP_KERNEL, "%s-spkid%d", sub, ret);
+ kfree(sub);
+ if (!cs35l56->system_name)
+ return -ENOMEM;
+ } else {
+ return ret;
+ }
+ }
+
+ cs35l56->base.reset_gpio = devm_gpiod_get_index_optional(cs35l56->base.dev,
+ "reset",
+ cs35l56->index,
+ GPIOD_OUT_LOW);
+ if (IS_ERR(cs35l56->base.reset_gpio)) {
+ ret = PTR_ERR(cs35l56->base.reset_gpio);
+
+ /*
+ * If RESET is shared the first amp to probe will grab the reset
+ * line and reset all the amps
+ */
+ if (ret != -EBUSY)
+ return dev_err_probe(cs35l56->base.dev, ret, "Failed to get reset GPIO\n");
+
+ dev_info(cs35l56->base.dev, "Reset GPIO busy, assume shared reset\n");
+ cs35l56->base.reset_gpio = NULL;
+ }
+
+ return 0;
+
+err:
+ if (ret != -ENODEV)
+ dev_err(cs35l56->base.dev, "Failed property %s: %d\n", property, ret);
+
+ return ret;
+}
+
+int cs35l56_hda_common_probe(struct cs35l56_hda *cs35l56, int hid, int id)
+{
+ int ret;
+
+ mutex_init(&cs35l56->base.irq_lock);
+ dev_set_drvdata(cs35l56->base.dev, cs35l56);
+
+ INIT_WORK(&cs35l56->dsp_work, cs35l56_hda_dsp_work);
+
+ ret = cs35l56_hda_read_acpi(cs35l56, hid, id);
+ if (ret)
+ goto err;
+
+ cs35l56->amp_name = devm_kasprintf(cs35l56->base.dev, GFP_KERNEL, "AMP%d",
+ cs35l56->index + 1);
+ if (!cs35l56->amp_name) {
+ ret = -ENOMEM;
+ goto err;
+ }
+
+ cs35l56->base.cal_index = -1;
+
+ cs35l56_init_cs_dsp(&cs35l56->base, &cs35l56->cs_dsp);
+ cs35l56->cs_dsp.client_ops = &cs35l56_hda_client_ops;
+
+ if (cs35l56->base.reset_gpio) {
+ dev_dbg(cs35l56->base.dev, "Hard reset\n");
+
+ /*
+ * The GPIOD_OUT_LOW to *_gpiod_get_*() will be ignored if the
+ * ACPI defines a different default state. So explicitly set low.
+ */
+ gpiod_set_value_cansleep(cs35l56->base.reset_gpio, 0);
+ cs35l56_wait_min_reset_pulse();
+ gpiod_set_value_cansleep(cs35l56->base.reset_gpio, 1);
+ }
+
+ ret = cs35l56_hw_init(&cs35l56->base);
+ if (ret < 0)
+ goto err;
+
+ /* Reset the device and wait for it to boot */
+ cs35l56_system_reset(&cs35l56->base, false);
+ ret = cs35l56_wait_for_firmware_boot(&cs35l56->base);
+ if (ret)
+ goto err;
+
+ regcache_cache_only(cs35l56->base.regmap, false);
+
+ ret = cs35l56_set_patch(&cs35l56->base);
+ if (ret)
+ goto err;
+
+ regcache_mark_dirty(cs35l56->base.regmap);
+ regcache_sync(cs35l56->base.regmap);
+
+ /* Disable auto-hibernate so that runtime_pm has control */
+ ret = cs35l56_mbox_send(&cs35l56->base, CS35L56_MBOX_CMD_PREVENT_AUTO_HIBERNATE);
+ if (ret)
+ goto err;
+
+ ret = cs35l56_get_calibration(&cs35l56->base);
+ if (ret)
+ goto err;
+
+ ret = cs_dsp_halo_init(&cs35l56->cs_dsp);
+ if (ret) {
+ dev_err_probe(cs35l56->base.dev, ret, "cs_dsp_halo_init failed\n");
+ goto err;
+ }
+
+ dev_info(cs35l56->base.dev, "DSP system name: '%s', amp name: '%s'\n",
+ cs35l56->system_name, cs35l56->amp_name);
+
+ regmap_multi_reg_write(cs35l56->base.regmap, cs35l56_hda_dai_config,
+ ARRAY_SIZE(cs35l56_hda_dai_config));
+
+ /*
+ * By default only enable one ASP1TXn, where n=amplifier index,
+ * This prevents multiple amps trying to drive the same slot.
+ */
+ cs35l56->asp_tx_mask = BIT(cs35l56->index);
+
+ pm_runtime_set_autosuspend_delay(cs35l56->base.dev, 3000);
+ pm_runtime_use_autosuspend(cs35l56->base.dev);
+ pm_runtime_set_active(cs35l56->base.dev);
+ pm_runtime_mark_last_busy(cs35l56->base.dev);
+ pm_runtime_enable(cs35l56->base.dev);
+
+ cs35l56->base.init_done = true;
+
+ ret = component_add(cs35l56->base.dev, &cs35l56_hda_comp_ops);
+ if (ret) {
+ dev_err(cs35l56->base.dev, "Register component failed: %d\n", ret);
+ goto pm_err;
+ }
+
+ return 0;
+
+pm_err:
+ pm_runtime_disable(cs35l56->base.dev);
+ cs_dsp_remove(&cs35l56->cs_dsp);
+err:
+ gpiod_set_value_cansleep(cs35l56->base.reset_gpio, 0);
+
+ return ret;
+}
+EXPORT_SYMBOL_NS_GPL(cs35l56_hda_common_probe, "SND_HDA_SCODEC_CS35L56");
+
+void cs35l56_hda_remove(struct device *dev)
+{
+ struct cs35l56_hda *cs35l56 = dev_get_drvdata(dev);
+
+ component_del(cs35l56->base.dev, &cs35l56_hda_comp_ops);
+
+ pm_runtime_dont_use_autosuspend(cs35l56->base.dev);
+ pm_runtime_get_sync(cs35l56->base.dev);
+ pm_runtime_disable(cs35l56->base.dev);
+
+ cs_dsp_remove(&cs35l56->cs_dsp);
+
+ kfree(cs35l56->system_name);
+ pm_runtime_put_noidle(cs35l56->base.dev);
+
+ gpiod_set_value_cansleep(cs35l56->base.reset_gpio, 0);
+}
+EXPORT_SYMBOL_NS_GPL(cs35l56_hda_remove, "SND_HDA_SCODEC_CS35L56");
+
+const struct dev_pm_ops cs35l56_hda_pm_ops = {
+ RUNTIME_PM_OPS(cs35l56_hda_runtime_suspend, cs35l56_hda_runtime_resume, NULL)
+ SYSTEM_SLEEP_PM_OPS(cs35l56_hda_system_suspend, cs35l56_hda_system_resume)
+ LATE_SYSTEM_SLEEP_PM_OPS(cs35l56_hda_system_suspend_late,
+ cs35l56_hda_system_resume_early)
+ NOIRQ_SYSTEM_SLEEP_PM_OPS(cs35l56_hda_system_suspend_no_irq,
+ cs35l56_hda_system_resume_no_irq)
+};
+EXPORT_SYMBOL_NS_GPL(cs35l56_hda_pm_ops, "SND_HDA_SCODEC_CS35L56");
+
+MODULE_DESCRIPTION("CS35L56 HDA Driver");
+MODULE_IMPORT_NS("FW_CS_DSP");
+MODULE_IMPORT_NS("SND_HDA_CIRRUS_SCODEC");
+MODULE_IMPORT_NS("SND_SOC_CS35L56_SHARED");
+MODULE_IMPORT_NS("SND_SOC_CS_AMP_LIB");
+MODULE_AUTHOR("Richard Fitzgerald <rf@opensource.cirrus.com>");
+MODULE_AUTHOR("Simon Trimmer <simont@opensource.cirrus.com>");
+MODULE_LICENSE("GPL");
+MODULE_FIRMWARE("cirrus/cs35l54-*.wmfw");
+MODULE_FIRMWARE("cirrus/cs35l54-*.bin");
+MODULE_FIRMWARE("cirrus/cs35l56-*.wmfw");
+MODULE_FIRMWARE("cirrus/cs35l56-*.bin");
diff --git a/sound/hda/codecs/side-codecs/cs35l56_hda.h b/sound/hda/codecs/side-codecs/cs35l56_hda.h
new file mode 100644
index 000000000000..38d94fb213a5
--- /dev/null
+++ b/sound/hda/codecs/side-codecs/cs35l56_hda.h
@@ -0,0 +1,50 @@
+/* SPDX-License-Identifier: GPL-2.0-only
+ *
+ * HDA audio driver for Cirrus Logic CS35L56 smart amp
+ *
+ * Copyright (C) 2023 Cirrus Logic, Inc. and
+ * Cirrus Logic International Semiconductor Ltd.
+ */
+
+#ifndef __CS35L56_HDA_H__
+#define __CS35L56_HDA_H__
+
+#include <linux/device.h>
+#include <linux/gpio/consumer.h>
+#include <linux/firmware/cirrus/cs_dsp.h>
+#include <linux/firmware/cirrus/wmfw.h>
+#include <linux/regulator/consumer.h>
+#include <linux/workqueue.h>
+#include <sound/cs35l56.h>
+
+struct dentry;
+
+struct cs35l56_hda {
+ struct cs35l56_base base;
+ struct hda_codec *codec;
+ struct work_struct dsp_work;
+
+ int index;
+ const char *system_name;
+ const char *amp_name;
+
+ struct cs_dsp cs_dsp;
+ bool playing;
+ bool suspended;
+ u8 asp_tx_mask;
+
+ struct snd_kcontrol *posture_ctl;
+ struct snd_kcontrol *volume_ctl;
+ struct snd_kcontrol *mixer_ctl[4];
+
+#if IS_ENABLED(CONFIG_SND_DEBUG)
+ struct dentry *debugfs_root;
+#endif
+};
+
+extern const struct dev_pm_ops cs35l56_hda_pm_ops;
+
+int cs35l56_hda_common_probe(struct cs35l56_hda *cs35l56, int hid, int id);
+void cs35l56_hda_remove(struct device *dev);
+
+#endif /*__CS35L56_HDA_H__*/
diff --git a/sound/hda/codecs/side-codecs/cs35l56_hda_i2c.c b/sound/hda/codecs/side-codecs/cs35l56_hda_i2c.c
new file mode 100644
index 000000000000..d10209e4eddd
--- /dev/null
+++ b/sound/hda/codecs/side-codecs/cs35l56_hda_i2c.c
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: GPL-2.0-only
+//
+// CS35L56 HDA audio driver I2C binding
+//
+// Copyright (C) 2023 Cirrus Logic, Inc. and
+// Cirrus Logic International Semiconductor Ltd.
+
+#include <linux/i2c.h>
+#include <linux/module.h>
+#include <linux/regmap.h>
+
+#include "cs35l56_hda.h"
+
+static int cs35l56_hda_i2c_probe(struct i2c_client *clt)
+{
+ const struct i2c_device_id *id = i2c_client_get_device_id(clt);
+ struct cs35l56_hda *cs35l56;
+ int ret;
+
+ cs35l56 = devm_kzalloc(&clt->dev, sizeof(*cs35l56), GFP_KERNEL);
+ if (!cs35l56)
+ return -ENOMEM;
+
+ cs35l56->base.dev = &clt->dev;
+
+#ifdef CS35L56_WAKE_HOLD_TIME_US
+ cs35l56->base.can_hibernate = true;
+#endif
+
+ cs35l56->base.fw_reg = &cs35l56_fw_reg;
+
+ cs35l56->base.regmap = devm_regmap_init_i2c(clt, &cs35l56_regmap_i2c);
+ if (IS_ERR(cs35l56->base.regmap)) {
+ ret = PTR_ERR(cs35l56->base.regmap);
+ dev_err(cs35l56->base.dev, "Failed to allocate register map: %d\n",
+ ret);
+ return ret;
+ }
+
+ ret = cs35l56_hda_common_probe(cs35l56, id->driver_data, clt->addr);
+ if (ret)
+ return ret;
+ ret = cs35l56_irq_request(&cs35l56->base, clt->irq);
+ if (ret < 0)
+ cs35l56_hda_remove(cs35l56->base.dev);
+
+ return ret;
+}
+
+static void cs35l56_hda_i2c_remove(struct i2c_client *clt)
+{
+ cs35l56_hda_remove(&clt->dev);
+}
+
+static const struct i2c_device_id cs35l56_hda_i2c_id[] = {
+ { "cs35l54-hda", 0x3554 },
+ { "cs35l56-hda", 0x3556 },
+ { "cs35l57-hda", 0x3557 },
+ {}
+};
+
+static const struct acpi_device_id cs35l56_acpi_hda_match[] = {
+ { "CSC3554", 0 },
+ { "CSC3556", 0 },
+ { "CSC3557", 0 },
+ {}
+};
+MODULE_DEVICE_TABLE(acpi, cs35l56_acpi_hda_match);
+
+static struct i2c_driver cs35l56_hda_i2c_driver = {
+ .driver = {
+ .name = "cs35l56-hda",
+ .acpi_match_table = cs35l56_acpi_hda_match,
+ .pm = &cs35l56_hda_pm_ops,
+ },
+ .id_table = cs35l56_hda_i2c_id,
+ .probe = cs35l56_hda_i2c_probe,
+ .remove = cs35l56_hda_i2c_remove,
+};
+module_i2c_driver(cs35l56_hda_i2c_driver);
+
+MODULE_DESCRIPTION("HDA CS35L56 I2C driver");
+MODULE_IMPORT_NS("SND_HDA_SCODEC_CS35L56");
+MODULE_IMPORT_NS("SND_SOC_CS35L56_SHARED");
+MODULE_AUTHOR("Richard Fitzgerald <rf@opensource.cirrus.com>");
+MODULE_AUTHOR("Simon Trimmer <simont@opensource.cirrus.com>");
+MODULE_LICENSE("GPL");
diff --git a/sound/hda/codecs/side-codecs/cs35l56_hda_spi.c b/sound/hda/codecs/side-codecs/cs35l56_hda_spi.c
new file mode 100644
index 000000000000..f57533d3d728
--- /dev/null
+++ b/sound/hda/codecs/side-codecs/cs35l56_hda_spi.c
@@ -0,0 +1,90 @@
+// SPDX-License-Identifier: GPL-2.0-only
+//
+// CS35L56 HDA audio driver SPI binding
+//
+// Copyright (C) 2023 Cirrus Logic, Inc. and
+// Cirrus Logic International Semiconductor Ltd.
+
+#include <linux/module.h>
+#include <linux/regmap.h>
+#include <linux/spi/spi.h>
+
+#include "cs35l56_hda.h"
+
+static int cs35l56_hda_spi_probe(struct spi_device *spi)
+{
+ const struct spi_device_id *id = spi_get_device_id(spi);
+ struct cs35l56_hda *cs35l56;
+ int ret;
+
+ cs35l56 = devm_kzalloc(&spi->dev, sizeof(*cs35l56), GFP_KERNEL);
+ if (!cs35l56)
+ return -ENOMEM;
+
+ cs35l56->base.dev = &spi->dev;
+ ret = cs35l56_init_config_for_spi(&cs35l56->base, spi);
+ if (ret)
+ return ret;
+
+#ifdef CS35L56_WAKE_HOLD_TIME_US
+ cs35l56->base.can_hibernate = true;
+#endif
+
+ cs35l56->base.fw_reg = &cs35l56_fw_reg;
+
+ cs35l56->base.regmap = devm_regmap_init_spi(spi, &cs35l56_regmap_spi);
+ if (IS_ERR(cs35l56->base.regmap)) {
+ ret = PTR_ERR(cs35l56->base.regmap);
+ dev_err(cs35l56->base.dev, "Failed to allocate register map: %d\n",
+ ret);
+ return ret;
+ }
+
+ ret = cs35l56_hda_common_probe(cs35l56, id->driver_data, spi_get_chipselect(spi, 0));
+ if (ret)
+ return ret;
+ ret = cs35l56_irq_request(&cs35l56->base, spi->irq);
+ if (ret < 0)
+ cs35l56_hda_remove(cs35l56->base.dev);
+
+ return ret;
+}
+
+static void cs35l56_hda_spi_remove(struct spi_device *spi)
+{
+ cs35l56_hda_remove(&spi->dev);
+}
+
+static const struct spi_device_id cs35l56_hda_spi_id[] = {
+ { "cs35l54-hda", 0x3554 },
+ { "cs35l56-hda", 0x3556 },
+ { "cs35l57-hda", 0x3557 },
+ {}
+};
+
+static const struct acpi_device_id cs35l56_acpi_hda_match[] = {
+ { "CSC3554", 0 },
+ { "CSC3556", 0 },
+ { "CSC3557", 0 },
+ {}
+};
+MODULE_DEVICE_TABLE(acpi, cs35l56_acpi_hda_match);
+
+static struct spi_driver cs35l56_hda_spi_driver = {
+ .driver = {
+ .name = "cs35l56-hda",
+ .acpi_match_table = cs35l56_acpi_hda_match,
+ .pm = &cs35l56_hda_pm_ops,
+ },
+ .id_table = cs35l56_hda_spi_id,
+ .probe = cs35l56_hda_spi_probe,
+ .remove = cs35l56_hda_spi_remove,
+};
+module_spi_driver(cs35l56_hda_spi_driver);
+
+MODULE_DESCRIPTION("HDA CS35L56 SPI driver");
+MODULE_IMPORT_NS("SND_HDA_SCODEC_CS35L56");
+MODULE_IMPORT_NS("SND_SOC_CS35L56_SHARED");
+MODULE_AUTHOR("Richard Fitzgerald <rf@opensource.cirrus.com>");
+MODULE_AUTHOR("Simon Trimmer <simont@opensource.cirrus.com>");
+MODULE_LICENSE("GPL");
diff --git a/sound/hda/codecs/side-codecs/hda_component.c b/sound/hda/codecs/side-codecs/hda_component.c
new file mode 100644
index 000000000000..71860e2d6377
--- /dev/null
+++ b/sound/hda/codecs/side-codecs/hda_component.c
@@ -0,0 +1,212 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * HD audio Component Binding Interface
+ *
+ * Copyright (C) 2021, 2023 Cirrus Logic, Inc. and
+ * Cirrus Logic International Semiconductor Ltd.
+ */
+
+#include <linux/acpi.h>
+#include <linux/component.h>
+#include <linux/module.h>
+#include <linux/slab.h>
+#include <sound/hda_codec.h>
+#include "hda_component.h"
+#include "hda_local.h"
+
+#ifdef CONFIG_ACPI
+void hda_component_acpi_device_notify(struct hda_component_parent *parent,
+ acpi_handle handle, u32 event, void *data)
+{
+ struct hda_component *comp;
+ int i;
+
+ mutex_lock(&parent->mutex);
+ for (i = 0; i < ARRAY_SIZE(parent->comps); i++) {
+ comp = hda_component_from_index(parent, i);
+ if (comp->dev && comp->acpi_notify)
+ comp->acpi_notify(acpi_device_handle(comp->adev), event, comp->dev);
+ }
+ mutex_unlock(&parent->mutex);
+}
+EXPORT_SYMBOL_NS_GPL(hda_component_acpi_device_notify, "SND_HDA_SCODEC_COMPONENT");
+
+int hda_component_manager_bind_acpi_notifications(struct hda_codec *cdc,
+ struct hda_component_parent *parent,
+ acpi_notify_handler handler, void *data)
+{
+ bool support_notifications = false;
+ struct acpi_device *adev;
+ struct hda_component *comp;
+ int ret;
+ int i;
+
+ adev = parent->comps[0].adev;
+ if (!acpi_device_handle(adev))
+ return 0;
+
+ for (i = 0; i < ARRAY_SIZE(parent->comps); i++) {
+ comp = hda_component_from_index(parent, i);
+ support_notifications = support_notifications ||
+ comp->acpi_notifications_supported;
+ }
+
+ if (support_notifications) {
+ ret = acpi_install_notify_handler(adev->handle, ACPI_DEVICE_NOTIFY,
+ handler, data);
+ if (ret < 0) {
+ codec_warn(cdc, "Failed to install notify handler: %d\n", ret);
+ return 0;
+ }
+
+ codec_dbg(cdc, "Notify handler installed\n");
+ }
+
+ return 0;
+}
+EXPORT_SYMBOL_NS_GPL(hda_component_manager_bind_acpi_notifications, "SND_HDA_SCODEC_COMPONENT");
+
+void hda_component_manager_unbind_acpi_notifications(struct hda_codec *cdc,
+ struct hda_component_parent *parent,
+ acpi_notify_handler handler)
+{
+ struct acpi_device *adev;
+ int ret;
+
+ adev = parent->comps[0].adev;
+ if (!acpi_device_handle(adev))
+ return;
+
+ ret = acpi_remove_notify_handler(adev->handle, ACPI_DEVICE_NOTIFY, handler);
+ if (ret < 0)
+ codec_warn(cdc, "Failed to uninstall notify handler: %d\n", ret);
+}
+EXPORT_SYMBOL_NS_GPL(hda_component_manager_unbind_acpi_notifications, "SND_HDA_SCODEC_COMPONENT");
+#endif /* ifdef CONFIG_ACPI */
+
+void hda_component_manager_playback_hook(struct hda_component_parent *parent, int action)
+{
+ struct hda_component *comp;
+ int i;
+
+ mutex_lock(&parent->mutex);
+ for (i = 0; i < ARRAY_SIZE(parent->comps); i++) {
+ comp = hda_component_from_index(parent, i);
+ if (comp->dev && comp->pre_playback_hook)
+ comp->pre_playback_hook(comp->dev, action);
+ }
+ for (i = 0; i < ARRAY_SIZE(parent->comps); i++) {
+ comp = hda_component_from_index(parent, i);
+ if (comp->dev && comp->playback_hook)
+ comp->playback_hook(comp->dev, action);
+ }
+ for (i = 0; i < ARRAY_SIZE(parent->comps); i++) {
+ comp = hda_component_from_index(parent, i);
+ if (comp->dev && comp->post_playback_hook)
+ comp->post_playback_hook(comp->dev, action);
+ }
+ mutex_unlock(&parent->mutex);
+}
+EXPORT_SYMBOL_NS_GPL(hda_component_manager_playback_hook, "SND_HDA_SCODEC_COMPONENT");
+
+struct hda_scodec_match {
+ const char *bus;
+ const char *hid;
+ const char *match_str;
+ int index;
+};
+
+/* match the device name in a slightly relaxed manner */
+static int hda_comp_match_dev_name(struct device *dev, void *data)
+{
+ struct hda_scodec_match *p = data;
+ const char *d = dev_name(dev);
+ int n = strlen(p->bus);
+ char tmp[32];
+
+ /* check the bus name */
+ if (strncmp(d, p->bus, n))
+ return 0;
+ /* skip the bus number */
+ if (isdigit(d[n]))
+ n++;
+ /* the rest must be exact matching */
+ snprintf(tmp, sizeof(tmp), p->match_str, p->hid, p->index);
+ return !strcmp(d + n, tmp);
+}
+
+int hda_component_manager_bind(struct hda_codec *cdc,
+ struct hda_component_parent *parent)
+{
+ int ret;
+
+ /* Init shared and component specific data */
+ memset(parent->comps, 0, sizeof(parent->comps));
+
+ mutex_lock(&parent->mutex);
+ ret = component_bind_all(hda_codec_dev(cdc), parent);
+ mutex_unlock(&parent->mutex);
+
+ return ret;
+}
+EXPORT_SYMBOL_NS_GPL(hda_component_manager_bind, "SND_HDA_SCODEC_COMPONENT");
+
+int hda_component_manager_init(struct hda_codec *cdc,
+ struct hda_component_parent *parent, int count,
+ const char *bus, const char *hid,
+ const char *match_str,
+ const struct component_master_ops *ops)
+{
+ struct device *dev = hda_codec_dev(cdc);
+ struct component_match *match = NULL;
+ struct hda_scodec_match *sm;
+ int ret, i;
+
+ if (parent->codec) {
+ codec_err(cdc, "Component binding already created (SSID: %x)\n",
+ cdc->core.subsystem_id);
+ return -EINVAL;
+ }
+ parent->codec = cdc;
+
+ mutex_init(&parent->mutex);
+
+ for (i = 0; i < count; i++) {
+ sm = devm_kmalloc(dev, sizeof(*sm), GFP_KERNEL);
+ if (!sm)
+ return -ENOMEM;
+
+ sm->bus = bus;
+ sm->hid = hid;
+ sm->match_str = match_str;
+ sm->index = i;
+ component_match_add(dev, &match, hda_comp_match_dev_name, sm);
+ }
+
+ ret = component_master_add_with_match(dev, ops, match);
+ if (ret)
+ codec_err(cdc, "Fail to register component aggregator %d\n", ret);
+
+ return ret;
+}
+EXPORT_SYMBOL_NS_GPL(hda_component_manager_init, "SND_HDA_SCODEC_COMPONENT");
+
+void hda_component_manager_free(struct hda_component_parent *parent,
+ const struct component_master_ops *ops)
+{
+ struct device *dev;
+
+ if (!parent->codec)
+ return;
+
+ dev = hda_codec_dev(parent->codec);
+
+ component_master_del(dev, ops);
+
+ parent->codec = NULL;
+}
+EXPORT_SYMBOL_NS_GPL(hda_component_manager_free, "SND_HDA_SCODEC_COMPONENT");
+
+MODULE_DESCRIPTION("HD Audio component binding library");
+MODULE_AUTHOR("Richard Fitzgerald <rf@opensource.cirrus.com>");
+MODULE_LICENSE("GPL");
diff --git a/sound/hda/codecs/side-codecs/hda_component.h b/sound/hda/codecs/side-codecs/hda_component.h
new file mode 100644
index 000000000000..7ee37154749f
--- /dev/null
+++ b/sound/hda/codecs/side-codecs/hda_component.h
@@ -0,0 +1,103 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * HD audio Component Binding Interface
+ *
+ * Copyright (C) 2021 Cirrus Logic, Inc. and
+ * Cirrus Logic International Semiconductor Ltd.
+ */
+
+#ifndef __HDA_COMPONENT_H__
+#define __HDA_COMPONENT_H__
+
+#include <linux/acpi.h>
+#include <linux/component.h>
+#include <linux/mutex.h>
+#include <sound/hda_codec.h>
+
+#define HDA_MAX_COMPONENTS 4
+#define HDA_MAX_NAME_SIZE 50
+
+struct hda_component {
+ struct device *dev;
+ char name[HDA_MAX_NAME_SIZE];
+ struct acpi_device *adev;
+ bool acpi_notifications_supported;
+ void (*acpi_notify)(acpi_handle handle, u32 event, struct device *dev);
+ void (*pre_playback_hook)(struct device *dev, int action);
+ void (*playback_hook)(struct device *dev, int action);
+ void (*post_playback_hook)(struct device *dev, int action);
+};
+
+struct hda_component_parent {
+ struct mutex mutex;
+ struct hda_codec *codec;
+ struct hda_component comps[HDA_MAX_COMPONENTS];
+};
+
+#ifdef CONFIG_ACPI
+void hda_component_acpi_device_notify(struct hda_component_parent *parent,
+ acpi_handle handle, u32 event, void *data);
+int hda_component_manager_bind_acpi_notifications(struct hda_codec *cdc,
+ struct hda_component_parent *parent,
+ acpi_notify_handler handler, void *data);
+void hda_component_manager_unbind_acpi_notifications(struct hda_codec *cdc,
+ struct hda_component_parent *parent,
+ acpi_notify_handler handler);
+#else
+static inline void hda_component_acpi_device_notify(struct hda_component_parent *parent,
+ acpi_handle handle,
+ u32 event,
+ void *data)
+{
+}
+
+static inline int hda_component_manager_bind_acpi_notifications(struct hda_codec *cdc,
+ struct hda_component_parent *parent,
+ acpi_notify_handler handler,
+ void *data)
+
+{
+ return 0;
+}
+
+static inline void hda_component_manager_unbind_acpi_notifications(struct hda_codec *cdc,
+ struct hda_component_parent *parent,
+ acpi_notify_handler handler)
+{
+}
+#endif /* ifdef CONFIG_ACPI */
+
+void hda_component_manager_playback_hook(struct hda_component_parent *parent, int action);
+
+int hda_component_manager_init(struct hda_codec *cdc,
+ struct hda_component_parent *parent, int count,
+ const char *bus, const char *hid,
+ const char *match_str,
+ const struct component_master_ops *ops);
+
+void hda_component_manager_free(struct hda_component_parent *parent,
+ const struct component_master_ops *ops);
+
+int hda_component_manager_bind(struct hda_codec *cdc, struct hda_component_parent *parent);
+
+static inline struct hda_component *hda_component_from_index(struct hda_component_parent *parent,
+ int index)
+{
+ if (!parent)
+ return NULL;
+
+ if (index < 0 || index >= ARRAY_SIZE(parent->comps))
+ return NULL;
+
+ return &parent->comps[index];
+}
+
+static inline void hda_component_manager_unbind(struct hda_codec *cdc,
+ struct hda_component_parent *parent)
+{
+ mutex_lock(&parent->mutex);
+ component_unbind_all(hda_codec_dev(cdc), parent);
+ mutex_unlock(&parent->mutex);
+}
+
+#endif /* ifndef __HDA_COMPONENT_H__ */
diff --git a/sound/hda/codecs/side-codecs/tas2781_hda.c b/sound/hda/codecs/side-codecs/tas2781_hda.c
new file mode 100644
index 000000000000..34217ce9f28e
--- /dev/null
+++ b/sound/hda/codecs/side-codecs/tas2781_hda.c
@@ -0,0 +1,379 @@
+// SPDX-License-Identifier: GPL-2.0
+//
+// TAS2781 HDA Shared Lib for I2C&SPI driver
+//
+// Copyright 2025 Texas Instruments, Inc.
+//
+// Author: Shenghao Ding <shenghao-ding@ti.com>
+
+#include <linux/component.h>
+#include <linux/crc8.h>
+#include <linux/crc32.h>
+#include <linux/efi.h>
+#include <linux/firmware.h>
+#include <linux/i2c.h>
+#include <linux/pm_runtime.h>
+#include <sound/soc.h>
+#include <sound/tas2781.h>
+
+#include "tas2781_hda.h"
+
+const efi_guid_t tasdev_fct_efi_guid[] = {
+ /* DELL */
+ EFI_GUID(0xcc92382d, 0x6337, 0x41cb, 0xa8, 0x8b, 0x8e, 0xce, 0x74,
+ 0x91, 0xea, 0x9f),
+ /* HP */
+ EFI_GUID(0x02f9af02, 0x7734, 0x4233, 0xb4, 0x3d, 0x93, 0xfe, 0x5a,
+ 0xa3, 0x5d, 0xb3),
+ /* LENOVO & OTHERS */
+ EFI_GUID(0x1f52d2a1, 0xbb3a, 0x457d, 0xbc, 0x09, 0x43, 0xa3, 0xf4,
+ 0x31, 0x0a, 0x92),
+};
+EXPORT_SYMBOL_NS_GPL(tasdev_fct_efi_guid, "SND_HDA_SCODEC_TAS2781");
+
+static void tas2781_apply_calib(struct tasdevice_priv *p)
+{
+ struct calidata *cali_data = &p->cali_data;
+ struct cali_reg *r = &cali_data->cali_reg_array;
+ unsigned char *data = cali_data->data;
+ unsigned int *tmp_val = (unsigned int *)data;
+ unsigned int cali_reg[TASDEV_CALIB_N] = {
+ TASDEVICE_REG(0, 0x17, 0x74),
+ TASDEVICE_REG(0, 0x18, 0x0c),
+ TASDEVICE_REG(0, 0x18, 0x14),
+ TASDEVICE_REG(0, 0x13, 0x70),
+ TASDEVICE_REG(0, 0x18, 0x7c),
+ };
+ unsigned int crc, oft, node_num;
+ unsigned char *buf;
+ int i, j, k, l;
+
+ if (tmp_val[0] == 2781) {
+ /*
+ * New features were added in calibrated Data V3:
+ * 1. Added calibration registers address define in
+ * a node, marked as Device id == 0x80.
+ * New features were added in calibrated Data V2:
+ * 1. Added some the fields to store the link_id and
+ * uniqie_id for multi-link solutions
+ * 2. Support flexible number of devices instead of
+ * fixed one in V1.
+ * Layout of calibrated data V2 in UEFI(total 256 bytes):
+ * ChipID (2781, 4 bytes)
+ * Data-Group-Sum (4 bytes)
+ * TimeStamp of Calibration (4 bytes)
+ * for (i = 0; i < Data-Group-Sum; i++) {
+ * if (Data type != 0x80) (4 bytes)
+ * Calibrated Data of Device #i (20 bytes)
+ * else
+ * Calibration registers address (5*4 = 20 bytes)
+ * # V2: No reg addr in data grp section.
+ * # V3: Normally the last grp is the reg addr.
+ * }
+ * CRC (4 bytes)
+ * Reserved (the rest)
+ */
+ crc = crc32(~0, data, (3 + tmp_val[1] * 6) * 4) ^ ~0;
+
+ if (crc != tmp_val[3 + tmp_val[1] * 6]) {
+ cali_data->total_sz = 0;
+ dev_err(p->dev, "%s: CRC error\n", __func__);
+ return;
+ }
+ node_num = tmp_val[1];
+
+ for (j = 0, k = 0; j < node_num; j++) {
+ oft = j * 6 + 3;
+ if (tmp_val[oft] == TASDEV_UEFI_CALI_REG_ADDR_FLG) {
+ for (i = 0; i < TASDEV_CALIB_N; i++) {
+ buf = &data[(oft + i + 1) * 4];
+ cali_reg[i] = TASDEVICE_REG(buf[1],
+ buf[2], buf[3]);
+ }
+ } else {
+ l = j * (cali_data->cali_dat_sz_per_dev + 1);
+ if (k >= p->ndev || l > oft * 4) {
+ dev_err(p->dev, "%s: dev sum error\n",
+ __func__);
+ cali_data->total_sz = 0;
+ return;
+ }
+
+ data[l] = k;
+ oft++;
+ for (i = 0; i < TASDEV_CALIB_N * 4; i++)
+ data[l + i + 1] = data[4 * oft + i];
+ k++;
+ }
+ }
+ } else {
+ /*
+ * Calibration data is in V1 format.
+ * struct cali_data {
+ * char cali_data[20];
+ * }
+ *
+ * struct {
+ * struct cali_data cali_data[4];
+ * int TimeStamp of Calibration (4 bytes)
+ * int CRC (4 bytes)
+ * } ueft;
+ */
+ crc = crc32(~0, data, 84) ^ ~0;
+ if (crc != tmp_val[21]) {
+ cali_data->total_sz = 0;
+ dev_err(p->dev, "%s: V1 CRC error\n", __func__);
+ return;
+ }
+
+ for (j = p->ndev - 1; j >= 0; j--) {
+ l = j * (cali_data->cali_dat_sz_per_dev + 1);
+ for (i = TASDEV_CALIB_N * 4; i > 0 ; i--)
+ data[l + i] = data[p->index * 5 + i];
+ data[l+i] = j;
+ }
+ }
+
+ if (p->dspbin_typ == TASDEV_BASIC) {
+ r->r0_reg = cali_reg[0];
+ r->invr0_reg = cali_reg[1];
+ r->r0_low_reg = cali_reg[2];
+ r->pow_reg = cali_reg[3];
+ r->tlimit_reg = cali_reg[4];
+ }
+
+ p->is_user_space_calidata = true;
+ cali_data->total_sz = p->ndev * (cali_data->cali_dat_sz_per_dev + 1);
+}
+
+/*
+ * Update the calibration data, including speaker impedance, f0, etc,
+ * into algo. Calibrate data is done by manufacturer in the factory.
+ * The data is used by Algo for calculating the speaker temperature,
+ * speaker membrane excursion and f0 in real time during playback.
+ * Calibration data format in EFI is V2, since 2024.
+ */
+int tas2781_save_calibration(struct tas2781_hda *hda)
+{
+ /*
+ * GUID was used for data access in BIOS, it was provided by board
+ * manufactory.
+ */
+ efi_guid_t efi_guid = tasdev_fct_efi_guid[LENOVO];
+ static efi_char16_t efi_name[] = TASDEVICE_CALIBRATION_DATA_NAME;
+ struct tasdevice_priv *p = hda->priv;
+ struct calidata *cali_data = &p->cali_data;
+ unsigned long total_sz = 0;
+ unsigned int attr, size;
+ unsigned char *data;
+ efi_status_t status;
+
+ if (hda->catlog_id < LENOVO)
+ efi_guid = tasdev_fct_efi_guid[hda->catlog_id];
+
+ cali_data->cali_dat_sz_per_dev = 20;
+ size = p->ndev * (cali_data->cali_dat_sz_per_dev + 1);
+ /* Get real size of UEFI variable */
+ status = efi.get_variable(efi_name, &efi_guid, &attr, &total_sz, NULL);
+ cali_data->total_sz = total_sz > size ? total_sz : size;
+ if (status == EFI_BUFFER_TOO_SMALL) {
+ /* Allocate data buffer of data_size bytes */
+ data = p->cali_data.data = devm_kzalloc(p->dev,
+ p->cali_data.total_sz, GFP_KERNEL);
+ if (!data) {
+ p->cali_data.total_sz = 0;
+ return -ENOMEM;
+ }
+ /* Get variable contents into buffer */
+ status = efi.get_variable(efi_name, &efi_guid, &attr,
+ &p->cali_data.total_sz, data);
+ }
+ if (status != EFI_SUCCESS) {
+ p->cali_data.total_sz = 0;
+ return status;
+ }
+
+ tas2781_apply_calib(p);
+
+ return 0;
+}
+EXPORT_SYMBOL_NS_GPL(tas2781_save_calibration, "SND_HDA_SCODEC_TAS2781");
+
+void tas2781_hda_remove(struct device *dev,
+ const struct component_ops *ops)
+{
+ struct tas2781_hda *tas_hda = dev_get_drvdata(dev);
+
+ component_del(tas_hda->dev, ops);
+
+ pm_runtime_get_sync(tas_hda->dev);
+ pm_runtime_disable(tas_hda->dev);
+
+ pm_runtime_put_noidle(tas_hda->dev);
+
+ tasdevice_remove(tas_hda->priv);
+}
+EXPORT_SYMBOL_NS_GPL(tas2781_hda_remove, "SND_HDA_SCODEC_TAS2781");
+
+int tasdevice_info_profile(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_info *uinfo)
+{
+ struct tasdevice_priv *tas_priv = snd_kcontrol_chip(kcontrol);
+
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = 0;
+ uinfo->value.integer.max = tas_priv->rcabin.ncfgs - 1;
+
+ return 0;
+}
+EXPORT_SYMBOL_NS_GPL(tasdevice_info_profile, "SND_HDA_SCODEC_TAS2781");
+
+int tasdevice_info_programs(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_info *uinfo)
+{
+ struct tasdevice_priv *tas_priv = snd_kcontrol_chip(kcontrol);
+
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = 0;
+ uinfo->value.integer.max = tas_priv->fmw->nr_programs - 1;
+
+ return 0;
+}
+EXPORT_SYMBOL_NS_GPL(tasdevice_info_programs, "SND_HDA_SCODEC_TAS2781");
+
+int tasdevice_info_config(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_info *uinfo)
+{
+ struct tasdevice_priv *tas_priv = snd_kcontrol_chip(kcontrol);
+ struct tasdevice_fw *tas_fw = tas_priv->fmw;
+
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = 0;
+ uinfo->value.integer.max = tas_fw->nr_configurations - 1;
+
+ return 0;
+}
+EXPORT_SYMBOL_NS_GPL(tasdevice_info_config, "SND_HDA_SCODEC_TAS2781");
+
+int tasdevice_get_profile_id(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct tasdevice_priv *tas_priv = snd_kcontrol_chip(kcontrol);
+
+ ucontrol->value.integer.value[0] = tas_priv->rcabin.profile_cfg_id;
+
+ dev_dbg(tas_priv->dev, "%s: kcontrol %s: %d\n", __func__,
+ kcontrol->id.name, tas_priv->rcabin.profile_cfg_id);
+
+ return 0;
+}
+EXPORT_SYMBOL_NS_GPL(tasdevice_get_profile_id, "SND_HDA_SCODEC_TAS2781");
+
+int tasdevice_set_profile_id(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct tasdevice_priv *tas_priv = snd_kcontrol_chip(kcontrol);
+ int profile_id = ucontrol->value.integer.value[0];
+ int max = tas_priv->rcabin.ncfgs - 1;
+ int val, ret = 0;
+
+ val = clamp(profile_id, 0, max);
+
+ guard(mutex)(&tas_priv->codec_lock);
+
+ dev_dbg(tas_priv->dev, "%s: kcontrol %s: %d -> %d\n", __func__,
+ kcontrol->id.name, tas_priv->rcabin.profile_cfg_id, val);
+
+ if (tas_priv->rcabin.profile_cfg_id != val) {
+ tas_priv->rcabin.profile_cfg_id = val;
+ ret = 1;
+ }
+
+ return ret;
+}
+EXPORT_SYMBOL_NS_GPL(tasdevice_set_profile_id, "SND_HDA_SCODEC_TAS2781");
+
+int tasdevice_program_get(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct tasdevice_priv *tas_priv = snd_kcontrol_chip(kcontrol);
+
+ ucontrol->value.integer.value[0] = tas_priv->cur_prog;
+
+ dev_dbg(tas_priv->dev, "%s: kcontrol %s: %d\n", __func__,
+ kcontrol->id.name, tas_priv->cur_prog);
+
+ return 0;
+}
+EXPORT_SYMBOL_NS_GPL(tasdevice_program_get, "SND_HDA_SCODEC_TAS2781");
+
+int tasdevice_program_put(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct tasdevice_priv *tas_priv = snd_kcontrol_chip(kcontrol);
+ struct tasdevice_fw *tas_fw = tas_priv->fmw;
+ int nr_program = ucontrol->value.integer.value[0];
+ int max = tas_fw->nr_programs - 1;
+ int val, ret = 0;
+
+ val = clamp(nr_program, 0, max);
+
+ guard(mutex)(&tas_priv->codec_lock);
+
+ dev_dbg(tas_priv->dev, "%s: kcontrol %s: %d -> %d\n", __func__,
+ kcontrol->id.name, tas_priv->cur_prog, val);
+
+ if (tas_priv->cur_prog != val) {
+ tas_priv->cur_prog = val;
+ ret = 1;
+ }
+
+ return ret;
+}
+EXPORT_SYMBOL_NS_GPL(tasdevice_program_put, "SND_HDA_SCODEC_TAS2781");
+
+int tasdevice_config_get(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct tasdevice_priv *tas_priv = snd_kcontrol_chip(kcontrol);
+
+ ucontrol->value.integer.value[0] = tas_priv->cur_conf;
+
+ dev_dbg(tas_priv->dev, "%s: kcontrol %s: %d\n", __func__,
+ kcontrol->id.name, tas_priv->cur_conf);
+
+ return 0;
+}
+EXPORT_SYMBOL_NS_GPL(tasdevice_config_get, "SND_HDA_SCODEC_TAS2781");
+
+int tasdevice_config_put(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct tasdevice_priv *tas_priv = snd_kcontrol_chip(kcontrol);
+ struct tasdevice_fw *tas_fw = tas_priv->fmw;
+ int nr_config = ucontrol->value.integer.value[0];
+ int max = tas_fw->nr_configurations - 1;
+ int val, ret = 0;
+
+ val = clamp(nr_config, 0, max);
+
+ guard(mutex)(&tas_priv->codec_lock);
+
+ dev_dbg(tas_priv->dev, "%s: kcontrol %s: %d -> %d\n", __func__,
+ kcontrol->id.name, tas_priv->cur_conf, val);
+
+ if (tas_priv->cur_conf != val) {
+ tas_priv->cur_conf = val;
+ ret = 1;
+ }
+
+ return ret;
+}
+EXPORT_SYMBOL_NS_GPL(tasdevice_config_put, "SND_HDA_SCODEC_TAS2781");
+
+MODULE_DESCRIPTION("TAS2781 HDA Driver");
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Shenghao Ding, TI, <shenghao-ding@ti.com>");
diff --git a/sound/hda/codecs/side-codecs/tas2781_hda.h b/sound/hda/codecs/side-codecs/tas2781_hda.h
new file mode 100644
index 000000000000..575a701c8dfb
--- /dev/null
+++ b/sound/hda/codecs/side-codecs/tas2781_hda.h
@@ -0,0 +1,90 @@
+/* SPDX-License-Identifier: GPL-2.0-only
+ *
+ * HDA audio driver for Texas Instruments TAS2781 smart amp
+ *
+ * Copyright (C) 2025 Texas Instruments, Inc.
+ */
+#ifndef __TAS2781_HDA_H__
+#define __TAS2781_HDA_H__
+
+#include <sound/asound.h>
+
+/* Flag of calibration registers address. */
+#define TASDEV_UEFI_CALI_REG_ADDR_FLG BIT(7)
+#define TASDEVICE_CALIBRATION_DATA_NAME L"CALI_DATA"
+#define TASDEV_CALIB_N 5
+
+/*
+ * No standard control callbacks for SNDRV_CTL_ELEM_IFACE_CARD
+ * Define two controls, one is Volume control callbacks, the other is
+ * flag setting control callbacks.
+ */
+
+/* Volume control callbacks for tas2781 */
+#define ACARD_SINGLE_RANGE_EXT_TLV(xname, xreg, xshift, xmin, xmax, xinvert, \
+ xhandler_get, xhandler_put, tlv_array) { \
+ .iface = SNDRV_CTL_ELEM_IFACE_CARD, .name = (xname), \
+ .access = SNDRV_CTL_ELEM_ACCESS_TLV_READ | \
+ SNDRV_CTL_ELEM_ACCESS_READWRITE, \
+ .tlv.p = (tlv_array), \
+ .info = snd_soc_info_volsw, \
+ .get = xhandler_get, .put = xhandler_put, \
+ .private_value = (unsigned long)&(struct soc_mixer_control) { \
+ .reg = xreg, .rreg = xreg, \
+ .shift = xshift, .rshift = xshift,\
+ .min = xmin, .max = xmax, .invert = xinvert, \
+ } \
+}
+
+/* Flag control callbacks for tas2781 */
+#define ACARD_SINGLE_BOOL_EXT(xname, xdata, xhandler_get, xhandler_put) { \
+ .iface = SNDRV_CTL_ELEM_IFACE_CARD, \
+ .name = xname, \
+ .info = snd_ctl_boolean_mono_info, \
+ .get = xhandler_get, \
+ .put = xhandler_put, \
+ .private_value = xdata, \
+}
+
+enum device_catlog_id {
+ DELL = 0,
+ HP,
+ LENOVO,
+ OTHERS
+};
+
+struct tas2781_hda {
+ struct device *dev;
+ struct tasdevice_priv *priv;
+ struct snd_kcontrol *dsp_prog_ctl;
+ struct snd_kcontrol *dsp_conf_ctl;
+ struct snd_kcontrol *prof_ctl;
+ enum device_catlog_id catlog_id;
+ void *hda_priv;
+};
+
+extern const efi_guid_t tasdev_fct_efi_guid[];
+
+int tas2781_save_calibration(struct tas2781_hda *p);
+void tas2781_hda_remove(struct device *dev,
+ const struct component_ops *ops);
+int tasdevice_info_profile(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uctl);
+int tasdevice_info_programs(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uctl);
+int tasdevice_info_config(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uctl);
+int tasdevice_set_profile_id(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *uctl);
+int tasdevice_get_profile_id(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *uctl);
+int tasdevice_program_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *uctl);
+int tasdevice_program_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *uctl);
+int tasdevice_config_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *uctl);
+int tasdevice_config_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *uctl);
+
+#endif
diff --git a/sound/hda/codecs/side-codecs/tas2781_hda_i2c.c b/sound/hda/codecs/side-codecs/tas2781_hda_i2c.c
new file mode 100644
index 000000000000..a0b132681804
--- /dev/null
+++ b/sound/hda/codecs/side-codecs/tas2781_hda_i2c.c
@@ -0,0 +1,798 @@
+// SPDX-License-Identifier: GPL-2.0
+//
+// TAS2781 HDA I2C driver
+//
+// Copyright 2023 - 2025 Texas Instruments, Inc.
+//
+// Author: Shenghao Ding <shenghao-ding@ti.com>
+// Current maintainer: Baojun Xu <baojun.xu@ti.com>
+
+#include <linux/unaligned.h>
+#include <linux/acpi.h>
+#include <linux/crc8.h>
+#include <linux/crc32.h>
+#include <linux/efi.h>
+#include <linux/firmware.h>
+#include <linux/i2c.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/pci_ids.h>
+#include <linux/pm_runtime.h>
+#include <linux/regmap.h>
+#include <sound/hda_codec.h>
+#include <sound/soc.h>
+#include <sound/tas2781.h>
+#include <sound/tas2781-comlib-i2c.h>
+#include <sound/tlv.h>
+#include <sound/tas2770-tlv.h>
+#include <sound/tas2781-tlv.h>
+
+#include "hda_local.h"
+#include "hda_auto_parser.h"
+#include "hda_component.h"
+#include "hda_jack.h"
+#include "../generic.h"
+#include "tas2781_hda.h"
+
+#define TAS2563_CAL_VAR_NAME_MAX 16
+#define TAS2563_CAL_ARRAY_SIZE 80
+#define TAS2563_CAL_DATA_SIZE 4
+#define TAS2563_MAX_CHANNELS 4
+#define TAS2563_CAL_CH_SIZE 20
+
+#define TAS2563_CAL_R0_LOW TASDEVICE_REG(0, 0x0f, 0x48)
+#define TAS2563_CAL_POWER TASDEVICE_REG(0, 0x0d, 0x3c)
+#define TAS2563_CAL_INVR0 TASDEVICE_REG(0, 0x0f, 0x40)
+#define TAS2563_CAL_TLIM TASDEVICE_REG(0, 0x10, 0x14)
+#define TAS2563_CAL_R0 TASDEVICE_REG(0, 0x0f, 0x34)
+
+enum device_chip_id {
+ HDA_TAS2563,
+ HDA_TAS2770,
+ HDA_TAS2781,
+ HDA_OTHERS
+};
+
+struct tas2781_hda_i2c_priv {
+ struct snd_kcontrol *snd_ctls[2];
+ int (*save_calibration)(struct tas2781_hda *h);
+
+ int hda_chip_id;
+};
+
+static int tas2781_get_i2c_res(struct acpi_resource *ares, void *data)
+{
+ struct tasdevice_priv *tas_priv = data;
+ struct acpi_resource_i2c_serialbus *sb;
+
+ if (i2c_acpi_get_i2c_resource(ares, &sb)) {
+ if (tas_priv->ndev < TASDEVICE_MAX_CHANNELS &&
+ sb->slave_address != tas_priv->global_addr) {
+ tas_priv->tasdevice[tas_priv->ndev].dev_addr =
+ (unsigned int)sb->slave_address;
+ tas_priv->ndev++;
+ }
+ }
+ return 1;
+}
+
+static const struct acpi_gpio_params speakerid_gpios = { 0, 0, false };
+
+static const struct acpi_gpio_mapping tas2781_speaker_id_gpios[] = {
+ { "speakerid-gpios", &speakerid_gpios, 1 },
+ { }
+};
+
+static int tas2781_read_acpi(struct tasdevice_priv *p, const char *hid)
+{
+ struct acpi_device *adev;
+ struct device *physdev;
+ LIST_HEAD(resources);
+ const char *sub;
+ uint32_t subid;
+ int ret;
+
+ adev = acpi_dev_get_first_match_dev(hid, NULL, -1);
+ if (!adev) {
+ dev_err(p->dev,
+ "Failed to find an ACPI device for %s\n", hid);
+ return -ENODEV;
+ }
+
+ physdev = get_device(acpi_get_first_physical_node(adev));
+ ret = acpi_dev_get_resources(adev, &resources, tas2781_get_i2c_res, p);
+ if (ret < 0) {
+ dev_err(p->dev, "Failed to get ACPI resource.\n");
+ goto err;
+ }
+ sub = acpi_get_subsystem_id(ACPI_HANDLE(physdev));
+ if (IS_ERR(sub)) {
+ /* No subsys id in older tas2563 projects. */
+ if (!strncmp(hid, "INT8866", sizeof("INT8866")))
+ goto end_2563;
+ dev_err(p->dev, "Failed to get SUBSYS ID.\n");
+ ret = PTR_ERR(sub);
+ goto err;
+ }
+ /* Speaker id was needed for ASUS projects. */
+ ret = kstrtou32(sub, 16, &subid);
+ if (!ret && upper_16_bits(subid) == PCI_VENDOR_ID_ASUSTEK) {
+ ret = devm_acpi_dev_add_driver_gpios(p->dev,
+ tas2781_speaker_id_gpios);
+ if (ret < 0)
+ dev_err(p->dev, "Failed to add driver gpio %d.\n",
+ ret);
+ p->speaker_id = devm_gpiod_get(p->dev, "speakerid", GPIOD_IN);
+ if (IS_ERR(p->speaker_id)) {
+ dev_err(p->dev, "Failed to get Speaker id.\n");
+ ret = PTR_ERR(p->speaker_id);
+ goto err;
+ }
+ } else {
+ p->speaker_id = NULL;
+ }
+
+end_2563:
+ acpi_dev_free_resource_list(&resources);
+ strscpy(p->dev_name, hid, sizeof(p->dev_name));
+ put_device(physdev);
+ acpi_dev_put(adev);
+
+ return 0;
+
+err:
+ dev_err(p->dev, "read acpi error, ret: %d\n", ret);
+ put_device(physdev);
+ acpi_dev_put(adev);
+
+ return ret;
+}
+
+static void tas2781_hda_playback_hook(struct device *dev, int action)
+{
+ struct tas2781_hda *tas_hda = dev_get_drvdata(dev);
+
+ dev_dbg(tas_hda->dev, "%s: action = %d\n", __func__, action);
+ switch (action) {
+ case HDA_GEN_PCM_ACT_OPEN:
+ pm_runtime_get_sync(dev);
+ mutex_lock(&tas_hda->priv->codec_lock);
+ tasdevice_tuning_switch(tas_hda->priv, 0);
+ tas_hda->priv->playback_started = true;
+ mutex_unlock(&tas_hda->priv->codec_lock);
+ break;
+ case HDA_GEN_PCM_ACT_CLOSE:
+ mutex_lock(&tas_hda->priv->codec_lock);
+ tasdevice_tuning_switch(tas_hda->priv, 1);
+ tas_hda->priv->playback_started = false;
+ mutex_unlock(&tas_hda->priv->codec_lock);
+
+ pm_runtime_put_autosuspend(dev);
+ break;
+ default:
+ break;
+ }
+}
+
+static int tas2781_amp_getvol(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct tasdevice_priv *tas_priv = snd_kcontrol_chip(kcontrol);
+ struct soc_mixer_control *mc =
+ (struct soc_mixer_control *)kcontrol->private_value;
+ int ret;
+
+ mutex_lock(&tas_priv->codec_lock);
+
+ ret = tasdevice_amp_getvol(tas_priv, ucontrol, mc);
+
+ dev_dbg(tas_priv->dev, "%s: kcontrol %s: %ld\n",
+ __func__, kcontrol->id.name, ucontrol->value.integer.value[0]);
+
+ mutex_unlock(&tas_priv->codec_lock);
+
+ return ret;
+}
+
+static int tas2781_amp_putvol(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct tasdevice_priv *tas_priv = snd_kcontrol_chip(kcontrol);
+ struct soc_mixer_control *mc =
+ (struct soc_mixer_control *)kcontrol->private_value;
+ int ret;
+
+ mutex_lock(&tas_priv->codec_lock);
+
+ dev_dbg(tas_priv->dev, "%s: kcontrol %s: -> %ld\n",
+ __func__, kcontrol->id.name, ucontrol->value.integer.value[0]);
+
+ /* The check of the given value is in tasdevice_amp_putvol. */
+ ret = tasdevice_amp_putvol(tas_priv, ucontrol, mc);
+
+ mutex_unlock(&tas_priv->codec_lock);
+
+ return ret;
+}
+
+static int tas2781_force_fwload_get(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct tasdevice_priv *tas_priv = snd_kcontrol_chip(kcontrol);
+
+ mutex_lock(&tas_priv->codec_lock);
+
+ ucontrol->value.integer.value[0] = (int)tas_priv->force_fwload_status;
+ dev_dbg(tas_priv->dev, "%s: kcontrol %s: %d\n",
+ __func__, kcontrol->id.name, tas_priv->force_fwload_status);
+
+ mutex_unlock(&tas_priv->codec_lock);
+
+ return 0;
+}
+
+static int tas2781_force_fwload_put(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct tasdevice_priv *tas_priv = snd_kcontrol_chip(kcontrol);
+ bool change, val = (bool)ucontrol->value.integer.value[0];
+
+ mutex_lock(&tas_priv->codec_lock);
+
+ dev_dbg(tas_priv->dev, "%s: kcontrol %s: %d -> %d\n",
+ __func__, kcontrol->id.name,
+ tas_priv->force_fwload_status, val);
+
+ if (tas_priv->force_fwload_status == val)
+ change = false;
+ else {
+ change = true;
+ tas_priv->force_fwload_status = val;
+ }
+
+ mutex_unlock(&tas_priv->codec_lock);
+
+ return change;
+}
+
+static const struct snd_kcontrol_new tas2770_snd_controls[] = {
+ ACARD_SINGLE_RANGE_EXT_TLV("Speaker Analog Volume", TAS2770_AMP_LEVEL,
+ 0, 0, 20, 0, tas2781_amp_getvol,
+ tas2781_amp_putvol, tas2770_amp_tlv),
+ ACARD_SINGLE_RANGE_EXT_TLV("Speaker Digital Volume", TAS2770_DVC_LEVEL,
+ 0, 0, 31, 0, tas2781_amp_getvol,
+ tas2781_amp_putvol, tas2770_dvc_tlv),
+};
+
+static const struct snd_kcontrol_new tas2781_snd_controls[] = {
+ ACARD_SINGLE_RANGE_EXT_TLV("Speaker Analog Gain", TAS2781_AMP_LEVEL,
+ 1, 0, 20, 0, tas2781_amp_getvol,
+ tas2781_amp_putvol, amp_vol_tlv),
+ ACARD_SINGLE_BOOL_EXT("Speaker Force Firmware Load", 0,
+ tas2781_force_fwload_get, tas2781_force_fwload_put),
+};
+
+static const struct snd_kcontrol_new tasdevice_prof_ctrl = {
+ .name = "Speaker Profile Id",
+ .iface = SNDRV_CTL_ELEM_IFACE_CARD,
+ .info = tasdevice_info_profile,
+ .get = tasdevice_get_profile_id,
+ .put = tasdevice_set_profile_id,
+};
+
+static const struct snd_kcontrol_new tasdevice_dsp_prog_ctrl = {
+ .name = "Speaker Program Id",
+ .iface = SNDRV_CTL_ELEM_IFACE_CARD,
+ .info = tasdevice_info_programs,
+ .get = tasdevice_program_get,
+ .put = tasdevice_program_put,
+};
+
+static const struct snd_kcontrol_new tasdevice_dsp_conf_ctrl = {
+ .name = "Speaker Config Id",
+ .iface = SNDRV_CTL_ELEM_IFACE_CARD,
+ .info = tasdevice_info_config,
+ .get = tasdevice_config_get,
+ .put = tasdevice_config_put,
+};
+
+static int tas2563_save_calibration(struct tas2781_hda *h)
+{
+ efi_guid_t efi_guid = tasdev_fct_efi_guid[LENOVO];
+ char *vars[TASDEV_CALIB_N] = {
+ "R0_%d", "InvR0_%d", "R0_Low_%d", "Power_%d", "TLim_%d"
+ };
+ efi_char16_t efi_name[TAS2563_CAL_VAR_NAME_MAX];
+ unsigned long max_size = TAS2563_CAL_DATA_SIZE;
+ unsigned char var8[TAS2563_CAL_VAR_NAME_MAX];
+ struct tasdevice_priv *p = h->hda_priv;
+ struct calidata *cd = &p->cali_data;
+ struct cali_reg *r = &cd->cali_reg_array;
+ unsigned int offset = 0;
+ unsigned char *data;
+ efi_status_t status;
+ unsigned int attr;
+ int ret, i, j, k;
+
+ cd->cali_dat_sz_per_dev = TAS2563_CAL_DATA_SIZE * TASDEV_CALIB_N;
+
+ /* extra byte for each device is the device number */
+ cd->total_sz = (cd->cali_dat_sz_per_dev + 1) * p->ndev;
+ data = cd->data = devm_kzalloc(p->dev, cd->total_sz,
+ GFP_KERNEL);
+ if (!data)
+ return -ENOMEM;
+
+ for (i = 0; i < p->ndev; ++i) {
+ data[offset] = i;
+ offset++;
+ for (j = 0; j < TASDEV_CALIB_N; ++j) {
+ ret = snprintf(var8, sizeof(var8), vars[j], i);
+
+ if (ret < 0 || ret >= sizeof(var8) - 1) {
+ dev_err(p->dev, "%s: Read %s failed\n",
+ __func__, var8);
+ return -EINVAL;
+ }
+ /*
+ * Our variable names are ASCII by construction, but
+ * EFI names are wide chars. Convert and zero-pad.
+ */
+ memset(efi_name, 0, sizeof(efi_name));
+ for (k = 0; k < sizeof(var8) && var8[k]; k++)
+ efi_name[k] = var8[k];
+ status = efi.get_variable(efi_name,
+ &efi_guid, &attr, &max_size,
+ &data[offset]);
+ if (status != EFI_SUCCESS ||
+ max_size != TAS2563_CAL_DATA_SIZE) {
+ dev_warn(p->dev,
+ "Dev %d: Caldat[%d] read failed %ld\n",
+ i, j, status);
+ return -EINVAL;
+ }
+ offset += TAS2563_CAL_DATA_SIZE;
+ }
+ }
+
+ if (cd->total_sz != offset) {
+ dev_err(p->dev, "%s: tot_size(%lu) and offset(%u) dismatch\n",
+ __func__, cd->total_sz, offset);
+ return -EINVAL;
+ }
+
+ r->r0_reg = TAS2563_CAL_R0;
+ r->invr0_reg = TAS2563_CAL_INVR0;
+ r->r0_low_reg = TAS2563_CAL_R0_LOW;
+ r->pow_reg = TAS2563_CAL_POWER;
+ r->tlimit_reg = TAS2563_CAL_TLIM;
+
+ /*
+ * TAS2781_FMWLIB supports two solutions of calibrated data. One is
+ * from the driver itself: driver reads the calibrated files directly
+ * during probe; The other from user space: during init of audio hal,
+ * the audio hal will pass the calibrated data via kcontrol interface.
+ * Driver will store this data in "struct calidata" for use. For hda
+ * device, calibrated data are usunally saved into UEFI. So Hda side
+ * codec driver use the mixture of these two solutions, driver reads
+ * the data from UEFI, then store this data in "struct calidata" for
+ * use.
+ */
+ p->is_user_space_calidata = true;
+
+ return 0;
+}
+
+static void tas2781_hda_remove_controls(struct tas2781_hda *tas_hda)
+{
+ struct tas2781_hda_i2c_priv *hda_priv = tas_hda->hda_priv;
+ struct hda_codec *codec = tas_hda->priv->codec;
+
+ snd_ctl_remove(codec->card, tas_hda->dsp_prog_ctl);
+ snd_ctl_remove(codec->card, tas_hda->dsp_conf_ctl);
+
+ for (int i = ARRAY_SIZE(hda_priv->snd_ctls) - 1; i >= 0; i--)
+ snd_ctl_remove(codec->card, hda_priv->snd_ctls[i]);
+
+ snd_ctl_remove(codec->card, tas_hda->prof_ctl);
+}
+
+static void tasdev_add_kcontrols(struct tasdevice_priv *tas_priv,
+ struct snd_kcontrol **ctls, struct hda_codec *codec,
+ const struct snd_kcontrol_new *tas_snd_ctrls, int num_ctls)
+{
+ int i, ret;
+
+ for (i = 0; i < num_ctls; i++) {
+ ctls[i] = snd_ctl_new1(
+ &tas_snd_ctrls[i], tas_priv);
+ ret = snd_ctl_add(codec->card, ctls[i]);
+ if (ret) {
+ dev_err(tas_priv->dev,
+ "Failed to add KControl %s = %d\n",
+ tas_snd_ctrls[i].name, ret);
+ break;
+ }
+ }
+}
+
+static void tasdevice_dspfw_init(void *context)
+{
+ struct tasdevice_priv *tas_priv = context;
+ struct tas2781_hda *tas_hda = dev_get_drvdata(tas_priv->dev);
+ struct tas2781_hda_i2c_priv *hda_priv = tas_hda->hda_priv;
+ struct hda_codec *codec = tas_priv->codec;
+ int ret, spk_id;
+
+ tasdevice_dsp_remove(tas_priv);
+ tas_priv->fw_state = TASDEVICE_DSP_FW_PENDING;
+ if (tas_priv->speaker_id != NULL) {
+ // Speaker id need to be checked for ASUS only.
+ spk_id = gpiod_get_value(tas_priv->speaker_id);
+ if (spk_id < 0) {
+ // Speaker id is not valid, use default.
+ dev_dbg(tas_priv->dev, "Wrong spk_id = %d\n", spk_id);
+ spk_id = 0;
+ }
+ snprintf(tas_priv->coef_binaryname,
+ sizeof(tas_priv->coef_binaryname),
+ "TAS2XXX%04X%d.bin",
+ lower_16_bits(codec->core.subsystem_id),
+ spk_id);
+ } else {
+ snprintf(tas_priv->coef_binaryname,
+ sizeof(tas_priv->coef_binaryname),
+ "TAS2XXX%04X.bin",
+ lower_16_bits(codec->core.subsystem_id));
+ }
+ ret = tasdevice_dsp_parser(tas_priv);
+ if (ret) {
+ dev_err(tas_priv->dev, "dspfw load %s error\n",
+ tas_priv->coef_binaryname);
+ tas_priv->fw_state = TASDEVICE_DSP_FW_FAIL;
+ return;
+ }
+ tasdev_add_kcontrols(tas_priv, &tas_hda->dsp_prog_ctl, codec,
+ &tasdevice_dsp_prog_ctrl, 1);
+ tasdev_add_kcontrols(tas_priv, &tas_hda->dsp_conf_ctl, codec,
+ &tasdevice_dsp_conf_ctrl, 1);
+
+ tas_priv->fw_state = TASDEVICE_DSP_FW_ALL_OK;
+ tasdevice_prmg_load(tas_priv, 0);
+ if (tas_priv->fmw->nr_programs > 0)
+ tas_priv->cur_prog = 0;
+ if (tas_priv->fmw->nr_configurations > 0)
+ tas_priv->cur_conf = 0;
+
+ /* If calibrated data occurs error, dsp will still works with default
+ * calibrated data inside algo.
+ */
+ hda_priv->save_calibration(tas_hda);
+}
+
+static void tasdev_fw_ready(const struct firmware *fmw, void *context)
+{
+ struct tasdevice_priv *tas_priv = context;
+ struct tas2781_hda *tas_hda = dev_get_drvdata(tas_priv->dev);
+ struct tas2781_hda_i2c_priv *hda_priv = tas_hda->hda_priv;
+ struct hda_codec *codec = tas_priv->codec;
+ int ret;
+
+ pm_runtime_get_sync(tas_priv->dev);
+ mutex_lock(&tas_priv->codec_lock);
+
+ ret = tasdevice_rca_parser(tas_priv, fmw);
+ if (ret)
+ goto out;
+
+ tas_priv->fw_state = TASDEVICE_RCA_FW_OK;
+ tasdev_add_kcontrols(tas_priv, &tas_hda->prof_ctl, codec,
+ &tasdevice_prof_ctrl, 1);
+
+ switch (hda_priv->hda_chip_id) {
+ case HDA_TAS2770:
+ tasdev_add_kcontrols(tas_priv, hda_priv->snd_ctls, codec,
+ &tas2770_snd_controls[0],
+ ARRAY_SIZE(tas2770_snd_controls));
+ break;
+ case HDA_TAS2781:
+ tasdev_add_kcontrols(tas_priv, hda_priv->snd_ctls, codec,
+ &tas2781_snd_controls[0],
+ ARRAY_SIZE(tas2781_snd_controls));
+ tasdevice_dspfw_init(context);
+ break;
+ case HDA_TAS2563:
+ tasdevice_dspfw_init(context);
+ break;
+ default:
+ break;
+ }
+
+out:
+ mutex_unlock(&tas_hda->priv->codec_lock);
+ release_firmware(fmw);
+ pm_runtime_put_autosuspend(tas_hda->dev);
+}
+
+static int tas2781_hda_bind(struct device *dev, struct device *master,
+ void *master_data)
+{
+ struct tas2781_hda *tas_hda = dev_get_drvdata(dev);
+ struct hda_component_parent *parent = master_data;
+ struct hda_component *comp;
+ struct hda_codec *codec;
+ unsigned int subid;
+ int ret;
+
+ comp = hda_component_from_index(parent, tas_hda->priv->index);
+ if (!comp)
+ return -EINVAL;
+
+ if (comp->dev)
+ return -EBUSY;
+
+ codec = parent->codec;
+ subid = codec->core.subsystem_id >> 16;
+
+ switch (subid) {
+ case 0x1028:
+ tas_hda->catlog_id = DELL;
+ break;
+ default:
+ tas_hda->catlog_id = LENOVO;
+ break;
+ }
+
+ pm_runtime_get_sync(dev);
+
+ comp->dev = dev;
+
+ strscpy(comp->name, dev_name(dev), sizeof(comp->name));
+
+ ret = tascodec_init(tas_hda->priv, codec, THIS_MODULE, tasdev_fw_ready);
+ if (!ret)
+ comp->playback_hook = tas2781_hda_playback_hook;
+
+ pm_runtime_put_autosuspend(dev);
+
+ return ret;
+}
+
+static void tas2781_hda_unbind(struct device *dev,
+ struct device *master, void *master_data)
+{
+ struct tas2781_hda *tas_hda = dev_get_drvdata(dev);
+ struct hda_component_parent *parent = master_data;
+ struct hda_component *comp;
+
+ comp = hda_component_from_index(parent, tas_hda->priv->index);
+ if (comp && (comp->dev == dev)) {
+ comp->dev = NULL;
+ memset(comp->name, 0, sizeof(comp->name));
+ comp->playback_hook = NULL;
+ }
+
+ tas2781_hda_remove_controls(tas_hda);
+
+ tasdevice_config_info_remove(tas_hda->priv);
+ tasdevice_dsp_remove(tas_hda->priv);
+
+ tas_hda->priv->fw_state = TASDEVICE_DSP_FW_PENDING;
+}
+
+static const struct component_ops tas2781_hda_comp_ops = {
+ .bind = tas2781_hda_bind,
+ .unbind = tas2781_hda_unbind,
+};
+
+static int tas2781_hda_i2c_probe(struct i2c_client *clt)
+{
+ struct tas2781_hda_i2c_priv *hda_priv;
+ struct tas2781_hda *tas_hda;
+ const char *device_name;
+ int ret;
+
+ tas_hda = devm_kzalloc(&clt->dev, sizeof(*tas_hda), GFP_KERNEL);
+ if (!tas_hda)
+ return -ENOMEM;
+
+ hda_priv = devm_kzalloc(&clt->dev, sizeof(*hda_priv), GFP_KERNEL);
+ if (!hda_priv)
+ return -ENOMEM;
+
+ tas_hda->hda_priv = hda_priv;
+
+ dev_set_drvdata(&clt->dev, tas_hda);
+ tas_hda->dev = &clt->dev;
+
+ tas_hda->priv = tasdevice_kzalloc(clt);
+ if (!tas_hda->priv)
+ return -ENOMEM;
+
+ if (strstr(dev_name(&clt->dev), "TIAS2781")) {
+ /*
+ * TAS2781, integrated on-chip DSP with
+ * global I2C address supported.
+ */
+ device_name = "TIAS2781";
+ hda_priv->hda_chip_id = HDA_TAS2781;
+ hda_priv->save_calibration = tas2781_save_calibration;
+ tas_hda->priv->global_addr = TAS2781_GLOBAL_ADDR;
+ } else if (strstarts(dev_name(&clt->dev), "i2c-TXNW2770")) {
+ /*
+ * TAS2770, has no on-chip DSP, so no calibration data
+ * required; has no global I2C address supported.
+ */
+ device_name = "TXNW2770";
+ hda_priv->hda_chip_id = HDA_TAS2770;
+ } else if (strstarts(dev_name(&clt->dev),
+ "i2c-TXNW2781:00-tas2781-hda.0")) {
+ device_name = "TXNW2781";
+ hda_priv->save_calibration = tas2781_save_calibration;
+ tas_hda->priv->global_addr = TAS2781_GLOBAL_ADDR;
+ } else if (strstr(dev_name(&clt->dev), "INT8866")) {
+ /*
+ * TAS2563, integrated on-chip DSP with
+ * global I2C address supported.
+ */
+ device_name = "INT8866";
+ hda_priv->hda_chip_id = HDA_TAS2563;
+ hda_priv->save_calibration = tas2563_save_calibration;
+ tas_hda->priv->global_addr = TAS2563_GLOBAL_ADDR;
+ } else {
+ return -ENODEV;
+ }
+
+ tas_hda->priv->irq = clt->irq;
+ ret = tas2781_read_acpi(tas_hda->priv, device_name);
+ if (ret)
+ return dev_err_probe(tas_hda->dev, ret,
+ "Platform not supported\n");
+
+ ret = tasdevice_init(tas_hda->priv);
+ if (ret)
+ goto err;
+
+ pm_runtime_set_autosuspend_delay(tas_hda->dev, 3000);
+ pm_runtime_use_autosuspend(tas_hda->dev);
+ pm_runtime_mark_last_busy(tas_hda->dev);
+ pm_runtime_set_active(tas_hda->dev);
+ pm_runtime_enable(tas_hda->dev);
+
+ tasdevice_reset(tas_hda->priv);
+
+ ret = component_add(tas_hda->dev, &tas2781_hda_comp_ops);
+ if (ret) {
+ dev_err(tas_hda->dev, "Register component failed: %d\n", ret);
+ pm_runtime_disable(tas_hda->dev);
+ }
+
+err:
+ if (ret)
+ tas2781_hda_remove(&clt->dev, &tas2781_hda_comp_ops);
+ return ret;
+}
+
+static void tas2781_hda_i2c_remove(struct i2c_client *clt)
+{
+ tas2781_hda_remove(&clt->dev, &tas2781_hda_comp_ops);
+}
+
+static int tas2781_runtime_suspend(struct device *dev)
+{
+ struct tas2781_hda *tas_hda = dev_get_drvdata(dev);
+
+ dev_dbg(tas_hda->dev, "Runtime Suspend\n");
+
+ mutex_lock(&tas_hda->priv->codec_lock);
+
+ /* The driver powers up the amplifiers at module load time.
+ * Stop the playback if it's unused.
+ */
+ if (tas_hda->priv->playback_started) {
+ tasdevice_tuning_switch(tas_hda->priv, 1);
+ tas_hda->priv->playback_started = false;
+ }
+
+ mutex_unlock(&tas_hda->priv->codec_lock);
+
+ return 0;
+}
+
+static int tas2781_runtime_resume(struct device *dev)
+{
+ struct tas2781_hda *tas_hda = dev_get_drvdata(dev);
+
+ dev_dbg(tas_hda->dev, "Runtime Resume\n");
+
+ mutex_lock(&tas_hda->priv->codec_lock);
+
+ tasdevice_prmg_load(tas_hda->priv, tas_hda->priv->cur_prog);
+
+ mutex_unlock(&tas_hda->priv->codec_lock);
+
+ return 0;
+}
+
+static int tas2781_system_suspend(struct device *dev)
+{
+ struct tas2781_hda *tas_hda = dev_get_drvdata(dev);
+
+ dev_dbg(tas_hda->priv->dev, "System Suspend\n");
+
+ mutex_lock(&tas_hda->priv->codec_lock);
+
+ /* Shutdown chip before system suspend */
+ if (tas_hda->priv->playback_started)
+ tasdevice_tuning_switch(tas_hda->priv, 1);
+
+ mutex_unlock(&tas_hda->priv->codec_lock);
+
+ /*
+ * Reset GPIO may be shared, so cannot reset here.
+ * However beyond this point, amps may be powered down.
+ */
+ return 0;
+}
+
+static int tas2781_system_resume(struct device *dev)
+{
+ struct tas2781_hda *tas_hda = dev_get_drvdata(dev);
+ int i;
+
+ dev_dbg(tas_hda->priv->dev, "System Resume\n");
+
+ mutex_lock(&tas_hda->priv->codec_lock);
+
+ for (i = 0; i < tas_hda->priv->ndev; i++) {
+ tas_hda->priv->tasdevice[i].cur_book = -1;
+ tas_hda->priv->tasdevice[i].cur_prog = -1;
+ tas_hda->priv->tasdevice[i].cur_conf = -1;
+ }
+ tasdevice_reset(tas_hda->priv);
+ tasdevice_prmg_load(tas_hda->priv, tas_hda->priv->cur_prog);
+
+ if (tas_hda->priv->playback_started)
+ tasdevice_tuning_switch(tas_hda->priv, 0);
+
+ mutex_unlock(&tas_hda->priv->codec_lock);
+
+ return 0;
+}
+
+static const struct dev_pm_ops tas2781_hda_pm_ops = {
+ RUNTIME_PM_OPS(tas2781_runtime_suspend, tas2781_runtime_resume, NULL)
+ SYSTEM_SLEEP_PM_OPS(tas2781_system_suspend, tas2781_system_resume)
+};
+
+static const struct i2c_device_id tas2781_hda_i2c_id[] = {
+ { "tas2781-hda" },
+ {}
+};
+
+static const struct acpi_device_id tas2781_acpi_hda_match[] = {
+ {"INT8866", 0 },
+ {"TIAS2781", 0 },
+ {"TXNW2770", 0 },
+ {"TXNW2781", 0 },
+ {}
+};
+MODULE_DEVICE_TABLE(acpi, tas2781_acpi_hda_match);
+
+static struct i2c_driver tas2781_hda_i2c_driver = {
+ .driver = {
+ .name = "tas2781-hda",
+ .acpi_match_table = tas2781_acpi_hda_match,
+ .pm = &tas2781_hda_pm_ops,
+ },
+ .id_table = tas2781_hda_i2c_id,
+ .probe = tas2781_hda_i2c_probe,
+ .remove = tas2781_hda_i2c_remove,
+};
+module_i2c_driver(tas2781_hda_i2c_driver);
+
+MODULE_DESCRIPTION("TAS2781 HDA Driver");
+MODULE_AUTHOR("Shenghao Ding, TI, <shenghao-ding@ti.com>");
+MODULE_LICENSE("GPL");
+MODULE_IMPORT_NS("SND_SOC_TAS2781_FMWLIB");
+MODULE_IMPORT_NS("SND_HDA_SCODEC_TAS2781");
diff --git a/sound/hda/codecs/side-codecs/tas2781_hda_spi.c b/sound/hda/codecs/side-codecs/tas2781_hda_spi.c
new file mode 100644
index 000000000000..09a5d0f131b2
--- /dev/null
+++ b/sound/hda/codecs/side-codecs/tas2781_hda_spi.c
@@ -0,0 +1,954 @@
+// SPDX-License-Identifier: GPL-2.0
+//
+// TAS2781 HDA SPI driver
+//
+// Copyright 2024 - 2025 Texas Instruments, Inc.
+//
+// Author: Baojun Xu <baojun.xu@ti.com>
+
+#include <linux/acpi.h>
+#include <linux/array_size.h>
+#include <linux/bits.h>
+#include <linux/cleanup.h>
+#include <linux/crc8.h>
+#include <linux/crc32.h>
+#include <linux/efi.h>
+#include <linux/firmware.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/pm_runtime.h>
+#include <linux/property.h>
+#include <linux/regmap.h>
+#include <linux/spi/spi.h>
+#include <linux/time.h>
+#include <linux/types.h>
+#include <linux/units.h>
+
+#include <sound/hda_codec.h>
+#include <sound/soc.h>
+#include <sound/tas2781.h>
+#include <sound/tlv.h>
+#include <sound/tas2781-tlv.h>
+
+#include "hda_local.h"
+#include "hda_auto_parser.h"
+#include "hda_component.h"
+#include "hda_jack.h"
+#include "../generic.h"
+#include "tas2781_hda.h"
+
+#define TASDEVICE_RANGE_MAX_SIZE (256 * 128)
+#define TASDEVICE_WIN_LEN 128
+#define TAS2781_SPI_MAX_FREQ (4 * HZ_PER_MHZ)
+/* Flag of calibration registers address. */
+#define TASDEVICE_CALIBRATION_REG_ADDRESS BIT(7)
+#define TASDEV_UEFI_CALI_REG_ADDR_FLG BIT(7)
+
+/* System Reset Check Register */
+#define TAS2781_REG_CLK_CONFIG TASDEVICE_REG(0x0, 0x0, 0x5c)
+#define TAS2781_REG_CLK_CONFIG_RESET 0x19
+
+struct tas2781_hda_spi_priv {
+ struct snd_kcontrol *snd_ctls[3];
+};
+
+static const struct regmap_range_cfg tasdevice_ranges[] = {
+ {
+ .range_min = 0,
+ .range_max = TASDEVICE_RANGE_MAX_SIZE,
+ .selector_reg = TASDEVICE_PAGE_SELECT,
+ .selector_mask = GENMASK(7, 0),
+ .selector_shift = 0,
+ .window_start = 0,
+ .window_len = TASDEVICE_WIN_LEN,
+ },
+};
+
+static const struct regmap_config tasdevice_regmap = {
+ .reg_bits = 8,
+ .val_bits = 8,
+ .zero_flag_mask = true,
+ .read_flag_mask = 0x01,
+ .reg_shift = -1,
+ .cache_type = REGCACHE_NONE,
+ .ranges = tasdevice_ranges,
+ .num_ranges = ARRAY_SIZE(tasdevice_ranges),
+ .max_register = TASDEVICE_RANGE_MAX_SIZE,
+};
+
+static int tasdevice_spi_dev_read(struct tasdevice_priv *tas_priv,
+ unsigned short chn, unsigned int reg, unsigned int *val)
+{
+ int ret;
+
+ /*
+ * In our TAS2781 SPI mode, if read from other book (not book 0),
+ * or read from page number larger than 1 in book 0, one more byte
+ * read is needed, and first byte is a dummy byte, need to be ignored.
+ */
+ if ((TASDEVICE_BOOK_ID(reg) > 0) || (TASDEVICE_PAGE_ID(reg) > 1)) {
+ unsigned char data[2];
+
+ ret = tasdevice_dev_bulk_read(tas_priv, chn, reg,
+ data, sizeof(data));
+ *val = data[1];
+ } else {
+ ret = tasdevice_dev_read(tas_priv, chn, reg, val);
+ }
+ if (ret < 0)
+ dev_err(tas_priv->dev, "%s, E=%d\n", __func__, ret);
+
+ return ret;
+}
+
+static int tasdevice_spi_dev_bulk_read(struct tasdevice_priv *tas_priv,
+ unsigned short chn, unsigned int reg, unsigned char *data,
+ unsigned int len)
+{
+ int ret;
+
+ /*
+ * In our TAS2781 SPI mode, if read from other book (not book 0),
+ * or read from page number larger than 1 in book 0, one more byte
+ * read is needed, and first byte is a dummy byte, need to be ignored.
+ */
+ if ((TASDEVICE_BOOK_ID(reg) > 0) || (TASDEVICE_PAGE_ID(reg) > 1)) {
+ unsigned char buf[TASDEVICE_WIN_LEN + 1];
+
+ ret = tasdevice_dev_bulk_read(tas_priv, chn, reg,
+ buf, len + 1);
+ memcpy(data, buf + 1, len);
+ } else {
+ ret = tasdevice_dev_bulk_read(tas_priv, chn, reg, data, len);
+ }
+ if (ret < 0)
+ dev_err(tas_priv->dev, "%s, E=%d\n", __func__, ret);
+
+ return ret;
+}
+
+static int tasdevice_spi_dev_update_bits(struct tasdevice_priv *tas_priv,
+ unsigned short chn, unsigned int reg, unsigned int mask,
+ unsigned int value)
+{
+ int ret, val;
+
+ /*
+ * In our TAS2781 SPI mode, read/write was masked in last bit of
+ * address, it cause regmap_update_bits() not work as expected.
+ */
+ ret = tasdevice_dev_read(tas_priv, chn, reg, &val);
+ if (ret < 0) {
+ dev_err(tas_priv->dev, "%s, E=%d\n", __func__, ret);
+ return ret;
+ }
+
+ ret = tasdevice_dev_write(tas_priv, chn, TASDEVICE_PAGE_REG(reg),
+ (val & ~mask) | (mask & value));
+ if (ret < 0)
+ dev_err(tas_priv->dev, "%s, E=%d\n", __func__, ret);
+
+ return ret;
+}
+
+static int tasdevice_spi_change_chn_book(struct tasdevice_priv *p,
+ unsigned short chn, int book)
+{
+ int ret = 0;
+
+ if (chn == p->index) {
+ struct tasdevice *tasdev = &p->tasdevice[chn];
+ struct regmap *map = p->regmap;
+
+ if (tasdev->cur_book != book) {
+ ret = regmap_write(map, TASDEVICE_BOOKCTL_REG, book);
+ if (ret < 0)
+ dev_err(p->dev, "%s, E=%d\n", __func__, ret);
+ else
+ tasdev->cur_book = book;
+ }
+ } else {
+ ret = -EXDEV;
+ dev_dbg(p->dev, "Not error, %s ignore channel(%d)\n",
+ __func__, chn);
+ }
+
+ return ret;
+}
+
+static void tas2781_spi_reset(struct tasdevice_priv *tas_dev)
+{
+ int ret;
+
+ if (tas_dev->reset) {
+ gpiod_set_value_cansleep(tas_dev->reset, 0);
+ fsleep(800);
+ gpiod_set_value_cansleep(tas_dev->reset, 1);
+ } else {
+ ret = tasdevice_dev_write(tas_dev, tas_dev->index,
+ TASDEVICE_REG_SWRESET, TASDEVICE_REG_SWRESET_RESET);
+ if (ret < 0) {
+ dev_err(tas_dev->dev, "dev sw-reset fail, %d\n", ret);
+ return;
+ }
+ fsleep(1000);
+ }
+}
+
+static int tascodec_spi_init(struct tasdevice_priv *tas_priv,
+ void *codec, struct module *module,
+ void (*cont)(const struct firmware *fw, void *context))
+{
+ int ret;
+
+ /*
+ * Codec Lock Hold to ensure that codec_probe and firmware parsing and
+ * loading do not simultaneously execute.
+ */
+ guard(mutex)(&tas_priv->codec_lock);
+
+ scnprintf(tas_priv->rca_binaryname,
+ sizeof(tas_priv->rca_binaryname), "%sRCA%d.bin",
+ tas_priv->dev_name, tas_priv->ndev);
+ crc8_populate_msb(tas_priv->crc8_lkp_tbl, TASDEVICE_CRC8_POLYNOMIAL);
+ tas_priv->codec = codec;
+ ret = request_firmware_nowait(module, FW_ACTION_UEVENT,
+ tas_priv->rca_binaryname, tas_priv->dev, GFP_KERNEL, tas_priv,
+ cont);
+ if (ret)
+ dev_err(tas_priv->dev, "request_firmware_nowait err:0x%08x\n",
+ ret);
+
+ return ret;
+}
+
+static void tasdevice_spi_init(struct tasdevice_priv *tas_priv)
+{
+ tas_priv->tasdevice[tas_priv->index].cur_book = -1;
+ tas_priv->tasdevice[tas_priv->index].cur_conf = -1;
+ tas_priv->tasdevice[tas_priv->index].cur_prog = -1;
+
+ tas_priv->isspi = true;
+
+ tas_priv->update_bits = tasdevice_spi_dev_update_bits;
+ tas_priv->change_chn_book = tasdevice_spi_change_chn_book;
+ tas_priv->dev_read = tasdevice_spi_dev_read;
+ tas_priv->dev_bulk_read = tasdevice_spi_dev_bulk_read;
+
+ mutex_init(&tas_priv->codec_lock);
+}
+
+static int tasdevice_spi_amp_putvol(struct tasdevice_priv *tas_priv,
+ struct snd_ctl_elem_value *ucontrol, struct soc_mixer_control *mc)
+{
+ unsigned int invert = mc->invert;
+ unsigned char mask;
+ int max = mc->max;
+ int val, ret;
+
+ mask = rounddown_pow_of_two(max);
+ mask <<= mc->shift;
+ val = clamp(invert ? max - ucontrol->value.integer.value[0] :
+ ucontrol->value.integer.value[0], 0, max);
+
+ ret = tasdevice_spi_dev_update_bits(tas_priv, tas_priv->index,
+ mc->reg, mask, (unsigned int)(val << mc->shift));
+ if (ret)
+ dev_err(tas_priv->dev, "set AMP vol error in dev %d\n",
+ tas_priv->index);
+
+ return ret;
+}
+
+static int tasdevice_spi_amp_getvol(struct tasdevice_priv *tas_priv,
+ struct snd_ctl_elem_value *ucontrol, struct soc_mixer_control *mc)
+{
+ unsigned int invert = mc->invert;
+ unsigned char mask = 0;
+ int max = mc->max;
+ int ret, val;
+
+ ret = tasdevice_spi_dev_read(tas_priv, tas_priv->index, mc->reg, &val);
+ if (ret) {
+ dev_err(tas_priv->dev, "%s, get AMP vol error\n", __func__);
+ return ret;
+ }
+
+ mask = rounddown_pow_of_two(max);
+ mask <<= mc->shift;
+ val = (val & mask) >> mc->shift;
+ val = clamp(invert ? max - val : val, 0, max);
+ ucontrol->value.integer.value[0] = val;
+
+ return ret;
+}
+
+static int tasdevice_spi_digital_putvol(struct tasdevice_priv *p,
+ struct snd_ctl_elem_value *ucontrol, struct soc_mixer_control *mc)
+{
+ unsigned int invert = mc->invert;
+ int max = mc->max;
+ int val, ret;
+
+ val = clamp(invert ? max - ucontrol->value.integer.value[0] :
+ ucontrol->value.integer.value[0], 0, max);
+ ret = tasdevice_dev_write(p, p->index, mc->reg, (unsigned int)val);
+ if (ret)
+ dev_err(p->dev, "set digital vol err in dev %d\n", p->index);
+
+ return ret;
+}
+
+static int tasdevice_spi_digital_getvol(struct tasdevice_priv *p,
+ struct snd_ctl_elem_value *ucontrol, struct soc_mixer_control *mc)
+{
+ unsigned int invert = mc->invert;
+ int max = mc->max;
+ int ret, val;
+
+ ret = tasdevice_spi_dev_read(p, p->index, mc->reg, &val);
+ if (ret) {
+ dev_err(p->dev, "%s, get digital vol err\n", __func__);
+ return ret;
+ }
+
+ val = clamp(invert ? max - val : val, 0, max);
+ ucontrol->value.integer.value[0] = val;
+
+ return ret;
+}
+
+static int tas2781_read_acpi(struct tas2781_hda *tas_hda,
+ const char *hid, int id)
+{
+ struct tasdevice_priv *p = tas_hda->priv;
+ struct acpi_device *adev;
+ struct device *physdev;
+ u32 values[HDA_MAX_COMPONENTS];
+ const char *property;
+ size_t nval;
+ int ret, i;
+
+ adev = acpi_dev_get_first_match_dev(hid, NULL, -1);
+ if (!adev) {
+ dev_err(p->dev, "Failed to find ACPI device: %s\n", hid);
+ return -ENODEV;
+ }
+
+ strscpy(p->dev_name, hid, sizeof(p->dev_name));
+ physdev = get_device(acpi_get_first_physical_node(adev));
+ acpi_dev_put(adev);
+
+ property = "ti,dev-index";
+ ret = device_property_count_u32(physdev, property);
+ if (ret <= 0 || ret > ARRAY_SIZE(values)) {
+ ret = -EINVAL;
+ goto err;
+ }
+ p->ndev = nval = ret;
+
+ ret = device_property_read_u32_array(physdev, property, values, nval);
+ if (ret)
+ goto err;
+
+ p->index = U8_MAX;
+ for (i = 0; i < nval; i++) {
+ if (values[i] == id) {
+ p->index = i;
+ break;
+ }
+ }
+ if (p->index == U8_MAX) {
+ dev_dbg(p->dev, "No index found in %s\n", property);
+ ret = -ENODEV;
+ goto err;
+ }
+
+ if (p->index == 0) {
+ /* All of amps share same RESET pin. */
+ p->reset = devm_gpiod_get_index_optional(physdev, "reset",
+ p->index, GPIOD_OUT_LOW);
+ if (IS_ERR(p->reset)) {
+ ret = PTR_ERR(p->reset);
+ dev_err_probe(p->dev, ret, "Failed on reset GPIO\n");
+ goto err;
+ }
+ }
+ put_device(physdev);
+
+ return 0;
+err:
+ dev_err(p->dev, "read acpi error, ret: %d\n", ret);
+ put_device(physdev);
+ acpi_dev_put(adev);
+
+ return ret;
+}
+
+static void tas2781_hda_playback_hook(struct device *dev, int action)
+{
+ struct tas2781_hda *tas_hda = dev_get_drvdata(dev);
+ struct tasdevice_priv *tas_priv = tas_hda->priv;
+
+ if (action == HDA_GEN_PCM_ACT_OPEN) {
+ pm_runtime_get_sync(dev);
+ guard(mutex)(&tas_priv->codec_lock);
+ if (tas_priv->fw_state == TASDEVICE_DSP_FW_ALL_OK)
+ tasdevice_tuning_switch(tas_hda->priv, 0);
+ } else if (action == HDA_GEN_PCM_ACT_CLOSE) {
+ guard(mutex)(&tas_priv->codec_lock);
+ if (tas_priv->fw_state == TASDEVICE_DSP_FW_ALL_OK)
+ tasdevice_tuning_switch(tas_priv, 1);
+ pm_runtime_put_autosuspend(dev);
+ }
+}
+
+/*
+ * tas2781_digital_getvol - get the volum control
+ * @kcontrol: control pointer
+ * @ucontrol: User data
+ *
+ * Customer Kcontrol for tas2781 is primarily for regmap booking, paging
+ * depends on internal regmap mechanism.
+ * tas2781 contains book and page two-level register map, especially
+ * book switching will set the register BXXP00R7F, after switching to the
+ * correct book, then leverage the mechanism for paging to access the
+ * register.
+ *
+ * Return 0 if succeeded.
+ */
+static int tas2781_digital_getvol(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct tasdevice_priv *tas_priv = snd_kcontrol_chip(kcontrol);
+ struct soc_mixer_control *mc =
+ (struct soc_mixer_control *)kcontrol->private_value;
+
+ guard(mutex)(&tas_priv->codec_lock);
+ return tasdevice_spi_digital_getvol(tas_priv, ucontrol, mc);
+}
+
+static int tas2781_amp_getvol(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct tasdevice_priv *tas_priv = snd_kcontrol_chip(kcontrol);
+ struct soc_mixer_control *mc =
+ (struct soc_mixer_control *)kcontrol->private_value;
+
+ guard(mutex)(&tas_priv->codec_lock);
+ return tasdevice_spi_amp_getvol(tas_priv, ucontrol, mc);
+}
+
+static int tas2781_digital_putvol(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct tasdevice_priv *tas_priv = snd_kcontrol_chip(kcontrol);
+ struct soc_mixer_control *mc =
+ (struct soc_mixer_control *)kcontrol->private_value;
+
+ guard(mutex)(&tas_priv->codec_lock);
+ return tasdevice_spi_digital_putvol(tas_priv, ucontrol, mc);
+}
+
+static int tas2781_amp_putvol(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct tasdevice_priv *tas_priv = snd_kcontrol_chip(kcontrol);
+ struct soc_mixer_control *mc =
+ (struct soc_mixer_control *)kcontrol->private_value;
+
+ guard(mutex)(&tas_priv->codec_lock);
+ return tasdevice_spi_amp_putvol(tas_priv, ucontrol, mc);
+}
+
+static int tas2781_force_fwload_get(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct tasdevice_priv *tas_priv = snd_kcontrol_chip(kcontrol);
+
+ ucontrol->value.integer.value[0] = (int)tas_priv->force_fwload_status;
+ dev_dbg(tas_priv->dev, "%s : Force FWload %s\n", __func__,
+ str_on_off(tas_priv->force_fwload_status));
+
+ return 0;
+}
+
+static int tas2781_force_fwload_put(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct tasdevice_priv *tas_priv = snd_kcontrol_chip(kcontrol);
+ bool change, val = (bool)ucontrol->value.integer.value[0];
+
+ if (tas_priv->force_fwload_status == val) {
+ change = false;
+ } else {
+ change = true;
+ tas_priv->force_fwload_status = val;
+ }
+ dev_dbg(tas_priv->dev, "%s : Force FWload %s\n", __func__,
+ str_on_off(tas_priv->force_fwload_status));
+
+ return change;
+}
+
+static struct snd_kcontrol_new tas2781_snd_ctls[] = {
+ ACARD_SINGLE_RANGE_EXT_TLV(NULL, TAS2781_AMP_LEVEL, 1, 0, 20, 0,
+ tas2781_amp_getvol, tas2781_amp_putvol, amp_vol_tlv),
+ ACARD_SINGLE_RANGE_EXT_TLV(NULL, TAS2781_DVC_LVL, 0, 0, 200, 1,
+ tas2781_digital_getvol, tas2781_digital_putvol, dvc_tlv),
+ ACARD_SINGLE_BOOL_EXT(NULL, 0, tas2781_force_fwload_get,
+ tas2781_force_fwload_put),
+};
+
+static struct snd_kcontrol_new tas2781_prof_ctl = {
+ .iface = SNDRV_CTL_ELEM_IFACE_CARD,
+ .info = tasdevice_info_profile,
+ .get = tasdevice_get_profile_id,
+ .put = tasdevice_set_profile_id,
+};
+
+static struct snd_kcontrol_new tas2781_dsp_ctls[] = {
+ /* Speaker Program */
+ {
+ .iface = SNDRV_CTL_ELEM_IFACE_CARD,
+ .info = tasdevice_info_programs,
+ .get = tasdevice_program_get,
+ .put = tasdevice_program_put,
+ },
+ /* Speaker Config */
+ {
+ .iface = SNDRV_CTL_ELEM_IFACE_CARD,
+ .info = tasdevice_info_config,
+ .get = tasdevice_config_get,
+ .put = tasdevice_config_put,
+ },
+};
+
+static void tas2781_hda_remove_controls(struct tas2781_hda *tas_hda)
+{
+ struct hda_codec *codec = tas_hda->priv->codec;
+ struct tas2781_hda_spi_priv *h_priv = tas_hda->hda_priv;
+
+ snd_ctl_remove(codec->card, tas_hda->dsp_prog_ctl);
+
+ snd_ctl_remove(codec->card, tas_hda->dsp_conf_ctl);
+
+ for (int i = ARRAY_SIZE(h_priv->snd_ctls) - 1; i >= 0; i--)
+ snd_ctl_remove(codec->card, h_priv->snd_ctls[i]);
+
+ snd_ctl_remove(codec->card, tas_hda->prof_ctl);
+}
+
+static int tas2781_hda_spi_prf_ctl(struct tas2781_hda *h)
+{
+ struct tasdevice_priv *p = h->priv;
+ struct hda_codec *c = p->codec;
+ char name[64];
+ int rc;
+
+ snprintf(name, sizeof(name), "Speaker-%d Profile Id", p->index);
+ tas2781_prof_ctl.name = name;
+ h->prof_ctl = snd_ctl_new1(&tas2781_prof_ctl, p);
+ rc = snd_ctl_add(c->card, h->prof_ctl);
+ if (rc)
+ dev_err(p->dev, "Failed to add KControl: %s, rc = %d\n",
+ tas2781_prof_ctl.name, rc);
+ return rc;
+}
+
+static int tas2781_hda_spi_snd_ctls(struct tas2781_hda *h)
+{
+ struct tas2781_hda_spi_priv *h_priv = h->hda_priv;
+ struct tasdevice_priv *p = h->priv;
+ struct hda_codec *c = p->codec;
+ char name[64];
+ int i = 0;
+ int rc;
+
+ snprintf(name, sizeof(name), "Speaker-%d Analog Volume", p->index);
+ tas2781_snd_ctls[i].name = name;
+ h_priv->snd_ctls[i] = snd_ctl_new1(&tas2781_snd_ctls[i], p);
+ rc = snd_ctl_add(c->card, h_priv->snd_ctls[i]);
+ if (rc) {
+ dev_err(p->dev, "Failed to add KControl: %s, rc = %d\n",
+ tas2781_snd_ctls[i].name, rc);
+ return rc;
+ }
+ i++;
+ snprintf(name, sizeof(name), "Speaker-%d Digital Volume", p->index);
+ tas2781_snd_ctls[i].name = name;
+ h_priv->snd_ctls[i] = snd_ctl_new1(&tas2781_snd_ctls[i], p);
+ rc = snd_ctl_add(c->card, h_priv->snd_ctls[i]);
+ if (rc) {
+ dev_err(p->dev, "Failed to add KControl: %s, rc = %d\n",
+ tas2781_snd_ctls[i].name, rc);
+ return rc;
+ }
+ i++;
+ snprintf(name, sizeof(name), "Froce Speaker-%d FW Load", p->index);
+ tas2781_snd_ctls[i].name = name;
+ h_priv->snd_ctls[i] = snd_ctl_new1(&tas2781_snd_ctls[i], p);
+ rc = snd_ctl_add(c->card, h_priv->snd_ctls[i]);
+ if (rc) {
+ dev_err(p->dev, "Failed to add KControl: %s, rc = %d\n",
+ tas2781_snd_ctls[i].name, rc);
+ }
+ return rc;
+}
+
+static int tas2781_hda_spi_dsp_ctls(struct tas2781_hda *h)
+{
+ struct tasdevice_priv *p = h->priv;
+ struct hda_codec *c = p->codec;
+ char name[64];
+ int i = 0;
+ int rc;
+
+ snprintf(name, sizeof(name), "Speaker-%d Program Id", p->index);
+ tas2781_dsp_ctls[i].name = name;
+ h->dsp_prog_ctl = snd_ctl_new1(&tas2781_dsp_ctls[i], p);
+ rc = snd_ctl_add(c->card, h->dsp_prog_ctl);
+ if (rc) {
+ dev_err(p->dev, "Failed to add KControl: %s, rc = %d\n",
+ tas2781_dsp_ctls[i].name, rc);
+ return rc;
+ }
+ i++;
+ snprintf(name, sizeof(name), "Speaker-%d Config Id", p->index);
+ tas2781_dsp_ctls[i].name = name;
+ h->dsp_conf_ctl = snd_ctl_new1(&tas2781_dsp_ctls[i], p);
+ rc = snd_ctl_add(c->card, h->dsp_conf_ctl);
+ if (rc) {
+ dev_err(p->dev, "Failed to add KControl: %s, rc = %d\n",
+ tas2781_dsp_ctls[i].name, rc);
+ }
+
+ return rc;
+}
+
+static void tasdev_fw_ready(const struct firmware *fmw, void *context)
+{
+ struct tasdevice_priv *tas_priv = context;
+ struct tas2781_hda *tas_hda = dev_get_drvdata(tas_priv->dev);
+ struct hda_codec *codec = tas_priv->codec;
+ int ret, val;
+
+ pm_runtime_get_sync(tas_priv->dev);
+ guard(mutex)(&tas_priv->codec_lock);
+
+ ret = tasdevice_rca_parser(tas_priv, fmw);
+ if (ret)
+ goto out;
+
+ /* Add control one time only. */
+ ret = tas2781_hda_spi_prf_ctl(tas_hda);
+ if (ret)
+ goto out;
+
+ ret = tas2781_hda_spi_snd_ctls(tas_hda);
+ if (ret)
+ goto out;
+
+ tasdevice_dsp_remove(tas_priv);
+
+ tas_priv->fw_state = TASDEVICE_DSP_FW_PENDING;
+ scnprintf(tas_priv->coef_binaryname, 64, "TAS2XXX%04X-%01d.bin",
+ lower_16_bits(codec->core.subsystem_id), tas_priv->index);
+ ret = tasdevice_dsp_parser(tas_priv);
+ if (ret) {
+ dev_err(tas_priv->dev, "dspfw load %s error\n",
+ tas_priv->coef_binaryname);
+ tas_priv->fw_state = TASDEVICE_DSP_FW_FAIL;
+ goto out;
+ }
+
+ ret = tas2781_hda_spi_dsp_ctls(tas_hda);
+ if (ret)
+ goto out;
+ /* Perform AMP reset before firmware download. */
+ tas2781_spi_reset(tas_priv);
+ tas_priv->rcabin.profile_cfg_id = 0;
+
+ tas_priv->fw_state = TASDEVICE_DSP_FW_ALL_OK;
+ ret = tasdevice_spi_dev_read(tas_priv, tas_priv->index,
+ TAS2781_REG_CLK_CONFIG, &val);
+ if (ret < 0)
+ goto out;
+
+ if (val == TAS2781_REG_CLK_CONFIG_RESET) {
+ ret = tasdevice_prmg_load(tas_priv, 0);
+ if (ret < 0) {
+ dev_err(tas_priv->dev, "FW download failed = %d\n",
+ ret);
+ goto out;
+ }
+ tas_priv->fw_state = TASDEVICE_DSP_FW_ALL_OK;
+ }
+ if (tas_priv->fmw->nr_programs > 0)
+ tas_priv->tasdevice[tas_priv->index].cur_prog = 0;
+ if (tas_priv->fmw->nr_configurations > 0)
+ tas_priv->tasdevice[tas_priv->index].cur_conf = 0;
+
+ /*
+ * If calibrated data occurs error, dsp will still works with default
+ * calibrated data inside algo.
+ */
+ tas2781_save_calibration(tas_hda);
+out:
+ release_firmware(fmw);
+ pm_runtime_put_autosuspend(tas_hda->priv->dev);
+}
+
+static int tas2781_hda_bind(struct device *dev, struct device *master,
+ void *master_data)
+{
+ struct tas2781_hda *tas_hda = dev_get_drvdata(dev);
+ struct hda_component_parent *parent = master_data;
+ struct hda_component *comp;
+ struct hda_codec *codec;
+ int ret;
+
+ comp = hda_component_from_index(parent, tas_hda->priv->index);
+ if (!comp)
+ return -EINVAL;
+
+ if (comp->dev)
+ return -EBUSY;
+
+ codec = parent->codec;
+
+ pm_runtime_get_sync(dev);
+
+ comp->dev = dev;
+
+ strscpy(comp->name, dev_name(dev), sizeof(comp->name));
+
+ ret = tascodec_spi_init(tas_hda->priv, codec, THIS_MODULE,
+ tasdev_fw_ready);
+ if (!ret)
+ comp->playback_hook = tas2781_hda_playback_hook;
+
+ pm_runtime_put_autosuspend(dev);
+
+ return ret;
+}
+
+static void tas2781_hda_unbind(struct device *dev, struct device *master,
+ void *master_data)
+{
+ struct tas2781_hda *tas_hda = dev_get_drvdata(dev);
+ struct hda_component_parent *parent = master_data;
+ struct tasdevice_priv *tas_priv = tas_hda->priv;
+ struct hda_component *comp;
+
+ comp = hda_component_from_index(parent, tas_priv->index);
+ if (comp && (comp->dev == dev)) {
+ comp->dev = NULL;
+ memset(comp->name, 0, sizeof(comp->name));
+ comp->playback_hook = NULL;
+ }
+
+ tas2781_hda_remove_controls(tas_hda);
+
+ tasdevice_config_info_remove(tas_priv);
+ tasdevice_dsp_remove(tas_priv);
+
+ tas_hda->priv->fw_state = TASDEVICE_DSP_FW_PENDING;
+}
+
+static const struct component_ops tas2781_hda_comp_ops = {
+ .bind = tas2781_hda_bind,
+ .unbind = tas2781_hda_unbind,
+};
+
+static int tas2781_hda_spi_probe(struct spi_device *spi)
+{
+ struct tas2781_hda_spi_priv *hda_priv;
+ struct tasdevice_priv *tas_priv;
+ struct tas2781_hda *tas_hda;
+ const char *device_name;
+ int ret = 0;
+
+ tas_hda = devm_kzalloc(&spi->dev, sizeof(*tas_hda), GFP_KERNEL);
+ if (!tas_hda)
+ return -ENOMEM;
+
+ hda_priv = devm_kzalloc(&spi->dev, sizeof(*hda_priv), GFP_KERNEL);
+ if (!hda_priv)
+ return -ENOMEM;
+
+ tas_hda->hda_priv = hda_priv;
+ spi->max_speed_hz = TAS2781_SPI_MAX_FREQ;
+
+ tas_priv = devm_kzalloc(&spi->dev, sizeof(*tas_priv), GFP_KERNEL);
+ if (!tas_priv)
+ return -ENOMEM;
+ tas_priv->dev = &spi->dev;
+ tas_hda->priv = tas_priv;
+ tas_priv->regmap = devm_regmap_init_spi(spi, &tasdevice_regmap);
+ if (IS_ERR(tas_priv->regmap)) {
+ ret = PTR_ERR(tas_priv->regmap);
+ dev_err(tas_priv->dev, "Failed to allocate regmap: %d\n",
+ ret);
+ return ret;
+ }
+ if (strstr(dev_name(&spi->dev), "TXNW2781")) {
+ device_name = "TXNW2781";
+ } else {
+ dev_err(tas_priv->dev, "Unmatched spi dev %s\n",
+ dev_name(&spi->dev));
+ return -ENODEV;
+ }
+
+ tas_priv->irq = spi->irq;
+ dev_set_drvdata(&spi->dev, tas_hda);
+ ret = tas2781_read_acpi(tas_hda, device_name,
+ spi_get_chipselect(spi, 0));
+ if (ret)
+ return dev_err_probe(tas_priv->dev, ret,
+ "Platform not supported\n");
+
+ tasdevice_spi_init(tas_priv);
+
+ pm_runtime_set_autosuspend_delay(tas_priv->dev, 3000);
+ pm_runtime_use_autosuspend(tas_priv->dev);
+ pm_runtime_set_active(tas_priv->dev);
+ pm_runtime_get_noresume(tas_priv->dev);
+ pm_runtime_enable(tas_priv->dev);
+
+ pm_runtime_put_autosuspend(tas_priv->dev);
+
+ ret = component_add(tas_priv->dev, &tas2781_hda_comp_ops);
+ if (ret) {
+ dev_err(tas_priv->dev, "Register component fail: %d\n", ret);
+ pm_runtime_disable(tas_priv->dev);
+ tas2781_hda_remove(&spi->dev, &tas2781_hda_comp_ops);
+ }
+
+ return ret;
+}
+
+static void tas2781_hda_spi_remove(struct spi_device *spi)
+{
+ tas2781_hda_remove(&spi->dev, &tas2781_hda_comp_ops);
+}
+
+static int tas2781_runtime_suspend(struct device *dev)
+{
+ struct tas2781_hda *tas_hda = dev_get_drvdata(dev);
+ struct tasdevice_priv *tas_priv = tas_hda->priv;
+
+ guard(mutex)(&tas_priv->codec_lock);
+
+ if (tas_priv->fw_state == TASDEVICE_DSP_FW_ALL_OK
+ && tas_priv->playback_started)
+ tasdevice_tuning_switch(tas_priv, 1);
+
+ tas_priv->tasdevice[tas_priv->index].cur_book = -1;
+ tas_priv->tasdevice[tas_priv->index].cur_conf = -1;
+
+ return 0;
+}
+
+static int tas2781_runtime_resume(struct device *dev)
+{
+ struct tas2781_hda *tas_hda = dev_get_drvdata(dev);
+ struct tasdevice_priv *tas_priv = tas_hda->priv;
+
+ guard(mutex)(&tas_priv->codec_lock);
+
+ if (tas_priv->fw_state == TASDEVICE_DSP_FW_ALL_OK
+ && tas_priv->playback_started)
+ tasdevice_tuning_switch(tas_priv, 0);
+
+ return 0;
+}
+
+static int tas2781_system_suspend(struct device *dev)
+{
+ struct tas2781_hda *tas_hda = dev_get_drvdata(dev);
+ struct tasdevice_priv *tas_priv = tas_hda->priv;
+ int ret;
+
+ ret = pm_runtime_force_suspend(dev);
+ if (ret)
+ return ret;
+
+ /* Shutdown chip before system suspend */
+ if (tas_priv->fw_state == TASDEVICE_DSP_FW_ALL_OK
+ && tas_priv->playback_started)
+ tasdevice_tuning_switch(tas_priv, 1);
+
+ return 0;
+}
+
+static int tas2781_system_resume(struct device *dev)
+{
+ struct tas2781_hda *tas_hda = dev_get_drvdata(dev);
+ struct tasdevice_priv *tas_priv = tas_hda->priv;
+ int ret, val;
+
+ ret = pm_runtime_force_resume(dev);
+ if (ret)
+ return ret;
+
+ guard(mutex)(&tas_priv->codec_lock);
+ ret = tas_priv->dev_read(tas_priv, tas_priv->index,
+ TAS2781_REG_CLK_CONFIG, &val);
+ if (ret < 0)
+ return ret;
+
+ if (val == TAS2781_REG_CLK_CONFIG_RESET) {
+ tas_priv->tasdevice[tas_priv->index].cur_book = -1;
+ tas_priv->tasdevice[tas_priv->index].cur_conf = -1;
+ tas_priv->tasdevice[tas_priv->index].cur_prog = -1;
+
+ ret = tasdevice_prmg_load(tas_priv, 0);
+ if (ret < 0) {
+ dev_err(tas_priv->dev,
+ "FW download failed = %d\n", ret);
+ return ret;
+ }
+ tas_priv->fw_state = TASDEVICE_DSP_FW_ALL_OK;
+
+ if (tas_priv->playback_started)
+ tasdevice_tuning_switch(tas_priv, 0);
+ }
+
+ return ret;
+}
+
+static const struct dev_pm_ops tas2781_hda_pm_ops = {
+ RUNTIME_PM_OPS(tas2781_runtime_suspend, tas2781_runtime_resume, NULL)
+ SYSTEM_SLEEP_PM_OPS(tas2781_system_suspend, tas2781_system_resume)
+};
+
+static const struct spi_device_id tas2781_hda_spi_id[] = {
+ { "tas2781-hda", },
+ {}
+};
+
+static const struct acpi_device_id tas2781_acpi_hda_match[] = {
+ {"TXNW2781", },
+ {}
+};
+MODULE_DEVICE_TABLE(acpi, tas2781_acpi_hda_match);
+
+static struct spi_driver tas2781_hda_spi_driver = {
+ .driver = {
+ .name = "tas2781-hda",
+ .acpi_match_table = tas2781_acpi_hda_match,
+ .pm = &tas2781_hda_pm_ops,
+ },
+ .id_table = tas2781_hda_spi_id,
+ .probe = tas2781_hda_spi_probe,
+ .remove = tas2781_hda_spi_remove,
+};
+module_spi_driver(tas2781_hda_spi_driver);
+
+MODULE_DESCRIPTION("TAS2781 HDA SPI Driver");
+MODULE_AUTHOR("Baojun, Xu, <baojun.xug@ti.com>");
+MODULE_LICENSE("GPL");
+MODULE_IMPORT_NS("SND_SOC_TAS2781_FMWLIB");
+MODULE_IMPORT_NS("SND_HDA_SCODEC_TAS2781");