summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMark Brown <broonie@kernel.org>2026-01-14 23:11:22 +0300
committerMark Brown <broonie@kernel.org>2026-01-14 23:11:22 +0300
commit32bc4e7dde407b7c2287915bfcb30c84e8e6ad46 (patch)
tree761f7186d047980153f4625f2059a0fce58a308f
parent6e22f08989f9bebd9c0c5e982165164e75091e7f (diff)
parentda7afdc79cba00f952df12cd579e44832d829c0a (diff)
downloadlinux-32bc4e7dde407b7c2287915bfcb30c84e8e6ad46.tar.xz
SDCA System Suspend Support
Merge series from Charles Keepax <ckeepax@opensource.cirrus.com>: Add support for system suspend into the class driver, now split out into a separate patch series. Where we got to on the previous discussion, was we don't currently have any parts requiring download on runtime resume, doing so will add noticeable delay to the runtime resume, and we are not blocking someone from adding support for firmware download on runtime resume in the future. Also as runtime resume is really a kernel concept and power rails are primarily controlled by ACPI it is quite unlikely anyone will actually power down the part on a runtime suspend anyway. So this version of the chain still only downloads firmware on probe and system resume.
-rw-r--r--include/sound/sdca_interrupts.h7
-rw-r--r--sound/soc/sdca/sdca_class.c34
-rw-r--r--sound/soc/sdca/sdca_class.h2
-rw-r--r--sound/soc/sdca/sdca_class_function.c126
-rw-r--r--sound/soc/sdca/sdca_interrupts.c93
5 files changed, 236 insertions, 26 deletions
diff --git a/include/sound/sdca_interrupts.h b/include/sound/sdca_interrupts.h
index 8f13417d129a..9bcb5d8fd592 100644
--- a/include/sound/sdca_interrupts.h
+++ b/include/sound/sdca_interrupts.h
@@ -84,4 +84,11 @@ int sdca_irq_populate(struct sdca_function_data *function,
struct sdca_interrupt_info *sdca_irq_allocate(struct device *dev,
struct regmap *regmap, int irq);
+void sdca_irq_enable_early(struct sdca_function_data *function,
+ struct sdca_interrupt_info *info);
+void sdca_irq_enable(struct sdca_function_data *function,
+ struct sdca_interrupt_info *info);
+void sdca_irq_disable(struct sdca_function_data *function,
+ struct sdca_interrupt_info *info);
+
#endif
diff --git a/sound/soc/sdca/sdca_class.c b/sound/soc/sdca/sdca_class.c
index 349d32933ba8..918b638acb57 100644
--- a/sound/soc/sdca/sdca_class.c
+++ b/sound/soc/sdca/sdca_class.c
@@ -205,6 +205,7 @@ static int class_sdw_probe(struct sdw_slave *sdw, const struct sdw_device_id *id
drv->dev = dev;
drv->sdw = sdw;
mutex_init(&drv->regmap_lock);
+ mutex_init(&drv->init_lock);
dev_set_drvdata(drv->dev, drv);
@@ -238,6 +239,38 @@ static int class_sdw_probe(struct sdw_slave *sdw, const struct sdw_device_id *id
return 0;
}
+static int class_suspend(struct device *dev)
+{
+ struct sdca_class_drv *drv = dev_get_drvdata(dev);
+ int ret;
+
+ disable_irq(drv->sdw->irq);
+
+ ret = pm_runtime_force_suspend(dev);
+ if (ret) {
+ dev_err(dev, "failed to force suspend: %d\n", ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+static int class_resume(struct device *dev)
+{
+ struct sdca_class_drv *drv = dev_get_drvdata(dev);
+ int ret;
+
+ ret = pm_runtime_force_resume(dev);
+ if (ret) {
+ dev_err(dev, "failed to force resume: %d\n", ret);
+ return ret;
+ }
+
+ enable_irq(drv->sdw->irq);
+
+ return 0;
+}
+
static int class_runtime_suspend(struct device *dev)
{
struct sdca_class_drv *drv = dev_get_drvdata(dev);
@@ -278,6 +311,7 @@ err:
}
static const struct dev_pm_ops class_pm_ops = {
+ SYSTEM_SLEEP_PM_OPS(class_suspend, class_resume)
RUNTIME_PM_OPS(class_runtime_suspend, class_runtime_resume, NULL)
};
diff --git a/sound/soc/sdca/sdca_class.h b/sound/soc/sdca/sdca_class.h
index bb4c9dd12429..6f24ea2bbd38 100644
--- a/sound/soc/sdca/sdca_class.h
+++ b/sound/soc/sdca/sdca_class.h
@@ -28,6 +28,8 @@ struct sdca_class_drv {
struct sdca_interrupt_info *irq_info;
struct mutex regmap_lock;
+ /* Serialise function initialisations */
+ struct mutex init_lock;
struct work_struct boot_work;
struct completion device_attach;
diff --git a/sound/soc/sdca/sdca_class_function.c b/sound/soc/sdca/sdca_class_function.c
index 416948cfb5cb..0afa41c1ee93 100644
--- a/sound/soc/sdca/sdca_class_function.c
+++ b/sound/soc/sdca/sdca_class_function.c
@@ -8,6 +8,7 @@
*/
#include <linux/auxiliary_bus.h>
+#include <linux/cleanup.h>
#include <linux/minmax.h>
#include <linux/module.h>
#include <linux/pm.h>
@@ -33,6 +34,7 @@ struct class_function_drv {
struct sdca_class_drv *core;
struct sdca_function_data *function;
+ bool suspended;
};
static void class_function_regmap_lock(void *data)
@@ -210,21 +212,12 @@ static const struct snd_soc_component_driver class_function_component_drv = {
.endianness = 1,
};
-static int class_function_boot(struct class_function_drv *drv)
+static int class_function_init_device(struct class_function_drv *drv,
+ unsigned int status)
{
- unsigned int reg = SDW_SDCA_CTL(drv->function->desc->adr,
- SDCA_ENTITY_TYPE_ENTITY_0,
- SDCA_CTL_ENTITY_0_FUNCTION_STATUS, 0);
- unsigned int val;
int ret;
- ret = regmap_read(drv->regmap, reg, &val);
- if (ret < 0) {
- dev_err(drv->dev, "failed to read function status: %d\n", ret);
- return ret;
- }
-
- if (!(val & SDCA_CTL_ENTITY_0_FUNCTION_HAS_BEEN_RESET)) {
+ if (!(status & SDCA_CTL_ENTITY_0_FUNCTION_HAS_BEEN_RESET)) {
dev_dbg(drv->dev, "reset function device\n");
ret = sdca_reset_function(drv->dev, drv->function, drv->regmap);
@@ -232,24 +225,38 @@ static int class_function_boot(struct class_function_drv *drv)
return ret;
}
- if (val & SDCA_CTL_ENTITY_0_FUNCTION_NEEDS_INITIALIZATION) {
+ if (status & SDCA_CTL_ENTITY_0_FUNCTION_NEEDS_INITIALIZATION) {
dev_dbg(drv->dev, "write initialisation\n");
ret = sdca_regmap_write_init(drv->dev, drv->core->dev_regmap,
drv->function);
if (ret)
return ret;
+ }
- ret = regmap_write(drv->regmap, reg,
- SDCA_CTL_ENTITY_0_FUNCTION_NEEDS_INITIALIZATION);
- if (ret < 0) {
- dev_err(drv->dev,
- "failed to clear function init status: %d\n",
- ret);
- return ret;
- }
+ return 0;
+}
+
+static int class_function_boot(struct class_function_drv *drv)
+{
+ unsigned int reg = SDW_SDCA_CTL(drv->function->desc->adr,
+ SDCA_ENTITY_TYPE_ENTITY_0,
+ SDCA_CTL_ENTITY_0_FUNCTION_STATUS, 0);
+ unsigned int val;
+ int ret;
+
+ guard(mutex)(&drv->core->init_lock);
+
+ ret = regmap_read(drv->regmap, reg, &val);
+ if (ret < 0) {
+ dev_err(drv->dev, "failed to read function status: %d\n", ret);
+ return ret;
}
+ ret = class_function_init_device(drv, val);
+ if (ret)
+ return ret;
+
/* Start FDL process */
ret = sdca_irq_populate_early(drv->dev, drv->regmap, drv->function,
drv->core->irq_info);
@@ -414,9 +421,44 @@ static int class_function_runtime_resume(struct device *dev)
struct class_function_drv *drv = auxiliary_get_drvdata(auxdev);
int ret;
+ guard(mutex)(&drv->core->init_lock);
+
regcache_mark_dirty(drv->regmap);
regcache_cache_only(drv->regmap, false);
+ if (drv->suspended) {
+ unsigned int reg = SDW_SDCA_CTL(drv->function->desc->adr,
+ SDCA_ENTITY_TYPE_ENTITY_0,
+ SDCA_CTL_ENTITY_0_FUNCTION_STATUS, 0);
+ unsigned int val;
+
+ ret = regmap_read(drv->regmap, reg, &val);
+ if (ret < 0) {
+ dev_err(drv->dev, "failed to read function status: %d\n", ret);
+ goto err;
+ }
+
+ ret = class_function_init_device(drv, val);
+ if (ret)
+ goto err;
+
+ sdca_irq_enable_early(drv->function, drv->core->irq_info);
+
+ ret = sdca_fdl_sync(drv->dev, drv->function, drv->core->irq_info);
+ if (ret)
+ goto err;
+
+ sdca_irq_enable(drv->function, drv->core->irq_info);
+
+ ret = regmap_write(drv->regmap, reg, 0xFF);
+ if (ret < 0) {
+ dev_err(drv->dev, "failed to clear function status: %d\n", ret);
+ goto err;
+ }
+
+ drv->suspended = false;
+ }
+
ret = regcache_sync(drv->regmap);
if (ret) {
dev_err(drv->dev, "failed to restore register cache: %d\n", ret);
@@ -431,7 +473,49 @@ err:
return ret;
}
+static int class_function_suspend(struct device *dev)
+{
+ struct auxiliary_device *auxdev = to_auxiliary_dev(dev);
+ struct class_function_drv *drv = auxiliary_get_drvdata(auxdev);
+ int ret;
+
+ drv->suspended = true;
+
+ /* Ensure runtime resume runs on resume */
+ ret = pm_runtime_resume_and_get(dev);
+ if (ret) {
+ dev_err(dev, "failed to resume for suspend: %d\n", ret);
+ return ret;
+ }
+
+ sdca_irq_disable(drv->function, drv->core->irq_info);
+
+ ret = pm_runtime_force_suspend(dev);
+ if (ret) {
+ dev_err(dev, "failed to force suspend: %d\n", ret);
+ return ret;
+ }
+
+ pm_runtime_put_noidle(dev);
+
+ return 0;
+}
+
+static int class_function_resume(struct device *dev)
+{
+ int ret;
+
+ ret = pm_runtime_force_resume(dev);
+ if (ret) {
+ dev_err(dev, "failed to force resume: %d\n", ret);
+ return ret;
+ }
+
+ return 0;
+}
+
static const struct dev_pm_ops class_function_pm_ops = {
+ SYSTEM_SLEEP_PM_OPS(class_function_suspend, class_function_resume)
RUNTIME_PM_OPS(class_function_runtime_suspend,
class_function_runtime_resume, NULL)
};
diff --git a/sound/soc/sdca/sdca_interrupts.c b/sound/soc/sdca/sdca_interrupts.c
index ff3a7e405fdc..cc40732c30cc 100644
--- a/sound/soc/sdca/sdca_interrupts.c
+++ b/sound/soc/sdca/sdca_interrupts.c
@@ -205,10 +205,16 @@ static irqreturn_t fdl_owner_handler(int irq, void *data)
irqreturn_t irqret = IRQ_NONE;
int ret;
- ret = pm_runtime_get_sync(dev);
- if (ret < 0) {
- dev_err(dev, "failed to resume for fdl: %d\n", ret);
- goto error;
+ /*
+ * FDL has to run from the system resume handler, at which point
+ * we can't wait for the pm runtime.
+ */
+ if (completion_done(&dev->power.completion)) {
+ ret = pm_runtime_get_sync(dev);
+ if (ret < 0) {
+ dev_err(dev, "failed to resume for fdl: %d\n", ret);
+ goto error;
+ }
}
ret = sdca_fdl_process(interrupt);
@@ -217,7 +223,8 @@ static irqreturn_t fdl_owner_handler(int irq, void *data)
irqret = IRQ_HANDLED;
error:
- pm_runtime_put(dev);
+ if (completion_done(&dev->power.completion))
+ pm_runtime_put(dev);
return irqret;
}
@@ -541,3 +548,79 @@ struct sdca_interrupt_info *sdca_irq_allocate(struct device *sdev,
return info;
}
EXPORT_SYMBOL_NS_GPL(sdca_irq_allocate, "SND_SOC_SDCA");
+
+static void irq_enable_flags(struct sdca_function_data *function,
+ struct sdca_interrupt_info *info, bool early)
+{
+ struct sdca_interrupt *interrupt;
+ int i;
+
+ for (i = 0; i < SDCA_MAX_INTERRUPTS; i++) {
+ interrupt = &info->irqs[i];
+
+ if (!interrupt || interrupt->function != function)
+ continue;
+
+ switch (SDCA_CTL_TYPE(interrupt->entity->type,
+ interrupt->control->sel)) {
+ case SDCA_CTL_TYPE_S(XU, FDL_CURRENTOWNER):
+ if (early)
+ enable_irq(interrupt->irq);
+ break;
+ default:
+ if (!early)
+ enable_irq(interrupt->irq);
+ break;
+ }
+ }
+}
+
+/**
+ * sdca_irq_enable_early - Re-enable early SDCA IRQs for a given function
+ * @function: Pointer to the SDCA Function.
+ * @info: Pointer to the SDCA interrupt info for this device.
+ *
+ * The early version of the IRQ enable allows enabling IRQs which may be
+ * necessary to bootstrap functionality for other IRQs, such as the FDL
+ * process.
+ */
+void sdca_irq_enable_early(struct sdca_function_data *function,
+ struct sdca_interrupt_info *info)
+{
+ irq_enable_flags(function, info, true);
+}
+EXPORT_SYMBOL_NS_GPL(sdca_irq_enable_early, "SND_SOC_SDCA");
+
+/**
+ * sdca_irq_enable - Re-enable SDCA IRQs for a given function
+ * @function: Pointer to the SDCA Function.
+ * @info: Pointer to the SDCA interrupt info for this device.
+ */
+void sdca_irq_enable(struct sdca_function_data *function,
+ struct sdca_interrupt_info *info)
+{
+ irq_enable_flags(function, info, false);
+}
+EXPORT_SYMBOL_NS_GPL(sdca_irq_enable, "SND_SOC_SDCA");
+
+/**
+ * sdca_irq_disable - Disable SDCA IRQs for a given function
+ * @function: Pointer to the SDCA Function.
+ * @info: Pointer to the SDCA interrupt info for this device.
+ */
+void sdca_irq_disable(struct sdca_function_data *function,
+ struct sdca_interrupt_info *info)
+{
+ struct sdca_interrupt *interrupt;
+ int i;
+
+ for (i = 0; i < SDCA_MAX_INTERRUPTS; i++) {
+ interrupt = &info->irqs[i];
+
+ if (!interrupt || interrupt->function != function)
+ continue;
+
+ disable_irq(interrupt->irq);
+ }
+}
+EXPORT_SYMBOL_NS_GPL(sdca_irq_disable, "SND_SOC_SDCA");