* [PATCH RESEND 1/7] firmware: cs_dsp: Fix error checking in wseq_write()
2025-02-04 23:18 [PATCH RESEND 0/7] Initial Support for CS40L26 Fred Treven
@ 2025-02-04 23:18 ` Fred Treven
2025-02-04 23:18 ` [PATCH RESEND 2/7] firmware: cs_dsp: Check for valid num_regs in cs_dsp_wseq_multi_write() Fred Treven
` (6 subsequent siblings)
7 siblings, 0 replies; 16+ messages in thread
From: Fred Treven @ 2025-02-04 23:18 UTC (permalink / raw)
To: Lee Jones, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Simon Trimmer, Charles Keepax, Richard Fitzgerald,
Dmitry Torokhov, James Ogletree, Ben Bright, Liam Girdwood,
Mark Brown, Jaroslav Kysela, Takashi Iwai, David Rhodes,
Jeff LaBundy, Heiko Stuebner, Karel Balej, Igor Prusov, Jack Yu,
Weidong Wang, Binbin Zhou, Prasad Kumpatla, Paul Handrigan,
Masahiro Yamada, Nuno Sa, Fred Treven
Cc: alsa-devel, patches, devicetree, linux-kernel, linux-input,
linux-sound
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;
--
2.34.1
^ permalink raw reply related [flat|nested] 16+ messages in thread* [PATCH RESEND 2/7] firmware: cs_dsp: Check for valid num_regs in cs_dsp_wseq_multi_write()
2025-02-04 23:18 [PATCH RESEND 0/7] Initial Support for CS40L26 Fred Treven
2025-02-04 23:18 ` [PATCH RESEND 1/7] firmware: cs_dsp: Fix error checking in wseq_write() Fred Treven
@ 2025-02-04 23:18 ` Fred Treven
2025-02-04 23:18 ` [PATCH RESEND 3/7] firmware: cs_dsp: Add ability to load multiple coefficient files Fred Treven
` (5 subsequent siblings)
7 siblings, 0 replies; 16+ messages in thread
From: Fred Treven @ 2025-02-04 23:18 UTC (permalink / raw)
To: Lee Jones, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Simon Trimmer, Charles Keepax, Richard Fitzgerald,
Dmitry Torokhov, James Ogletree, Ben Bright, Liam Girdwood,
Mark Brown, Jaroslav Kysela, Takashi Iwai, David Rhodes,
Jeff LaBundy, Heiko Stuebner, Karel Balej, Igor Prusov, Jack Yu,
Weidong Wang, Binbin Zhou, Prasad Kumpatla, Paul Handrigan,
Masahiro Yamada, Nuno Sa, Fred Treven
Cc: alsa-devel, patches, devicetree, linux-kernel, linux-input,
linux-sound
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);
--
2.34.1
^ permalink raw reply related [flat|nested] 16+ messages in thread* [PATCH RESEND 3/7] firmware: cs_dsp: Add ability to load multiple coefficient files
2025-02-04 23:18 [PATCH RESEND 0/7] Initial Support for CS40L26 Fred Treven
2025-02-04 23:18 ` [PATCH RESEND 1/7] firmware: cs_dsp: Fix error checking in wseq_write() Fred Treven
2025-02-04 23:18 ` [PATCH RESEND 2/7] firmware: cs_dsp: Check for valid num_regs in cs_dsp_wseq_multi_write() Fred Treven
@ 2025-02-04 23:18 ` Fred Treven
2025-02-04 23:18 ` [PATCH RESEND 4/7] dt-bindings: mfd: cirrus,cs40l26: Support for CS40L26 Fred Treven
` (4 subsequent siblings)
7 siblings, 0 replies; 16+ messages in thread
From: Fred Treven @ 2025-02-04 23:18 UTC (permalink / raw)
To: Lee Jones, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Simon Trimmer, Charles Keepax, Richard Fitzgerald,
Dmitry Torokhov, James Ogletree, Ben Bright, Liam Girdwood,
Mark Brown, Jaroslav Kysela, Takashi Iwai, David Rhodes,
Jeff LaBundy, Heiko Stuebner, Karel Balej, Igor Prusov, Jack Yu,
Weidong Wang, Binbin Zhou, Prasad Kumpatla, Paul Handrigan,
Masahiro Yamada, Nuno Sa, Fred Treven
Cc: alsa-devel, patches, devicetree, linux-kernel, linux-input,
linux-sound
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,
--
2.34.1
^ permalink raw reply related [flat|nested] 16+ messages in thread* [PATCH RESEND 4/7] dt-bindings: mfd: cirrus,cs40l26: Support for CS40L26
2025-02-04 23:18 [PATCH RESEND 0/7] Initial Support for CS40L26 Fred Treven
` (2 preceding siblings ...)
2025-02-04 23:18 ` [PATCH RESEND 3/7] firmware: cs_dsp: Add ability to load multiple coefficient files Fred Treven
@ 2025-02-04 23:18 ` Fred Treven
2025-02-05 10:09 ` Krzysztof Kozlowski
2025-02-04 23:18 ` [PATCH RESEND 5/7] mfd: cs40l26: Add support for CS40L26 core driver Fred Treven
` (3 subsequent siblings)
7 siblings, 1 reply; 16+ messages in thread
From: Fred Treven @ 2025-02-04 23:18 UTC (permalink / raw)
To: Lee Jones, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Simon Trimmer, Charles Keepax, Richard Fitzgerald,
Dmitry Torokhov, James Ogletree, Ben Bright, Liam Girdwood,
Mark Brown, Jaroslav Kysela, Takashi Iwai, David Rhodes,
Jeff LaBundy, Heiko Stuebner, Karel Balej, Igor Prusov, Jack Yu,
Weidong Wang, Binbin Zhou, Prasad Kumpatla, Paul Handrigan,
Masahiro Yamada, Nuno Sa, Fred Treven
Cc: alsa-devel, patches, devicetree, linux-kernel, linux-input,
linux-sound
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*
--
2.34.1
^ permalink raw reply related [flat|nested] 16+ messages in thread* Re: [PATCH RESEND 4/7] dt-bindings: mfd: cirrus,cs40l26: Support for CS40L26
2025-02-04 23:18 ` [PATCH RESEND 4/7] dt-bindings: mfd: cirrus,cs40l26: Support for CS40L26 Fred Treven
@ 2025-02-05 10:09 ` Krzysztof Kozlowski
0 siblings, 0 replies; 16+ messages in thread
From: Krzysztof Kozlowski @ 2025-02-05 10:09 UTC (permalink / raw)
To: Fred Treven
Cc: Lee Jones, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Simon Trimmer, Charles Keepax, Richard Fitzgerald,
Dmitry Torokhov, James Ogletree, Ben Bright, Liam Girdwood,
Mark Brown, Jaroslav Kysela, Takashi Iwai, David Rhodes,
Jeff LaBundy, Heiko Stuebner, Karel Balej, Igor Prusov, Jack Yu,
Weidong Wang, Binbin Zhou, Prasad Kumpatla, Paul Handrigan,
Masahiro Yamada, Nuno Sa, alsa-devel, patches, devicetree,
linux-kernel, linux-input, linux-sound
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
^ permalink raw reply [flat|nested] 16+ messages in thread
* [PATCH RESEND 5/7] mfd: cs40l26: Add support for CS40L26 core driver
2025-02-04 23:18 [PATCH RESEND 0/7] Initial Support for CS40L26 Fred Treven
` (3 preceding siblings ...)
2025-02-04 23:18 ` [PATCH RESEND 4/7] dt-bindings: mfd: cirrus,cs40l26: Support for CS40L26 Fred Treven
@ 2025-02-04 23:18 ` Fred Treven
2025-02-05 10:34 ` Krzysztof Kozlowski
2025-02-04 23:18 ` [PATCH RESEND 6/7] ASoC: cs40l26: Support I2S streaming to CS40L26 Fred Treven
` (2 subsequent siblings)
7 siblings, 1 reply; 16+ messages in thread
From: Fred Treven @ 2025-02-04 23:18 UTC (permalink / raw)
To: Lee Jones, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Simon Trimmer, Charles Keepax, Richard Fitzgerald,
Dmitry Torokhov, James Ogletree, Ben Bright, Liam Girdwood,
Mark Brown, Jaroslav Kysela, Takashi Iwai, David Rhodes,
Jeff LaBundy, Heiko Stuebner, Karel Balej, Igor Prusov, Jack Yu,
Weidong Wang, Binbin Zhou, Prasad Kumpatla, Paul Handrigan,
Masahiro Yamada, Nuno Sa, Fred Treven
Cc: alsa-devel, patches, devicetree, linux-kernel, linux-input,
linux-sound
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__ */
--
2.34.1
^ permalink raw reply related [flat|nested] 16+ messages in thread* Re: [PATCH RESEND 5/7] mfd: cs40l26: Add support for CS40L26 core driver
2025-02-04 23:18 ` [PATCH RESEND 5/7] mfd: cs40l26: Add support for CS40L26 core driver Fred Treven
@ 2025-02-05 10:34 ` Krzysztof Kozlowski
2025-02-11 21:16 ` Fred Treven
0 siblings, 1 reply; 16+ messages in thread
From: Krzysztof Kozlowski @ 2025-02-05 10:34 UTC (permalink / raw)
To: Fred Treven, Lee Jones, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Simon Trimmer, Charles Keepax, Richard Fitzgerald,
Dmitry Torokhov, James Ogletree, Ben Bright, Liam Girdwood,
Mark Brown, Jaroslav Kysela, Takashi Iwai, David Rhodes,
Jeff LaBundy, Heiko Stuebner, Karel Balej, Igor Prusov, Jack Yu,
Weidong Wang, Binbin Zhou, Prasad Kumpatla, Paul Handrigan,
Masahiro Yamada, Nuno Sa
Cc: alsa-devel, patches, devicetree, linux-kernel, linux-input,
linux-sound
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/submitting-patches.rst#L597
> +
> +#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
^ permalink raw reply [flat|nested] 16+ messages in thread* Re: [PATCH RESEND 5/7] mfd: cs40l26: Add support for CS40L26 core driver
2025-02-05 10:34 ` Krzysztof Kozlowski
@ 2025-02-11 21:16 ` Fred Treven
2025-02-12 6:19 ` Krzysztof Kozlowski
` (2 more replies)
0 siblings, 3 replies; 16+ messages in thread
From: Fred Treven @ 2025-02-11 21:16 UTC (permalink / raw)
To: Krzysztof Kozlowski, Lee Jones, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Simon Trimmer, Charles Keepax, Richard Fitzgerald,
Dmitry Torokhov, James Ogletree, Ben Bright, Liam Girdwood,
Mark Brown, Jaroslav Kysela, Takashi Iwai, David Rhodes,
Jeff LaBundy, Heiko Stuebner, Karel Balej, Igor Prusov, Jack Yu,
Weidong Wang, Binbin Zhou, Prasad Kumpatla, Paul Handrigan,
Masahiro Yamada, Nuno Sa
Cc: alsa-devel, patches, devicetree, linux-kernel, linux-input,
linux-sound
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/submitting-patches.rst#L597
>
>
>> +
>> +#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
^ permalink raw reply [flat|nested] 16+ messages in thread* Re: [PATCH RESEND 5/7] mfd: cs40l26: Add support for CS40L26 core driver
2025-02-11 21:16 ` Fred Treven
@ 2025-02-12 6:19 ` Krzysztof Kozlowski
2025-02-12 15:10 ` Richard Fitzgerald
2025-02-12 15:50 ` Lee Jones
2 siblings, 0 replies; 16+ messages in thread
From: Krzysztof Kozlowski @ 2025-02-12 6:19 UTC (permalink / raw)
To: Fred Treven, Lee Jones, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Simon Trimmer, Charles Keepax, Richard Fitzgerald,
Dmitry Torokhov, James Ogletree, Ben Bright, Liam Girdwood,
Mark Brown, Jaroslav Kysela, Takashi Iwai, David Rhodes,
Jeff LaBundy, Heiko Stuebner, Karel Balej, Igor Prusov, Jack Yu,
Weidong Wang, Binbin Zhou, Prasad Kumpatla, Paul Handrigan,
Masahiro Yamada, Nuno Sa
Cc: alsa-devel, patches, devicetree, linux-kernel, linux-input,
linux-sound
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
^ permalink raw reply [flat|nested] 16+ messages in thread* Re: [PATCH RESEND 5/7] mfd: cs40l26: Add support for CS40L26 core driver
2025-02-11 21:16 ` Fred Treven
2025-02-12 6:19 ` Krzysztof Kozlowski
@ 2025-02-12 15:10 ` Richard Fitzgerald
2025-02-12 15:50 ` Lee Jones
2 siblings, 0 replies; 16+ messages in thread
From: Richard Fitzgerald @ 2025-02-12 15:10 UTC (permalink / raw)
To: Fred Treven, Krzysztof Kozlowski, Lee Jones, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Simon Trimmer, Charles Keepax,
Dmitry Torokhov, James Ogletree, Ben Bright, Liam Girdwood,
Mark Brown, Jaroslav Kysela, Takashi Iwai, David Rhodes,
Jeff LaBundy, Heiko Stuebner, Karel Balej, Igor Prusov, Jack Yu,
Weidong Wang, Binbin Zhou, Prasad Kumpatla, Paul Handrigan,
Masahiro Yamada, Nuno Sa
Cc: alsa-devel, patches, devicetree, linux-kernel, linux-input,
linux-sound
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.
^ permalink raw reply [flat|nested] 16+ messages in thread* Re: [PATCH RESEND 5/7] mfd: cs40l26: Add support for CS40L26 core driver
2025-02-11 21:16 ` Fred Treven
2025-02-12 6:19 ` Krzysztof Kozlowski
2025-02-12 15:10 ` Richard Fitzgerald
@ 2025-02-12 15:50 ` Lee Jones
2025-02-16 22:10 ` Jeff LaBundy
2 siblings, 1 reply; 16+ messages in thread
From: Lee Jones @ 2025-02-12 15:50 UTC (permalink / raw)
To: Fred Treven
Cc: Krzysztof Kozlowski, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Simon Trimmer, Charles Keepax, Richard Fitzgerald,
Dmitry Torokhov, James Ogletree, Ben Bright, Liam Girdwood,
Mark Brown, Jaroslav Kysela, Takashi Iwai, David Rhodes,
Jeff LaBundy, Heiko Stuebner, Karel Balej, Igor Prusov, Jack Yu,
Weidong Wang, Binbin Zhou, Prasad Kumpatla, Paul Handrigan,
Masahiro Yamada, Nuno Sa, alsa-devel, patches, devicetree,
linux-kernel, linux-input, linux-sound
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/submitting-patches.rst#L597
> >
> >
> > > +
> > > +#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.
--
Lee Jones [李琼斯]
^ permalink raw reply [flat|nested] 16+ messages in thread* Re: [PATCH RESEND 5/7] mfd: cs40l26: Add support for CS40L26 core driver
2025-02-12 15:50 ` Lee Jones
@ 2025-02-16 22:10 ` Jeff LaBundy
0 siblings, 0 replies; 16+ messages in thread
From: Jeff LaBundy @ 2025-02-16 22:10 UTC (permalink / raw)
To: Lee Jones
Cc: Fred Treven, Krzysztof Kozlowski, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Simon Trimmer, Charles Keepax,
Richard Fitzgerald, Dmitry Torokhov, James Ogletree, Ben Bright,
Liam Girdwood, Mark Brown, Jaroslav Kysela, Takashi Iwai,
David Rhodes, Heiko Stuebner, Karel Balej, Igor Prusov, Jack Yu,
Weidong Wang, Binbin Zhou, Prasad Kumpatla, Paul Handrigan,
Masahiro Yamada, Nuno Sa, alsa-devel, patches, devicetree,
linux-kernel, linux-input, linux-sound
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
^ permalink raw reply [flat|nested] 16+ messages in thread
* [PATCH RESEND 6/7] ASoC: cs40l26: Support I2S streaming to CS40L26
2025-02-04 23:18 [PATCH RESEND 0/7] Initial Support for CS40L26 Fred Treven
` (4 preceding siblings ...)
2025-02-04 23:18 ` [PATCH RESEND 5/7] mfd: cs40l26: Add support for CS40L26 core driver Fred Treven
@ 2025-02-04 23:18 ` Fred Treven
2025-02-04 23:18 ` [PATCH RESEND 7/7] Input: cs40l26 - Add support for CS40L26 haptic driver Fred Treven
2025-02-16 21:13 ` [PATCH RESEND 0/7] Initial Support for CS40L26 Jeff LaBundy
7 siblings, 0 replies; 16+ messages in thread
From: Fred Treven @ 2025-02-04 23:18 UTC (permalink / raw)
To: Lee Jones, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Simon Trimmer, Charles Keepax, Richard Fitzgerald,
Dmitry Torokhov, James Ogletree, Ben Bright, Liam Girdwood,
Mark Brown, Jaroslav Kysela, Takashi Iwai, David Rhodes,
Jeff LaBundy, Heiko Stuebner, Karel Balej, Igor Prusov, Jack Yu,
Weidong Wang, Binbin Zhou, Prasad Kumpatla, Paul Handrigan,
Masahiro Yamada, Nuno Sa, Fred Treven
Cc: alsa-devel, patches, devicetree, linux-kernel, linux-input,
linux-sound
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");
--
2.34.1
^ permalink raw reply related [flat|nested] 16+ messages in thread* [PATCH RESEND 7/7] Input: cs40l26 - Add support for CS40L26 haptic driver
2025-02-04 23:18 [PATCH RESEND 0/7] Initial Support for CS40L26 Fred Treven
` (5 preceding siblings ...)
2025-02-04 23:18 ` [PATCH RESEND 6/7] ASoC: cs40l26: Support I2S streaming to CS40L26 Fred Treven
@ 2025-02-04 23:18 ` Fred Treven
2025-02-16 21:13 ` [PATCH RESEND 0/7] Initial Support for CS40L26 Jeff LaBundy
7 siblings, 0 replies; 16+ messages in thread
From: Fred Treven @ 2025-02-04 23:18 UTC (permalink / raw)
To: Lee Jones, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Simon Trimmer, Charles Keepax, Richard Fitzgerald,
Dmitry Torokhov, James Ogletree, Ben Bright, Liam Girdwood,
Mark Brown, Jaroslav Kysela, Takashi Iwai, David Rhodes,
Jeff LaBundy, Heiko Stuebner, Karel Balej, Igor Prusov, Jack Yu,
Weidong Wang, Binbin Zhou, Prasad Kumpatla, Paul Handrigan,
Masahiro Yamada, Nuno Sa, Fred Treven
Cc: alsa-devel, patches, devicetree, linux-kernel, linux-input,
linux-sound
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");
--
2.34.1
^ permalink raw reply related [flat|nested] 16+ messages in thread* Re: [PATCH RESEND 0/7] Initial Support for CS40L26
2025-02-04 23:18 [PATCH RESEND 0/7] Initial Support for CS40L26 Fred Treven
` (6 preceding siblings ...)
2025-02-04 23:18 ` [PATCH RESEND 7/7] Input: cs40l26 - Add support for CS40L26 haptic driver Fred Treven
@ 2025-02-16 21:13 ` Jeff LaBundy
7 siblings, 0 replies; 16+ messages in thread
From: Jeff LaBundy @ 2025-02-16 21:13 UTC (permalink / raw)
To: Fred Treven
Cc: Lee Jones, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Simon Trimmer, Charles Keepax, Richard Fitzgerald,
Dmitry Torokhov, James Ogletree, Ben Bright, Liam Girdwood,
Mark Brown, Jaroslav Kysela, Takashi Iwai, David Rhodes,
Heiko Stuebner, Karel Balej, Igor Prusov, Jack Yu, Weidong Wang,
Binbin Zhou, Prasad Kumpatla, Paul Handrigan, Masahiro Yamada,
Nuno Sa, alsa-devel, patches, devicetree, linux-kernel,
linux-input, linux-sound
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
^ permalink raw reply [flat|nested] 16+ messages in thread