[PATCH RESEND 0/7] Initial Support for CS40L26

Introduce driver for Cirrus Logic Device CS40L26: A boosted haptics driver with integrated DSP and waveform memory with advanced closed loop algorithms and LRA protection.
The core CS40L26 driver is in MFD and touches the Input Force Feedback subsystem for haptics and the ASoC subsystem for audio to haptics streaming.
This patchset includes changes to the CS DSP firmware driver which fixes two bugs and introduces support for multiple coefficient files.
Fred Treven (7): firmware: cs_dsp: Fix error checking in wseq_write() firmware: cs_dsp: Check for valid num_regs in cs_dsp_wseq_multi_write() firmware: cs_dsp: Add ability to load multiple coefficient files dt-bindings: mfd: cirrus,cs40l26: Support for CS40L26 mfd: cs40l26: Add support for CS40L26 core driver ASoC: cs40l26: Support I2S streaming to CS40L26 Input: cs40l26 - Add support for CS40L26 haptic driver
.../bindings/mfd/cirrus,cs40l26.yaml | 81 + MAINTAINERS | 4 +- drivers/firmware/cirrus/cs_dsp.c | 70 +- drivers/input/misc/Kconfig | 10 + drivers/input/misc/Makefile | 1 + drivers/input/misc/cs40l26-vibra.c | 669 ++++++++ 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/firmware/cirrus/cs_dsp.h | 14 + include/linux/mfd/cs40l26.h | 341 ++++ sound/soc/codecs/Kconfig | 12 + sound/soc/codecs/Makefile | 2 + sound/soc/codecs/cs40l26-codec.c | 523 ++++++ 16 files changed, 3281 insertions(+), 17 deletions(-) create mode 100644 Documentation/devicetree/bindings/mfd/cirrus,cs40l26.yaml create mode 100644 drivers/input/misc/cs40l26-vibra.c 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 create mode 100644 sound/soc/codecs/cs40l26-codec.c

