[PATCH v2 2/3] ALSA: hda/scodec: add AW88399 HDA side codec driver
Marco Giunta <[email protected]> Wed, 29 Jul 2026 11:33:14 +0200
| Newsgroups | org.kernel.vger.platform-driver-x86,org.kernel.vger.linux-acpi,org.kernel.vger.linux-kernel,org.kernel.vger.linux-sound |
|---|---|
| Message-ID | <DS7PR19MB77247D67E739956AA4611ACEFCCA2@DS7PR19MB7724.namprd19.prod.outlook.com> |
Add an HDA side codec driver for the AWINIC AW88399 smart amplifier, enabling its use as a companion amplifier on HDA systems where the chip is connected via I2C to the host and driven alongside a primary HDA codec (such as Realtek ALC287). The driver is structured after the existing side codec drivers: * aw88399_hda_i2c.c: I2C bus driver matching ACPI HID "AWDZ8399" and serial-multi-instantiate device name "aw88399-hda". Creates the regmap and passes it to the shared probe function, following the CS35L41/CS35L56/TAS2781 pattern. * aw88399_hda.c: Core driver implementing HDA component binding, playback hooks (using the shared library's start/stop functions), ACPI subsystem ID retrieval, and runtime/system power management. Includes per-model quirk infrastructure using ACPI subsystem ID matching; the quirk table is empty in this patch and populated in the next patch along with the corresponding Realtek fixups that activate the driver. The driver includes <sound/aw88399.h> for shared definitions and depends on SND_SOC_AW88399_LIB for chip initialization, firmware loading, and playback control, avoiding any dependency on the full ASoC codec module. Tested-by: Nadim Kobeissi <[email protected]> Tested-by: Xia Yun'an <[email protected]> Tested-by: Munzir Taha <[email protected]> Co-developed-by: Yakov Till <[email protected]> Signed-off-by: Yakov Till <[email protected]> Signed-off-by: Marco Giunta <[email protected]> --- sound/hda/codecs/side-codecs/Kconfig | 18 + sound/hda/codecs/side-codecs/Makefile | 4 + sound/hda/codecs/side-codecs/aw88399_hda.c | 330 ++++++++++++++++++ sound/hda/codecs/side-codecs/aw88399_hda.h | 36 ++ .../hda/codecs/side-codecs/aw88399_hda_i2c.c | 54 +++ 5 files changed, 442 insertions(+) create mode 100644 sound/hda/codecs/side-codecs/aw88399_hda.c create mode 100644 sound/hda/codecs/side-codecs/aw88399_hda.h create mode 100644 sound/hda/codecs/side-codecs/aw88399_hda_i2c.c diff --git a/sound/hda/codecs/side-codecs/Kconfig b/sound/hda/codecs/side-codecs/Kconfig index 1cfd83e251e4..f90f1dfcec68 100644 --- a/sound/hda/codecs/side-codecs/Kconfig +++ b/sound/hda/codecs/side-codecs/Kconfig @@ -13,6 +13,24 @@ config SND_HDA_CIRRUS_SCODEC_KUNIT_TEST Documentation/dev-tools/kunit/. If in doubt, say "N". +config SND_HDA_SCODEC_AW88399 + tristate + select SND_HDA_GENERIC + +config SND_HDA_SCODEC_AW88399_I2C + tristate "Build AW88399 HD-audio side codec support for I2C Bus" + depends on I2C + depends on ACPI + depends on SND_SOC + select SND_HDA_SCODEC_AW88399 + select SND_SOC_AW88399_LIB + help + Say Y or M here to include AW88399 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_AW88399_I2C=m + config SND_HDA_SCODEC_CS35L41 tristate select SND_HDA_GENERIC diff --git a/sound/hda/codecs/side-codecs/Makefile b/sound/hda/codecs/side-codecs/Makefile index 245e84f6a121..7dd010c68f78 100644 --- a/sound/hda/codecs/side-codecs/Makefile +++ b/sound/hda/codecs/side-codecs/Makefile @@ -3,6 +3,8 @@ 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-aw88399-y := aw88399_hda.o +snd-hda-scodec-aw88399-i2c-y := aw88399_hda_i2c.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 @@ -16,6 +18,8 @@ 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_AW88399) += snd-hda-scodec-aw88399.o +obj-$(CONFIG_SND_HDA_SCODEC_AW88399_I2C) += snd-hda-scodec-aw88399-i2c.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 diff --git a/sound/hda/codecs/side-codecs/aw88399_hda.c b/sound/hda/codecs/side-codecs/aw88399_hda.c new file mode 100644 index 000000000000..ed57d9a4a39d --- /dev/null +++ b/sound/hda/codecs/side-codecs/aw88399_hda.c @@ -0,0 +1,330 @@ +// SPDX-License-Identifier: GPL-2.0-only +// +// AW88399 HDA side codec driver +// +// Based on cs35l41_hda.c and aw88399.c +// + +#include <linux/acpi.h> +#include <linux/gpio/consumer.h> +#include <linux/i2c.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/pm_runtime.h> +#include <linux/string.h> +#include <sound/hda_codec.h> +#include "../generic.h" +#include "hda_component.h" +#include "aw88399_hda.h" + +#define AW88399_HDA_I2C_BASE_ADDR 0x34 + +static void aw88399_hda_playback_hook(struct device *dev, int action) +{ + struct aw88399_hda *aw88399 = dev_get_drvdata(dev); + struct aw88399 *core = aw88399->core; + int ret = 0; + + dev_dbg(aw88399->dev, "Playback action: %d\n", action); + + switch (action) { + case HDA_GEN_PCM_ACT_OPEN: + pm_runtime_get_sync(dev); + aw88399->playing = true; + break; + case HDA_GEN_PCM_ACT_PREPARE: + if (core) + aw88399_start(core, AW88399_SYNC_START); + break; + case HDA_GEN_PCM_ACT_CLEANUP: + if (aw88399->aw_dev) + ret = aw88399_stop(aw88399->aw_dev); + if (ret) + dev_err(aw88399->dev, "Failed to stop amplifier: %d\n", ret); + break; + case HDA_GEN_PCM_ACT_CLOSE: + if (aw88399->aw_dev) + aw88399_stop(aw88399->aw_dev); + aw88399->playing = false; + pm_runtime_mark_last_busy(dev); + pm_runtime_put_autosuspend(dev); + break; + default: + dev_warn(aw88399->dev, "Unsupported action: %d\n", action); + break; + } +} + +static int aw88399_hda_bind(struct device *dev, struct device *master, void *master_data) +{ + struct aw88399_hda *aw88399 = dev_get_drvdata(dev); + struct hda_component_parent *parent = master_data; + struct hda_component *comp; + + comp = hda_component_from_index(parent, aw88399->index); + if (!comp) + return -EINVAL; + + if (comp->dev) + return -EBUSY; + + comp->dev = dev; + + strscpy(comp->name, dev_name(dev), sizeof(comp->name)); + + comp->playback_hook = aw88399_hda_playback_hook; + + dev_info(aw88399->dev, + "AW88399 Bound - SSID: %s, channel: %d\n", + aw88399->acpi_subsystem_id, aw88399->channel); + + return 0; +} + +static void aw88399_hda_unbind(struct device *dev, struct device *master, void *master_data) +{ + struct aw88399_hda *aw88399 = dev_get_drvdata(dev); + struct hda_component_parent *parent = master_data; + struct hda_component *comp; + + comp = hda_component_from_index(parent, aw88399->index); + if (comp && (comp->dev == dev)) + memset(comp, 0, sizeof(*comp)); + + dev_dbg(aw88399->dev, "Unbound from HDA codec\n"); +} + +static const struct component_ops aw88399_hda_comp_ops = { + .bind = aw88399_hda_bind, + .unbind = aw88399_hda_unbind, +}; + +static int aw88399_hda_index_from_i2c(struct aw88399_hda *aw88399) +{ + return to_i2c_client(aw88399->dev)->addr - AW88399_HDA_I2C_BASE_ADDR; +} + +static int aw88399_hda_init(struct aw88399_hda *aw88399) +{ + struct device *dev = aw88399->dev; + struct i2c_client *i2c = to_i2c_client(dev); + struct aw88399 *core; + int ret; + + core = devm_kzalloc(dev, sizeof(*core), GFP_KERNEL); + if (!core) + return -ENOMEM; + + mutex_init(&core->lock); + core->reset_gpio = aw88399->reset_gpio; + core->regmap = aw88399->regmap; + core->bsts_unreliable = aw88399->bsts_unreliable; + + aw88399_hw_reset(core); + + ret = aw88399_init(core, i2c, aw88399->regmap); + if (ret) + return ret; + + /* Set channel BEFORE loading firmware so ACF parser sees correct value */ + if (core->aw_pa) + aw88399_dev_set_channel(core, aw88399->channel); + + ret = aw88399_request_firmware_file(core); + if (ret) + return ret; + + aw88399->core = core; + aw88399->aw_dev = core->aw_pa; + + return 0; +} + +struct aw88399_prop_model { + const char *ssid; + int (*apply_prop)(struct aw88399_hda *aw88399); +}; + +static const struct aw88399_prop_model aw88399_prop_model_table[] = { + { } +}; + +static int aw88399_hda_acpi_probe(struct aw88399_hda *aw88399) +{ + struct acpi_device *adev; + struct device *physdev; + const char *sub; + const struct aw88399_prop_model *model; + + aw88399->index = aw88399_hda_index_from_i2c(aw88399); + aw88399->channel = aw88399->index; + aw88399->acpi_subsystem_id = NULL; + + adev = acpi_dev_get_first_match_dev("AWDZ8399", NULL, -1); + if (!adev) { + dev_err(aw88399->dev, "Failed to find an ACPI device for AWDZ8399\n"); + return -ENODEV; + } + + physdev = get_device(acpi_get_first_physical_node(adev)); + acpi_dev_put(adev); + if (!physdev) + return -ENODEV; + + sub = acpi_get_subsystem_id(ACPI_HANDLE(physdev)); + put_device(physdev); + if (IS_ERR_OR_NULL(sub)) + return 0; + + aw88399->acpi_subsystem_id = devm_kstrdup(aw88399->dev, sub, GFP_KERNEL); + kfree(sub); + if (!aw88399->acpi_subsystem_id) + return -ENOMEM; + + for (model = aw88399_prop_model_table; model->ssid; model++) { + if (!strcasecmp(model->ssid, aw88399->acpi_subsystem_id)) { + dev_info(aw88399->dev, + "Applying properties for SSID %s\n", + aw88399->acpi_subsystem_id); + return model->apply_prop(aw88399); + } + } + + return 0; +} + +int aw88399_hda_probe(struct device *dev, struct regmap *regmap) +{ + struct aw88399_hda *aw88399; + int ret; + + aw88399 = devm_kzalloc(dev, sizeof(*aw88399), GFP_KERNEL); + if (!aw88399) + return -ENOMEM; + + if (IS_ERR(regmap)) + return dev_err_probe(dev, PTR_ERR(regmap), "Failed to obtain regmap\n"); + + aw88399->dev = dev; + aw88399->regmap = regmap; + dev_set_drvdata(dev, aw88399); + + aw88399->reset_gpio = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_LOW); + if (IS_ERR(aw88399->reset_gpio)) + return dev_err_probe(dev, PTR_ERR(aw88399->reset_gpio), + "Failed to get reset GPIO\n"); + + ret = aw88399_hda_acpi_probe(aw88399); + if (ret) + return dev_err_probe(dev, ret, "ACPI probe failed\n"); + + ret = aw88399_hda_init(aw88399); + if (ret) + return dev_err_probe(dev, ret, "Chip initialization failed\n"); + + /* Enable runtime PM */ + pm_runtime_set_autosuspend_delay(dev, 3000); + pm_runtime_use_autosuspend(dev); + pm_runtime_mark_last_busy(dev); + pm_runtime_set_active(dev); + pm_runtime_enable(dev); + + ret = component_add(dev, &aw88399_hda_comp_ops); + if (ret) { + pm_runtime_disable(dev); + return dev_err_probe(dev, ret, "Failed to register component\n"); + } + + dev_info(dev, "AW88399 HDA side codec registered successfully\n"); + + return 0; +} +EXPORT_SYMBOL_NS_GPL(aw88399_hda_probe, "SND_HDA_SCODEC_AW88399"); + +void aw88399_hda_remove(struct device *dev) +{ + struct aw88399_hda *aw88399 = dev_get_drvdata(dev); + + pm_runtime_disable(dev); + + if (aw88399->aw_dev) + aw88399_stop(aw88399->aw_dev); + + component_del(dev, &aw88399_hda_comp_ops); + + dev_dbg(aw88399->dev, "AW88399 HDA side codec removed\n"); +} +EXPORT_SYMBOL_NS_GPL(aw88399_hda_remove, "SND_HDA_SCODEC_AW88399"); + +static int aw88399_hda_runtime_suspend(struct device *dev) +{ + struct aw88399_hda *aw88399 = dev_get_drvdata(dev); + + dev_dbg(aw88399->dev, "Runtime suspend\n"); + + if (aw88399->aw_dev && aw88399->playing) + aw88399_stop(aw88399->aw_dev); + + return 0; +} + +static int aw88399_hda_runtime_resume(struct device *dev) +{ + struct aw88399_hda *aw88399 = dev_get_drvdata(dev); + + dev_dbg(aw88399->dev, "Runtime resume\n"); + + if (aw88399->core && aw88399->aw_dev && aw88399->playing) + aw88399_start(aw88399->core, AW88399_SYNC_START); + + return 0; +} + +static int aw88399_hda_system_suspend(struct device *dev) +{ + struct aw88399_hda *aw88399 = dev_get_drvdata(dev); + int ret; + + dev_dbg(aw88399->dev, "System suspend\n"); + + if (aw88399->aw_dev && aw88399->playing) + aw88399_stop(aw88399->aw_dev); + + if (aw88399->core) + aw88399->core->fw_needs_reload = true; + + ret = pm_runtime_force_suspend(dev); + if (ret) + dev_err(aw88399->dev, "Runtime force suspend failed: %d\n", ret); + + return ret; +} + +static int aw88399_hda_system_resume(struct device *dev) +{ + struct aw88399_hda *aw88399 = dev_get_drvdata(dev); + int ret; + + dev_dbg(aw88399->dev, "System resume\n"); + + if (aw88399->aw_dev) + aw88399_hw_reset(aw88399->core); + + ret = pm_runtime_force_resume(dev); + if (ret) + dev_err(aw88399->dev, "Runtime force resume failed: %d\n", ret); + + return ret; +} + +const struct dev_pm_ops aw88399_hda_pm_ops = { + RUNTIME_PM_OPS(aw88399_hda_runtime_suspend, aw88399_hda_runtime_resume, NULL) + SYSTEM_SLEEP_PM_OPS(aw88399_hda_system_suspend, aw88399_hda_system_resume) +}; +EXPORT_SYMBOL_NS_GPL(aw88399_hda_pm_ops, "SND_HDA_SCODEC_AW88399"); + +MODULE_DESCRIPTION("AW88399 HDA driver"); +MODULE_AUTHOR("Yakov Till <[email protected]>"); +MODULE_AUTHOR("Marco Giunta <[email protected]>"); +MODULE_LICENSE("GPL"); +MODULE_FIRMWARE("aw88399_acf.bin"); diff --git a/sound/hda/codecs/side-codecs/aw88399_hda.h b/sound/hda/codecs/side-codecs/aw88399_hda.h new file mode 100644 index 000000000000..ca0aa5279298 --- /dev/null +++ b/sound/hda/codecs/side-codecs/aw88399_hda.h @@ -0,0 +1,36 @@ +/* SPDX-License-Identifier: GPL-2.0-only + * + * AW88399 HDA side codec driver + */ + +#ifndef __AW88399_HDA_H__ +#define __AW88399_HDA_H__ + +#include <linux/device.h> +#include <linux/gpio/consumer.h> +#include <sound/aw88399.h> + +struct aw88399; +struct aw_device; + +struct aw88399_hda { + struct device *dev; + struct regmap *regmap; + struct gpio_desc *reset_gpio; + struct aw_device *aw_dev; + struct aw88399 *core; + bool bsts_unreliable; + + const char *acpi_subsystem_id; + int index; + int channel; + + bool playing; +}; + +int aw88399_hda_probe(struct device *dev, struct regmap *regmap); +void aw88399_hda_remove(struct device *dev); + +extern const struct dev_pm_ops aw88399_hda_pm_ops; + +#endif /* __AW88399_HDA_H__ */ diff --git a/sound/hda/codecs/side-codecs/aw88399_hda_i2c.c b/sound/hda/codecs/side-codecs/aw88399_hda_i2c.c new file mode 100644 index 000000000000..186e13adbc6c --- /dev/null +++ b/sound/hda/codecs/side-codecs/aw88399_hda_i2c.c @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-only +// +// AW88399 HDA I2C driver +// +// Based on cs35l41_hda_i2c.c +// + +#include <linux/i2c.h> +#include <linux/module.h> +#include <linux/regmap.h> + +#include "aw88399_hda.h" + +static int aw88399_hda_i2c_probe(struct i2c_client *clt) +{ + if (!strstr(dev_name(&clt->dev), "AWDZ8399")) + return -ENODEV; + + return aw88399_hda_probe(&clt->dev, + devm_regmap_init_i2c(clt, &aw88399_remap_config)); +} + +static void aw88399_hda_i2c_remove(struct i2c_client *clt) +{ + aw88399_hda_remove(&clt->dev); +} + +static const struct i2c_device_id aw88399_hda_i2c_id[] = { + { .name = "aw88399-hda" }, + { } +}; + +static const struct acpi_device_id aw88399_acpi_hda_match[] = { + { "AWDZ8399", 0 }, + { } +}; +MODULE_DEVICE_TABLE(acpi, aw88399_acpi_hda_match); + +static struct i2c_driver aw88399_hda_i2c_driver = { + .driver = { + .name = "aw88399-hda", + .acpi_match_table = aw88399_acpi_hda_match, + .pm = &aw88399_hda_pm_ops, + }, + .probe = aw88399_hda_i2c_probe, + .remove = aw88399_hda_i2c_remove, + .id_table = aw88399_hda_i2c_id, +}; +module_i2c_driver(aw88399_hda_i2c_driver); + +MODULE_DESCRIPTION("HDA AW88399 I2C driver"); +MODULE_IMPORT_NS("SND_HDA_SCODEC_AW88399"); +MODULE_AUTHOR("Yakov Till <[email protected]>"); +MODULE_LICENSE("GPL"); -- 2.55.0