summaryrefslogtreecommitdiff
path: root/sound/soc/codecs
diff options
context:
space:
mode:
authorRichard Fitzgerald <rf@opensource.cirrus.com>2023-04-11 18:25:27 +0300
committerMark Brown <broonie@kernel.org>2023-04-12 19:34:34 +0300
commitf9dc6b875ec0a6a6d4091cd9603d193ec98c75a2 (patch)
treebae667395ee51d8efd2b7ad3c8ca3e57380a8ae6 /sound/soc/codecs
parentf00abaddf0300bd9ca87918148a26bdb748129db (diff)
downloadlinux-f9dc6b875ec0a6a6d4091cd9603d193ec98c75a2.tar.xz
ASoC: cs35l56: Add basic system suspend handling
This adds the main handling for system suspend but does not handle re-patching the firmware after system resume. This is a multi-stage suspend and resume because if there is a RESET line it is almost certain that it will be shared by all the amps. So every amp must have done its suspend before we can assert RESET. Likewise we must de-assert RESET before the amps can resume. It's preferable to assert RESET before we turning off regulators, and while they power up. The actual suspend and resume is done by using the pair pm_runtime_force_suspend() and pm_runtime_force_resume() to re-use our runtime suspend/resume sequences. pm_runtime_force_suspend() will disable our pm_runtime. If we were runtime-resumed it calls our runtime_suspend(). pm_runtime_force_resume() re-enables pm_runtime and if we were originally runtime-resumed before the pm_runtime_force_suspend() it calls our runtime_resume(). Otherwise it leaves us runtime-suspended. The general process is therefore: suspend() -> finish dsp_work and then run our runtime_suspend suspend_late() -> assert RESET and turn off supplies resume_early() -> enable supplies and de-assert RESET resume() -> pm_runtime_force_resume() In addition, to prevent the IRQ handler running in the period between pm_runtime_force_suspend() and pm_runtime_force_resume() the parent IRQ is temporarily disabled: - from suspend until suspend_noirq - from resume_noirq until resume Signed-off-by: Richard Fitzgerald <rf@opensource.cirrus.com> Link: https://lore.kernel.org/r/20230411152528.329803-6-rf@opensource.cirrus.com Signed-off-by: Mark Brown <broonie@kernel.org>
Diffstat (limited to 'sound/soc/codecs')
-rw-r--r--sound/soc/codecs/cs35l56-sdw.c26
-rw-r--r--sound/soc/codecs/cs35l56.c126
-rw-r--r--sound/soc/codecs/cs35l56.h6
3 files changed, 158 insertions, 0 deletions
diff --git a/sound/soc/codecs/cs35l56-sdw.c b/sound/soc/codecs/cs35l56-sdw.c
index 448ef3609f4c..947d4e5f4dc9 100644
--- a/sound/soc/codecs/cs35l56-sdw.c
+++ b/sound/soc/codecs/cs35l56-sdw.c
@@ -450,6 +450,29 @@ static int __maybe_unused cs35l56_sdw_runtime_resume(struct device *dev)
return 0;
}
+static int __maybe_unused cs35l56_sdw_system_suspend(struct device *dev)
+{
+ struct cs35l56_private *cs35l56 = dev_get_drvdata(dev);
+
+ if (!cs35l56->init_done)
+ return 0;
+
+ /*
+ * Disable SoundWire interrupts.
+ * Flush - don't cancel because that could leave an unbalanced pm_runtime_get.
+ */
+ cs35l56->sdw_irq_no_unmask = true;
+ flush_work(&cs35l56->sdw_irq_work);
+
+ /* Mask interrupts and flush in case sdw_irq_work was queued again */
+ sdw_write_no_pm(cs35l56->sdw_peripheral, CS35L56_SDW_GEN_INT_MASK_1, 0);
+ sdw_read_no_pm(cs35l56->sdw_peripheral, CS35L56_SDW_GEN_INT_STAT_1);
+ sdw_write_no_pm(cs35l56->sdw_peripheral, CS35L56_SDW_GEN_INT_STAT_1, 0xFF);
+ flush_work(&cs35l56->sdw_irq_work);
+
+ return cs35l56_system_suspend(dev);
+}
+
static int cs35l56_sdw_probe(struct sdw_slave *peripheral, const struct sdw_device_id *id)
{
struct device *dev = &peripheral->dev;
@@ -499,6 +522,9 @@ static int cs35l56_sdw_remove(struct sdw_slave *peripheral)
static const struct dev_pm_ops cs35l56_sdw_pm = {
SET_RUNTIME_PM_OPS(cs35l56_sdw_runtime_suspend, cs35l56_sdw_runtime_resume, NULL)
+ SYSTEM_SLEEP_PM_OPS(cs35l56_sdw_system_suspend, cs35l56_system_resume)
+ LATE_SYSTEM_SLEEP_PM_OPS(cs35l56_system_suspend_late, cs35l56_system_resume_early)
+ /* NOIRQ stage not needed, SoundWire doesn't use a hard IRQ */
};
static const struct sdw_device_id cs35l56_sdw_id[] = {
diff --git a/sound/soc/codecs/cs35l56.c b/sound/soc/codecs/cs35l56.c
index 372b07fef827..74e8de2f81f5 100644
--- a/sound/soc/codecs/cs35l56.c
+++ b/sound/soc/codecs/cs35l56.c
@@ -11,8 +11,10 @@
#include <linux/delay.h>
#include <linux/err.h>
#include <linux/gpio/consumer.h>
+#include <linux/interrupt.h>
#include <linux/math.h>
#include <linux/module.h>
+#include <linux/pm.h>
#include <linux/pm_runtime.h>
#include <linux/regmap.h>
#include <linux/regulator/consumer.h>
@@ -1160,6 +1162,127 @@ err:
}
EXPORT_SYMBOL_NS_GPL(cs35l56_runtime_resume_common, SND_SOC_CS35L56_CORE);
+int cs35l56_system_suspend(struct device *dev)
+{
+ struct cs35l56_private *cs35l56 = dev_get_drvdata(dev);
+
+ dev_dbg(dev, "system_suspend\n");
+
+ if (cs35l56->component)
+ flush_work(&cs35l56->dsp_work);
+
+ /*
+ * 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->irq)
+ disable_irq(cs35l56->irq);
+
+ return pm_runtime_force_suspend(dev);
+}
+EXPORT_SYMBOL_GPL(cs35l56_system_suspend);
+
+int cs35l56_system_suspend_late(struct device *dev)
+{
+ struct cs35l56_private *cs35l56 = dev_get_drvdata(dev);
+
+ dev_dbg(dev, "system_suspend_late\n");
+
+ /*
+ * Assert RESET before removing supplies.
+ * RESET is usually shared by all amps so it must not be asserted until
+ * all driver instances have done their suspend() stage.
+ */
+ if (cs35l56->reset_gpio) {
+ gpiod_set_value_cansleep(cs35l56->reset_gpio, 0);
+ usleep_range(CS35L56_RESET_PULSE_MIN_US, CS35L56_RESET_PULSE_MIN_US + 400);
+ }
+
+ regulator_bulk_disable(ARRAY_SIZE(cs35l56->supplies), cs35l56->supplies);
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(cs35l56_system_suspend_late);
+
+int cs35l56_system_suspend_no_irq(struct device *dev)
+{
+ struct cs35l56_private *cs35l56 = dev_get_drvdata(dev);
+
+ dev_dbg(dev, "system_suspend_no_irq\n");
+
+ /* Handlers are now disabled so the parent IRQ can safely be re-enabled. */
+ if (cs35l56->irq)
+ enable_irq(cs35l56->irq);
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(cs35l56_system_suspend_no_irq);
+
+int cs35l56_system_resume_no_irq(struct device *dev)
+{
+ struct cs35l56_private *cs35l56 = dev_get_drvdata(dev);
+
+ dev_dbg(dev, "system_resume_no_irq\n");
+
+ /*
+ * 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->irq)
+ disable_irq(cs35l56->irq);
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(cs35l56_system_resume_no_irq);
+
+int cs35l56_system_resume_early(struct device *dev)
+{
+ struct cs35l56_private *cs35l56 = dev_get_drvdata(dev);
+ int ret;
+
+ dev_dbg(dev, "system_resume_early\n");
+
+ /* Ensure a spec-compliant RESET pulse. */
+ if (cs35l56->reset_gpio) {
+ gpiod_set_value_cansleep(cs35l56->reset_gpio, 0);
+ usleep_range(CS35L56_RESET_PULSE_MIN_US, CS35L56_RESET_PULSE_MIN_US + 400);
+ }
+
+ /* Enable supplies before releasing RESET. */
+ ret = regulator_bulk_enable(ARRAY_SIZE(cs35l56->supplies), cs35l56->supplies);
+ if (ret) {
+ dev_err(dev, "system_resume_early failed to enable supplies: %d\n", ret);
+ return ret;
+ }
+
+ /* Release shared RESET before drivers start resume(). */
+ gpiod_set_value_cansleep(cs35l56->reset_gpio, 1);
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(cs35l56_system_resume_early);
+
+int cs35l56_system_resume(struct device *dev)
+{
+ struct cs35l56_private *cs35l56 = dev_get_drvdata(dev);
+ int ret;
+
+ dev_dbg(dev, "system_resume\n");
+
+ /* Undo pm_runtime_force_suspend() before re-enabling the irq */
+ ret = pm_runtime_force_resume(dev);
+ if (cs35l56->irq)
+ enable_irq(cs35l56->irq);
+
+ return ret;
+}
+EXPORT_SYMBOL_GPL(cs35l56_system_resume);
+
static int cs35l56_dsp_init(struct cs35l56_private *cs35l56)
{
struct wm_adsp *dsp;
@@ -1457,6 +1580,9 @@ EXPORT_SYMBOL_NS_GPL(cs35l56_remove, SND_SOC_CS35L56_CORE);
const struct dev_pm_ops cs35l56_pm_ops_i2c_spi = {
SET_RUNTIME_PM_OPS(cs35l56_runtime_suspend, cs35l56_runtime_resume_i2c_spi, NULL)
+ SYSTEM_SLEEP_PM_OPS(cs35l56_system_suspend, cs35l56_system_resume)
+ LATE_SYSTEM_SLEEP_PM_OPS(cs35l56_system_suspend_late, cs35l56_system_resume_early)
+ NOIRQ_SYSTEM_SLEEP_PM_OPS(cs35l56_system_suspend_no_irq, cs35l56_system_resume_no_irq)
};
EXPORT_SYMBOL_NS_GPL(cs35l56_pm_ops_i2c_spi, SND_SOC_CS35L56_CORE);
diff --git a/sound/soc/codecs/cs35l56.h b/sound/soc/codecs/cs35l56.h
index efc4b99180f9..919ba0267ef9 100644
--- a/sound/soc/codecs/cs35l56.h
+++ b/sound/soc/codecs/cs35l56.h
@@ -68,6 +68,12 @@ extern const struct dev_pm_ops cs35l56_pm_ops_i2c_spi;
int cs35l56_runtime_suspend(struct device *dev);
int cs35l56_runtime_resume_common(struct cs35l56_private *cs35l56);
+int cs35l56_system_suspend(struct device *dev);
+int cs35l56_system_suspend_late(struct device *dev);
+int cs35l56_system_suspend_no_irq(struct device *dev);
+int cs35l56_system_resume_no_irq(struct device *dev);
+int cs35l56_system_resume_early(struct device *dev);
+int cs35l56_system_resume(struct device *dev);
irqreturn_t cs35l56_irq(int irq, void *data);
int cs35l56_irq_request(struct cs35l56_private *cs35l56);
int cs35l56_common_probe(struct cs35l56_private *cs35l56);