cs_dsp_coeff_write_ctrl() may return a non-zero value (1) upon success. Change error checking in the write sequencer code such that it checks for negative errnos rather than any non-zero value when using this function.
Signed-off-by: Fred Treven ftreven@opensource.cirrus.com --- drivers/firmware/cirrus/cs_dsp.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/drivers/firmware/cirrus/cs_dsp.c b/drivers/firmware/cirrus/cs_dsp.c index 5365e9a43000..56315b0b5583 100644 --- a/drivers/firmware/cirrus/cs_dsp.c +++ b/drivers/firmware/cirrus/cs_dsp.c @@ -3702,7 +3702,7 @@ int cs_dsp_wseq_write(struct cs_dsp *dsp, struct cs_dsp_wseq *wseq,
ret = cs_dsp_coeff_write_ctrl(wseq->ctl, op_end->offset / sizeof(u32), &op_end->data, sizeof(u32)); - if (ret) + if (ret < 0) goto op_new_free;
list_add_tail(&op_new->list, &op_end->list); @@ -3710,7 +3710,7 @@ int cs_dsp_wseq_write(struct cs_dsp *dsp, struct cs_dsp_wseq *wseq,
ret = cs_dsp_coeff_write_ctrl(wseq->ctl, op_new->offset / sizeof(u32), words, new_op_size); - if (ret) + if (ret < 0) goto op_new_free;
return 0;

If a value of 0 or below is passed into cs_dsp_wseq_multi_write() the function will never enter its for loop.
Verify that num_regs passed into the function is valid and throw a user-visible error if not.
Signed-off-by: Fred Treven ftreven@opensource.cirrus.com --- drivers/firmware/cirrus/cs_dsp.c | 5 +++++ 1 file changed, 5 insertions(+)
diff --git a/drivers/firmware/cirrus/cs_dsp.c b/drivers/firmware/cirrus/cs_dsp.c index 56315b0b5583..aacf6960d1ea 100644 --- a/drivers/firmware/cirrus/cs_dsp.c +++ b/drivers/firmware/cirrus/cs_dsp.c @@ -3743,6 +3743,11 @@ int cs_dsp_wseq_multi_write(struct cs_dsp *dsp, struct cs_dsp_wseq *wseq, { int i, ret;
+ if (num_regs <= 0) { + cs_dsp_err(dsp, "Invalid number of regs: %d\n", num_regs); + return -EINVAL; + } + for (i = 0; i < num_regs; i++) { ret = cs_dsp_wseq_write(dsp, wseq, reg_seq[i].reg, reg_seq[i].def, op_code, update);

Add cs_dsp_power_up_multiple() which accepts an array of cs_dsp_coeff_desc firmware-filename pairs to load.
This enables the user to load more than one tuning file along with the associated firmware.
Change cs_dsp_power_up() to make use of the new function with a single coefficient file.
Signed-off-by: Fred Treven ftreven@opensource.cirrus.com --- drivers/firmware/cirrus/cs_dsp.c | 61 ++++++++++++++++++++------ include/linux/firmware/cirrus/cs_dsp.h | 14 ++++++ 2 files changed, 62 insertions(+), 13 deletions(-)
diff --git a/drivers/firmware/cirrus/cs_dsp.c b/drivers/firmware/cirrus/cs_dsp.c index aacf6960d1ea..68563186637e 100644 --- a/drivers/firmware/cirrus/cs_dsp.c +++ b/drivers/firmware/cirrus/cs_dsp.c @@ -2695,28 +2695,29 @@ static void cs_dsp_halo_stop_watchdog(struct cs_dsp *dsp) }
/** - * cs_dsp_power_up() - Downloads firmware to the DSP - * @dsp: pointer to DSP structure + * cs_dsp_power_up_multiple() - Downloads firmware and multiple coefficient files to the DSP + * @dsp: pointer to the DSP structure * @wmfw_firmware: the firmware to be sent * @wmfw_filename: file name of firmware to be sent - * @coeff_firmware: the coefficient data to be sent - * @coeff_filename: file name of coefficient to data be sent + * @coeffs: coefficient data and filename pairs to be sent + * @num_coeffs: number of coefficient files to be sent * @fw_name: the user-friendly firmware name * * This function is used on ADSP2 and Halo DSP cores, it powers-up the DSP core * and downloads the firmware but does not start the firmware running. The * cs_dsp booted flag will be set once completed and if the core has a low-power * memory retention mode it will be put into this state after the firmware is - * downloaded. + * downloaded. Differs from cs_dsp_power_up() in that it allows for multiple + * coefficient files to be downloaded. * * Return: Zero for success, a negative number on error. */ -int cs_dsp_power_up(struct cs_dsp *dsp, - const struct firmware *wmfw_firmware, const char *wmfw_filename, - const struct firmware *coeff_firmware, const char *coeff_filename, - const char *fw_name) +int cs_dsp_power_up_multiple(struct cs_dsp *dsp, + const struct firmware *wmfw_firmware, const char *wmfw_filename, + struct cs_dsp_coeff_desc *coeffs, int num_coeffs, + const char *fw_name) { - int ret; + int i, ret;
mutex_lock(&dsp->pwr_lock);
@@ -2742,9 +2743,12 @@ int cs_dsp_power_up(struct cs_dsp *dsp, if (ret != 0) goto err_ena;
- ret = cs_dsp_load_coeff(dsp, coeff_firmware, coeff_filename); - if (ret != 0) - goto err_ena; + for (i = 0; i < num_coeffs; i++) { + ret = cs_dsp_load_coeff(dsp, coeffs[i].coeff_firmware, + coeffs[i].coeff_filename); + if (ret != 0) + goto err_ena; + }
/* Initialize caches for enabled and unset controls */ ret = cs_dsp_coeff_init_control_caches(dsp); @@ -2770,6 +2774,37 @@ int cs_dsp_power_up(struct cs_dsp *dsp,
return ret; } +EXPORT_SYMBOL_NS_GPL(cs_dsp_power_up_multiple, "FW_CS_DSP"); + +/** + * cs_dsp_power_up() - Downloads firmware to the DSP + * @dsp: pointer to DSP structure + * @wmfw_firmware: the firmware to be sent + * @wmfw_filename: file name of firmware to be sent + * @coeff_firmware: the coefficient data to be sent + * @coeff_filename: file name of coefficient to data be sent + * @fw_name: the user-friendly firmware name + * + * This function is used on ADSP2 and Halo DSP cores, it powers-up the DSP core + * and downloads the firmware but does not start the firmware running. The + * cs_dsp booted flag will be set once completed and if the core has a low-power + * memory retention mode it will be put into this state after the firmware is + * downloaded. + * + * Return: Zero for success, a negative number on error. + */ +int cs_dsp_power_up(struct cs_dsp *dsp, + const struct firmware *wmfw_firmware, const char *wmfw_filename, + const struct firmware *coeff_firmware, const char *coeff_filename, + const char *fw_name) +{ + struct cs_dsp_coeff_desc coeff_desc; + + coeff_desc.coeff_firmware = coeff_firmware; + coeff_desc.coeff_filename = coeff_filename; + + return cs_dsp_power_up_multiple(dsp, wmfw_firmware, wmfw_filename, &coeff_desc, 1, fw_name); +} EXPORT_SYMBOL_NS_GPL(cs_dsp_power_up, "FW_CS_DSP");
/** diff --git a/include/linux/firmware/cirrus/cs_dsp.h b/include/linux/firmware/cirrus/cs_dsp.h index 7cae703b3137..4c4e746be6fa 100644 --- a/include/linux/firmware/cirrus/cs_dsp.h +++ b/include/linux/firmware/cirrus/cs_dsp.h @@ -52,6 +52,16 @@ #define CS_DSP_WSEQ_UNLOCK 0xFD #define CS_DSP_WSEQ_END 0xFF
+/** + * struct cs_dsp_coeff_desc - Describes a coeff. file + filename pair + * @coeff_firmware: Firmware struct to populate with coeff. data + * @coeff_filename: File from which coeff. data is loaded + */ +struct cs_dsp_coeff_desc { + const struct firmware *coeff_firmware; + const char *coeff_filename; +}; + /** * struct cs_dsp_region - Describes a logical memory region in DSP address space * @type: Memory region type @@ -227,6 +237,10 @@ int cs_dsp_adsp1_power_up(struct cs_dsp *dsp, const struct firmware *coeff_firmware, const char *coeff_filename, const char *fw_name); void cs_dsp_adsp1_power_down(struct cs_dsp *dsp); +int cs_dsp_power_up_multiple(struct cs_dsp *dsp, + const struct firmware *wmfw_firmware, const char *wmfw_filename, + struct cs_dsp_coeff_desc *coeffs, int num_coeffs, + const char *fw_name); int cs_dsp_power_up(struct cs_dsp *dsp, const struct firmware *wmfw_firmware, const char *wmfw_filename, const struct firmware *coeff_firmware, const char *coeff_filename,

Introduce required basic devicetree parameters for the initial commit of CS40L26.
Signed-off-by: Fred Treven ftreven@opensource.cirrus.com --- .../bindings/mfd/cirrus,cs40l26.yaml | 81 +++++++++++++++++++ MAINTAINERS | 4 +- 2 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 Documentation/devicetree/bindings/mfd/cirrus,cs40l26.yaml
diff --git a/Documentation/devicetree/bindings/mfd/cirrus,cs40l26.yaml b/Documentation/devicetree/bindings/mfd/cirrus,cs40l26.yaml new file mode 100644 index 000000000000..a3cccb1a2d92 --- /dev/null +++ b/Documentation/devicetree/bindings/mfd/cirrus,cs40l26.yaml @@ -0,0 +1,81 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/mfd/cirrus,cs40l26.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Cirrus Logic CS40L26 Boosted Haptic Amplifier + +maintainers: + - Fred Treven ftreven@opensource.cirrus.com + - patches@opensource.cirrus.com + +description: + CS40L26 is a Boosted Haptic Driver with Integrated DSP, Waveform Memory, + Advanced Closed Loop Algorithms, and LRA protection + +properties: + compatible: + enum: + - cirrus,cs40l26a + - cirrus,cs40l27b + + reg: + maxItems: 1 + + interrupts: + maxItems: 1 + + reset-gpios: + maxItems: 1 + + va-supply: + description: Regulator for VA analog voltage + + vp-supply: + description: Regulator for VP voltage + + cirrus,bst-ipk-microamp: + description: + Maximum current that can be drawn by the device's boost converter. + multipleOf: 50000 + minimum: 1600000 + maximum: 4800000 + default: 4500000 + + cirrus,bst-ctl-microvolt: + description: Maximum target voltage to which DSP may increase the VBST supply. + multipleOf: 50000 + minimum: 2550000 + maximum: 11000000 + default: 11000000 + +required: + - compatible + - reg + - interrupts + - reset-gpios + +additionalProperties: false + +examples: + - | + #include <dt-bindings/gpio/gpio.h> + #include <dt-bindings/interrupt-controller/irq.h> + + i2c { + #address-cells = <1>; + #size-cells = <0>; + + haptic-driver@58 { + compatible = "cirrus,cs40l26a"; + reg = <0x58>; + interrupt-parent = <&gpio>; + interrupts = <57 IRQ_TYPE_LEVEL_LOW>; + reset-gpios = <&gpio 54 GPIO_ACTIVE_LOW>; + va-supply = <&vreg>; + vp-supply = <&vreg>; + cirrus,bst-ctl-microvolt = <2600000>; + cirrus,bst-ipk-microamp = <1650000>; + }; + }; diff --git a/MAINTAINERS b/MAINTAINERS index bc8ce7af3303..9c4105bf0a32 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -5546,11 +5546,11 @@ F: sound/soc/codecs/cs*
CIRRUS LOGIC HAPTIC DRIVERS M: James Ogletree jogletre@opensource.cirrus.com -M: Fred Treven fred.treven@cirrus.com +M: Fred Treven ftreven@opensource.cirrus.com M: Ben Bright ben.bright@cirrus.com L: patches@opensource.cirrus.com S: Supported -F: Documentation/devicetree/bindings/input/cirrus,cs40l50.yaml +F: Documentation/devicetree/bindings/input/cirrus,cs40l* F: drivers/input/misc/cs40l* F: drivers/mfd/cs40l* F: include/linux/mfd/cs40l*

On Tue, Feb 04, 2025 at 05:18:33PM -0600, Fred Treven wrote:
Introduce required basic devicetree parameters for the initial commit of CS40L26.
Signed-off-by: Fred Treven ftreven@opensource.cirrus.com
.../bindings/mfd/cirrus,cs40l26.yaml | 81 +++++++++++++++++++ MAINTAINERS | 4 +-
I don't understand why you decided to resend the same two days *AFTER* you received review.
No, implement the review you already got. Resending the same in such case is not only unnecessary noise but actually ignores/skips the review.
NAK
Best regards, Krzysztof

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@opensource.cirrus.com --- 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@opensource.cirrus.com + */ + +#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@opensource.cirrus.com"); +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@opensource.cirrus.com + */ + +#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@opensource.cirrus.com"); +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@opensource.cirrus.com + */ + +#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@opensource.cirrus.com"); +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@opensource.cirrus.com + */ + +#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__ */

On 05/02/2025 00:18, Fred Treven wrote:
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.
Please wrap commit message according to Linux coding style / submission process (neither too early nor over the limit): https://elixir.bootlin.com/linux/v6.4-rc1/source/Documentation/process/submi...
+#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)
Exported function and inlined? This feels odd. Anyway, don't use any inline keywords in C units.
+{
- 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);
Looks like an array, so kcalloc
- 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,
All your exported functions should have kerneldoc.
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;
+}
...
+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);
This looks way past simple MFD driver. Not only this - entire file. You configure there quite a lot and for example setting PLLs is not job for MFD. This should be placed in appropriate subsystem.
+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[] = {
This cannto be const?
- { .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;
- }
...
+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__);
Drop. No need to re-implement tracing.
- 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");
Drop.
- 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");
Drop.
- 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__);
Drop.
- 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");
Drop.
- 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");
Drop. ...
+static int cs40l26_spi_probe(struct spi_device *spi) +{
- struct cs40l26 *cs40l26;
- cs40l26 = devm_kzalloc(&spi->dev, sizeof(struct cs40l26), GFP_KERNEL);
sizeof(*)
- 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 },
What are these 0 and 1?
- {}
+}; +MODULE_DEVICE_TABLE(spi, cs40l26_id_spi);
+static const struct of_device_id cs40l26_of_match[] = {
- { .compatible = "cirrus,cs40l26a" },
- { .compatible = "cirrus,cs40l27b" },
So devices are compatible? Or rather this is unsynced with other ID table.
Best regards, Krzysztof

On 2/5/25 04:34, Krzysztof Kozlowski wrote:
On 05/02/2025 00:18, Fred Treven wrote:
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.
Please wrap commit message according to Linux coding style / submission process (neither too early nor over the limit): https://elixir.bootlin.com/linux/v6.4-rc1/source/Documentation/process/submi...
+#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)
Exported function and inlined? This feels odd. Anyway, don't use any inline keywords in C units.
+{
- 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);
Looks like an array, so kcalloc
- 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,
All your exported functions should have kerneldoc.
I'm happy to add this, but I don't know where this directive comes from. Could you share where in the kernel style guide (or elsewhere) this is stated? There are also hundreds of examples in MFD in which exported functions do not have kerneldoc which is why I'm curious.
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;
+}
...
+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);
This looks way past simple MFD driver. Not only this - entire file. You configure there quite a lot and for example setting PLLs is not job for MFD. This should be placed in appropriate subsystem.
I disagree here because the configuration being done in this file is essential to the core operation of the part. For instance, setting the PLL to open-loop here is required to prevent any external interference (e.g. GPIO events) from interrupting the part while loading firmware.
The other hardware configuration being done here is required for both the Input and ASoC operations of the part.
Lastly, these need to be done in order and independently of which child driver (ASoC or input) the user adds. If this is moved to cs40l26-vibra.c (the input driver), for instance, and that module is then not added, it will disturb the required setup for use by the ASoC driver.
I would really like to get Lee's opinion here because it does not make sense to me why this is inappropriate when the configuration done in the core MFD driver is required for use by all of its children.
+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[] = {
This cannto be const?
This cannot be const since coeff_firmware needs to be requested and assigned at the time of .bin file loading. Only the names are available beforehand.
- { .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;
- }
...
+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__);
Drop. No need to re-implement tracing.
- 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");
Drop.
- 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");
Drop.
- 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__);
Drop.
- 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");
Drop.
- 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");
Drop. ...
+static int cs40l26_spi_probe(struct spi_device *spi) +{
- struct cs40l26 *cs40l26;
- cs40l26 = devm_kzalloc(&spi->dev, sizeof(struct cs40l26), GFP_KERNEL);
sizeof(*)
- 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 },
What are these 0 and 1?
I will make it clear that these are enumerating the different possible device variants.
- {}
+}; +MODULE_DEVICE_TABLE(spi, cs40l26_id_spi);
+static const struct of_device_id cs40l26_of_match[] = {
- { .compatible = "cirrus,cs40l26a" },
- { .compatible = "cirrus,cs40l27b" },
So devices are compatible? Or rather this is unsynced with other ID table.
I'm not sure what you mean by this.
Best regards, Krzysztof
Thanks for your review. I will include the agreed upon fixes in v2.
Best regards, Fred

On 11/02/2025 22:16, Fred Treven wrote:
- 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,
All your exported functions should have kerneldoc.
I'm happy to add this, but I don't know where this directive comes from. Could you share where in the kernel style guide (or elsewhere) this is stated? There are also hundreds of examples in MFD in which exported functions do not have kerneldoc which is why I'm curious.
You are not looking hard enough. It's explicitly mentioned in kernel doc documentation.
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;
+}
...
+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);
This looks way past simple MFD driver. Not only this - entire file. You configure there quite a lot and for example setting PLLs is not job for MFD. This should be placed in appropriate subsystem.
I disagree here because the configuration being done in this file is essential to the core operation of the part. For instance, setting the PLL to open-loop here is required to prevent any external interference (e.g. GPIO events) from interrupting the part while loading firmware.
The other hardware configuration being done here is required for both the Input and ASoC operations of the part.
Lastly, these need to be done in order and independently of which child driver (ASoC or input) the user adds. If this is moved to cs40l26-vibra.c (the input driver), for instance, and that module is then not added, it will disturb the required setup for use by the ASoC driver.
I would really like to get Lee's opinion here because it does not make sense to me why this is inappropriate when the configuration done in the core MFD driver is required for use by all of its children.
Sure.
...
- {}
+}; +MODULE_DEVICE_TABLE(spi, cs40l26_id_spi);
+static const struct of_device_id cs40l26_of_match[] = {
- { .compatible = "cirrus,cs40l26a" },
- { .compatible = "cirrus,cs40l27b" },
So devices are compatible? Or rather this is unsynced with other ID table.
I'm not sure what you mean by this.
Lack of driver data means devices are compatible or some sort of other problem (e.g. ID tables not being in sync, because they are supposed to be always in sync). Choose, but it is almost never correct. Either correct the issue or mention why exception is justified.
Best regards, Krzysztof

On 11/02/2025 9:16 pm, Fred Treven wrote:
On 2/5/25 04:34, Krzysztof Kozlowski wrote:
On 05/02/2025 00:18, Fred Treven wrote:
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.
<SNIP>
+static const struct spi_device_id cs40l26_id_spi[] = { + { "cs40l26a", 0 }, + { "cs40l27b", 1 },
What are these 0 and 1?
I will make it clear that these are enumerating the different possible device variants.
+ {} +}; +MODULE_DEVICE_TABLE(spi, cs40l26_id_spi);
+static const struct of_device_id cs40l26_of_match[] = { + { .compatible = "cirrus,cs40l26a" }, + { .compatible = "cirrus,cs40l27b" },
So devices are compatible? Or rather this is unsynced with other ID table.
I'm not sure what you mean by this.
cs40l26_id_spi[] has the 0/1 cookie values to indicate which part variant is being instantiated. But cs40l26_of_match[] doesn't have these cookie values to indicate which part ID was matched.

On Tue, 11 Feb 2025, Fred Treven wrote:
On 2/5/25 04:34, Krzysztof Kozlowski wrote:
On 05/02/2025 00:18, Fred Treven wrote:
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.
Please wrap commit message according to Linux coding style / submission process (neither too early nor over the limit): https://elixir.bootlin.com/linux/v6.4-rc1/source/Documentation/process/submi...
+#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)
Exported function and inlined? This feels odd. Anyway, don't use any inline keywords in C units.
+{
- 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);
Looks like an array, so kcalloc
- 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,
All your exported functions should have kerneldoc.
I'm happy to add this, but I don't know where this directive comes from. Could you share where in the kernel style guide (or elsewhere) this is stated? There are also hundreds of examples in MFD in which exported functions do not have kerneldoc which is why I'm curious.
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;
+}
...
+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);
This looks way past simple MFD driver. Not only this - entire file. You configure there quite a lot and for example setting PLLs is not job for MFD. This should be placed in appropriate subsystem.
I disagree here because the configuration being done in this file is essential to the core operation of the part. For instance, setting the PLL to open-loop here is required to prevent any external interference (e.g. GPIO events) from interrupting the part while loading firmware.
The other hardware configuration being done here is required for both the Input and ASoC operations of the part.
Lastly, these need to be done in order and independently of which child driver (ASoC or input) the user adds. If this is moved to cs40l26-vibra.c (the input driver), for instance, and that module is then not added, it will disturb the required setup for use by the ASoC driver.
I would really like to get Lee's opinion here because it does not make sense to me why this is inappropriate when the configuration done in the core MFD driver is required for use by all of its children.
FWIW, I agree with Krzysztof.
There's a bunch of functionality in here that should be exported out to leaf drivers which should reside in their associated subsystems. From just a quick glance that looks to include, but not necessary limited to; IRQs, GPIOs and PLLs (Clocks).
MFD has been used for a dumping ground under the premise of "core functionality" before. Tolerance for those arguments are now fairly low.

