diff mbox series

[v2,2/2] ASoC: cs35l41: Add support for hibernate memory retention mode

Message ID 20220107160636.6555-3-ckeepax@opensource.cirrus.com (mailing list archive)
State Accepted
Commit f517ba4924ad026f2583553db02f3c8bc69de88b
Headers show
Series Add low power hibernation support to cs35l41 | expand

Commit Message

Charles Keepax Jan. 7, 2022, 4:06 p.m. UTC
The cs35l41 supports a low power DSP memory retention mode. Add support
for entering this mode when then device is not in use.

Co-authored-by: David Rhodes <david.rhodes@cirrus.com>
Signed-off-by: David Rhodes <david.rhodes@cirrus.com>
Signed-off-by: Charles Keepax <ckeepax@opensource.cirrus.com>
---

Changes since v1:
 - Add missing export symbol for cs35l41_pm_ops

 include/sound/cs35l41.h        |   5 +
 sound/soc/codecs/cs35l41-i2c.c |   1 +
 sound/soc/codecs/cs35l41-lib.c |   6 ++
 sound/soc/codecs/cs35l41-spi.c |   1 +
 sound/soc/codecs/cs35l41.c     | 201 ++++++++++++++++++++++++++++++++++++++++-
 sound/soc/codecs/cs35l41.h     |   4 +
 6 files changed, 214 insertions(+), 4 deletions(-)
diff mbox series

Patch

diff --git a/include/sound/cs35l41.h b/include/sound/cs35l41.h
index 56289b67b9a0e..bf7f9a9aeba04 100644
--- a/include/sound/cs35l41.h
+++ b/include/sound/cs35l41.h
@@ -40,6 +40,9 @@ 
 #define CS35L41_PROTECT_REL_ERR_IGN	0x00002034
 #define CS35L41_GPIO_PAD_CONTROL	0x0000242C
 #define CS35L41_JTAG_CONTROL		0x00002438
+#define CS35L41_PWRMGT_CTL		0x00002900
+#define CS35L41_WAKESRC_CTL		0x00002904
+#define CS35L41_PWRMGT_STS		0x00002908
 #define CS35L41_PLL_CLK_CTRL		0x00002C04
 #define CS35L41_DSP_CLK_CTRL		0x00002C08
 #define CS35L41_GLOBAL_CLK_CTRL		0x00002C0C
@@ -635,6 +638,8 @@ 
 #define CS35L41_INPUT_DSP_TX1		0x32
 #define CS35L41_INPUT_DSP_TX2		0x33
 
+#define CS35L41_WR_PEND_STS_MASK	0x2
+
 #define CS35L41_PLL_CLK_SEL_MASK	0x07
 #define CS35L41_PLL_CLK_SEL_SHIFT	0
 #define CS35L41_PLL_CLK_EN_MASK		0x10
