Introduce support for Cirrus Logic Device CS40L26: A boosted haptic driver with integrated DSP and waveform memory with advanced closed loop algorithms and LRA protection. Signed-off-by: Fred Treven <ftreven@xxxxxxxxxxxxxxxxxxxxx> --- drivers/mfd/Kconfig | 29 + drivers/mfd/Makefile | 4 + drivers/mfd/cs40l26-core.c | 1412 +++++++++++++++++++++++++++++++++++ drivers/mfd/cs40l26-i2c.c | 63 ++ drivers/mfd/cs40l26-spi.c | 63 ++ include/linux/mfd/cs40l26.h | 341 +++++++++ 6 files changed, 1912 insertions(+) create mode 100644 drivers/mfd/cs40l26-core.c create mode 100644 drivers/mfd/cs40l26-i2c.c create mode 100644 drivers/mfd/cs40l26-spi.c create mode 100644 include/linux/mfd/cs40l26.h diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig index 6b0682af6e32..93a60fa9551a 100644 --- a/drivers/mfd/Kconfig +++ b/drivers/mfd/Kconfig @@ -2293,6 +2293,35 @@ config MCP_UCB1200_TS endmenu +config MFD_CS40L26_CORE + tristate + select MFD_CORE + select FW_CS_DSP + +config MFD_CS40L26_I2C + tristate "Cirrus Logic CS40L26 (I2C)" + select REGMAP_I2C + select MFD_CS40L26_CORE + depends on I2C + help + Select this to support the Cirrus Logic CS40L26 Haptic + Driver over I2C. + + This driver can be built as a module. If built as a module it will be + called "cs40l26-i2c". + +config MFD_CS40L26_SPI + tristate "Cirrus Logic CS40L26 (SPI)" + select REGMAP_SPI + select MFD_CS40L26_CORE + depends on SPI + help + Select this to support the Cirrus Logic CS40L26 Haptic + Driver over SPI. + + This driver can be built as a module. If built as a module it will be + called "cs40l26-spi". + config MFD_CS40L50_CORE tristate select MFD_CORE diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile index 9220eaf7cf12..8a245f36d73d 100644 --- a/drivers/mfd/Makefile +++ b/drivers/mfd/Makefile @@ -90,6 +90,10 @@ obj-$(CONFIG_MFD_MADERA) += madera.o obj-$(CONFIG_MFD_MADERA_I2C) += madera-i2c.o obj-$(CONFIG_MFD_MADERA_SPI) += madera-spi.o +obj-$(CONFIG_MFD_CS40L26_CORE) += cs40l26-core.o +obj-$(CONFIG_MFD_CS40L26_I2C) += cs40l26-i2c.o +obj-$(CONFIG_MFD_CS40L26_SPI) += cs40l26-spi.o + obj-$(CONFIG_MFD_CS40L50_CORE) += cs40l50-core.o obj-$(CONFIG_MFD_CS40L50_I2C) += cs40l50-i2c.o obj-$(CONFIG_MFD_CS40L50_SPI) += cs40l50-spi.o diff --git a/drivers/mfd/cs40l26-core.c b/drivers/mfd/cs40l26-core.c new file mode 100644 index 000000000000..b314f820de1e --- /dev/null +++ b/drivers/mfd/cs40l26-core.c @@ -0,0 +1,1412 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * CS40L26 Advanced Haptic Driver with waveform memory, + * integrated DSP, and closed-loop algorithms + * + * Copyright 2025 Cirrus Logic, Inc. + * + * Author: Fred Treven <ftreven@xxxxxxxxxxxxxxxxxxxxx> + */ + +#include <linux/cleanup.h> +#include <linux/mfd/core.h> +#include <linux/mfd/cs40l26.h> +#include <linux/property.h> +#include <linux/regulator/consumer.h> + +static const struct mfd_cell cs40l26_devs[] = { + { .name = "cs40l26-codec", }, + { .name = "cs40l26-vibra", }, +}; + +const struct regmap_config cs40l26_regmap = { + .reg_bits = 32, + .val_bits = 32, + .reg_stride = 4, + .reg_format_endian = REGMAP_ENDIAN_BIG, + .val_format_endian = REGMAP_ENDIAN_BIG, + .max_register = CS40L26_LASTREG, + .cache_type = REGCACHE_NONE, +}; +EXPORT_SYMBOL_GPL(cs40l26_regmap); + +static const char *const cs40l26_supplies[] = { + "va", "vp", +}; + +inline void cs40l26_pm_exit(struct device *dev) +{ + pm_runtime_mark_last_busy(dev); + pm_runtime_put_autosuspend(dev); +} +EXPORT_SYMBOL_GPL(cs40l26_pm_exit); + +static int cs40l26_fw_write_raw(struct cs_dsp *dsp, const char *const name, + const unsigned int algo_id, const u32 offset_words, + const size_t len_words, u32 *buf) +{ + struct cs_dsp_coeff_ctl *ctl; + __be32 *val; + int i, ret; + + ctl = cs_dsp_get_ctl(dsp, name, WMFW_ADSP2_XM, algo_id); + if (!ctl) { + dev_err(dsp->dev, "Failed to find FW control %s\n", name); + return -EINVAL; + } + + val = kzalloc(len_words * sizeof(u32), GFP_KERNEL); + if (!val) + return -ENOMEM; + + for (i = 0; i < len_words; i++) + val[i] = cpu_to_be32(buf[i]); + + ret = cs_dsp_coeff_write_ctrl(ctl, offset_words, val, len_words * sizeof(u32)); + if (ret < 0) + dev_err(dsp->dev, "Failed to write FW control %s\n", name); + + kfree(val); + + return (ret < 0) ? ret : 0; +} + +inline int cs40l26_fw_write(struct cs_dsp *dsp, const char *const name, const unsigned int algo_id, + u32 val) +{ + return cs40l26_fw_write_raw(dsp, name, algo_id, 0, 1, &val); +} +EXPORT_SYMBOL_GPL(cs40l26_fw_write); + +static int cs40l26_fw_read_raw(struct cs_dsp *dsp, const char *const name, + const unsigned int algo_id, const unsigned int offset_words, + const size_t len_words, u32 *buf) +{ + struct cs_dsp_coeff_ctl *ctl; + int i, ret; + + ctl = cs_dsp_get_ctl(dsp, name, WMFW_ADSP2_XM, algo_id); + if (!ctl) { + dev_err(dsp->dev, "Failed to find FW control %s\n", name); + return -EINVAL; + } + + ret = cs_dsp_coeff_read_ctrl(ctl, offset_words, buf, len_words * sizeof(u32)); + if (ret) { + dev_err(dsp->dev, "Failed to read FW control %s\n", name); + return ret; + } + + for (i = 0; i < len_words; i++) + buf[i] = be32_to_cpu(buf[i]); + + return 0; +} + +inline int cs40l26_fw_read(struct cs_dsp *dsp, const char *const name, const unsigned int algo_id, + u32 *buf) +{ + return cs40l26_fw_read_raw(dsp, name, algo_id, 0, 1, buf); +} +EXPORT_SYMBOL_GPL(cs40l26_fw_read); + +static struct cs40l26_irq *cs40l26_get_irq(struct cs40l26 *cs40l26, const int num, const int bit); + +static int cs40l26_gpio1_rise_irq(void *data) +{ + struct cs40l26 *cs40l26 = data; + + if (cs40l26->wksrc_sts & CS40L26_WKSRC_STS_EN) + dev_dbg(cs40l26->dev, "GPIO1 Rising Edge Detected\n"); + + cs40l26->wksrc_sts |= CS40L26_WKSRC_STS_EN; + + return 0; +} + +static int cs40l26_gpio1_fall_irq(void *data) +{ + struct cs40l26 *cs40l26 = data; + + if (cs40l26->wksrc_sts & CS40L26_WKSRC_STS_EN) + dev_dbg(cs40l26->dev, "GPIO1 Falling Edge Detected\n"); + + cs40l26->wksrc_sts |= CS40L26_WKSRC_STS_EN; + + return 0; +} + +static int cs40l26_wksrc_any_irq(void *data) +{ + struct cs40l26 *cs40l26 = data; + u32 last_wksrc, pwrmgt_sts; + int ret; + + guard(mutex)(&cs40l26->dsp.pwr_lock); + + ret = regmap_read(cs40l26->regmap, CS40L26_PWRMGT_STS, &pwrmgt_sts); + if (ret) + return ret; + + cs40l26->wksrc_sts = (u8)FIELD_GET(CS40L26_WKSRC_STS_MASK, pwrmgt_sts); + + ret = cs40l26_fw_read(&cs40l26->dsp, "LAST_WAKESRC_CTL", cs40l26->dsp.fw_id, &last_wksrc); + if (ret) + return ret; + + cs40l26->last_wksrc_pol = (u8)(last_wksrc & CS40L26_WKSRC_GPIO_POL_MASK); + + return 0; +} + +static int cs40l26_wksrc_gpio1_irq(void *data) +{ + struct cs40l26 *cs40l26 = data; + + /* + * The GPIO wakesource and event interrupts are not able to reliably determine + * the GPIO edge that triggered the interrupt (rising/falling). + * + * The driver must therefore perform this logic in order to determine the edge + * of the GPIO event for two cases: + * 1. The GPIO event is waking the device from hibernation. + * 2. The GPIO event occurs when the device is already awake. + */ + + if (cs40l26->wksrc_sts & cs40l26->last_wksrc_pol) { + dev_dbg(cs40l26->dev, "GPIO1 Falling Edge Detected\n"); + cs40l26->wksrc_sts |= CS40L26_WKSRC_STS_EN; + } else { + dev_dbg(cs40l26->dev, "GPIO1 rising edge detected\n"); + } + + return 0; +} + +static int cs40l26_error_release(struct cs40l26 *cs40l26, const enum cs40l26_error err) +{ + int ret; + + dev_err(cs40l26->dev, "Device Reported Error: %u\n", (unsigned int)BIT(err)); + + ret = regmap_clear_bits(cs40l26->regmap, CS40L26_ERROR_RELEASE, BIT(err)); + if (ret) + return ret; + + ret = regmap_set_bits(cs40l26->regmap, CS40L26_ERROR_RELEASE, BIT(err)); + if (ret) + return ret; + + return regmap_clear_bits(cs40l26->regmap, CS40L26_ERROR_RELEASE, BIT(err)); +} + +static int cs40l26_bst_ovp_err_irq(void *data) +{ + struct cs40l26 *cs40l26 = data; + + return cs40l26_error_release(cs40l26, CS40L26_ERROR_BST_OVP); +} + +static int cs40l26_bst_dcm_uvp_err_irq(void *data) +{ + struct cs40l26 *cs40l26 = data; + + return cs40l26_error_release(cs40l26, CS40L26_ERROR_BST_DCM_UVP); +} + +static int cs40l26_bst_short_err_irq(void *data) +{ + struct cs40l26 *cs40l26 = data; + + return cs40l26_error_release(cs40l26, CS40L26_ERROR_BST_SHORT); +} + +static int cs40l26_temp_warn_rise_irq(void *data) +{ + struct cs40l26 *cs40l26 = data; + + return cs40l26_error_release(cs40l26, CS40L26_ERROR_TEMP_WARN); +} + +static int cs40l26_temp_err_irq(void *data) +{ + struct cs40l26 *cs40l26 = data; + + return cs40l26_error_release(cs40l26, CS40L26_ERROR_TEMP_ERR); +} + +static int cs40l26_amp_short_err_irq(void *data) +{ + struct cs40l26 *cs40l26 = data; + + return cs40l26_error_release(cs40l26, CS40L26_ERROR_AMP_SHORT); +} + +static int cs40l26_dsp_queue_buffer_read(struct cs40l26 *cs40l26, u32 *val) +{ + u32 queue_rd, queue_wt, sts; + int ret; + + guard(mutex)(&cs40l26->dsp.pwr_lock); + + ret = cs40l26_fw_read(&cs40l26->dsp, "QUEUE_WT", CS40L26_DSP_ALGO_ID, &queue_wt); + if (ret) + return ret; + + ret = cs40l26_fw_read(&cs40l26->dsp, "QUEUE_RD", CS40L26_DSP_ALGO_ID, &queue_rd); + if (ret) + return ret; + + if (queue_rd - sizeof(u32) == queue_wt) { + ret = cs40l26_fw_read(&cs40l26->dsp, "STATUS", CS40L26_DSP_ALGO_ID, &sts); + if (ret) + return ret; + + if (sts) { + dev_err(cs40l26->dev, "DSP Queue Buffer is full, message(s) missed\n"); + return -ENOSPC; + } + } + + if (queue_rd == queue_wt) /* DSP Queue is Empty */ + return 1; + + ret = regmap_read(cs40l26->regmap, queue_rd, val); + if (ret) + return ret; + + if (queue_rd == cs40l26->queue_last) + queue_rd = cs40l26->queue_base; + else + queue_rd += sizeof(u32); + + return cs40l26_fw_write(&cs40l26->dsp, "QUEUE_RD", CS40L26_DSP_ALGO_ID, queue_rd); +} + +static int cs40l26_dsp_queue_irq(void *data) +{ + struct cs40l26 *cs40l26 = data; + bool end = false; + int ret; + u32 val; + + ret = cs40l26_dsp_queue_buffer_read(cs40l26, &val); + if (ret == 1) + return 0; + else if (ret) + return ret; + + while (!end) { + if ((val & CS40L26_DSP_CMD_INDEX_MASK) == CS40L26_DSP_PANIC) { + dev_err(cs40l26->dev, "DSP Panic! Error: 0x%06X\n", + (u32)(val & CS40L26_DSP_CMD_PAYLOAD_MASK)); + return -ENOTRECOVERABLE; + } + + switch (val) { + case CS40L26_DSP_COMPLETE_CP: + dev_dbg(cs40l26->dev, "DSP Queue: Control Port Haptics Completed\n"); + break; + case CS40L26_DSP_COMPLETE_I2S: + dev_dbg(cs40l26->dev, "DSP Queue: I2S Stream Completed\n"); + break; + case CS40L26_DSP_TRIGGER_CP: + dev_dbg(cs40l26->dev, "DSP Queue: Control Port Haptics Triggered\n"); + break; + case CS40L26_DSP_TRIGGER_I2S: + dev_dbg(cs40l26->dev, "DSP Queue: I2S Stream Triggered\n"); + break; + case CS40L26_DSP_PM_AWAKE: + cs40l26->wksrc_sts |= CS40L26_WKSRC_STS_EN; + dev_dbg(cs40l26->dev, "DSP Queue: AWAKE\n"); + break; + case CS40L26_DSP_SYS_ACK: + dev_dbg(cs40l26->dev, "DSP Queue: Inbound PING received\n"); + break; + default: + dev_err(cs40l26->dev, "DSP Queue value (0x%X) unrecognized\n", val); + return -EINVAL; + } + + ret = cs40l26_dsp_queue_buffer_read(cs40l26, &val); + if (ret == 1) + end = true; + else if (ret) + return ret; + } + + return 0; +} + +static struct reg_sequence cs40l26_irq_masks[] = { + REG_SEQ0(CS40L26_IRQ1_MASK_1, CS40L26_IRQ_1_ALL_MASKED), + REG_SEQ0(CS40L26_IRQ1_MASK_2, CS40L26_IRQ_2_ALL_MASKED), +}; + +static void cs40l26_irq_unmask(struct cs40l26 *cs40l26, const int num, const int virq) +{ + struct cs40l26_irq *irq; + + if (num != 1 && num != 2) { + dev_err(cs40l26->dev, "Invalid IRQ number %d\n", num); + return; + } + + irq = cs40l26_get_irq(cs40l26, num, virq); + if (!irq) + return; + + cs40l26->irq_masks[num - 1].def &= ~irq->mask; +} + +static struct cs40l26_irq cs40l26_irqs_1[] = { + CS40L26_IRQ(GPIO1_RISE, "GPIO1 Rise", cs40l26_gpio1_rise_irq), + CS40L26_IRQ(GPIO1_FALL, "GPIO1 Fall", cs40l26_gpio1_fall_irq), + CS40L26_IRQ(WKSRC_STS_ANY, "ANY Wake", cs40l26_wksrc_any_irq), + CS40L26_IRQ(WKSRC_STS_GPIO1, "GPIO1 Wake", cs40l26_wksrc_gpio1_irq), + CS40L26_IRQ(WKSRC_STS_SPI, "SPI Wake", NULL), + CS40L26_IRQ(WKSRC_STS_I2C, "I2C Wake", NULL), + CS40L26_IRQ(BST_OVP_FLAG_RISE, "BST OVP Rise", NULL), + CS40L26_IRQ(BST_OVP_FLAG_FALL, "BST OVP Fall", NULL), + CS40L26_IRQ(BST_OVP_ERR, "BST OVP Error", cs40l26_bst_ovp_err_irq), + CS40L26_IRQ(BST_DCM_UVP_ERR, "BST UVP Error", cs40l26_bst_dcm_uvp_err_irq), + CS40L26_IRQ(BST_SHORT_ERR, "BST Short Error", cs40l26_bst_short_err_irq), + CS40L26_IRQ(BST_IPK_FLAG, "BST IPK Flag", NULL), + CS40L26_IRQ(TEMP_WARN_RISE, "TEMP Warn Rise", cs40l26_temp_warn_rise_irq), + CS40L26_IRQ(TEMP_WARN_FALL, "TEMP Warn Fall", NULL), + CS40L26_IRQ(TEMP_ERR, "TEMP Error", cs40l26_temp_err_irq), + CS40L26_IRQ(AMP_ERR, "AMP Error", cs40l26_amp_short_err_irq), + CS40L26_IRQ(DSP_RX_QUEUE, "DSP Rx", cs40l26_dsp_queue_irq), +}; + +static struct cs40l26_irq cs40l26_irqs_2[] = { + CS40L26_IRQ(REFCLK_PRESENT, "REFCLK Present", NULL), + CS40L26_IRQ(REFCLK_MISSING_FALL, "REFCLK Missing Fall", NULL), + CS40L26_IRQ(REFCLK_MISSING_RISE, "REFCLK Missing Rise", NULL), + CS40L26_IRQ(VPMON_CLIPPED, "VPMON Clipped", NULL), + CS40L26_IRQ(VBSTMON_CLIPPED, "VBSTMON Clipped", NULL), + CS40L26_IRQ(VMON_CLIPPED, "VMON Clipped", NULL), + CS40L26_IRQ(IMON_CLIPPED, "IMON Clipped", NULL), +}; + +static struct cs40l26_irq *cs40l26_get_irq(struct cs40l26 *cs40l26, const int num, const int bit) +{ + int i; + + if (num == 1) { + for (i = 0; i < ARRAY_SIZE(cs40l26_irqs_1); i++) { + if (cs40l26_irqs_1[i].virq == bit) + return &cs40l26_irqs_1[i]; + } + } else if (num == 2) { + for (i = 0; i < ARRAY_SIZE(cs40l26_irqs_2); i++) { + if (cs40l26_irqs_2[i].virq == bit) + return &cs40l26_irqs_2[i]; + } + } else { + dev_err(cs40l26->dev, "Invalid IRQ number %d\n", num); + return NULL; + } + + dev_err(cs40l26->dev, "Failed to find IRQ corresponding to bit in IRQ%d %d\n", bit, num); + + return NULL; +} + +static irqreturn_t cs40l26_irq_handler(int irq, void *data) +{ + struct cs40l26 *cs40l26 = data; + struct cs40l26_irq *irq_s; + unsigned long handle_bits; + u32 eint, mask, sts; + int i, j, ret; + + if (pm_runtime_resume_and_get(cs40l26->dev)) { + dev_err(cs40l26->dev, "Failed to exit hibernate to service interrupt\n"); + return IRQ_NONE; + } + + guard(mutex)(&cs40l26->lock); + + ret = regmap_read(cs40l26->regmap, CS40L26_IRQ1_STATUS, &sts); + if (ret) + goto err_pm; + + if (!(sts & CS40L26_IRQ_STATUS_ASSERT)) { + dev_err(cs40l26->dev, "IRQ1 asserted with no pending interrupts\n"); + ret = -EIO; + goto err_pm; + } + + for (j = 0; j < 2; j++) { + ret = regmap_read(cs40l26->regmap, CS40L26_IRQ1_MASK_1 + j * 4, &mask); + if (ret) + goto err_pm; + + ret = regmap_read(cs40l26->regmap, CS40L26_IRQ1_EINT_1 + j * 4, &eint); + if (ret) + goto err_pm; + + handle_bits = eint & ~mask; + + for_each_set_bit(i, &handle_bits, j ? CS40L26_IRQ_2_NBITS : CS40L26_IRQ_1_NBITS) { + irq_s = cs40l26_get_irq(cs40l26, j + 1, i); + if (!irq_s) + continue; + + dev_dbg(cs40l26->dev, "%s", irq_s->name); + + if (irq_s->handler) { + ret = irq_s->handler(cs40l26); + if (ret) + goto err_pm; + } + + ret = regmap_write(cs40l26->regmap, CS40L26_IRQ1_EINT_1 + j * 4, BIT(i)); + if (ret) + goto err_pm; + } + } + +err_pm: + cs40l26_pm_exit(cs40l26->dev); + + return IRQ_RETVAL(ret); +} + +int cs40l26_dsp_write(struct cs40l26 *cs40l26, const u32 val) +{ + int i, ret; + u32 ack; + + /* Device NAKs if hibernating, so retry if this is the case */ + for (i = 0; i < CS40L26_DSP_TIMEOUT_COUNT; i++) { + ret = regmap_write(cs40l26->regmap, CS40L26_DSP_QUEUE, val); + if (!ret) + break; + + usleep_range(CS40L26_DSP_POLL_US, CS40L26_DSP_POLL_US + 100); + } + + if (i == CS40L26_DSP_TIMEOUT_COUNT) { + dev_err(cs40l26->dev, "Timed out writing %#X to DSP\n", val); + return -ETIMEDOUT; + } + + ret = regmap_read_poll_timeout(cs40l26->regmap, CS40L26_DSP_QUEUE, ack, !ack, + CS40L26_DSP_POLL_US, + CS40L26_DSP_POLL_US * CS40L26_DSP_TIMEOUT_COUNT); + if (ret) + dev_err(cs40l26->dev, "DSP failed to ACK %#X: %d\n", val, ret); + + return ret; +} +EXPORT_SYMBOL_GPL(cs40l26_dsp_write); + +int cs40l26_dsp_state_get(struct cs40l26 *cs40l26, u32 *state) +{ + u32 dsp_state = CS40L26_DSP_STATE_NONE; + int i, ret; + + if (cs40l26->dsp.running) { + for (i = 0; i < CS40L26_DSP_TIMEOUT_COUNT; i++) { + ret = cs40l26_fw_read(&cs40l26->dsp, "PM_CUR_STATE", CS40L26_PM_ALGO_ID, + &dsp_state); + if (ret) + return ret; + + if (dsp_state != CS40L26_DSP_STATE_NONE) + break; + + usleep_range(CS40L26_DSP_POLL_US, CS40L26_DSP_POLL_US + 100); + } + + if (i == CS40L26_DSP_TIMEOUT_COUNT) { + dev_err(cs40l26->dev, "Timed out reading PM_CUR_STATE\n"); + return -ETIMEDOUT; + } + } else { + ret = regmap_read_poll_timeout(cs40l26->regmap, + cs40l26->variant->info->pm_cur_state, dsp_state, + dsp_state != CS40L26_DSP_STATE_NONE, + CS40L26_DSP_POLL_US, + CS40L26_DSP_POLL_US * CS40L26_DSP_TIMEOUT_COUNT); + if (ret) { + dev_err(cs40l26->dev, "Failed to read poll for static PM_CUR_STATE\n"); + return ret; + } + } + + *state = dsp_state; + + return 0; +} +EXPORT_SYMBOL_GPL(cs40l26_dsp_state_get); + +static bool cs40l26_dsp_can_run(struct cs40l26 *cs40l26) +{ + struct regmap *regmap = cs40l26->regmap; + u32 dsp_state, pm_state_locks; + int ret; + + ret = cs40l26_dsp_state_get(cs40l26, &dsp_state); + if (ret) + return false; + + if (dsp_state == CS40L26_DSP_STATE_ACTIVE) + return true; + + if (dsp_state != CS40L26_DSP_STATE_STANDBY) { + dev_err(cs40l26->dev, "DSP in bad state: %u\n", dsp_state); + return false; + } + + if (cs40l26->dsp.running) + ret = cs40l26_fw_read_raw(&cs40l26->dsp, "PM_STATE_LOCKS", CS40L26_PM_ALGO_ID, + CS40L26_DSP_LOCK3_OFFSET_WORDS, 1, &pm_state_locks); + else + ret = regmap_read(regmap, cs40l26->variant->info->pm_state_locks3, &pm_state_locks); + + if (ret) { + dev_err(cs40l26->dev, "Failed to read PM_STATE_LOCKS\n"); + return false; + } + + return pm_state_locks & CS40L26_DSP_LOCK3_MASK; +} + +static int cs40l26_prevent_hiber(struct cs40l26 *cs40l26) +{ + int i, ret; + + for (i = 0; i < CS40L26_DSP_TIMEOUT_COUNT; i++) { + ret = cs40l26_dsp_write(cs40l26, CS40L26_DSP_CMD_PREVENT_HIBER); + if (ret) + return ret; + + usleep_range(CS40L26_DSP_POLL_US, CS40L26_DSP_POLL_US + 100); + + if (cs40l26_dsp_can_run(cs40l26)) + break; + } + + if (i == CS40L26_DSP_TIMEOUT_COUNT) { + dev_err(cs40l26->dev, "Failed to prevent hibernation\n"); + return -ETIMEDOUT; + } + + return 0; +} + +static int cs40l26_lbst_short_test(struct cs40l26 *cs40l26) +{ + u32 err, vbst_ctl_1, vbst_ctl_2; + int ret; + + /* Read initial values to restore after test is complete */ + ret = regmap_read(cs40l26->regmap, CS40L26_VBST_CTL_2, &vbst_ctl_2); + if (ret) + return ret; + + ret = regmap_read(cs40l26->regmap, CS40L26_VBST_CTL_1, &vbst_ctl_1); + if (ret) + return ret; + + ret = regmap_update_bits(cs40l26->regmap, CS40L26_VBST_CTL_2, CS40L26_BST_CTL_SEL_MASK, + CS40L26_BST_CTL_SEL_FIXED); + if (ret) + return ret; + + ret = regmap_update_bits(cs40l26->regmap, CS40L26_VBST_CTL_1, CS40L26_BST_CTL_MASK, + CS40L26_BST_CTL_VP); + if (ret) + return ret; + + ret = regmap_set_bits(cs40l26->regmap, CS40L26_GLOBAL_ENABLES, CS40L26_GLOBAL_EN); + if (ret) + return ret; + + /* Wait for boost converter to power up */ + usleep_range(CS40L26_BST_TIME_US, CS40L26_BST_TIME_US + 100); + + ret = regmap_read(cs40l26->regmap, CS40L26_ERROR_RELEASE, &err); + if (ret) + return ret; + + if (err & BIT(CS40L26_ERROR_BST_SHORT)) { + dev_err(cs40l26->dev, "Boost shorted at startup\n"); + return -ENOTRECOVERABLE; + } + + /* Return to previous state before test */ + ret = regmap_clear_bits(cs40l26->regmap, CS40L26_GLOBAL_ENABLES, CS40L26_GLOBAL_EN); + if (ret) + return ret; + + ret = regmap_write(cs40l26->regmap, CS40L26_VBST_CTL_1, vbst_ctl_1); + if (ret) + return ret; + + return regmap_write(cs40l26->regmap, CS40L26_VBST_CTL_2, vbst_ctl_2); +} + +static const struct reg_sequence cs40l26_a1_b1_errata[] = { + { CS40L26_PLL_REFCLK_DETECT_0, CS40L26_PLL_REFCLK_DET_DISABLE }, + { 0x00000040, 0x00000055 }, + { 0x00000040, 0x000000AA }, + { CS40L26_TEST_LBST, CS40L26_DISABLE_EXPL_MODE }, +}; + +static int cs40l26_a1_b1_handle_errata(struct cs40l26 *cs40l26) +{ + int ret; + + /* + * Boost Exploratory Mode must be disabled on 0xA1/0xB1 devices in order to ensure there is + * no unintentional damage to the boost inductor. Any boost short that occurs after the + * LBST short test at probe will not be detected. + */ + ret = cs40l26_lbst_short_test(cs40l26); + if (ret) + return ret; + + ret = regmap_multi_reg_write(cs40l26->regmap, cs40l26_a1_b1_errata, + ARRAY_SIZE(cs40l26_a1_b1_errata)); + if (ret) + return ret; + + return cs_dsp_wseq_multi_write(&cs40l26->dsp, &cs40l26->wseqs[CS40L26_WSEQ_POWER_ON], + cs40l26_a1_b1_errata, ARRAY_SIZE(cs40l26_a1_b1_errata), + CS_DSP_WSEQ_FULL, false); +} + +static const struct cs40l26_variant_info cs40l26_a1_b1_info = { + .pm_cur_state = CS40L26_A1_B1_PM_CUR_STATE, + .pm_state_locks = CS40L26_A1_B1_PM_STATE_LOCKS, + .pm_state_locks3 = CS40L26_A1_B1_PM_STATE_LOCKS3, + .pm_stdby_ticks = CS40L26_A1_B1_PM_STDBY_TICKS, + .pm_active_ticks = CS40L26_A1_B1_PM_ACTIVE_TICKS, + .halo_state = CS40L26_A1_B1_HALO_STATE, + .event_map_1 = CS40L26_A1_B1_EVENT_MAP_1, + .event_map_2 = CS40L26_A1_B1_EVENT_MAP_2, + .fw_min_rev = CS40L26_FW_A1_B1_MIN_REV, + .ram_ext_algo_id = CS40L26_EXT_ALGO_ID, + .vibegen_algo_id = CS40L26_VIBEGEN_ALGO_ID_A1, +}; + +static const struct cs40l26_variant_info cs40l26_b2_info = { + .pm_cur_state = CS40L26_B2_PM_CUR_STATE, + .pm_state_locks = CS40L26_B2_PM_STATE_LOCKS, + .pm_state_locks3 = CS40L26_B2_PM_STATE_LOCKS3, + .pm_stdby_ticks = CS40L26_B2_PM_STDBY_TICKS, + .pm_active_ticks = CS40L26_B2_PM_ACTIVE_TICKS, + .halo_state = CS40L26_B2_HALO_STATE, + .event_map_1 = CS40L26_B2_EVENT_MAP_1, + .event_map_2 = CS40L26_B2_EVENT_MAP_2, + .fw_min_rev = CS40L26_FW_B2_MIN_REV, + .ram_ext_algo_id = CS40L26_FW_ID, + .vibegen_algo_id = CS40L26_VIBEGEN_ALGO_ID_B2, +}; + +static const struct cs40l26_variant cs40l26_a1_b1_variant = { + .info = &cs40l26_a1_b1_info, + .handle_errata = &cs40l26_a1_b1_handle_errata, +}; + +static const struct cs40l26_variant cs40l26_b2_variant = { + .info = &cs40l26_b2_info, + .handle_errata = NULL, +}; + +static inline int cs40l26_pm_timeout_ms_set(struct cs40l26 *cs40l26, const u32 dsp_state, + const u32 timeout_ms) +{ + return regmap_write(cs40l26->regmap, + dsp_state == CS40L26_DSP_STATE_STANDBY ? + cs40l26->variant->info->pm_stdby_ticks : + cs40l26->variant->info->pm_active_ticks, + (timeout_ms * CS40L26_PM_TICKS_PER_SEC) / 1000); +} + +static int cs40l26_pm_timeout_ms_get(struct cs40l26 *cs40l26, const u32 dsp_state, u32 *timeout_ms) +{ + u32 timeout_ticks; + int ret; + + ret = regmap_read(cs40l26->regmap, + dsp_state == CS40L26_DSP_STATE_STANDBY ? + cs40l26->variant->info->pm_stdby_ticks : + cs40l26->variant->info->pm_active_ticks, + &timeout_ticks); + if (ret) + return ret; + + *timeout_ms = DIV_ROUND_UP(timeout_ticks * 1000, CS40L26_PM_TICKS_PER_SEC); + + return 0; +} + +static int cs40l26_pm_runtime_setup(struct device *dev) +{ + int ret; + + pm_runtime_set_autosuspend_delay(dev, CS40L26_AUTOSUSPEND_DELAY_MS); + pm_runtime_use_autosuspend(dev); + pm_runtime_get_noresume(dev); + ret = pm_runtime_set_active(dev); + if (ret) + return ret; + + return devm_pm_runtime_enable(dev); +} + +static int cs40l26_dsp_pre_config(struct cs40l26 *cs40l26) +{ + u32 dsp_state, halo_state, timeout_ms; + int i, ret; + + ret = regmap_read(cs40l26->regmap, cs40l26->variant->info->halo_state, &halo_state); + if (ret) + return ret; + + if (halo_state != CS40L26_DSP_HALO_STATE_RUN) { + dev_err(cs40l26->dev, "Invalid DSP state: %u\n", halo_state); + return -EINVAL; + } + + ret = cs40l26_pm_timeout_ms_get(cs40l26, CS40L26_DSP_STATE_ACTIVE, &timeout_ms); + if (ret) { + dev_err(cs40l26->dev, "Failed to get ACTIVE timeout\n"); + return ret; + } + + for (i = 0; i < CS40L26_DSP_STATE_TIMEOUT_COUNT; i++) { + ret = cs40l26_dsp_state_get(cs40l26, &dsp_state); + if (ret) + return ret; + + if (dsp_state != CS40L26_DSP_STATE_SHUTDOWN && + dsp_state != CS40L26_DSP_STATE_STANDBY) + dev_warn(cs40l26->dev, "DSP core not safe to kill\n"); + else + break; + + usleep_range(timeout_ms * 1000, (timeout_ms * 1000) + 100); + } + + if (i == CS40L26_DSP_STATE_TIMEOUT_COUNT) { + dev_err(cs40l26->dev, "DSP Core could not be shut down\n"); + return -ETIMEDOUT; + } + + return regmap_write(cs40l26->regmap, CS40L26_DSP1_CCM_CORE_CONTROL, + CS40L26_DSP_CCM_CORE_KILL); +} + +static const struct cs_dsp_region cs40l26_dsp_regions[] = { + { .type = WMFW_HALO_PM_PACKED, .base = CS40L26_DSP1_PMEM_0 }, + { .type = WMFW_HALO_XM_PACKED, .base = CS40L26_DSP1_XMEM_PACKED_0 }, + { .type = WMFW_HALO_YM_PACKED, .base = CS40L26_DSP1_YMEM_PACKED_0 }, + { .type = WMFW_ADSP2_XM, .base = CS40L26_DSP1_XMEM_UNPACKED24_0 }, + { .type = WMFW_ADSP2_YM, .base = CS40L26_DSP1_YMEM_UNPACKED24_0 }, +}; + +static int cs40l26_get_model(struct cs40l26 *cs40l26) +{ + int ret; + + ret = regmap_read(cs40l26->regmap, CS40L26_DEVID, &cs40l26->devid); + if (ret) + return ret; + + ret = regmap_read(cs40l26->regmap, CS40L26_REVID, &cs40l26->revid); + if (ret) + return ret; + + switch (cs40l26->devid) { + case CS40L26_DEVID_L26: + if (cs40l26->revid != CS40L26_REVID_A1 && cs40l26->revid != CS40L26_REVID_B1) + goto err; + + cs40l26->variant = &cs40l26_a1_b1_variant; + break; + case CS40L26_DEVID_L27: + if (cs40l26->revid != CS40L26_REVID_B2) + goto err; + + cs40l26->variant = &cs40l26_b2_variant; + break; + default: + dev_err(cs40l26->dev, "Invalid device ID 0x%06X\n", cs40l26->devid); + return -EINVAL; + } + + dev_info(cs40l26->dev, "Cirrus Logic CS40L26 ID: 0x%06X, Revision: 0x%02X\n", + cs40l26->devid, cs40l26->revid); + + return 0; + +err: + dev_err(cs40l26->dev, "Invalid revision 0x%02X for device 0x%06X\n", cs40l26->revid, + cs40l26->devid); + return -EINVAL; +} + +int cs40l26_set_pll_loop(struct cs40l26 *cs40l26, const u32 pll_loop) +{ + int i; + + /* Retry in case DSP is hibernating */ + for (i = 0; i < CS40L26_PLL_NUM_SET_ATTEMPTS; i++) { + if (!regmap_update_bits(cs40l26->regmap, CS40L26_REFCLK_INPUT, + CS40L26_PLL_REFCLK_LOOP_MASK, + pll_loop << CS40L26_PLL_REFCLK_LOOP_SHIFT)) + break; + } + + if (i == CS40L26_PLL_NUM_SET_ATTEMPTS) { + dev_err(cs40l26->dev, "Failed to configure PLL\n"); + return -ETIMEDOUT; + } + + return 0; +} +EXPORT_SYMBOL_GPL(cs40l26_set_pll_loop); + +static int cs40l26_wseq_init(struct cs40l26 *cs40l26) +{ + struct cs_dsp *dsp = &cs40l26->dsp; + + cs40l26->wseqs[CS40L26_WSEQ_POWER_ON].ctl = + cs_dsp_get_ctl(dsp, "POWER_ON_SEQUENCE", WMFW_ADSP2_XM, CS40L26_PM_ALGO_ID); + if (!cs40l26->wseqs[CS40L26_WSEQ_POWER_ON].ctl) { + dev_err(cs40l26->dev, "POWER_ON write sequence not found\n"); + return -EINVAL; + } + + cs40l26->wseqs[CS40L26_WSEQ_ACTIVE].ctl = + cs_dsp_get_ctl(dsp, "ACTIVE_SEQUENCE", WMFW_ADSP2_XM, CS40L26_PM_ALGO_ID); + if (!cs40l26->wseqs[CS40L26_WSEQ_ACTIVE].ctl) { + dev_err(cs40l26->dev, "ACTIVE write sequence not found\n"); + return -EINVAL; + } + + cs40l26->wseqs[CS40L26_WSEQ_STANDBY].ctl = + cs_dsp_get_ctl(dsp, "STANDBY_SEQUENCE", WMFW_ADSP2_XM, CS40L26_PM_ALGO_ID); + if (!cs40l26->wseqs[CS40L26_WSEQ_STANDBY].ctl) { + dev_err(cs40l26->dev, "STANDBY write sequence not found\n"); + return -EINVAL; + } + + return cs_dsp_wseq_init(dsp, cs40l26->wseqs, CS40L26_NUM_WSEQS); +} + +static int cs40l26_wksrc_config(struct cs40l26 *cs40l26) +{ + u32 wksrc; + int ret; + + if (!strncmp(cs40l26->bus->name, "spi", strlen(cs40l26->bus->name))) { + cs40l26_irq_unmask(cs40l26, 1, CS40L26_IRQ_WKSRC_STS_SPI); + wksrc = CS40L26_WKSRC_POL_SPI | CS40L26_WKSRC_EN_SPI; + } else if (!strncmp(cs40l26->bus->name, "i2c", strlen(cs40l26->bus->name))) { + cs40l26_irq_unmask(cs40l26, 1, CS40L26_IRQ_WKSRC_STS_I2C); + wksrc = CS40L26_WKSRC_EN_I2C; + } else { + dev_err(cs40l26->dev, "Invalid bus type\n"); + return -EINVAL; + } + + cs40l26_irq_unmask(cs40l26, 1, CS40L26_IRQ_WKSRC_STS_GPIO1); + cs40l26_irq_unmask(cs40l26, 1, CS40L26_IRQ_WKSRC_STS_ANY); + + ret = regmap_write(cs40l26->regmap, CS40L26_WAKESRC_CTL, wksrc); + if (ret) + return ret; + + return cs_dsp_wseq_write(&cs40l26->dsp, &cs40l26->wseqs[CS40L26_WSEQ_POWER_ON], + CS40L26_WAKESRC_CTL, wksrc, CS_DSP_WSEQ_L16, true); +} + +static inline void cs40l26_gpio_config(struct cs40l26 *cs40l26) +{ + cs40l26_irq_unmask(cs40l26, 1, CS40L26_IRQ_GPIO1_RISE); + cs40l26_irq_unmask(cs40l26, 1, CS40L26_IRQ_GPIO1_FALL); +} + +static int cs40l26_bst_ipk_config(struct cs40l26 *cs40l26) +{ + u32 bst_ipk; + int ret; + + bst_ipk = (clamp_val(cs40l26->bst_ipk_ua, CS40L26_BST_IPK_UA_MIN, CS40L26_BST_IPK_UA_MAX) - + CS40L26_BST_IPK_UA_OFFSET) / CS40L26_BST_IPK_UA_STEP; + + cs40l26_irq_unmask(cs40l26, 1, CS40L26_IRQ_BST_IPK_FLAG); + + ret = regmap_write(cs40l26->regmap, CS40L26_BST_IPK_CTL, bst_ipk); + if (ret) + return ret; + + return cs_dsp_wseq_write(&cs40l26->dsp, &cs40l26->wseqs[CS40L26_WSEQ_POWER_ON], + CS40L26_BST_IPK_CTL, bst_ipk, CS_DSP_WSEQ_L16, true); +} + +static int cs40l26_bst_ctl_config(struct cs40l26 *cs40l26) +{ + u32 bst_ctl, bst_ctl_lim; + int ret; + + ret = regmap_read(cs40l26->regmap, CS40L26_VBST_CTL_2, &bst_ctl_lim); + if (ret) + return ret; + + bst_ctl_lim |= FIELD_PREP(CS40L26_BST_CTL_LIM_EN_MASK, CS40L26_BST_CTL_LIM_EN); + + ret = regmap_write(cs40l26->regmap, CS40L26_VBST_CTL_2, bst_ctl_lim); + if (ret) + return ret; + + ret = cs_dsp_wseq_write(&cs40l26->dsp, &cs40l26->wseqs[CS40L26_WSEQ_POWER_ON], + CS40L26_VBST_CTL_2, bst_ctl_lim, CS_DSP_WSEQ_L16, true); + if (ret) + return ret; + + bst_ctl = (clamp_val(cs40l26->vbst_uv, CS40L26_BST_UV_MIN, CS40L26_BST_UV_MAX) - + CS40L26_BST_UV_MIN) / CS40L26_BST_UV_STEP; + + ret = regmap_write(cs40l26->regmap, CS40L26_VBST_CTL_1, bst_ctl); + if (ret) + return ret; + + return cs_dsp_wseq_write(&cs40l26->dsp, &cs40l26->wseqs[CS40L26_WSEQ_POWER_ON], + CS40L26_VBST_CTL_1, bst_ctl, CS_DSP_WSEQ_L16, true); +} + +static int cs40l26_irq_init(struct cs40l26 *cs40l26) +{ + int i, ret; + + /* Unmask relevant warnings and error interrupts */ + for (i = CS40L26_IRQ_BST_OVP_FLAG_RISE; i <= CS40L26_IRQ_AMP_ERR; i++) + cs40l26_irq_unmask(cs40l26, 1, i); + + cs40l26_irq_unmask(cs40l26, 1, CS40L26_IRQ_DSP_RX_QUEUE); + + for (i = CS40L26_IRQ_VPMON_CLIPPED; i <= CS40L26_IRQ_IMON_CLIPPED; i++) + cs40l26_irq_unmask(cs40l26, 2, i); + + for (i = CS40L26_IRQ_REFCLK_PRESENT; i <= CS40L26_IRQ_REFCLK_MISSING_RISE; i++) + cs40l26_irq_unmask(cs40l26, 2, i); + + ret = regmap_multi_reg_write(cs40l26->regmap, cs40l26_irq_masks, 2); + if (ret) + return ret; + + ret = cs_dsp_wseq_multi_write(&cs40l26->dsp, &cs40l26->wseqs[CS40L26_WSEQ_POWER_ON], + cs40l26_irq_masks, 2, CS_DSP_WSEQ_FULL, true); + if (ret) + return ret; + + ret = devm_request_threaded_irq(cs40l26->dev, cs40l26->irq, NULL, cs40l26_irq_handler, + IRQF_ONESHOT, "cs40l26", cs40l26); + if (ret) + dev_err(cs40l26->dev, "Failed to request IRQ\n"); + + return ret; +} + +static int cs40l26_hw_init(struct cs40l26 *cs40l26) +{ + int ret; + + cs40l26->irq_masks = cs40l26_irq_masks; + + ret = cs40l26_wksrc_config(cs40l26); + if (ret) + return ret; + + cs40l26_gpio_config(cs40l26); + + ret = cs40l26_bst_ipk_config(cs40l26); + if (ret) + return ret; + + ret = cs40l26_bst_ctl_config(cs40l26); + if (ret) + return ret; + + return cs40l26_irq_init(cs40l26); +} + +static int cs40l26_cs_dsp_pre_run(struct cs_dsp *dsp) +{ + struct cs40l26 *cs40l26 = container_of(dsp, struct cs40l26, dsp); + int ret; + + ret = cs40l26_pm_timeout_ms_set(cs40l26, CS40L26_DSP_STATE_STANDBY, 100); + if (ret) { + dev_err(cs40l26->dev, "Failed to set standby timeout\n"); + return ret; + } + + ret = cs40l26_pm_timeout_ms_set(cs40l26, CS40L26_DSP_STATE_ACTIVE, 250); + if (ret) { + dev_err(cs40l26->dev, "Failed to set active timeout\n"); + return ret; + } + + ret = regmap_set_bits(cs40l26->regmap, CS40L26_PWRMGT_CTL, CS40L26_MEM_RDY); + if (ret) { + dev_err(cs40l26->dev, "Failed to set MEM_RDY\n"); + return ret; + } + + ret = cs40l26_fw_read(dsp, "QUEUE_BASE", CS40L26_DSP_ALGO_ID, &cs40l26->queue_base); + if (ret) + return ret; + + ret = cs40l26_fw_read(dsp, "QUEUE_LEN", CS40L26_DSP_ALGO_ID, &cs40l26->queue_len); + if (ret) + return ret; + + cs40l26->queue_last = cs40l26->queue_base + ((cs40l26->queue_len - 1) * sizeof(u32)); + + ret = cs40l26_fw_write(dsp, "CALL_RAM_INIT", dsp->fw_id, 1); + if (ret) + return ret; + + ret = cs40l26_wseq_init(cs40l26); + if (ret) + return ret; + + if (cs40l26->variant->handle_errata) + return cs40l26->variant->handle_errata(cs40l26); + else + return 0; +} + +static int cs40l26_cs_dsp_post_run(struct cs_dsp *dsp) +{ + struct cs40l26 *cs40l26 = container_of(dsp, struct cs40l26, dsp); + u32 halo_state; + int ret; + + /* + * cs_dsp_halo_start_core() has reset the DSP core at this point. + * Hibernation must be disabled again. + */ + ret = cs40l26_prevent_hiber(cs40l26); + if (ret) + return ret; + + ret = cs40l26_fw_read(dsp, "HALO_STATE", dsp->fw_id, &halo_state); + if (ret) + return ret; + + if (halo_state != CS40L26_DSP_HALO_STATE_RUN) { + dev_err(dsp->dev, "Invalid DSP state: %u\n", halo_state); + return -EINVAL; + } + + ret = cs40l26_hw_init(cs40l26); + if (ret) + return ret; + + dev_dbg(dsp->dev, "CS40L26/L27 DSP started successfully\n"); + + ret = devm_mfd_add_devices(cs40l26->dev, PLATFORM_DEVID_AUTO, cs40l26_devs, + ARRAY_SIZE(cs40l26_devs), NULL, 0, NULL); + if (ret) + dev_err(cs40l26->dev, "Failed to add MFD child devices: %d\n", ret); + + return ret; +} + +static const struct cs_dsp_client_ops cs40l26_cs_dsp_client_ops = { + .pre_run = cs40l26_cs_dsp_pre_run, + .post_run = cs40l26_cs_dsp_post_run, +}; + +static void cs40l26_cs_dsp_remove(void *data) +{ + cs_dsp_remove((struct cs_dsp *)data); +} + +static struct cs_dsp_coeff_desc cs40l26_coeffs[] = { + { .coeff_firmware = NULL, .coeff_filename = "cs40l26.bin" }, + { .coeff_firmware = NULL, .coeff_filename = "cs40l26-svc.bin" }, + { .coeff_firmware = NULL, .coeff_filename = "cs40l26-dvl.bin" }, +}; + +static int cs40l26_cs_dsp_init(struct cs40l26 *cs40l26) +{ + struct cs_dsp *dsp = &cs40l26->dsp; + int ret; + + dsp->num = 1; + dsp->type = WMFW_HALO; + dsp->dev = cs40l26->dev; + dsp->regmap = cs40l26->regmap; + dsp->base = CS40L26_DSP_CTRL_BASE; + dsp->base_sysinfo = CS40L26_DSP1_SYS_INFO_ID; + dsp->mem = cs40l26_dsp_regions; + dsp->num_mems = ARRAY_SIZE(cs40l26_dsp_regions); + dsp->client_ops = &cs40l26_cs_dsp_client_ops; + + ret = cs_dsp_halo_init(dsp); + if (ret) { + dev_err(cs40l26->dev, "Failed to initialize HALO core\n"); + return ret; + } + + return devm_add_action_or_reset(cs40l26->dev, cs40l26_cs_dsp_remove, dsp); +} + +static void cs40l26_dsp_start(struct cs40l26 *cs40l26) +{ + int i, ret; + + ret = cs40l26_dsp_pre_config(cs40l26); + if (ret) { + dev_err(cs40l26->dev, "DSP Pre Config. Failed: %d\n", ret); + goto err_fw_rls; + } + + guard(mutex)(&cs40l26->lock); + + ret = cs_dsp_power_up_multiple(&cs40l26->dsp, cs40l26->wmfw, "cs40l26.wmfw", cs40l26_coeffs, + CS40L26_NUM_COEFF_FILES, "cs40l26"); + if (ret) { + dev_err(cs40l26->dev, "Failed to Power Up DSP\n"); + goto err_fw_rls; + } + + if (cs40l26->dsp.fw_id != CS40L26_FW_ID) { + dev_err(cs40l26->dev, "Invalid firmware ID: 0x%X\n", cs40l26->dsp.fw_id); + goto err_fw_rls; + } + + if (cs40l26->dsp.fw_id_version < cs40l26->variant->info->fw_min_rev) { + dev_err(cs40l26->dev, "Invalid firmware revision: 0x%X\n", + cs40l26->dsp.fw_id_version); + goto err_fw_rls; + } + + ret = cs_dsp_run(&cs40l26->dsp); + if (ret) + dev_err(cs40l26->dev, "DSP Failed to run: %d\n", ret); + +err_fw_rls: + for (i = 0; i < CS40L26_NUM_COEFF_FILES; i++) + release_firmware(cs40l26_coeffs[i].coeff_firmware); + + release_firmware(cs40l26->wmfw); +} + +static void cs40l26_fw_upload(const struct firmware *wmfw, void *context) +{ + struct cs40l26 *cs40l26 = (struct cs40l26 *)context; + const struct firmware *coeff; + int i, ret; + + if (!wmfw) { + dev_err(cs40l26->dev, "Failed to request firmware file\n"); + return; + } + + cs40l26->wmfw = wmfw; + + for (i = 0; i < CS40L26_NUM_COEFF_FILES; i++) { + ret = request_firmware(&coeff, cs40l26_coeffs[i].coeff_filename, cs40l26->dev); + if (ret) + continue; + + cs40l26_coeffs[i].coeff_firmware = coeff; + } + + return cs40l26_dsp_start(cs40l26); +} + +static int cs40l26_init(struct cs40l26 *cs40l26) +{ + int ret; + + cs40l26->bst_ipk_ua = CS40L26_BST_IPK_UA_DEFAULT; + cs40l26->vbst_uv = CS40L26_BST_UV_MAX; + /* + * Set the PLL to open-loop and remove default GPI mappings to prevent DSP lockup while + * the driver configures RAM firmware. + * + * The firmware will set the PLL back to closed-loop when the DSP has been started. + */ + ret = cs40l26_set_pll_loop(cs40l26, CS40L26_PLL_OPEN); + if (ret) + return ret; + + ret = regmap_write(cs40l26->regmap, cs40l26->variant->info->event_map_1, + CS40L26_EVENT_MAP_GPI_DISABLE); + if (ret) + return ret; + + ret = regmap_write(cs40l26->regmap, cs40l26->variant->info->event_map_2, + CS40L26_EVENT_MAP_GPI_DISABLE); + if (ret) + return ret; + + /* Set LRA to HI-Z to avoid fault conditions */ + return regmap_set_bits(cs40l26->regmap, CS40L26_TST_DAC_MSM_CONFIG, + CS40L26_SPK_DEFAULT_HIZ); +} + +static int cs40l26_parse_properties(struct cs40l26 *cs40l26) +{ + struct device *dev = cs40l26->dev; + int ret; + + ret = device_property_read_u32(dev, "cirrus,bst-ctl-microvolt", &cs40l26->vbst_uv); + if (ret && ret != -EINVAL) + return ret; + + ret = device_property_read_u32(dev, "cirrus,bst-ipk-microamp", &cs40l26->bst_ipk_ua); + if (ret && ret != -EINVAL) + return ret; + + return 0; +} + +int cs40l26_probe(struct cs40l26 *cs40l26) +{ + int ret; + + mutex_init(&cs40l26->lock); + + cs40l26->reset_gpio = devm_gpiod_get_optional(cs40l26->dev, "reset", GPIOD_OUT_HIGH); + if (!cs40l26->reset_gpio) + return dev_err_probe(cs40l26->dev, -EINVAL, "Failed to get reset GPIO\n"); + + ret = devm_regulator_bulk_get_enable(cs40l26->dev, ARRAY_SIZE(cs40l26_supplies), + cs40l26_supplies); + if (ret) + return dev_err_probe(cs40l26->dev, ret, "Failed to get supplies\n"); + + usleep_range(CS40L26_MIN_RESET_PULSE_US, CS40L26_MIN_RESET_PULSE_US + 100); + + gpiod_set_value_cansleep(cs40l26->reset_gpio, 0); + + usleep_range(CS40L26_CP_READY_DELAY_US, CS40L26_CP_READY_DELAY_US + 100); + + ret = cs40l26_get_model(cs40l26); + if (ret) + return dev_err_probe(cs40l26->dev, ret, "Failed to get part number\n"); + + ret = cs40l26_prevent_hiber(cs40l26); + if (ret) + return dev_err_probe(cs40l26->dev, ret, "Failed to prevent hibernation\n"); + + ret = cs40l26_init(cs40l26); + if (ret) + return dev_err_probe(cs40l26->dev, ret, "Failed to initialize device\n"); + + ret = cs40l26_parse_properties(cs40l26); + if (ret) + return dev_err_probe(cs40l26->dev, ret, "Failed to parse devicetree\n"); + + ret = cs40l26_cs_dsp_init(cs40l26); + if (ret) + return dev_err_probe(cs40l26->dev, ret, "Failed to initialize CS DSP\n"); + + ret = cs40l26_pm_runtime_setup(cs40l26->dev); + if (ret) + return dev_err_probe(cs40l26->dev, ret, "Failed to set up PM Runtime\n"); + + ret = request_firmware_nowait(THIS_MODULE, FW_ACTION_UEVENT, "cs40l26.wmfw", cs40l26->dev, + GFP_KERNEL, cs40l26, cs40l26_fw_upload); + if (ret) + return dev_err_probe(cs40l26->dev, ret, "Failed to load firmware\n"); + + cs40l26_pm_exit(cs40l26->dev); + + return 0; +} +EXPORT_SYMBOL_GPL(cs40l26_probe); + +static int __maybe_unused cs40l26_suspend(struct device *dev) +{ + struct cs40l26 *cs40l26 = dev_get_drvdata(dev); + + guard(mutex)(&cs40l26->lock); + + dev_dbg(dev, "%s: Enabling hibernation\n", __func__); + + cs40l26->wksrc_sts = 0x00; + + /* Don't poll DSP since reading for ACK will wake the device again */ + return regmap_write(cs40l26->regmap, CS40L26_DSP_QUEUE, CS40L26_DSP_CMD_ALLOW_HIBER); +} + +static int __maybe_unused cs40l26_sys_suspend(struct device *dev) +{ + struct cs40l26 *cs40l26 = dev_get_drvdata(dev); + + dev_dbg(dev, "System suspend, disabling IRQ\n"); + + disable_irq(cs40l26->irq); + + return 0; +} + +static int __maybe_unused cs40l26_sys_suspend_noirq(struct device *dev) +{ + struct cs40l26 *cs40l26 = dev_get_drvdata(dev); + + dev_dbg(dev, "Late system suspend, re-enabling IRQ\n"); + + enable_irq(cs40l26->irq); + + return 0; +} + +static int __maybe_unused cs40l26_resume(struct device *dev) +{ + struct cs40l26 *cs40l26 = dev_get_drvdata(dev); + + dev_dbg(dev, "%s: Disabling hibernation\n", __func__); + + guard(mutex)(&cs40l26->dsp.pwr_lock); + + return cs40l26_prevent_hiber(cs40l26); +} + +static int __maybe_unused cs40l26_sys_resume(struct device *dev) +{ + struct cs40l26 *cs40l26 = dev_get_drvdata(dev); + + dev_dbg(dev, "System resume, re-enabling IRQ\n"); + + enable_irq(cs40l26->irq); + + return 0; +} + +static int __maybe_unused cs40l26_sys_resume_noirq(struct device *dev) +{ + struct cs40l26 *cs40l26 = dev_get_drvdata(dev); + + dev_dbg(dev, "Early system resume, disabling IRQ\n"); + + disable_irq(cs40l26->irq); + + return 0; +} + +EXPORT_GPL_DEV_PM_OPS(cs40l26_pm_ops) = { + RUNTIME_PM_OPS(cs40l26_suspend, cs40l26_resume, NULL) + SYSTEM_SLEEP_PM_OPS(cs40l26_sys_suspend, cs40l26_sys_resume) + NOIRQ_SYSTEM_SLEEP_PM_OPS(cs40l26_sys_suspend_noirq, cs40l26_sys_resume_noirq) +}; + +MODULE_DESCRIPTION("CS40L26 Boosted Class D Amplifier for Haptics"); +MODULE_AUTHOR("Fred Treven, Cirrus Logic Inc. <ftreven@xxxxxxxxxxxxxxxxxxxxx>"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("FW_CS_DSP"); diff --git a/drivers/mfd/cs40l26-i2c.c b/drivers/mfd/cs40l26-i2c.c new file mode 100644 index 000000000000..c6e4118775a2 --- /dev/null +++ b/drivers/mfd/cs40l26-i2c.c @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * CS40L26 Boosted Haptic Driver with Integrated DSP and + * Waveform Memory with Advanced Closed Loop Algorithms and LRA protection + * + * Copyright 2025 Cirrus Logic, Inc. + * + * Author: Fred Treven <ftreven@xxxxxxxxxxxxxxxxxxxxx> + */ + +#include <linux/i2c.h> +#include <linux/mfd/cs40l26.h> + +static int cs40l26_i2c_probe(struct i2c_client *i2c) +{ + struct cs40l26 *cs40l26; + + cs40l26 = devm_kzalloc(&i2c->dev, sizeof(struct cs40l26), GFP_KERNEL); + if (!cs40l26) + return -ENOMEM; + + i2c_set_clientdata(i2c, cs40l26); + + cs40l26->dev = &i2c->dev; + cs40l26->irq = i2c->irq; + cs40l26->bus = &i2c_bus_type; + + cs40l26->regmap = devm_regmap_init_i2c(i2c, &cs40l26_regmap); + if (IS_ERR(cs40l26->regmap)) + return dev_err_probe(cs40l26->dev, PTR_ERR(cs40l26->regmap), + "Failed to allocate register map\n"); + + return cs40l26_probe(cs40l26); +} + +static const struct i2c_device_id cs40l26_id_i2c[] = { + { "cs40l26a", 0 }, + { "cs40l27b", 1 }, + {} +}; +MODULE_DEVICE_TABLE(i2c, cs40l26_id_i2c); + +static const struct of_device_id cs40l26_of_match[] = { + { .compatible = "cirrus,cs40l26a" }, + { .compatible = "cirrus,cs40l27b" }, + {} +}; +MODULE_DEVICE_TABLE(of, cs40l26_of_match); + +static struct i2c_driver cs40l26_i2c_driver = { + .driver = { + .name = "cs40l26", + .of_match_table = cs40l26_of_match, + .pm = pm_ptr(&cs40l26_pm_ops), + }, + .id_table = cs40l26_id_i2c, + .probe = cs40l26_i2c_probe, +}; +module_i2c_driver(cs40l26_i2c_driver); + +MODULE_DESCRIPTION("CS40L26 I2C Driver"); +MODULE_AUTHOR("Fred Treven, Cirrus Logic Inc. <ftreven@xxxxxxxxxxxxxxxxxxxxx>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/mfd/cs40l26-spi.c b/drivers/mfd/cs40l26-spi.c new file mode 100644 index 000000000000..57fc92356d9d --- /dev/null +++ b/drivers/mfd/cs40l26-spi.c @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * CS40L26 Boosted Haptic Driver with Integrated DSP and + * Waveform Memory with Advanced Closed Loop Algorithms and LRA protection + * + * Copyright 2025 Cirrus Logic, Inc. + * + * Author: Fred Treven <ftreven@xxxxxxxxxxxxxxxxxxxxx> + */ + +#include <linux/mfd/cs40l26.h> +#include <linux/spi/spi.h> + +static int cs40l26_spi_probe(struct spi_device *spi) +{ + struct cs40l26 *cs40l26; + + cs40l26 = devm_kzalloc(&spi->dev, sizeof(struct cs40l26), GFP_KERNEL); + if (!cs40l26) + return -ENOMEM; + + spi_set_drvdata(spi, cs40l26); + + cs40l26->dev = &spi->dev; + cs40l26->irq = spi->irq; + cs40l26->bus = &spi_bus_type; + + cs40l26->regmap = devm_regmap_init_spi(spi, &cs40l26_regmap); + if (IS_ERR(cs40l26->regmap)) + return dev_err_probe(cs40l26->dev, PTR_ERR(cs40l26->regmap), + "Failed to allocate register map\n"); + + return cs40l26_probe(cs40l26); +} + +static const struct spi_device_id cs40l26_id_spi[] = { + { "cs40l26a", 0 }, + { "cs40l27b", 1 }, + {} +}; +MODULE_DEVICE_TABLE(spi, cs40l26_id_spi); + +static const struct of_device_id cs40l26_of_match[] = { + { .compatible = "cirrus,cs40l26a" }, + { .compatible = "cirrus,cs40l27b" }, + {} +}; +MODULE_DEVICE_TABLE(of, cs40l26_of_match); + +static struct spi_driver cs40l26_spi_driver = { + .driver = { + .name = "cs40l26", + .of_match_table = cs40l26_of_match, + .pm = pm_ptr(&cs40l26_pm_ops), + }, + .id_table = cs40l26_id_spi, + .probe = cs40l26_spi_probe, +}; +module_spi_driver(cs40l26_spi_driver); + +MODULE_DESCRIPTION("CS40L26 SPI Driver"); +MODULE_AUTHOR("Fred Treven, Cirrus Logic Inc. <ftreven@xxxxxxxxxxxxxxxxxxxxx>"); +MODULE_LICENSE("GPL"); diff --git a/include/linux/mfd/cs40l26.h b/include/linux/mfd/cs40l26.h new file mode 100644 index 000000000000..c0647c09e24d --- /dev/null +++ b/include/linux/mfd/cs40l26.h @@ -0,0 +1,341 @@ +/* SPDX-License-Identifier: GPL-2.0 + * + * CS40L26 Boosted Haptic Driver with Integrated DSP and + * Waveform Memory with Advanced Closed Loop Algorithms and LRA protection + * + * Copyright 2025 Cirrus Logic, Inc. + * + * Author: Fred Treven <ftreven@xxxxxxxxxxxxxxxxxxxxx> + */ + +#ifndef __MFD_CS40L26_H__ +#define __MFD_CS40L26_H__ + +#include <linux/bitops.h> +#include <linux/firmware/cirrus/cs_dsp.h> +#include <linux/firmware/cirrus/wmfw.h> +#include <linux/gpio/consumer.h> +#include <linux/interrupt.h> +#include <linux/platform_device.h> +#include <linux/pm_runtime.h> +#include <linux/regmap.h> + +/* Register Addresses */ +#define CS40L26_LASTREG 0x3C7DFE8 +#define CS40L26_DEVID 0x0 +#define CS40L26_REVID 0x4 +#define CS40L26_GLOBAL_ENABLES 0x2014 +#define CS40L26_ERROR_RELEASE 0x2034 +#define CS40L26_PWRMGT_CTL 0x2900 +#define CS40L26_WAKESRC_CTL 0x2904 +#define CS40L26_PWRMGT_STS 0x290C +#define CS40L26_REFCLK_INPUT 0x2C04 +#define CS40L26_PLL_REFCLK_DETECT_0 0x2C28 +#define CS40L26_VBST_CTL_1 0x3800 +#define CS40L26_VBST_CTL_2 0x3804 +#define CS40L26_BST_IPK_CTL 0x3808 +#define CS40L26_TEST_LBST 0x391C +#define CS40L26_DAC_MSM_CONFIG 0x7400 +#define CS40L26_TST_DAC_MSM_CONFIG 0x7404 +#define CS40L26_IRQ1_STATUS 0x10004 +#define CS40L26_IRQ1_EINT_1 0x10010 +#define CS40L26_IRQ1_EINT_2 0x10014 +#define CS40L26_IRQ1_MASK_1 0x10110 +#define CS40L26_IRQ1_MASK_2 0x10114 +#define CS40L26_DSP_QUEUE 0x13020 +#define CS40L26_DSP1_XMEM_PACKED_0 0x2000000 +#define CS40L26_DSP1_SYS_INFO_ID 0x25E0000 +#define CS40L26_DSP1_XMEM_UNPACKED24_0 0x2800000 +#define CS40L26_DSP1_CCM_CORE_CONTROL 0x2BC1000 +#define CS40L26_DSP1_YMEM_PACKED_0 0x2C00000 +#define CS40L26_DSP1_YMEM_UNPACKED32_0 0x3000000 +#define CS40L26_DSP1_YMEM_UNPACKED24_0 0x3400000 +#define CS40L26_DSP1_PMEM_0 0x3800000 + +/* Device */ +#define CS40L26_DEVID_L26 0x40A260 +#define CS40L26_DEVID_L27 0x40A270 +#define CS40L26_REVID_A1 0xA1 +#define CS40L26_REVID_B1 0xB1 +#define CS40L26_REVID_B2 0xB2 +#define CS40L26_MIN_RESET_PULSE_US 1500 +#define CS40L26_CP_READY_DELAY_US 6000 +#define CS40L26_SPK_DEFAULT_HIZ BIT(28) +#define CS40L26_DSP_CCM_CORE_KILL 0x00000080 +#define CS40L26_MEM_RDY BIT(1) + +/* Errata */ +#define CS40L26_DISABLE_EXPL_MODE 0x0100C080 + +#define CS40L26_PLL_REFCLK_DET_DISABLE 0x0 + +/* Boost Converter Control */ +#define CS40L26_GLOBAL_EN BIT(0) + +#define CS40L26_BST_IPK_UA_MAX 4800000 +#define CS40L26_BST_IPK_UA_DEFAULT 4500000 +#define CS40L26_BST_IPK_UA_MIN 1600000 +#define CS40L26_BST_IPK_UA_STEP 50000 +#define CS40L26_BST_IPK_UA_OFFSET 800000 + +#define CS40L26_BST_UV_MIN 2500000 +#define CS40L26_BST_UV_MAX 11000000 +#define CS40L26_BST_UV_STEP 50000 + +#define CS40L26_BST_CTL_VP 0x00 +#define CS40L26_BST_CTL_MASK GENMASK(7, 0) +#define CS40L26_BST_CTL_SEL_MASK GENMASK(1, 0) +#define CS40L26_BST_CTL_SEL_FIXED 0x0 +#define CS40L26_BST_CTL_LIM_EN_MASK BIT(2) +#define CS40L26_BST_CTL_LIM_EN 1 + +#define CS40L26_BST_TIME_US 10000 + +/* Phase Locked Loop */ +#define CS40L26_PLL_REFCLK_LOOP_MASK BIT(11) +#define CS40L26_PLL_REFCLK_LOOP_SHIFT 11 +#define CS40L26_PLL_NUM_SET_ATTEMPTS 5 + +/* GPIO */ +#define CS40L26_EVENT_MAP_GPI_DISABLE 0x1FF + +#define CS40L26_A1_B1_EVENT_MAP_1 0x02806FC4 +#define CS40L26_A1_B1_EVENT_MAP_2 0x02806FC8 + +#define CS40L26_B2_EVENT_MAP_1 0x02806FB0 +#define CS40L26_B2_EVENT_MAP_2 0x02806FB4 + +/* Power Management */ +#define CS40L26_PM_STDBY_TICKS_OFFSET 16 +#define CS40L26_PM_ACTIVE_TICKS_OFFSET 24 + +#define CS40L26_A1_B1_PM_CUR_STATE 0x02800370 +#define CS40L26_A1_B1_PM_STATE_LOCKS 0x02800378 +#define CS40L26_A1_B1_PM_STATE_LOCKS3 (CS40L26_A1_B1_PM_STATE_LOCKS + \ + CS40L26_DSP_LOCK3_OFFSET_BYTES) + +#define CS40L26_A1_B1_PM_TIMEOUT_TICKS 0x02800350 +#define CS40L26_A1_B1_PM_STDBY_TICKS (CS40L26_A1_B1_PM_TIMEOUT_TICKS + \ + CS40L26_PM_STDBY_TICKS_OFFSET) +#define CS40L26_A1_B1_PM_ACTIVE_TICKS (CS40L26_A1_B1_PM_TIMEOUT_TICKS + \ + CS40L26_PM_ACTIVE_TICKS_OFFSET) + +#define CS40L26_A1_B1_HALO_STATE 0x02800FA8 + +#define CS40L26_B2_PM_CUR_STATE 0x02801F98 +#define CS40L26_B2_PM_STATE_LOCKS 0x02801FA0 +#define CS40L26_B2_PM_STATE_LOCKS3 (CS40L26_B2_PM_STATE_LOCKS + CS40L26_DSP_LOCK3_OFFSET_BYTES) +#define CS40L26_B2_PM_TIMEOUT_TICKS 0x02801F78 +#define CS40L26_B2_PM_STDBY_TICKS (CS40L26_B2_PM_TIMEOUT_TICKS + \ + CS40L26_PM_STDBY_TICKS_OFFSET) +#define CS40L26_B2_PM_ACTIVE_TICKS (CS40L26_B2_PM_TIMEOUT_TICKS + \ + CS40L26_PM_ACTIVE_TICKS_OFFSET) + +#define CS40L26_B2_HALO_STATE 0x02806AF8 + +#define CS40L26_AUTOSUSPEND_DELAY_MS 2000 +#define CS40L26_PM_TICKS_PER_SEC 32768 + +/* Firmware Handling */ +#define CS40L26_FW_ID 0x1800D4 +#define CS40L26_FW_A1_B1_MIN_REV 0x070247 +#define CS40L26_FW_B2_MIN_REV 0x0A0000 + +#define CS40L26_NUM_COEFF_FILES 3 + +/* Algorithms */ +#define CS40L26_VIBEGEN_ALGO_ID_A1 0x000400BD +#define CS40L26_VIBEGEN_ALGO_ID_B2 0x000A00BD + +#define CS40L26_BUZZGEN_ALGO_ID 0x0004F202 +#define CS40L26_A2H_ALGO_ID 0x00040110 +#define CS40L26_EXT_ALGO_ID 0x0004013C +#define CS40L26_DSP_ALGO_ID 0x0004F203 +#define CS40L26_PM_ALGO_ID 0x0004F206 + +/* DSP */ +#define CS40L26_DSP_LOCK3_OFFSET_BYTES 8 +#define CS40L26_DSP_LOCK3_OFFSET_WORDS (CS40L26_DSP_LOCK3_OFFSET_BYTES / sizeof(u32)) +#define CS40L26_DSP_LOCK3_MASK BIT(1) +#define CS40L26_DSP_HALO_STATE_RUN 2 +#define CS40L26_DSP_CTRL_BASE 0x2B80000 +#define CS40L26_DSP_POLL_US 1000 +#define CS40L26_DSP_TIMEOUT_COUNT 100 +#define CS40L26_PM_LOCKS_TIMEOUT_COUNT 10 +#define CS40L26_DSP_STATE_TIMEOUT_COUNT 10 + +#define CS40L26_DSP_CMD_PREVENT_HIBER 0x02000003 +#define CS40L26_DSP_CMD_ALLOW_HIBER 0x02000004 +#define CS40L26_DSP_CMD_INDEX_MASK GENMASK(28, 24) +#define CS40L26_DSP_CMD_PAYLOAD_MASK GENMASK(23, 0) + +#define CS40L26_DSP_COMPLETE_CP 0x01000000 +#define CS40L26_DSP_COMPLETE_I2S 0x01000002 +#define CS40L26_DSP_TRIGGER_CP 0x01000010 +#define CS40L26_DSP_TRIGGER_I2S 0x01000012 +#define CS40L26_DSP_PM_AWAKE 0x02000002 +#define CS40L26_DSP_SYS_ACK 0x0A000000 +#define CS40L26_DSP_PANIC 0x0C000000 + +/* Wake Sources */ +#define CS40L26_WKSRC_STS_MASK GENMASK(9, 4) +#define CS40L26_WKSRC_STS_SHIFT 4 +#define CS40L26_WKSRC_STS_EN BIT(7) +#define CS40L26_WKSRC_POL_SPI BIT(4) +#define CS40L26_WKSRC_EN_SPI BIT(9) +#define CS40L26_WKSRC_EN_I2C BIT(10) +#define CS40L26_WKSRC_GPIO_POL_MASK GENMASK(3, 0) + +/* Interrupts */ +#define CS40L26_IRQ_GPIO1_RISE 0 +#define CS40L26_IRQ_GPIO1_FALL 1 +#define CS40L26_IRQ_WKSRC_STS_ANY 8 +#define CS40L26_IRQ_WKSRC_STS_GPIO1 9 +#define CS40L26_IRQ_WKSRC_STS_SPI 13 +#define CS40L26_IRQ_WKSRC_STS_I2C 14 +#define CS40L26_IRQ_BST_OVP_FLAG_RISE 18 +#define CS40L26_IRQ_BST_OVP_FLAG_FALL 19 +#define CS40L26_IRQ_BST_OVP_ERR 20 +#define CS40L26_IRQ_BST_DCM_UVP_ERR 21 +#define CS40L26_IRQ_BST_SHORT_ERR 22 +#define CS40L26_IRQ_BST_IPK_FLAG 23 +#define CS40L26_IRQ_TEMP_WARN_RISE 24 +#define CS40L26_IRQ_TEMP_WARN_FALL 25 +#define CS40L26_IRQ_TEMP_ERR 26 +#define CS40L26_IRQ_AMP_ERR 27 +#define CS40L26_IRQ_DSP_RX_QUEUE 31 + +#define CS40L26_IRQ_1_NBITS 32 + +#define CS40L26_IRQ_REFCLK_PRESENT 6 +#define CS40L26_IRQ_REFCLK_MISSING_FALL 7 +#define CS40L26_IRQ_REFCLK_MISSING_RISE 8 +#define CS40L26_IRQ_VPMON_CLIPPED 23 +#define CS40L26_IRQ_VBSTMON_CLIPPED 24 +#define CS40L26_IRQ_VMON_CLIPPED 25 +#define CS40L26_IRQ_IMON_CLIPPED 26 + +#define CS40L26_IRQ_2_NBITS 30 + +#define CS40L26_IRQ_1_ALL_MASKED 0xFFFFFFFF +#define CS40L26_IRQ_2_ALL_MASKED 0x3FFFFFFF + +#define CS40L26_IRQ_STATUS_ASSERT 0x1 + +/* Playback */ +#define CS40L26_STOP_PLAYBACK 0x05000000 + +#define CS40L26_START_I2S 0x03000002 +#define CS40L26_STOP_I2S 0x03000003 + +/* Error Release */ +enum cs40l26_error { + CS40L26_ERROR_NONE, + CS40L26_ERROR_AMP_SHORT, + CS40L26_ERROR_BST_SHORT, + CS40L26_ERROR_BST_OVP, + CS40L26_ERROR_BST_DCM_UVP, + CS40L26_ERROR_TEMP_WARN, + CS40L26_ERROR_TEMP_ERR, +}; + +struct cs40l26_irq { + int virq; + u32 mask; + const char *name; + int (*handler)(void *data); +}; + +#define CS40L26_IRQ(_irq, _name, _hand) \ + { \ + .virq = CS40L26_IRQ_ ## _irq, \ + .mask = BIT(CS40L26_ ## IRQ_ ## _irq), \ + .name = _name, \ + .handler = _hand, \ + } + +enum cs40l26_dsp_state { + CS40L26_DSP_STATE_HIBERNATE, + CS40L26_DSP_STATE_SHUTDOWN, + CS40L26_DSP_STATE_STANDBY, + CS40L26_DSP_STATE_ACTIVE, + CS40L26_DSP_STATE_NONE, +}; + +enum cs40l26_gpio_map { + CS40L26_GPIO_MAP_A_PRESS, + CS40L26_GPIO_MAP_A_RELEASE, + CS40L26_GPIO_MAP_NUM_AVAILABLE, + CS40L26_GPIO_MAP_INVALID, +}; + +enum cs40l26_pll { + CS40L26_PLL_CLOSED, + CS40L26_PLL_OPEN, +}; + +enum cs40l50_wseqs { + CS40L26_WSEQ_POWER_ON, + CS40L26_WSEQ_ACTIVE, + CS40L26_WSEQ_STANDBY, + CS40L26_NUM_WSEQS, +}; + +struct cs40l26_variant_info { + u32 pm_cur_state; + u32 pm_state_locks; + u32 pm_state_locks3; + u32 pm_stdby_ticks; + u32 pm_active_ticks; + u32 halo_state; + u32 event_map_1; + u32 event_map_2; + u32 fw_min_rev; + u32 ram_ext_algo_id; + u32 vibegen_algo_id; +}; + +struct cs40l26_variant; + +struct cs40l26 { + struct device *dev; + struct regmap *regmap; + struct cs_dsp dsp; + int irq; + struct mutex lock; + struct gpio_desc *reset_gpio; + u32 devid; + u32 revid; + const struct cs40l26_variant *variant; + struct cs_dsp_wseq wseqs[CS40L26_NUM_WSEQS]; + u8 wksrc_sts; + u8 last_wksrc_pol; + u32 queue_base; + u32 queue_len; + u32 queue_last; + unsigned int bst_ipk_ua; + unsigned int vbst_uv; + const struct firmware *wmfw; + const struct bus_type *bus; + struct reg_sequence *irq_masks; +}; + +struct cs40l26_variant { + const struct cs40l26_variant_info *info; + int (*handle_errata)(struct cs40l26 *cs40l26); +}; + +inline void cs40l26_pm_exit(struct device *dev); +int cs40l26_probe(struct cs40l26 *cs40l26); +int cs40l26_set_pll_loop(struct cs40l26 *cs40l26, const u32 pll_loop); +int cs40l26_dsp_write(struct cs40l26 *cs40l26, const u32 val); +int cs40l26_dsp_state_get(struct cs40l26 *cs40l26, u32 *state); +inline int cs40l26_fw_write(struct cs_dsp *dsp, const char *const name, + const unsigned int algo_id, u32 val); +inline int cs40l26_fw_read(struct cs_dsp *dsp, const char *const name, + const unsigned int algo_id, u32 *buf); + +extern const struct regmap_config cs40l26_regmap; +extern const struct dev_pm_ops cs40l26_pm_ops; + +#endif /* __CS40L26_H__ */ -- 2.34.1