Hi Lee, Krzysztof and Fred,
On Wed, Feb 12, 2025 at 03:50:50PM +0000, Lee Jones wrote:
On Tue, 11 Feb 2025, Fred Treven wrote:
On 2/5/25 04:34, Krzysztof Kozlowski wrote:
On 05/02/2025 00:18, Fred Treven wrote:
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.
[...]
+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);
This looks way past simple MFD driver. Not only this - entire file. You configure there quite a lot and for example setting PLLs is not job for MFD. This should be placed in appropriate subsystem.
I disagree here because the configuration being done in this file is essential to the core operation of the part. For instance, setting the PLL to open-loop here is required to prevent any external interference (e.g. GPIO events) from interrupting the part while loading firmware.
The other hardware configuration being done here is required for both the Input and ASoC operations of the part.
Lastly, these need to be done in order and independently of which child driver (ASoC or input) the user adds. If this is moved to cs40l26-vibra.c (the input driver), for instance, and that module is then not added, it will disturb the required setup for use by the ASoC driver.
I would really like to get Lee's opinion here because it does not make sense to me why this is inappropriate when the configuration done in the core MFD driver is required for use by all of its children.
FWIW, I agree with Krzysztof.
There's a bunch of functionality in here that should be exported out to leaf drivers which should reside in their associated subsystems. From just a quick glance that looks to include, but not necessary limited to; IRQs, GPIOs and PLLs (Clocks).
MFD has been used for a dumping ground under the premise of "core functionality" before. Tolerance for those arguments are now fairly low.
I think all three of you are right here. MFD should not be a "kitchen sink", but I'm also cautious to call this particular series one such offender. Let's consider how this hardware is logically organized, keeping in mind what I personally consider a core tenet of MFD, which is that each child should serve a purpose in the absence of all others.
This device is fundamentally a haptic chip; like many others in input, it has a low-latency GPIO trigger. As far as I can tell, the chip itself is the only consumer of this GPIO; there is no practical scenario where another chip would access it.
What makes this chip is unique is that it has an I2S port; from that perspective, it's also a DAC with a motor instead of a speaker. Any stimulus that drives the motor (e.g. external GPIO trigger, FF ioctl, audio-like stream, etc.) is therefore capable of asserting an interrupt (e.g. hardware short), which is why the parent (i.e. MFD) seems like the most logical place to handle this work. This chip would never be an interrupt-parent to anything else on the board, so an irqchip driver does not seem useful here.
I do see a lot of GPIO-related ISRs, but it looks like we can simply drop these; all they seem to do is print a debug message, and perform some housekeeping to track the edge polarity so as to print the correct debug message. If I'm mistaken and the driver really does need to perform some critical tasks in response to GPIO edges, then I do agree a gpiochip driver seems useful here, albeit an extremely limited one.
I agree there is a case here for a lightweight clock driver representing the device's internal PLL output, but it looks like all the children need to do here is one regmap call. I'm guessing the reason this regmap call got put in a wrapper in the MFD is to avoid duplicating a small retry loop in every child driver. Simply replicating this in only two children seems like less code then a whole clock driver solely for that purpose; alternatively, maybe there is some way for any child setting the PLL via regmap to better understand whether the device is asleep and may NAK.
Maybe v2 can start small and do the following:
1. Remove what can be considered "customer bring-up" code (e.g. liberal use of dev_dbg); this may get rid of all GPIO-related noise.
2. Consider whether regmap alone can handle anything related to IRQs and the PLL.
3. Consider whether anything in cs40l26_dsp_*() is better suited for the cs_dsp driver in firmware. A lot of this work seems suitable for any HALO DSP based device, even if CS40L26 is the only user for now. Maybe some of it can be consolidated with CS40L50?
With the series pared down in this way, it will hopefully become clearer how much remaining functionality, if any, should live as a separate stub or leaf driver.
-- Lee Jones [李琼斯]
Kind regards, Jeff LaBundy