diff --git a/sound/soc/codecs/cs35l41-i2c.c b/sound/soc/codecs/cs35l41-i2c.c
index eb8dfb6d9c950..faad5c638cb82 100644
--- a/sound/soc/codecs/cs35l41-i2c.c
+++ b/sound/soc/codecs/cs35l41-i2c.c
@@ -86,6 +86,7 @@  MODULE_DEVICE_TABLE(acpi, cs35l41_acpi_match);
 static struct i2c_driver cs35l41_i2c_driver = {
 	.driver = {
 		.name		= "cs35l41",
+		.pm		= &cs35l41_pm_ops,
 		.of_match_table = of_match_ptr(cs35l41_of_match),
 		.acpi_match_table = ACPI_PTR(cs35l41_acpi_match),
 	},
diff --git a/sound/soc/codecs/cs35l41-lib.c b/sound/soc/codecs/cs35l41-lib.c
index ecaf67fd76531..e5a56bcbb223d 100644
--- a/sound/soc/codecs/cs35l41-lib.c
+++ b/sound/soc/codecs/cs35l41-lib.c
@@ -90,6 +90,9 @@  static bool cs35l41_readable_reg(struct device *dev, unsigned int reg)
 	case CS35L41_PROTECT_REL_ERR_IGN:
 	case CS35L41_GPIO_PAD_CONTROL:
 	case CS35L41_JTAG_CONTROL:
+	case CS35L41_PWRMGT_CTL:
+	case CS35L41_WAKESRC_CTL:
+	case CS35L41_PWRMGT_STS:
 	case CS35L41_PLL_CLK_CTRL:
 	case CS35L41_DSP_CLK_CTRL:
 	case CS35L41_GLOBAL_CLK_CTRL:
@@ -376,6 +379,9 @@  static bool cs35l41_volatile_reg(struct device *dev, unsigned int reg)
 	case CS35L41_OTPID:
 	case CS35L41_TEST_KEY_CTL:
 	case CS35L41_USER_KEY_CTL:
+	case CS35L41_PWRMGT_CTL:
+	case CS35L41_WAKESRC_CTL:
+	case CS35L41_PWRMGT_STS:
 	case CS35L41_DTEMP_EN:
 	case CS35L41_IRQ1_STATUS:
 	case CS35L41_IRQ1_STATUS1:
diff --git a/sound/soc/codecs/cs35l41-spi.c b/sound/soc/codecs/cs35l41-spi.c
index 86bbe2fba956e..6dfd5459aa207 100644
--- a/sound/soc/codecs/cs35l41-spi.c
+++ b/sound/soc/codecs/cs35l41-spi.c
@@ -84,6 +84,7 @@  MODULE_DEVICE_TABLE(acpi, cs35l41_acpi_match);
 static struct spi_driver cs35l41_spi_driver = {
 	.driver = {
 		.name		= "cs35l41",
+		.pm		= &cs35l41_pm_ops,
 		.of_match_table = of_match_ptr(cs35l41_of_match),
 		.acpi_match_table = ACPI_PTR(cs35l41_acpi_match),
 	},
diff --git a/sound/soc/codecs/cs35l41.c b/sound/soc/codecs/cs35l41.c
index e1b9fd8ee9966..77a0176946459 100644
--- a/sound/soc/codecs/cs35l41.c
+++ b/sound/soc/codecs/cs35l41.c
@@ -13,6 +13,7 @@ 
 #include <linux/module.h>
 #include <linux/moduleparam.h>
 #include <linux/of_device.h>
+#include <linux/pm_runtime.h>
 #include <linux/property.h>
 #include <sound/initval.h>
 #include <sound/pcm.h>
@@ -187,8 +188,14 @@  static int cs35l41_dsp_preload_ev(struct snd_soc_dapm_widget *w,
 
 	switch (event) {
 	case SND_SOC_DAPM_PRE_PMU:
+		if (cs35l41->dsp.cs_dsp.booted)
+			return 0;
+
 		return wm_adsp_early_event(w, kcontrol, event);
 	case SND_SOC_DAPM_PRE_PMD:
+		if (cs35l41->dsp.preloaded)
+			return 0;
+
 		if (cs35l41->dsp.cs_dsp.running) {
 			ret = wm_adsp_event(w, kcontrol, event);
 			if (ret)
@@ -209,6 +216,7 @@  static bool cs35l41_check_cspl_mbox_sts(enum cs35l41_cspl_mbox_cmd cmd,
 	case CSPL_MBOX_CMD_UNKNOWN_CMD:
 		return true;
 	case CSPL_MBOX_CMD_PAUSE:
+	case CSPL_MBOX_CMD_OUT_OF_HIBERNATE:
 		return (sts == CSPL_MBOX_STS_PAUSED);
 	case CSPL_MBOX_CMD_RESUME:
 		return (sts == CSPL_MBOX_STS_RUNNING);
@@ -230,7 +238,8 @@  static int cs35l41_set_cspl_mbox_cmd(struct cs35l41_private *cs35l41,
 	// Set mailbox cmd
 	ret = regmap_write(cs35l41->regmap, CS35L41_DSP_VIRT1_MBOX_1, cmd);
 	if (ret < 0) {
-		dev_err(cs35l41->dev, "Failed to write MBOX: %d\n", ret);
+		if (cmd != CSPL_MBOX_CMD_OUT_OF_HIBERNATE)
+			dev_err(cs35l41->dev, "Failed to write MBOX: %d\n", ret);
 		return ret;
 	}
 
@@ -413,6 +422,8 @@  static irqreturn_t cs35l41_irq(int irq, void *data)
 	int ret = IRQ_NONE;
 	unsigned int i;
 
+	pm_runtime_get_sync(cs35l41->dev);
+
 	for (i = 0; i < ARRAY_SIZE(status); i++) {
 		regmap_read(cs35l41->regmap,
 			    CS35L41_IRQ1_STATUS1 + (i * CS35L41_REGSTRIDE),
@@ -425,7 +436,7 @@  static irqreturn_t cs35l41_irq(int irq, void *data)
 	/* Check to see if unmasked bits are active */
 	if (!(status[0] & ~masks[0]) && !(status[1] & ~masks[1]) &&
 	    !(status[2] & ~masks[2]) && !(status[3] & ~masks[3]))
-		return IRQ_NONE;
+		goto done;
 
 	if (status[3] & CS35L41_OTP_BOOT_DONE) {
 		regmap_update_bits(cs35l41->regmap, CS35L41_IRQ1_MASK4,
@@ -530,6 +541,10 @@  static irqreturn_t cs35l41_irq(int irq, void *data)
 		ret = IRQ_HANDLED;
 	}
 
+done:
+	pm_runtime_mark_last_busy(cs35l41->dev);
+	pm_runtime_put_autosuspend(cs35l41->dev);
+
 	return ret;
 }
 
@@ -1180,6 +1195,7 @@  static int cs35l41_dsp_init(struct cs35l41_private *cs35l41)
 	dsp->cs_dsp.type = WMFW_HALO;
 	dsp->cs_dsp.rev = 0;
 	dsp->fw = 9; /* 9 is WM_ADSP_FW_SPK_PROT in wm_adsp.c */
+	dsp->toggle_preload = true;
 	dsp->cs_dsp.dev = cs35l41->dev;
 	dsp->cs_dsp.regmap = cs35l41->regmap;
 	dsp->cs_dsp.base = CS35L41_DSP1_CTRL_BASE;
@@ -1367,20 +1383,32 @@  int cs35l41_probe(struct cs35l41_private *cs35l41,
 	if (ret < 0)
 		goto err;
 
+	pm_runtime_set_autosuspend_delay(cs35l41->dev, 3000);
+	pm_runtime_use_autosuspend(cs35l41->dev);
+	pm_runtime_mark_last_busy(cs35l41->dev);
+	pm_runtime_set_active(cs35l41->dev);
+	pm_runtime_get_noresume(cs35l41->dev);
+	pm_runtime_enable(cs35l41->dev);
+
 	ret = devm_snd_soc_register_component(cs35l41->dev,
 					      &soc_component_dev_cs35l41,
 					      cs35l41_dai, ARRAY_SIZE(cs35l41_dai));
 	if (ret < 0) {
 		dev_err(cs35l41->dev, "Register codec failed: %d\n", ret);
-		goto err_dsp;
+		goto err_pm;
 	}
 
+	pm_runtime_put_autosuspend(cs35l41->dev);
+
 	dev_info(cs35l41->dev, "Cirrus Logic CS35L41 (%x), Revision: %02X\n",
 		 regid, reg_revid);
 
 	return 0;
 
-err_dsp:
+err_pm:
+	pm_runtime_disable(cs35l41->dev);
+	pm_runtime_put_noidle(cs35l41->dev);
+
 	wm_adsp2_remove(&cs35l41->dsp);
 err:
 	regulator_bulk_disable(CS35L41_NUM_SUPPLIES, cs35l41->supplies);
@@ -1392,13 +1420,178 @@  EXPORT_SYMBOL_GPL(cs35l41_probe);
 
 void cs35l41_remove(struct cs35l41_private *cs35l41)
 {
+	pm_runtime_get_sync(cs35l41->dev);
+	pm_runtime_disable(cs35l41->dev);
+
 	regmap_write(cs35l41->regmap, CS35L41_IRQ1_MASK1, 0xFFFFFFFF);
 	wm_adsp2_remove(&cs35l41->dsp);
+
+	pm_runtime_put_noidle(cs35l41->dev);
+
 	regulator_bulk_disable(CS35L41_NUM_SUPPLIES, cs35l41->supplies);
 	gpiod_set_value_cansleep(cs35l41->reset_gpio, 0);
 }
 EXPORT_SYMBOL_GPL(cs35l41_remove);
 
+static int __maybe_unused cs35l41_runtime_suspend(struct device *dev)
+{
+	struct cs35l41_private *cs35l41 = dev_get_drvdata(dev);
+
+	dev_dbg(cs35l41->dev, "Runtime suspend\n");
+
+	if (!cs35l41->dsp.preloaded || !cs35l41->dsp.cs_dsp.running)
+		return 0;
+
+	dev_dbg(cs35l41->dev, "Enter hibernate\n");
+
+	regmap_write(cs35l41->regmap, CS35L41_WAKESRC_CTL, 0x0088);
+	regmap_write(cs35l41->regmap, CS35L41_WAKESRC_CTL, 0x0188);
+
+	// Don't wait for ACK since bus activity would wake the device
+	regmap_write(cs35l41->regmap, CS35L41_DSP_VIRT1_MBOX_1,
+		     CSPL_MBOX_CMD_HIBERNATE);
+
+	regcache_cache_only(cs35l41->regmap, true);
+	regcache_mark_dirty(cs35l41->regmap);
+
+	return 0;
+}
+
+static void cs35l41_wait_for_pwrmgt_sts(struct cs35l41_private *cs35l41)
+{
+	const int pwrmgt_retries = 10;
+	unsigned int sts;
+	int i, ret;
+
+	for (i = 0; i < pwrmgt_retries; i++) {
+		ret = regmap_read(cs35l41->regmap, CS35L41_PWRMGT_STS, &sts);
+		if (ret)
+			dev_err(cs35l41->dev, "Failed to read PWRMGT_STS: %d\n", ret);
+		else if (!(sts & CS35L41_WR_PEND_STS_MASK))
+			return;
+
+		udelay(20);
+	}
+
+	dev_err(cs35l41->dev, "Timed out reading PWRMGT_STS\n");
+}
+
+static int cs35l41_exit_hibernate(struct cs35l41_private *cs35l41)
+{
+	const int wake_retries = 20;
+	const int sleep_retries = 5;
+	int ret, i, j;
+
+	for (i = 0; i < sleep_retries; i++) {
+		dev_dbg(cs35l41->dev, "Exit hibernate\n");
+
+		for (j = 0; j < wake_retries; j++) {
+			ret = cs35l41_set_cspl_mbox_cmd(cs35l41,
+							CSPL_MBOX_CMD_OUT_OF_HIBERNATE);
+			if (!ret)
+				break;
+
+			usleep_range(100, 200);
+		}
+
+		if (j < wake_retries) {
+			dev_dbg(cs35l41->dev, "Wake success at cycle: %d\n", j);
+			return 0;
+		}
+
+		dev_err(cs35l41->dev, "Wake failed, re-enter hibernate: %d\n", ret);
+
+		cs35l41_wait_for_pwrmgt_sts(cs35l41);
+		regmap_write(cs35l41->regmap, CS35L41_WAKESRC_CTL, 0x0088);
+
+		cs35l41_wait_for_pwrmgt_sts(cs35l41);
+		regmap_write(cs35l41->regmap, CS35L41_WAKESRC_CTL, 0x0188);
+
+		cs35l41_wait_for_pwrmgt_sts(cs35l41);
+		regmap_write(cs35l41->regmap, CS35L41_PWRMGT_CTL, 0x3);
+	}
+
+	dev_err(cs35l41->dev, "Timed out waking device\n");
+
+	return -ETIMEDOUT;
+}
+
+static int __maybe_unused cs35l41_runtime_resume(struct device *dev)
+{
+	struct cs35l41_private *cs35l41 = dev_get_drvdata(dev);
+	int ret;
+
+	dev_dbg(cs35l41->dev, "Runtime resume\n");
+
+	if (!cs35l41->dsp.preloaded || !cs35l41->dsp.cs_dsp.running)
+		return 0;
+
+	regcache_cache_only(cs35l41->regmap, false);
+
+	ret = cs35l41_exit_hibernate(cs35l41);
+	if (ret)
+		return ret;
+
+	/* 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);
+		return ret;
+	}
+
+	return 0;
+}
+
+static int __maybe_unused cs35l41_sys_suspend(struct device *dev)
+{
+	struct cs35l41_private *cs35l41 = dev_get_drvdata(dev);
+
+	dev_dbg(cs35l41->dev, "System suspend, disabling IRQ\n");
+	disable_irq(cs35l41->irq);
+
+	return 0;
+}
+
+static int __maybe_unused cs35l41_sys_suspend_noirq(struct device *dev)
+{
+	struct cs35l41_private *cs35l41 = dev_get_drvdata(dev);
+
+	dev_dbg(cs35l41->dev, "Late system suspend, reenabling IRQ\n");
+	enable_irq(cs35l41->irq);
+
+	return 0;
+}
+
+static int __maybe_unused cs35l41_sys_resume_noirq(struct device *dev)
+{
+	struct cs35l41_private *cs35l41 = dev_get_drvdata(dev);
+
+	dev_dbg(cs35l41->dev, "Early system resume, disabling IRQ\n");
+	disable_irq(cs35l41->irq);
+
+	return 0;
+}
+
+static int __maybe_unused cs35l41_sys_resume(struct device *dev)
+{
+	struct cs35l41_private *cs35l41 = dev_get_drvdata(dev);
+
+	dev_dbg(cs35l41->dev, "System resume, reenabling IRQ\n");
+	enable_irq(cs35l41->irq);
+
+	return 0;
+}
+
+const struct dev_pm_ops cs35l41_pm_ops = {
+	SET_RUNTIME_PM_OPS(cs35l41_runtime_suspend, cs35l41_runtime_resume, NULL)
+
+	SET_SYSTEM_SLEEP_PM_OPS(cs35l41_sys_suspend, cs35l41_sys_resume)
+	SET_NOIRQ_SYSTEM_SLEEP_PM_OPS(cs35l41_sys_suspend_noirq, cs35l41_sys_resume_noirq)
+};
+EXPORT_SYMBOL_GPL(cs35l41_pm_ops);
+
 MODULE_DESCRIPTION("ASoC CS35L41 driver");
 MODULE_AUTHOR("David Rhodes, Cirrus Logic Inc, <david.rhodes@cirrus.com>");
 MODULE_LICENSE("GPL");
diff --git a/sound/soc/codecs/cs35l41.h b/sound/soc/codecs/cs35l41.h
index 26a08d58a8c34..88a3d6e3434fb 100644
--- a/sound/soc/codecs/cs35l41.h
+++ b/sound/soc/codecs/cs35l41.h
@@ -21,6 +21,8 @@ 
 #define CS35L41_RX_FORMATS (SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S24_LE)
 #define CS35L41_TX_FORMATS (SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S24_LE)
 
+extern const struct dev_pm_ops cs35l41_pm_ops;
+
 enum cs35l41_cspl_mbox_status {
 	CSPL_MBOX_STS_RUNNING = 0,
 	CSPL_MBOX_STS_PAUSED = 1,
@@ -33,6 +35,8 @@  enum cs35l41_cspl_mbox_cmd {
 	CSPL_MBOX_CMD_RESUME = 2,
 	CSPL_MBOX_CMD_REINIT = 3,
 	CSPL_MBOX_CMD_STOP_PRE_REINIT = 4,
+	CSPL_MBOX_CMD_HIBERNATE = 5,
+	CSPL_MBOX_CMD_OUT_OF_HIBERNATE = 6,
 	CSPL_MBOX_CMD_UNKNOWN_CMD = -1,
 	CSPL_MBOX_CMD_INVALID_SEQUENCE = -2,
 };