Hi Fred,
kernel test robot noticed the following build errors:
[auto build test ERROR on lee-mfd/for-mfd-next] [also build test ERROR on broonie-sound/for-next dtor-input/next dtor-input/for-linus linus/master v6.14-rc3 next-20250218] [cannot apply to lee-mfd/for-mfd-fixes] [If your patch is applied to the wrong git tree, kindly drop us a note. And when submitting patch, we suggest to use '--base' as documented in https://git-scm.com/docs/git-format-patch#_base_tree_information]
url: https://github.com/intel-lab-lkp/linux/commits/Fred-Treven/firmware-cs_dsp-F... base: https://git.kernel.org/pub/scm/linux/kernel/git/lee/mfd.git for-mfd-next patch link: https://lore.kernel.org/r/20250204231835.2000457-6-ftreven%40opensource.cirr... patch subject: [PATCH RESEND 5/7] mfd: cs40l26: Add support for CS40L26 core driver config: s390-allmodconfig (https://download.01.org/0day-ci/archive/20250219/202502190628.g4aG7l2q-lkp@i...) compiler: clang version 19.1.3 (https://github.com/llvm/llvm-project ab51eccf88f5321e7c60591c5546b254b6afab99) reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20250219/202502190628.g4aG7l2q-lkp@i...)
If you fix the issue in a separate patch/commit (i.e. not just a new version of the same patch/commit), kindly add following tags | Reported-by: kernel test robot lkp@intel.com | Closes: https://lore.kernel.org/oe-kbuild-all/202502190628.g4aG7l2q-lkp@intel.com/
All errors (new ones prefixed by >>):
In file included from drivers/mfd/cs40l26-core.c:12: In file included from include/linux/mfd/core.h:13: In file included from include/linux/platform_device.h:13: In file included from include/linux/device.h:32: In file included from include/linux/device/driver.h:21: In file included from include/linux/module.h:19: In file included from include/linux/elf.h:6: In file included from arch/s390/include/asm/elf.h:181: In file included from arch/s390/include/asm/mmu_context.h:11: In file included from arch/s390/include/asm/pgalloc.h:18: In file included from include/linux/mm.h:2224: include/linux/vmstat.h:504:43: warning: arithmetic between different enumeration types ('enum zone_stat_item' and 'enum numa_stat_item') [-Wenum-enum-conversion] 504 | return vmstat_text[NR_VM_ZONE_STAT_ITEMS + | ~~~~~~~~~~~~~~~~~~~~~ ^ 505 | item]; | ~~~~ include/linux/vmstat.h:511:43: warning: arithmetic between different enumeration types ('enum zone_stat_item' and 'enum numa_stat_item') [-Wenum-enum-conversion] 511 | return vmstat_text[NR_VM_ZONE_STAT_ITEMS + | ~~~~~~~~~~~~~~~~~~~~~ ^ 512 | NR_VM_NUMA_EVENT_ITEMS + | ~~~~~~~~~~~~~~~~~~~~~~ include/linux/vmstat.h:524:43: warning: arithmetic between different enumeration types ('enum zone_stat_item' and 'enum numa_stat_item') [-Wenum-enum-conversion] 524 | return vmstat_text[NR_VM_ZONE_STAT_ITEMS + | ~~~~~~~~~~~~~~~~~~~~~ ^ 525 | NR_VM_NUMA_EVENT_ITEMS + | ~~~~~~~~~~~~~~~~~~~~~~
drivers/mfd/cs40l26-core.c:1173:3: error: cannot jump from this goto statement to its label
1173 | goto err_fw_rls; | ^ drivers/mfd/cs40l26-core.c:1176:2: note: jump bypasses initialization of variable with __attribute__((cleanup)) 1176 | guard(mutex)(&cs40l26->lock); | ^ include/linux/cleanup.h:309:15: note: expanded from macro 'guard' 309 | CLASS(_name, __UNIQUE_ID(guard)) | ^ include/linux/compiler.h:166:29: note: expanded from macro '__UNIQUE_ID' 166 | #define __UNIQUE_ID(prefix) __PASTE(__PASTE(__UNIQUE_ID_, prefix), __COUNTER__) | ^ include/linux/compiler_types.h:84:22: note: expanded from macro '__PASTE' 84 | #define __PASTE(a,b) ___PASTE(a,b) | ^ include/linux/compiler_types.h:83:23: note: expanded from macro '___PASTE' 83 | #define ___PASTE(a,b) a##b | ^ <scratch space>:42:1: note: expanded from here 42 | __UNIQUE_ID_guard697 | ^ 3 warnings and 1 error generated.
vim +1173 drivers/mfd/cs40l26-core.c
1165 1166 static void cs40l26_dsp_start(struct cs40l26 *cs40l26) 1167 { 1168 int i, ret; 1169 1170 ret = cs40l26_dsp_pre_config(cs40l26); 1171 if (ret) { 1172 dev_err(cs40l26->dev, "DSP Pre Config. Failed: %d\n", ret);
1173 goto err_fw_rls;
1174 } 1175 1176 guard(mutex)(&cs40l26->lock); 1177 1178 ret = cs_dsp_power_up_multiple(&cs40l26->dsp, cs40l26->wmfw, "cs40l26.wmfw", cs40l26_coeffs, 1179 CS40L26_NUM_COEFF_FILES, "cs40l26"); 1180 if (ret) { 1181 dev_err(cs40l26->dev, "Failed to Power Up DSP\n"); 1182 goto err_fw_rls; 1183 } 1184 1185 if (cs40l26->dsp.fw_id != CS40L26_FW_ID) { 1186 dev_err(cs40l26->dev, "Invalid firmware ID: 0x%X\n", cs40l26->dsp.fw_id); 1187 goto err_fw_rls; 1188 } 1189 1190 if (cs40l26->dsp.fw_id_version < cs40l26->variant->info->fw_min_rev) { 1191 dev_err(cs40l26->dev, "Invalid firmware revision: 0x%X\n", 1192 cs40l26->dsp.fw_id_version); 1193 goto err_fw_rls; 1194 } 1195 1196 ret = cs_dsp_run(&cs40l26->dsp); 1197 if (ret) 1198 dev_err(cs40l26->dev, "DSP Failed to run: %d\n", ret); 1199 1200 err_fw_rls: 1201 for (i = 0; i < CS40L26_NUM_COEFF_FILES; i++) 1202 release_firmware(cs40l26_coeffs[i].coeff_firmware); 1203 1204 release_firmware(cs40l26->wmfw); 1205 } 1206

Introduce codec support for Cirrus Logic Device CS40L26.
The ASoC driver enables I2S streaming to the device.
Signed-off-by: Fred Treven ftreven@opensource.cirrus.com --- sound/soc/codecs/Kconfig | 12 + sound/soc/codecs/Makefile | 2 + sound/soc/codecs/cs40l26-codec.c | 523 +++++++++++++++++++++++++++++++ 3 files changed, 537 insertions(+) create mode 100644 sound/soc/codecs/cs40l26-codec.c
diff --git a/sound/soc/codecs/Kconfig b/sound/soc/codecs/Kconfig index ee35f3aa5521..850b5fab984c 100644 --- a/sound/soc/codecs/Kconfig +++ b/sound/soc/codecs/Kconfig @@ -77,6 +77,7 @@ config SND_SOC_ALL_CODECS imply SND_SOC_CS35L56_I2C imply SND_SOC_CS35L56_SPI imply SND_SOC_CS35L56_SDW + imply SND_SOC_CS40L26 imply SND_SOC_CS40L50 imply SND_SOC_CS42L42 imply SND_SOC_CS42L42_SDW @@ -875,6 +876,17 @@ config SND_SOC_CS35L56_SDW help Enable support for Cirrus Logic CS35L56 boosted amplifier with SoundWire control
+config SND_SOC_CS40L26 + tristate "Cirrus Logic CS40L26 CODEC" + depends on MFD_CS40L26_CORE + help + This option enables support for I2S streaming to Cirrus Logic CS40L26. + + CS40L26 is a boosted haptic driver with integrated DSP and waveform + memory with advanced closed loop algorithms and LRA protection. + + If built as a module, it will be named snd-soc-cs40l26. + config SND_SOC_CS40L50 tristate "Cirrus Logic CS40L50 CODEC" depends on MFD_CS40L50_CORE diff --git a/sound/soc/codecs/Makefile b/sound/soc/codecs/Makefile index d7ad795603c1..086e18964e60 100644 --- a/sound/soc/codecs/Makefile +++ b/sound/soc/codecs/Makefile @@ -80,6 +80,7 @@ snd-soc-cs35l56-shared-y := cs35l56-shared.o snd-soc-cs35l56-i2c-y := cs35l56-i2c.o snd-soc-cs35l56-spi-y := cs35l56-spi.o snd-soc-cs35l56-sdw-y := cs35l56-sdw.o +snd-soc-cs40l26-y := cs40l26-codec.o snd-soc-cs40l50-y := cs40l50-codec.o snd-soc-cs42l42-y := cs42l42.o snd-soc-cs42l42-i2c-y := cs42l42-i2c.o @@ -497,6 +498,7 @@ obj-$(CONFIG_SND_SOC_CS35L56_SHARED) += snd-soc-cs35l56-shared.o obj-$(CONFIG_SND_SOC_CS35L56_I2C) += snd-soc-cs35l56-i2c.o obj-$(CONFIG_SND_SOC_CS35L56_SPI) += snd-soc-cs35l56-spi.o obj-$(CONFIG_SND_SOC_CS35L56_SDW) += snd-soc-cs35l56-sdw.o +obj-$(CONFIG_SND_SOC_CS40L26) += snd-soc-cs40l26.o obj-$(CONFIG_SND_SOC_CS40L50) += snd-soc-cs40l50.o obj-$(CONFIG_SND_SOC_CS42L42_CORE) += snd-soc-cs42l42.o obj-$(CONFIG_SND_SOC_CS42L42) += snd-soc-cs42l42-i2c.o diff --git a/sound/soc/codecs/cs40l26-codec.c b/sound/soc/codecs/cs40l26-codec.c new file mode 100644 index 000000000000..5bfaff0683a5 --- /dev/null +++ b/sound/soc/codecs/cs40l26-codec.c @@ -0,0 +1,523 @@ +// 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@opensource.cirrus.com + +#include <linux/bitfield.h> +#include <linux/mfd/cs40l26.h> +#include <sound/pcm_params.h> +#include <sound/soc.h> + +#define CS40L26_MONITOR_FILT 0x4008 +#define CS40L26_ASP_ENABLES1 0x4800 +#define CS40L26_ASP_CONTROL2 0x4808 +#define CS40L26_ASP_FRAME_CONTROL5 0x4820 +#define CS40L26_ASP_DATA_CONTROL5 0x4840 +#define CS40L26_DACPCM1_INPUT 0x4C00 +#define CS40L26_ASPTX1_INPUT 0x4C20 + +#define CS40L26_PLL_CLK_SEL_BCLK 0x0 +#define CS40L26_PLL_CLK_SEL_MCLK 0x5 + +#define CS40L26_PLL_CLK_FREQ_MASK GENMASK(31, 0) + +#define CS40L26_FORMATS (SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S24_LE) +#define CS40L26_RATES (SNDRV_PCM_RATE_48000 | SNDRV_PCM_RATE_96000) + +#define CS40L26_ASP_RX_WIDTH_MASK GENMASK(31, 24) +#define CS40L26_ASP_FMT_MASK GENMASK(10, 8) +#define CS40L26_ASP_BCLK_INV_MASK BIT(6) +#define CS40L26_ASP_FSYNC_INV_MASK BIT(2) +#define CS40L26_ASP_FSYNC_INV_SHIFT 2 + +#define CS40L26_ASP_FMT_TDM1_DSPA 0x0 +#define CS40L26_ASP_FMT_I2S 0x2 + +#define CS40L26_PLL_REFCLK_BCLK 0x0 +#define CS40L26_PLL_REFCLK_FSYNC 0x1 +#define CS40L26_PLL_REFCLK_MCLK 0x5 + +#define CS40L26_PLL_REFCLK_SEL_MASK GENMASK(2, 0) +#define CS40L26_PLL_REFCLK_FREQ_MASK GENMASK(10, 5) +#define CS40L26_PLL_REFCLK_FREQ_SHIFT 5 +#define CS40L26_PLL_REFCLK_LOOP_MASK BIT(11) + +#define CS40L26_ASP_RX_WL_MASK GENMASK(5, 0) + +#define CS40L26_DATA_SRC_DSP1TX1 0x32 + +#define CS40L26_DATA_SRC_MASK GENMASK(6, 0) + +#define CS40L26_ASP_TX1_EN_MASK BIT(0) +#define CS40L26_ASP_TX2_EN_MASK BIT(1) +#define CS40L26_ASP_RX1_EN_MASK BIT(16) +#define CS40L26_ASP_RX2_EN_MASK BIT(17) +#define CS40L26_ASP_ENABLE_MASK \ + (CS40L26_ASP_TX1_EN_MASK | CS40L26_ASP_TX2_EN_MASK | CS40L26_ASP_RX1_EN_MASK | \ + CS40L26_ASP_RX2_EN_MASK) + +#define CS40L26_ASP_RX1_SLOT_MASK GENMASK(5, 0) +#define CS40L26_ASP_RX2_SLOT_MASK GENMASK(13, 8) + +#define CS40L26_VIMON_DUAL_RATE_MASK BIT(16) + +struct cs40l26_pll_sysclk_config { + u32 freq; + u8 cfg; +}; + +struct cs40l26_codec { + struct cs40l26 *core; + struct device *dev; + struct regmap *regmap; + unsigned int rate; + u32 daifmt; + int tdm_width; + int tdm_slot[2]; + u32 refclk_input; +}; + +static const struct cs40l26_pll_sysclk_config cs40l26_pll_sysclk[] = { + { 32768, 0x00 }, + { 1536000, 0x1B }, + { 3072000, 0x21 }, + { 6144000, 0x28 }, + { 9600000, 0x30 }, + { 12288000, 0x33 }, +}; + +static int cs40l26_get_clk_config(struct cs40l26_codec *codec, u32 freq, u8 *clk_cfg) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(cs40l26_pll_sysclk); i++) { + if (cs40l26_pll_sysclk[i].freq == freq) { + *clk_cfg = cs40l26_pll_sysclk[i].cfg; + return 0; + } + } + + dev_err(codec->dev, "Invalid clock frequency: %u Hz\n", freq); + + return -EINVAL; +} + +static int cs40l26_swap_ext_clk(struct cs40l26_codec *codec, u8 clk_src) +{ + u8 clk_cfg, clk_sel; + int ret; + + switch (clk_src) { + case CS40L26_PLL_REFCLK_BCLK: + clk_sel = CS40L26_PLL_CLK_SEL_BCLK; + ret = cs40l26_get_clk_config(codec, codec->rate, &clk_cfg); + break; + case CS40L26_PLL_REFCLK_MCLK: + clk_sel = CS40L26_PLL_CLK_SEL_MCLK; + ret = cs40l26_get_clk_config(codec, 32768, &clk_cfg); + break; + case CS40L26_PLL_REFCLK_FSYNC: + ret = -EPERM; + break; + default: + ret = -EINVAL; + } + + if (ret) { + dev_err(codec->dev, "Failed to get clock configuration\n"); + return ret; + } + + ret = cs40l26_set_pll_loop(codec->core, CS40L26_PLL_OPEN); + if (ret) + return ret; + + ret = regmap_update_bits(codec->regmap, CS40L26_REFCLK_INPUT, + CS40L26_PLL_REFCLK_FREQ_MASK | CS40L26_PLL_REFCLK_SEL_MASK, + (clk_cfg << CS40L26_PLL_REFCLK_FREQ_SHIFT) | clk_sel); + if (ret) + return ret; + + return cs40l26_set_pll_loop(codec->core, CS40L26_PLL_CLOSED); +} + +static int cs40l26_clk_en(struct snd_soc_dapm_widget *w, struct snd_kcontrol *kcontrol, int event) +{ + struct snd_soc_component *component = snd_soc_dapm_to_component(w->dapm); + struct cs40l26_codec *codec = snd_soc_component_get_drvdata(component); + struct cs40l26 *cs40l26 = codec->core; + int ret; + + guard(mutex)(&cs40l26->lock); + + switch (event) { + case SND_SOC_DAPM_POST_PMU: + ret = cs40l26_dsp_write(cs40l26, CS40L26_STOP_PLAYBACK); + if (ret) + return ret; + + ret = regmap_read(codec->regmap, CS40L26_REFCLK_INPUT, &codec->refclk_input); + if (ret) + return ret; + + ret = cs40l26_dsp_write(cs40l26, CS40L26_START_I2S); + if (ret) + return ret; + + ret = cs40l26_swap_ext_clk(codec, CS40L26_PLL_REFCLK_BCLK); + if (ret) + return ret; + break; + case SND_SOC_DAPM_PRE_PMD: + ret = cs40l26_swap_ext_clk(codec, CS40L26_PLL_REFCLK_MCLK); + if (ret) + return ret; + + /* Restore PLL Configuration */ + ret = cs40l26_set_pll_loop(cs40l26, (u32)FIELD_GET(CS40L26_PLL_REFCLK_LOOP_MASK, + codec->refclk_input)); + if (ret) + return ret; + break; + default: + dev_err(codec->dev, "Invalid event: %d\n", event); + return -EINVAL; + } + + return 0; +} + +static int cs40l26_dsp_tx(struct snd_soc_dapm_widget *w, struct snd_kcontrol *kcontrol, int event) +{ + struct snd_soc_component *component = snd_soc_dapm_to_component(w->dapm); + struct cs40l26_codec *codec = snd_soc_component_get_drvdata(component); + struct cs40l26 *cs40l26 = codec->core; + int ret; + + switch (event) { + case SND_SOC_DAPM_POST_PMU: + ret = cs40l26_fw_write(&cs40l26->dsp, "A2HEN", CS40L26_A2H_ALGO_ID, 1); + break; + case SND_SOC_DAPM_PRE_PMD: + ret = cs40l26_fw_write(&cs40l26->dsp, "A2HEN", CS40L26_A2H_ALGO_ID, 0); + break; + default: + dev_err(codec->dev, "Invalid DSPTX event: %d\n", event); + ret = -EINVAL; + } + + return ret; +} + +static int cs40l26_asp_rx(struct snd_soc_dapm_widget *w, struct snd_kcontrol *kcontrol, int event) +{ + struct cs40l26_codec *codec; + struct cs40l26 *cs40l26; + int ret; + + codec = snd_soc_component_get_drvdata(snd_soc_dapm_to_component(w->dapm)); + + cs40l26 = codec->core; + + guard(mutex)(&cs40l26->lock); + + switch (event) { + case SND_SOC_DAPM_POST_PMU: + ret = regmap_update_bits(codec->regmap, CS40L26_DACPCM1_INPUT, + CS40L26_DATA_SRC_MASK, CS40L26_DATA_SRC_DSP1TX1); + if (ret) + return ret; + + ret = regmap_update_bits(codec->regmap, CS40L26_ASPTX1_INPUT, CS40L26_DATA_SRC_MASK, + CS40L26_DATA_SRC_DSP1TX1); + if (ret) + return ret; + + ret = regmap_set_bits(codec->regmap, CS40L26_ASP_ENABLES1, CS40L26_ASP_ENABLE_MASK); + if (ret) + return ret; + break; + case SND_SOC_DAPM_PRE_PMD: + ret = cs40l26_dsp_write(cs40l26, CS40L26_STOP_I2S); + if (ret) + return ret; + + ret = regmap_clear_bits(codec->regmap, CS40L26_ASP_ENABLES1, + CS40L26_ASP_ENABLE_MASK); + if (ret) + return ret; + break; + default: + dev_err(codec->dev, "Invalid ASPRX event: %d\n", event); + return -EINVAL; + } + + return 0; +} + +static int cs40l26_component_set_sysclk(struct snd_soc_component *component, int clk_id, int source, + unsigned int freq, int dir) +{ + struct cs40l26_codec *codec = snd_soc_component_get_drvdata(component); + u8 clk_cfg; + int ret; + + ret = cs40l26_get_clk_config(codec, (u32)(CS40L26_PLL_CLK_FREQ_MASK & freq), &clk_cfg); + if (ret) + return ret; + + if (clk_id) { + dev_err(codec->dev, "Invalid input clock (ID: %d)\n", clk_id); + return -EINVAL; + } + + codec->rate = CS40L26_PLL_CLK_FREQ_MASK & freq; + + return 0; +} + +static int cs40l26_set_dai_fmt(struct snd_soc_dai *codec_dai, unsigned int fmt) +{ + struct cs40l26_codec *codec = snd_soc_component_get_drvdata(codec_dai->component); + + if ((fmt & SND_SOC_DAIFMT_MASTER_MASK) != SND_SOC_DAIFMT_CBC_CFC) { + dev_err(codec->dev, "Device can not be master\n"); + return -EINVAL; + } + + switch (fmt & SND_SOC_DAIFMT_INV_MASK) { + case SND_SOC_DAIFMT_NB_NF: + codec->daifmt = 0; + break; + case SND_SOC_DAIFMT_NB_IF: + codec->daifmt = CS40L26_ASP_FSYNC_INV_MASK; + break; + case SND_SOC_DAIFMT_IB_NF: + codec->daifmt = CS40L26_ASP_BCLK_INV_MASK; + break; + case SND_SOC_DAIFMT_IB_IF: + codec->daifmt = CS40L26_ASP_FSYNC_INV_MASK | CS40L26_ASP_BCLK_INV_MASK; + break; + default: + dev_err(codec->dev, "Invalid clock inversion\n"); + return -EINVAL; + } + + switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) { + case SND_SOC_DAIFMT_DSP_A: + codec->daifmt |= FIELD_PREP(CS40L26_ASP_FMT_MASK, CS40L26_ASP_FMT_TDM1_DSPA); + break; + case SND_SOC_DAIFMT_I2S: + codec->daifmt |= FIELD_PREP(CS40L26_ASP_FMT_MASK, CS40L26_ASP_FMT_I2S); + break; + default: + dev_err(codec->dev, "Invalid DAI format: 0x%X\n", fmt & SND_SOC_DAIFMT_FORMAT_MASK); + return -EINVAL; + } + + return 0; +} + +static int cs40l26_pcm_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, struct snd_soc_dai *dai) +{ + struct cs40l26_codec *codec = snd_soc_component_get_drvdata(dai->component); + u32 asp_rx_wl, asp_rx_width; + int ret; + + ret = pm_runtime_resume_and_get(codec->core->dev); + if (ret) + return ret; + + switch (params_rate(params)) { + case 48000: + ret = regmap_clear_bits(codec->regmap, CS40L26_MONITOR_FILT, + CS40L26_VIMON_DUAL_RATE_MASK); + break; + case 96000: + ret = regmap_set_bits(codec->regmap, CS40L26_MONITOR_FILT, + CS40L26_VIMON_DUAL_RATE_MASK); + break; + default: + dev_err(codec->dev, "Unsupported sample rate: %d Hz\n", params_rate(params)); + ret = -EINVAL; + } + + if (ret) + goto pm_exit; + + asp_rx_wl = params_width(params); + + ret = regmap_update_bits(codec->regmap, CS40L26_ASP_DATA_CONTROL5, CS40L26_ASP_RX_WL_MASK, + asp_rx_wl); + if (ret) + goto pm_exit; + + + asp_rx_width = codec->tdm_width ? codec->tdm_width : asp_rx_wl; + + codec->daifmt |= FIELD_PREP(CS40L26_ASP_RX_WIDTH_MASK, asp_rx_width); + + ret = regmap_update_bits(codec->regmap, CS40L26_ASP_CONTROL2, + CS40L26_ASP_FSYNC_INV_MASK | CS40L26_ASP_BCLK_INV_MASK | + CS40L26_ASP_FMT_MASK | CS40L26_ASP_RX_WIDTH_MASK, codec->daifmt); + if (ret) + goto pm_exit; + + ret = regmap_update_bits(codec->regmap, CS40L26_ASP_FRAME_CONTROL5, + CS40L26_ASP_RX1_SLOT_MASK | CS40L26_ASP_RX2_SLOT_MASK, + codec->tdm_slot[0] | + FIELD_PREP(CS40L26_ASP_RX2_SLOT_MASK, codec->tdm_slot[1])); + if (ret) + goto pm_exit; + + dev_dbg(codec->dev, "ASP: %d bits in %d bit slots, slot #s: %d, %d\n", asp_rx_wl, + asp_rx_width, codec->tdm_slot[0], codec->tdm_slot[1]); + +pm_exit: + cs40l26_pm_exit(codec->core->dev); + + return ret; +} + +static int cs40l26_set_tdm_slot(struct snd_soc_dai *dai, unsigned int tx_mask, unsigned int rx_mask, + int slots, int slot_width) +{ + struct cs40l26_codec *codec = snd_soc_component_get_drvdata(dai->component); + + codec->tdm_width = slot_width; + + /* + * Reset slots if TDM is being disabled, and catch the case in which both RX1 and RX2 + * would be set to slot 0 which would cause the hardware to flag an error + */ + if (!slots || rx_mask == 0x1) + rx_mask = 0x3; + + codec->tdm_slot[0] = ffs(rx_mask) - 1; + rx_mask &= ~BIT(codec->tdm_slot[0]); + codec->tdm_slot[1] = ffs(rx_mask) - 1; + + return 0; +} + +static const struct snd_soc_dai_ops cs40l26_dai_ops = { + .set_fmt = cs40l26_set_dai_fmt, + .set_tdm_slot = cs40l26_set_tdm_slot, + .hw_params = cs40l26_pcm_hw_params, +}; + +static struct snd_soc_dai_driver cs40l26_dai[] = { + { + .name = "cs40l26-pcm", + .id = 0, + .playback = { + .stream_name = "ASP Playback", + .channels_min = 1, + .channels_max = 2, + .rates = CS40L26_RATES, + .formats = CS40L26_FORMATS, + }, + .ops = &cs40l26_dai_ops, + .symmetric_rate = 1, + }, +}; + +static const char *const cs40l26_out_mux_texts[] = { "Off", "ASP", "DSP" }; +static SOC_ENUM_SINGLE_VIRT_DECL(cs40l26_out_mux_enum, cs40l26_out_mux_texts); +static const struct snd_kcontrol_new cs40l26_out_mux = + SOC_DAPM_ENUM("Haptics Source", cs40l26_out_mux_enum); + +static const struct snd_soc_dapm_widget cs40l26_dapm_widgets[] = { + SND_SOC_DAPM_SUPPLY_S("ASP PLL", 0, SND_SOC_NOPM, 0, 0, cs40l26_clk_en, + SND_SOC_DAPM_POST_PMU | SND_SOC_DAPM_PRE_PMD), + SND_SOC_DAPM_AIF_IN("ASPRX1", NULL, 0, SND_SOC_NOPM, 0, 0), + SND_SOC_DAPM_AIF_IN("ASPRX2", NULL, 0, SND_SOC_NOPM, 0, 0), + + SND_SOC_DAPM_PGA_E("ASP", SND_SOC_NOPM, 0, 0, NULL, 0, cs40l26_asp_rx, + SND_SOC_DAPM_POST_PMU | SND_SOC_DAPM_PRE_PMD), + SND_SOC_DAPM_PGA_E("DSP", SND_SOC_NOPM, 0, 0, NULL, 0, cs40l26_dsp_tx, + SND_SOC_DAPM_POST_PMU | SND_SOC_DAPM_PRE_PMD), + + SND_SOC_DAPM_MUX("Haptics Source", SND_SOC_NOPM, 0, 0, &cs40l26_out_mux), + SND_SOC_DAPM_OUTPUT("OUT"), +}; + +static const struct snd_soc_dapm_route cs40l26_dapm_routes[] = { + { "ASP Playback", NULL, "ASP PLL" }, + { "ASPRX1", NULL, "ASP Playback" }, + { "ASPRX2", NULL, "ASP Playback" }, + + { "ASP", NULL, "ASPRX1" }, + { "ASP", NULL, "ASPRX2" }, + { "DSP", NULL, "ASP" }, + + { "Haptics Source", "ASP", "ASP" }, + { "Haptics Source", "DSP", "DSP" }, + { "OUT", NULL, "Haptics Source" }, +}; + +static int cs40l26_codec_probe(struct snd_soc_component *component) +{ + struct cs40l26_codec *codec = snd_soc_component_get_drvdata(component); + + /* Default audio SCLK frequency */ + codec->rate = 1536000; + + codec->tdm_slot[0] = 0; + codec->tdm_slot[1] = 1; + + return 0; +} + +static const struct snd_soc_component_driver soc_codec_dev_cs40l26 = { + .probe = cs40l26_codec_probe, + .set_sysclk = cs40l26_component_set_sysclk, + .dapm_widgets = cs40l26_dapm_widgets, + .num_dapm_widgets = ARRAY_SIZE(cs40l26_dapm_widgets), + .dapm_routes = cs40l26_dapm_routes, + .num_dapm_routes = ARRAY_SIZE(cs40l26_dapm_routes), +}; + +static int cs40l26_platform_probe(struct platform_device *pdev) +{ + struct cs40l26 *cs40l26 = dev_get_drvdata(pdev->dev.parent); + struct cs40l26_codec *codec; + + codec = devm_kzalloc(&pdev->dev, sizeof(struct cs40l26_codec), GFP_KERNEL); + if (!codec) + return -ENOMEM; + + codec->core = cs40l26; + codec->regmap = cs40l26->regmap; + codec->dev = &pdev->dev; + + platform_set_drvdata(pdev, codec); + + return snd_soc_register_component(&pdev->dev, &soc_codec_dev_cs40l26, cs40l26_dai, + ARRAY_SIZE(cs40l26_dai)); +} + +static const struct platform_device_id cs40l26_id[] = { + { "cs40l26-codec", }, + {} +}; +MODULE_DEVICE_TABLE(platform, cs40l26_id); + +static struct platform_driver cs40l26_codec_driver = { + .probe = cs40l26_platform_probe, + .id_table = cs40l26_id, + .driver = { + .name = "cs40l26-codec", + }, +}; +module_platform_driver(cs40l26_codec_driver); + +MODULE_DESCRIPTION("ASoC CS40L26 driver"); +MODULE_AUTHOR("Fred Treven ftreven@opensource.cirrus.com"); +MODULE_LICENSE("GPL");

Introduce support for Cirrus Logic Device CS40L26: a boosted haptics driver with integrated DSP and waveform memory with advanced closed loop algorithms and LRA protection.
The input driver provides the interface for control of haptic effects through the device.
Signed-off-by: Fred Treven ftreven@opensource.cirrus.com --- drivers/input/misc/Kconfig | 10 + drivers/input/misc/Makefile | 1 + drivers/input/misc/cs40l26-vibra.c | 669 +++++++++++++++++++++++++++++ 3 files changed, 680 insertions(+) create mode 100644 drivers/input/misc/cs40l26-vibra.c
diff --git a/drivers/input/misc/Kconfig b/drivers/input/misc/Kconfig index 13d135257e06..2c9496c873e7 100644 --- a/drivers/input/misc/Kconfig +++ b/drivers/input/misc/Kconfig @@ -147,6 +147,16 @@ config INPUT_BMA150 To compile this driver as a module, choose M here: the module will be called bma150.
+config INPUT_CS40L26_VIBRA + tristate "CS40L26 Haptic Driver support" + depends on MFD_CS40L26_CORE + help + Say Y here to enable support for Cirrus Logic's CS40L26 + haptic driver. + + To compile this driver as a module, choose M here: the + module will be called cs40l26-vibra. + config INPUT_CS40L50_VIBRA tristate "CS40L50 Haptic Driver support" depends on MFD_CS40L50_CORE diff --git a/drivers/input/misc/Makefile b/drivers/input/misc/Makefile index 6d91804d0a6f..b6274a937a94 100644 --- a/drivers/input/misc/Makefile +++ b/drivers/input/misc/Makefile @@ -29,6 +29,7 @@ obj-$(CONFIG_INPUT_CMA3000) += cma3000_d0x.o obj-$(CONFIG_INPUT_CMA3000_I2C) += cma3000_d0x_i2c.o obj-$(CONFIG_INPUT_COBALT_BTNS) += cobalt_btns.o obj-$(CONFIG_INPUT_CPCAP_PWRBUTTON) += cpcap-pwrbutton.o +obj-$(CONFIG_INPUT_CS40L26_VIBRA) += cs40l26-vibra.o obj-$(CONFIG_INPUT_CS40L50_VIBRA) += cs40l50-vibra.o obj-$(CONFIG_INPUT_DA7280_HAPTICS) += da7280.o obj-$(CONFIG_INPUT_DA9052_ONKEY) += da9052_onkey.o diff --git a/drivers/input/misc/cs40l26-vibra.c b/drivers/input/misc/cs40l26-vibra.c new file mode 100644 index 000000000000..d083be714a3a --- /dev/null +++ b/drivers/input/misc/cs40l26-vibra.c @@ -0,0 +1,669 @@ +// 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@opensource.cirrus.com + */ + +#include <linux/bitfield.h> +#include <linux/input.h> +#include <linux/mfd/cs40l26.h> + +#define CS40L26_EFFECTS_MAX 1 + +#define CS40L26_NUM_PCT_MAP_VALUES 101 + +#define CS40L26_STOP_PLAYBACK 0x05000000 + +#define CS40L26_MAX_INDEX_MASK GENMASK(15, 0) + +#define CS40L26_RAM_INDEX_START 0x01000000 +#define CS40L26_RAM_INDEX_END 0x0100007F + +#define CS40L26_ROM_INDEX_START 0x01800000 +#define CS40L26_ROM_INDEX_END 0x01800026 +#define CS40L26_NUM_ROM_WAVES (CS40L26_ROM_INDEX_END - CS40L26_ROM_INDEX_START + 1) + +#define CS40L26_BUZZGEN_INDEX_START 0x01800080 +#define CS40L26_BUZZGEN_INDEX_END 0x01800085 + +#define CS40L26_BUZZGEN_PER_MS_MAX 10 +#define CS40L26_BUZZGEN_PER_MS_MIN 4 + +#define CS40L26_BUZZGEN_LEVEL_MIN 0x00 +#define CS40L26_BUZZGEN_LEVEL_MAX 0xFF + +#define CS40L26_BUZZGEN_NUM_CONFIGS (CS40L26_BUZZGEN_INDEX_END - CS40L26_BUZZGEN_INDEX_START) + +enum cs40l26_bank { + CS40L26_BANK_RAM, + CS40L26_BANK_ROM, + CS40L26_BANK_BUZ, +}; + +struct cs40l26_effect { + enum cs40l26_bank bank; + u32 index; + int id; + struct list_head list; +}; + +struct cs40l26_vibra { + struct cs40l26 *cs40l26; + struct input_dev *input; + struct workqueue_struct *vib_wq; + struct list_head effect_head; +}; + +struct cs40l26_work { + struct ff_effect *ff_effect; + struct cs40l26_vibra *vib; + struct work_struct work; + s16 *custom_data; + int custom_len; + u16 gain_pct; + int count; + int error; +}; + +struct cs40l26_buzzgen_config { + const char *duration_name; + const char *freq_name; + const char *level_name; + int effect_id; +}; + +static struct cs40l26_buzzgen_config cs40l26_buzzgen_configs[] = { + { + .duration_name = "BUZZ_EFFECTS2_BUZZ_DURATION", + .freq_name = "BUZZ_EFFECTS2_BUZZ_FREQ", + .level_name = "BUZZ_EFFECTS2_BUZZ_LEVEL", + .effect_id = -1 + }, + { + .duration_name = "BUZZ_EFFECTS3_BUZZ_DURATION", + .freq_name = "BUZZ_EFFECTS3_BUZZ_FREQ", + .level_name = "BUZZ_EFFECTS3_BUZZ_LEVEL", + .effect_id = -1 + }, + { + .duration_name = "BUZZ_EFFECTS4_BUZZ_DURATION", + .freq_name = "BUZZ_EFFECTS4_BUZZ_FREQ", + .level_name = "BUZZ_EFFECTS4_BUZZ_LEVEL", + .effect_id = -1 + }, + { + .duration_name = "BUZZ_EFFECTS5_BUZZ_DURATION", + .freq_name = "BUZZ_EFFECTS5_BUZZ_FREQ", + .level_name = "BUZZ_EFFECTS5_BUZZ_LEVEL", + .effect_id = -1 + }, + { + .duration_name = "BUZZ_EFFECTS6_BUZZ_DURATION", + .freq_name = "BUZZ_EFFECTS6_BUZZ_FREQ", + .level_name = "BUZZ_EFFECTS6_BUZZ_LEVEL", + .effect_id = -1 + }, +}; + +static int cs40l26_buzzgen_find_slot(int id) +{ + int effect_id, lowest_available_slot = -1, slot; + + for (slot = CS40L26_BUZZGEN_NUM_CONFIGS - 1; slot >= 0; slot--) { + effect_id = cs40l26_buzzgen_configs[slot].effect_id; + + if (effect_id == id) + return slot; + else if (effect_id == -1) + lowest_available_slot = slot; + } + + return lowest_available_slot; +} + +static int cs40l26_sine_upload(struct cs40l26_vibra *vib, struct cs40l26_work *work_data, + struct cs40l26_effect *effect) +{ + struct cs_dsp *dsp = &vib->cs40l26->dsp; + unsigned int duration, freq, level; + int error, slot; + + slot = cs40l26_buzzgen_find_slot(work_data->ff_effect->id); + if (slot == -1) { + dev_err(vib->cs40l26->dev, "No free BUZZGEN slot available\n"); + return -ENOSPC; + } + + cs40l26_buzzgen_configs[slot].effect_id = work_data->ff_effect->id; + + /* Firmware expects duration in ms divided by 4 */ + duration = (unsigned int)DIV_ROUND_UP(work_data->ff_effect->replay.length, 4); + + freq = (unsigned int)(1000 / clamp_val(work_data->ff_effect->u.periodic.period, + CS40L26_BUZZGEN_PER_MS_MIN, + CS40L26_BUZZGEN_PER_MS_MAX)); + + level = (unsigned int)clamp_val(work_data->ff_effect->u.periodic.magnitude, + CS40L26_BUZZGEN_LEVEL_MIN, CS40L26_BUZZGEN_LEVEL_MAX); + + guard(mutex)(&dsp->pwr_lock); + + error = cs40l26_fw_write(dsp, cs40l26_buzzgen_configs[slot].duration_name, + CS40L26_BUZZGEN_ALGO_ID, duration); + if (error) + return error; + + error = cs40l26_fw_write(dsp, cs40l26_buzzgen_configs[slot].freq_name, + CS40L26_BUZZGEN_ALGO_ID, freq); + if (error) + return error; + + error = cs40l26_fw_write(dsp, cs40l26_buzzgen_configs[slot].level_name, + CS40L26_BUZZGEN_ALGO_ID, level); + if (error) + return error; + + effect->id = work_data->ff_effect->id; + effect->bank = CS40L26_BANK_BUZ; + + /* BUZZGEN slot 1 is reserved for OTP buzz so offset of 1 required */ + effect->index = CS40L26_BUZZGEN_INDEX_START + slot + 1; + + return 0; +} + +static int cs40l26_num_ram_waves(struct cs40l26_vibra *vib) +{ + u32 nwaves; + int error; + + guard(mutex)(&vib->cs40l26->dsp.pwr_lock); + + error = cs40l26_fw_read(&vib->cs40l26->dsp, "NUM_OF_WAVES", + vib->cs40l26->variant->info->vibegen_algo_id, &nwaves); + + return error ? error : (int)nwaves; +} + +static int cs40l26_trigger_index_get(struct cs40l26_vibra *vib, struct cs40l26_work *work_data, + enum cs40l26_bank bank, u32 *trigger_index) +{ + u16 index = (u16)(work_data->custom_data[1] & CS40L26_MAX_INDEX_MASK); + struct device *dev = vib->cs40l26->dev; + int error = 0, nwaves; + u32 index_start; + + switch (bank) { + case CS40L26_BANK_RAM: + nwaves = cs40l26_num_ram_waves(vib); + if (nwaves < 0) { + error = nwaves; + } else if (nwaves == 0) { + dev_err(dev, "No waveforms in RAM bank\n"); + error = -ENODATA; + } + + index_start = CS40L26_RAM_INDEX_START; + break; + case CS40L26_BANK_ROM: + nwaves = CS40L26_NUM_ROM_WAVES; + index_start = CS40L26_ROM_INDEX_START; + break; + default: + dev_err(dev, "Invalid bank %u\n", bank); + error = -EINVAL; + } + + if (error) + return error; + + if (index > nwaves - 1) { + dev_err(dev, "Index %u invalid for bank %u (%d waveforms)\n", index, bank, nwaves); + return -EINVAL; + } + + *trigger_index = index + index_start; + + return 0; +} + +static int cs40l26_custom_upload(struct cs40l26_vibra *vib, struct cs40l26_work *work_data, + struct cs40l26_effect *effect) +{ + size_t data_len = work_data->ff_effect->u.periodic.custom_len; + enum cs40l26_bank bank; + int error; + + if (data_len != 2) { + dev_err(vib->cs40l26->dev, "Invalid custom data length %zd\n", data_len); + return -EINVAL; + } + + bank = (enum cs40l26_bank)work_data->custom_data[0]; + + error = cs40l26_trigger_index_get(vib, work_data, bank, &effect->index); + if (error) + return error; + + effect->id = work_data->ff_effect->id; + effect->bank = bank; + + return 0; +} + +static struct cs40l26_effect *cs40l26_find_effect(struct cs40l26_vibra *vib, int id) +{ + struct cs40l26_effect *effect; + + if (list_empty(&vib->effect_head)) + return NULL; + + list_for_each_entry(effect, &vib->effect_head, list) { + if (effect->id == id) + return effect; + } + + return NULL; +} + +static void cs40l26_upload_worker(struct work_struct *work) +{ + struct cs40l26_work *work_data = container_of(work, struct cs40l26_work, work); + struct cs40l26_vibra *vib = work_data->vib; + struct device *dev = vib->cs40l26->dev; + struct cs40l26_effect *effect; + bool new_effect = false; + int error; + + error = pm_runtime_resume_and_get(dev); + if (error) { + work_data->error = error; + return; + } + + effect = cs40l26_find_effect(vib, work_data->ff_effect->id); + if (!effect) { + effect = devm_kzalloc(dev, sizeof(struct cs40l26_effect), GFP_KERNEL); + if (!effect) { + cs40l26_pm_exit(dev); + + work_data->error = -ENOMEM; + return; + } + + new_effect = true; + } + + if (work_data->ff_effect->u.periodic.waveform == FF_CUSTOM) { + error = cs40l26_custom_upload(vib, work_data, effect); + } else if (work_data->ff_effect->u.periodic.waveform == FF_SINE) { + error = cs40l26_sine_upload(vib, work_data, effect); + } else { + dev_err(dev, "Type 0x%X unsupported\n", work_data->ff_effect->u.periodic.waveform); + error = -EINVAL; + } + + if (error) { + if (new_effect) + devm_kfree(dev, effect); + + cs40l26_pm_exit(dev); + + work_data->error = error; + return; + } + + if (new_effect) + list_add(&effect->list, &vib->effect_head); + + cs40l26_pm_exit(dev); + + work_data->error = 0; +} + +static int cs40l26_upload(struct input_dev *dev, struct ff_effect *effect, struct ff_effect *old) +{ + struct cs40l26_vibra *vib = input_get_drvdata(dev); + bool custom = false; + struct cs40l26_work *work_data; + int error; + + work_data = kzalloc(sizeof(struct cs40l26_work), GFP_KERNEL); + if (!work_data) + return -ENOMEM; + + if (effect->u.periodic.waveform == FF_CUSTOM) { + work_data->custom_data = memdup_array_user(effect->u.periodic.custom_data, + effect->u.periodic.custom_len, + sizeof(s16)); + if (IS_ERR(work_data->custom_data)) { + error = PTR_ERR(work_data->custom_data); + goto out_free; + } + + custom = true; + work_data->custom_len = effect->u.periodic.custom_len; + } + + work_data->vib = vib; + work_data->ff_effect = effect; + + INIT_WORK(&work_data->work, cs40l26_upload_worker); + + queue_work(vib->vib_wq, &work_data->work); + flush_work(&work_data->work); + + error = work_data->error; + +out_free: + if (custom) + kfree(work_data->custom_data); + + kfree(work_data); + + return error; +} + +static void cs40l26_stop_playback_worker(struct work_struct *work) +{ + struct cs40l26_work *work_data = container_of(work, struct cs40l26_work, work); + struct cs40l26_vibra *vib = work_data->vib; + + if (pm_runtime_resume_and_get(vib->cs40l26->dev)) + goto out_free; + + if (cs40l26_dsp_write(vib->cs40l26, CS40L26_STOP_PLAYBACK)) + dev_err(vib->cs40l26->dev, "Failed to stop haptic playback\n"); + + cs40l26_pm_exit(vib->cs40l26->dev); +out_free: + kfree(work_data); +} + +static void cs40l26_start_playback_worker(struct work_struct *work) +{ + struct cs40l26_work *work_data = container_of(work, struct cs40l26_work, work); + struct cs40l26 *cs40l26 = work_data->vib->cs40l26; + struct cs40l26_effect *effect; + u16 duration; + int id; + + id = work_data->ff_effect->id; + + duration = work_data->ff_effect->replay.length; + + if (pm_runtime_resume_and_get(cs40l26->dev)) + goto out_free; + + guard(mutex)(&cs40l26->dsp.pwr_lock); + + if (cs40l26_fw_write(&cs40l26->dsp, "TIMEOUT_MS", cs40l26->variant->info->vibegen_algo_id, + duration)) + goto out_pm; + + effect = cs40l26_find_effect(work_data->vib, id); + if (effect) { + while (--work_data->count >= 0) { + if (cs40l26_dsp_write(cs40l26, effect->index)) + goto out_pm; + + usleep_range(duration, duration + 100); + } + } else { + dev_err(cs40l26->dev, "No effect found with ID %d\n", id); + } + +out_pm: + cs40l26_pm_exit(cs40l26->dev); + +out_free: + kfree(work_data); +} + +static int cs40l26_playback(struct input_dev *dev, int effect_id, int val) +{ + struct cs40l26_vibra *vib = input_get_drvdata(dev); + struct cs40l26_work *work_data; + + work_data = kzalloc(sizeof(struct cs40l26_work), GFP_ATOMIC); + if (!work_data) + return -ENOMEM; + + work_data->vib = vib; + + if (val > 0) { + work_data->ff_effect = &dev->ff->effects[effect_id]; + work_data->count = val; + INIT_WORK(&work_data->work, cs40l26_start_playback_worker); + } else { + INIT_WORK(&work_data->work, cs40l26_stop_playback_worker); + } + + queue_work(vib->vib_wq, &work_data->work); + + return 0; +} + +static int cs40l26_sine_erase(struct cs40l26_vibra *vib, int id) +{ + int slot = cs40l26_buzzgen_find_slot(id); + + if (slot == -1) { + dev_err(vib->cs40l26->dev, "No BUZZGEN ID matching %d\n", id); + return -EINVAL; + } + + cs40l26_buzzgen_configs[slot].effect_id = -1; + + return 0; +} + +static void cs40l26_erase_worker(struct work_struct *work) +{ + struct cs40l26_work *work_data = container_of(work, struct cs40l26_work, work); + struct cs40l26_vibra *vib = work_data->vib; + struct device *dev = vib->cs40l26->dev; + int id = work_data->ff_effect->id; + struct cs40l26_effect *effect; + int error; + + error = pm_runtime_resume_and_get(dev); + if (error) { + work_data->error = error; + return; + } + + effect = cs40l26_find_effect(vib, id); + if (!effect) { + dev_err(dev, "Cannot erase effect with ID %d, no such effect\n", id); + error = -EINVAL; + goto out_pm; + } + + if (effect->bank == CS40L26_BANK_BUZ) { + error = cs40l26_sine_erase(vib, id); + if (error) + goto out_pm; + } + + list_del(&effect->list); + devm_kfree(dev, effect); + +out_pm: + cs40l26_pm_exit(dev); + + work_data->error = error; +} + +static int cs40l26_erase(struct input_dev *dev, int effect_id) +{ + struct cs40l26_vibra *vib = input_get_drvdata(dev); + struct cs40l26_work *work_data; + int error; + + work_data = kzalloc(sizeof(struct cs40l26_work), GFP_KERNEL); + if (!work_data) + return -ENOMEM; + + work_data->vib = vib; + work_data->error = 0; + work_data->ff_effect = &dev->ff->effects[effect_id]; + + INIT_WORK(&work_data->work, cs40l26_erase_worker); + + queue_work(vib->vib_wq, &work_data->work); + flush_work(&work_data->work); + + error = work_data->error; + + kfree(work_data); + + return error; +} + +/* LUT for converting gain percentage to attenuation in dB */ +static const u32 cs40l26_atten_lut_q21_2[CS40L26_NUM_PCT_MAP_VALUES] = { + /* MUTE */ 400, 160, 136, 122, 112, 104, 98, 92, 88, 84, 80, 77, 74, + 71, 68, 66, 64, 62, 60, 58, 56, 54, 53, 51, 50, 48, 47, 45, 44, 43, + 42, 41, 40, 39, 37, 36, 35, 35, 34, 33, 32, 31, 30, 29, 29, 28, 27, + 26, 26, 25, 24, 23, 23, 22, 21, 21, 20, 20, 19, 18, 18, 17, 17, 16, + 16, 15, 14, 14, 13, 13, 12, 12, 11, 11, 10, 10, 10, 9, 9, 8, 8, 7, + 7, 6, 6, 6, 5, 5, 4, 4, 4, 3, 3, 3, 2, 2, 1, 1, 1, 0, 0, /* 100% */ +}; + +static void cs40l26_set_gain_worker(struct work_struct *work) +{ + struct cs40l26_work *work_data = container_of(work, struct cs40l26_work, work); + struct cs40l26_vibra *vib = work_data->vib; + struct cs40l26 *cs40l26 = vib->cs40l26; + int error; + + error = pm_runtime_resume_and_get(vib->cs40l26->dev); + if (error) { + dev_err(vib->cs40l26->dev, "%s: Failed to exit hibernate\n", __func__); + goto out_free; + } + + guard(mutex)(&vib->cs40l26->dsp.pwr_lock); + + error = cs40l26_fw_write(&vib->cs40l26->dsp, "SOURCE_ATTENUATION", + cs40l26->variant->info->ram_ext_algo_id, + cs40l26_atten_lut_q21_2[work_data->gain_pct]); + if (error) + dev_err(vib->cs40l26->dev, "Failed to set attenuation\n"); + + cs40l26_pm_exit(vib->cs40l26->dev); + +out_free: + kfree(work_data); +} + +static void cs40l26_set_gain(struct input_dev *dev, u16 gain) +{ + struct cs40l26_vibra *vib = input_get_drvdata(dev); + struct cs40l26_work *work_data; + + if (gain >= CS40L26_NUM_PCT_MAP_VALUES) { + dev_err(vib->cs40l26->dev, "Gain value %u%% out of bounds\n", gain); + return; + } + + work_data = kzalloc(sizeof(struct cs40l26_work), GFP_ATOMIC); + if (!work_data) + return; + + work_data->gain_pct = gain; + work_data->vib = vib; + + INIT_WORK(&work_data->work, cs40l26_set_gain_worker); + + queue_work(vib->vib_wq, &work_data->work); +} + +static void cs40l26_remove_wq(void *data) +{ + flush_workqueue(data); + destroy_workqueue((struct workqueue_struct *)data); +} + +static int cs40l26_vibra_probe(struct platform_device *pdev) +{ + struct cs40l26 *cs40l26 = dev_get_drvdata(pdev->dev.parent); + struct cs40l26_vibra *vib; + int error; + + vib = devm_kzalloc(cs40l26->dev, sizeof(struct cs40l26_vibra), GFP_KERNEL); + if (!vib) + return -ENOMEM; + + vib->cs40l26 = cs40l26; + + vib->input = devm_input_allocate_device(vib->cs40l26->dev); + if (!vib->input) + return -ENOMEM; + + vib->input->id.product = cs40l26->devid; + vib->input->id.version = cs40l26->revid; + vib->input->name = "cs40l26_vibra"; + + input_set_drvdata(vib->input, vib); + input_set_capability(vib->input, EV_FF, FF_PERIODIC); + input_set_capability(vib->input, EV_FF, FF_CUSTOM); + input_set_capability(vib->input, EV_FF, FF_SINE); + input_set_capability(vib->input, EV_FF, FF_GAIN); + + error = input_ff_create(vib->input, 1); + if (error) { + dev_err(vib->cs40l26->dev, "Failed to create input device\n"); + return error; + } + + clear_bit(FF_RUMBLE, vib->input->ffbit); + + vib->input->ff->upload = cs40l26_upload; + vib->input->ff->playback = cs40l26_playback; + vib->input->ff->set_gain = cs40l26_set_gain; + vib->input->ff->erase = cs40l26_erase; + + INIT_LIST_HEAD(&vib->effect_head); + + vib->vib_wq = alloc_ordered_workqueue("vib_wq", WQ_HIGHPRI); + if (!vib->vib_wq) + return -ENOMEM; + + error = devm_add_action_or_reset(vib->cs40l26->dev, cs40l26_remove_wq, vib->vib_wq); + if (error) + return error; + + error = input_register_device(vib->input); + if (error) + return error; + + dev_info(vib->cs40l26->dev, "Loaded cs40l26-vibra with %d RAM waveforms\n", + cs40l26_num_ram_waves(vib)); + + return 0; +} + +static const struct platform_device_id cs40l26_vibra_id_match[] = { + { "cs40l26-vibra", }, + {} +}; +MODULE_DEVICE_TABLE(platform, cs40l26_vibra_id_match); + +static struct platform_driver cs40l26_vibra_driver = { + .probe = cs40l26_vibra_probe, + .id_table = cs40l26_vibra_id_match, + .driver = { + .name = "cs40l26-vibra", + }, +}; +module_platform_driver(cs40l26_vibra_driver); + +MODULE_DESCRIPTION("CS40L26 Boosted Class D Amplifier for Haptics"); +MODULE_AUTHOR("Fred Treven, Cirrus Logic Inc. ftreven@opensource.cirrus.com"); +MODULE_LICENSE("GPL");

Hi Fred,
kernel test robot noticed the following build errors:
[auto build test ERROR on lee-mfd/for-mfd-next] [also build test ERROR on broonie-sound/for-next linus/master v6.14-rc3 next-20250218] [cannot apply to dtor-input/next dtor-input/for-linus lee-mfd/for-mfd-fixes] [If your patch is applied to the wrong git tree, kindly drop us a note. And when submitting patch, we suggest to use '--base' as documented in https://git-scm.com/docs/git-format-patch#_base_tree_information]
url: https://github.com/intel-lab-lkp/linux/commits/Fred-Treven/firmware-cs_dsp-F... base: https://git.kernel.org/pub/scm/linux/kernel/git/lee/mfd.git for-mfd-next patch link: https://lore.kernel.org/r/20250204231835.2000457-8-ftreven%40opensource.cirr... patch subject: [PATCH RESEND 7/7] Input: cs40l26 - Add support for CS40L26 haptic driver config: s390-allmodconfig (https://download.01.org/0day-ci/archive/20250219/202502190805.6XI1dg3G-lkp@i...) compiler: clang version 19.1.3 (https://github.com/llvm/llvm-project ab51eccf88f5321e7c60591c5546b254b6afab99) reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20250219/202502190805.6XI1dg3G-lkp@i...)
If you fix the issue in a separate patch/commit (i.e. not just a new version of the same patch/commit), kindly add following tags | Reported-by: kernel test robot lkp@intel.com | Closes: https://lore.kernel.org/oe-kbuild-all/202502190805.6XI1dg3G-lkp@intel.com/
All errors (new ones prefixed by >>):
In file included from drivers/input/misc/cs40l26-vibra.c:12: In file included from include/linux/input.h:19: In file included from include/linux/device.h:32: In file included from include/linux/device/driver.h:21: In file included from include/linux/module.h:19: In file included from include/linux/elf.h:6: In file included from arch/s390/include/asm/elf.h:181: In file included from arch/s390/include/asm/mmu_context.h:11: In file included from arch/s390/include/asm/pgalloc.h:18: In file included from include/linux/mm.h:2224: include/linux/vmstat.h:504:43: warning: arithmetic between different enumeration types ('enum zone_stat_item' and 'enum numa_stat_item') [-Wenum-enum-conversion] 504 | return vmstat_text[NR_VM_ZONE_STAT_ITEMS + | ~~~~~~~~~~~~~~~~~~~~~ ^ 505 | item]; | ~~~~ include/linux/vmstat.h:511:43: warning: arithmetic between different enumeration types ('enum zone_stat_item' and 'enum numa_stat_item') [-Wenum-enum-conversion] 511 | return vmstat_text[NR_VM_ZONE_STAT_ITEMS + | ~~~~~~~~~~~~~~~~~~~~~ ^ 512 | NR_VM_NUMA_EVENT_ITEMS + | ~~~~~~~~~~~~~~~~~~~~~~ include/linux/vmstat.h:524:43: warning: arithmetic between different enumeration types ('enum zone_stat_item' and 'enum numa_stat_item') [-Wenum-enum-conversion] 524 | return vmstat_text[NR_VM_ZONE_STAT_ITEMS + | ~~~~~~~~~~~~~~~~~~~~~ ^ 525 | NR_VM_NUMA_EVENT_ITEMS + | ~~~~~~~~~~~~~~~~~~~~~~
drivers/input/misc/cs40l26-vibra.c:400:3: error: cannot jump from this goto statement to its label
400 | goto out_free; | ^ drivers/input/misc/cs40l26-vibra.c:402:2: note: jump bypasses initialization of variable with __attribute__((cleanup)) 402 | guard(mutex)(&cs40l26->dsp.pwr_lock); | ^ include/linux/cleanup.h:309:15: note: expanded from macro 'guard' 309 | CLASS(_name, __UNIQUE_ID(guard)) | ^ include/linux/compiler.h:166:29: note: expanded from macro '__UNIQUE_ID' 166 | #define __UNIQUE_ID(prefix) __PASTE(__PASTE(__UNIQUE_ID_, prefix), __COUNTER__) | ^ include/linux/compiler_types.h:84:22: note: expanded from macro '__PASTE' 84 | #define __PASTE(a,b) ___PASTE(a,b) | ^ include/linux/compiler_types.h:83:23: note: expanded from macro '___PASTE' 83 | #define ___PASTE(a,b) a##b | ^ <scratch space>:141:1: note: expanded from here 141 | __UNIQUE_ID_guard557 | ^ drivers/input/misc/cs40l26-vibra.c:548:3: error: cannot jump from this goto statement to its label 548 | goto out_free; | ^ drivers/input/misc/cs40l26-vibra.c:551:2: note: jump bypasses initialization of variable with __attribute__((cleanup)) 551 | guard(mutex)(&vib->cs40l26->dsp.pwr_lock); | ^ include/linux/cleanup.h:309:15: note: expanded from macro 'guard' 309 | CLASS(_name, __UNIQUE_ID(guard)) | ^ include/linux/compiler.h:166:29: note: expanded from macro '__UNIQUE_ID' 166 | #define __UNIQUE_ID(prefix) __PASTE(__PASTE(__UNIQUE_ID_, prefix), __COUNTER__) | ^ include/linux/compiler_types.h:84:22: note: expanded from macro '__PASTE' 84 | #define __PASTE(a,b) ___PASTE(a,b) | ^ include/linux/compiler_types.h:83:23: note: expanded from macro '___PASTE' 83 | #define ___PASTE(a,b) a##b | ^ <scratch space>:169:1: note: expanded from here 169 | __UNIQUE_ID_guard558 | ^ 3 warnings and 2 errors generated.
vim +400 drivers/input/misc/cs40l26-vibra.c
386 387 static void cs40l26_start_playback_worker(struct work_struct *work) 388 { 389 struct cs40l26_work *work_data = container_of(work, struct cs40l26_work, work); 390 struct cs40l26 *cs40l26 = work_data->vib->cs40l26; 391 struct cs40l26_effect *effect; 392 u16 duration; 393 int id; 394 395 id = work_data->ff_effect->id; 396 397 duration = work_data->ff_effect->replay.length; 398 399 if (pm_runtime_resume_and_get(cs40l26->dev))
400 goto out_free;
401 402 guard(mutex)(&cs40l26->dsp.pwr_lock); 403 404 if (cs40l26_fw_write(&cs40l26->dsp, "TIMEOUT_MS", cs40l26->variant->info->vibegen_algo_id, 405 duration)) 406 goto out_pm; 407 408 effect = cs40l26_find_effect(work_data->vib, id); 409 if (effect) { 410 while (--work_data->count >= 0) { 411 if (cs40l26_dsp_write(cs40l26, effect->index)) 412 goto out_pm; 413 414 usleep_range(duration, duration + 100); 415 } 416 } else { 417 dev_err(cs40l26->dev, "No effect found with ID %d\n", id); 418 } 419 420 out_pm: 421 cs40l26_pm_exit(cs40l26->dev); 422 423 out_free: 424 kfree(work_data); 425 } 426

Hi Fred,
On Tue, Feb 04, 2025 at 05:18:29PM -0600, Fred Treven wrote:
Introduce driver for Cirrus Logic Device CS40L26: A boosted haptics driver with integrated DSP and waveform memory with advanced closed loop algorithms and LRA protection.
The core CS40L26 driver is in MFD and touches the Input Force Feedback subsystem for haptics and the ASoC subsystem for audio to haptics streaming.
This patchset includes changes to the CS DSP firmware driver which fixes two bugs and introduces support for multiple coefficient files.
Fred Treven (7): firmware: cs_dsp: Fix error checking in wseq_write() firmware: cs_dsp: Check for valid num_regs in cs_dsp_wseq_multi_write() firmware: cs_dsp: Add ability to load multiple coefficient files dt-bindings: mfd: cirrus,cs40l26: Support for CS40L26 mfd: cs40l26: Add support for CS40L26 core driver ASoC: cs40l26: Support I2S streaming to CS40L26 Input: cs40l26 - Add support for CS40L26 haptic driver
.../bindings/mfd/cirrus,cs40l26.yaml | 81 + MAINTAINERS | 4 +- drivers/firmware/cirrus/cs_dsp.c | 70 +- drivers/input/misc/Kconfig | 10 + drivers/input/misc/Makefile | 1 + drivers/input/misc/cs40l26-vibra.c | 669 ++++++++ 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/firmware/cirrus/cs_dsp.h | 14 + include/linux/mfd/cs40l26.h | 341 ++++ sound/soc/codecs/Kconfig | 12 + sound/soc/codecs/Makefile | 2 + sound/soc/codecs/cs40l26-codec.c | 523 ++++++ 16 files changed, 3281 insertions(+), 17 deletions(-) create mode 100644 Documentation/devicetree/bindings/mfd/cirrus,cs40l26.yaml create mode 100644 drivers/input/misc/cs40l26-vibra.c 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 create mode 100644 sound/soc/codecs/cs40l26-codec.c
-- 2.34.1
Thank you for this high-quality series!
When the CS40L50 MFD landed last year, I (probably naively) imagined that CS40L26 and CS40L50 were close cousins. My understanding was that CS40L50 implements some basic DSP functionality in ROM, which can be overridden with firmware uploaded at runtime, while CS40L26 requires firmware to be uploaded at runtime in any circumstance.
To that end, I recall during the CS40L50 MFD review that there was some hope the CS40L50 MFD could be gently extended to support both devices, and a separate input driver may be required. At the very least, I was surprised to see an all-new codec driver, since the codec child is essentially just a stub driver in both cases.
If a completely new MFD and children are utlimately required here, then so be it; I just wanted to ask whether you had considered if it's possible to re-use and/or extend any existing driver(s) here. Assuming so, I think it can be helpful to speak to this exercise in the cover letter and explain your reasoning to the review audience.
Kind regards, Jeff LaBundy
participants (6)
-
Fred Treven
-
Jeff LaBundy
-
kernel test robot
-
Krzysztof Kozlowski
-
Lee Jones
-
Richard Fitzgerald