diff mbox series

[2/2] Input: cs40l50 - Initial support for Cirrus Logic CS40L50

Message ID 20230808172511.665787-2-james.ogletree@cirrus.com
State Superseded
Headers show
Series [1/2] dt-bindings: input: cirrus,cs40l50: Support for CS40L50 | expand

Commit Message

James Ogletree Aug. 8, 2023, 5:25 p.m. UTC
Introduce support for Cirrus Logic Device CS40L50: a
haptics driver with waveform memory DSP and closed-loop
algorithms.

Change-Id: I468e8f726e81072ebea19b2dfbe23ca775fedc97
Signed-off-by: James Ogletree <james.ogletree@cirrus.com>
---
 MAINTAINERS                      |    2 +
 drivers/input/misc/Kconfig       |   33 +
 drivers/input/misc/Makefile      |    3 +
 drivers/input/misc/cs40l50-i2c.c |   67 ++
 drivers/input/misc/cs40l50-spi.c |   67 ++
 drivers/input/misc/cs40l50.c     | 1013 ++++++++++++++++++++++++++++++
 include/linux/input/cs40l50.h    |  321 ++++++++++
 7 files changed, 1506 insertions(+)
 create mode 100644 drivers/input/misc/cs40l50-i2c.c
 create mode 100644 drivers/input/misc/cs40l50-spi.c
 create mode 100644 drivers/input/misc/cs40l50.c
 create mode 100644 include/linux/input/cs40l50.h

Comments

Krzysztof Kozlowski Aug. 9, 2023, 6:47 a.m. UTC | #1
On 08/08/2023 19:25, James Ogletree wrote:
> Introduce support for Cirrus Logic Device CS40L50: a
> haptics driver with waveform memory DSP and closed-loop
> algorithms.
> 
> Change-Id: I468e8f726e81072ebea19b2dfbe23ca775fedc97

Please run scripts/checkpatch.pl and fix reported warnings. Some
warnings can be ignored, but the code here looks like it needs a fix.
Feel free to get in touch if the warning is not clear.

> Signed-off-by: James Ogletree <james.ogletree@cirrus.com>
> ---
>  MAINTAINERS                      |    2 +
>  drivers/input/misc/Kconfig       |   33 +
>  drivers/input/misc/Makefile      |    3 +
>  drivers/input/misc/cs40l50-i2c.c |   67 ++
>  drivers/input/misc/cs40l50-spi.c |   67 ++
>  drivers/input/misc/cs40l50.c     | 1013 ++++++++++++++++++++++++++++++
>  include/linux/input/cs40l50.h    |  321 ++++++++++
>  7 files changed, 1506 insertions(+)
>  create mode 100644 drivers/input/misc/cs40l50-i2c.c
>  create mode 100644 drivers/input/misc/cs40l50-spi.c
>  create mode 100644 drivers/input/misc/cs40l50.c
>  create mode 100644 include/linux/input/cs40l50.h
> 
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 016e5ff3b831..f1d913166166 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -2089,6 +2089,8 @@ L:	patches@Qopensource.cirrus.com
>  S:	Supported
>  T:	git https://github.com/CirrusLogic/linux-drivers.git
>  F:	Documentation/devicetree/bindings/input/cirrus,cs40l50.yaml
> +F:	drivers/input/misc/cs40l*
> +F:	include/linux/input/cs40l*
>  
>  ARM/CLKDEV SUPPORT
>  M:	Russell King <linux@armlinux.org.uk>
> diff --git a/drivers/input/misc/Kconfig b/drivers/input/misc/Kconfig
> index c47eecc6f83b..fa0bdd23f87d 100644
> --- a/drivers/input/misc/Kconfig
> +++ b/drivers/input/misc/Kconfig
> @@ -140,6 +140,39 @@ config INPUT_BMA150
>  	  To compile this driver as a module, choose M here: the
>  	  module will be called bma150.
>  
> +config INPUT_CS40L50
> +	tristate "Cirrus Logic CS40L50 Haptic Driver support"
> +	select CS_DSP
> +	select REGMAP_IRQ
> +	help
> +	  Say Y here to enable support for CS40L50 boosted haptic
> +	  driver.
> +
> +	  To compile the driver as a module choose M here: the module
> +	  will be called cs40l50_core.
> +
> +config INPUT_CS40L50_I2C
> +	tristate "Support I2C bus connection"
> +	depends on INPUT_CS40L50 && I2C
> +	select REGMAP_I2C
> +	help
> +	  Say Y here if you have Cirrus Logic's CS40L50 connected
> +	  to an I2C bus.
> +
> +	  To compile the driver as a module choose M here: the
> +	  module will be called cs40l50_i2c
> +
> +config INPUT_CS40L50_SPI
> +	tristate "Support SPI bus connection"
> +	depends on INPUT_CS40L50 && SPI
> +	select REGMAP_SPI
> +	help
> +	  Say Y here if you have Cirrus Logic's CS40L50 connected
> +	  to a SPI bus.
> +
> +	  To compile the driver as a module choose M here: the
> +	  module will be called cs40l50_spi.
> +
>  config INPUT_E3X0_BUTTON
>  	tristate "NI Ettus Research USRP E3xx Button support."
>  	default n
> diff --git a/drivers/input/misc/Makefile b/drivers/input/misc/Makefile
> index 04296a4abe8e..77f34a33c364 100644
> --- a/drivers/input/misc/Makefile
> +++ b/drivers/input/misc/Makefile
> @@ -28,6 +28,9 @@ 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_CS40L50)		+= cs40l50.o
> +obj-$(CONFIG_INPUT_CS40L50_I2C)		+= cs40l50-i2c.o
> +obj-$(CONFIG_INPUT_CS40L50_SPI)		+= cs40l50-spi.o
>  obj-$(CONFIG_INPUT_DA7280_HAPTICS)	+= da7280.o
>  obj-$(CONFIG_INPUT_DA9052_ONKEY)	+= da9052_onkey.o
>  obj-$(CONFIG_INPUT_DA9055_ONKEY)	+= da9055_onkey.o
> diff --git a/drivers/input/misc/cs40l50-i2c.c b/drivers/input/misc/cs40l50-i2c.c
> new file mode 100644
> index 000000000000..bab07debdaa9
> --- /dev/null
> +++ b/drivers/input/misc/cs40l50-i2c.c
> @@ -0,0 +1,67 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * CS40L50 Advanced Haptic Driver with Waveform Memory DSP
> + * and Closed-Loop Algorithms
> + *
> + * Copyright 2023 Cirrus Logic, Inc.
> + *
> + */
> +#include <linux/i2c.h>
> +#include <linux/input/cs40l50.h>
> +
> +static const struct i2c_device_id cs40l50_id_i2c[] = {
> +	{"cs40l50", 0},
> +	{}
> +};
> +MODULE_DEVICE_TABLE(i2c, cs40l50_id_i2c);
> +
> +static const struct of_device_id cs40l50_of_match[] = {
> +	{ .compatible = "cirrus,cs40l50" },
> +	{}
> +};
> +MODULE_DEVICE_TABLE(of, cs40l50_of_match);
> +
> +static int cs40l50_i2c_probe(struct i2c_client *client)
> +{
> +	struct device *dev = &client->dev;
> +	struct cs40l50_private *cs40l50;
> +
> +	cs40l50 = devm_kzalloc(dev, sizeof(struct cs40l50_private), GFP_KERNEL);

sizeof(*)

> +	if (!cs40l50)
> +		return -ENOMEM;
> +
> +	i2c_set_clientdata(client, cs40l50);
> +
> +	cs40l50->regmap = devm_regmap_init_i2c(client, &cs40l50_regmap);
> +	if (IS_ERR(cs40l50->regmap))
> +		return dev_err_probe(cs40l50->dev, PTR_ERR(cs40l50->regmap),
> +				"Failed to initialize register map\n");
> +
> +	cs40l50->dev = dev;
> +	cs40l50->irq = client->irq;
> +
> +	return cs40l50_probe(cs40l50);
> +}
> +
> +static void cs40l50_i2c_remove(struct i2c_client *client)
> +{
> +	struct cs40l50_private *cs40l50 = i2c_get_clientdata(client);
> +
> +	cs40l50_remove(cs40l50);
> +}
> +
> +static struct i2c_driver cs40l50_i2c_driver = {
> +	.driver = {
> +		.name = "cs40l50",
> +		.of_match_table = cs40l50_of_match,
> +	},
> +	.id_table = cs40l50_id_i2c,
> +	.probe_new = cs40l50_i2c_probe,
> +	.remove = cs40l50_i2c_remove,
> +};
> +
> +module_i2c_driver(cs40l50_i2c_driver);
> +
> +MODULE_DESCRIPTION("CS40L50 I2C Driver");
> +MODULE_AUTHOR("James Ogletree, Cirrus Logic Inc. <james.ogletree@cirrus.com>");
> +MODULE_LICENSE("GPL");
> diff --git a/drivers/input/misc/cs40l50-spi.c b/drivers/input/misc/cs40l50-spi.c
> new file mode 100644
> index 000000000000..dbaa185b6bbd
> --- /dev/null
> +++ b/drivers/input/misc/cs40l50-spi.c
> @@ -0,0 +1,67 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * CS40L50 Advanced Haptic Driver with Waveform Memory DSP
> + * and Closed-Loop Algorithms
> + *
> + * Copyright 2023 Cirrus Logic, Inc.
> + *
> + */
> +#include <linux/spi/spi.h>
> +#include <linux/input/cs40l50.h>
> +
> +static const struct spi_device_id cs40l50_id_spi[] = {
> +	{"cs40l50", 0},
> +	{}
> +};
> +MODULE_DEVICE_TABLE(spi, cs40l50_id_spi);
> +
> +static const struct of_device_id cs40l50_of_match[] = {
> +	{ .compatible = "cirrus,cs40l50" },
> +	{}
> +};
> +MODULE_DEVICE_TABLE(of, cs40l50_of_match);
> +
> +static int cs40l50_spi_probe(struct spi_device *spi)
> +{
> +	struct cs40l50_private *cs40l50;
> +	struct device *dev = &spi->dev;
> +
> +	cs40l50 = devm_kzalloc(dev, sizeof(struct cs40l50_private), GFP_KERNEL);

sizeof(*)
In all other places as well.


...


> +
> +int cs40l50_probe(struct cs40l50_private *cs40l50)
> +{
> +	int error, i, irq;
> +	u32 val;
> +
> +	mutex_init(&cs40l50->lock);
> +
> +	error = devm_regulator_bulk_get(cs40l50->dev, ARRAY_SIZE(cs40l50_supplies),
> +			cs40l50_supplies);
> +	if (error) {
> +		dev_err(cs40l50->dev, "Failed to request supplies\n");

dev_err_probe

> +		goto err;

So you call cs40l50_remove() here? Are you sure you tested this path?

> +	}
> +
> +	error = regulator_bulk_enable(ARRAY_SIZE(cs40l50_supplies), cs40l50_supplies);
> +	if (error) {
> +		dev_err(cs40l50->dev, "Failed to enable supplies\n");

This for sure is wrong. Why do you disable regulators when enabling them
failed?

> +		goto err;
> +	}
> +
> +	cs40l50->reset_gpio = devm_gpiod_get_optional(cs40l50->dev, "reset", GPIOD_OUT_HIGH);
> +	if (IS_ERR(cs40l50->reset_gpio)) {
> +		error = PTR_ERR(cs40l50->reset_gpio);
> +		goto err;
> +	}
> +
> +	usleep_range(CS40L50_MIN_RESET_PULSE_WIDTH_US, CS40L50_MIN_RESET_PULSE_WIDTH_US + 100);
> +
> +	gpiod_set_value_cansleep(cs40l50->reset_gpio, 0);
> +
> +	usleep_range(CS40L50_CP_READY_DELAY_US, CS40L50_CP_READY_DELAY_US + 1000);
> +
> +	for (i = 0; i < CS40L50_DSP_TIMEOUT_COUNT; i++) {
> +		error = cs40l50_dsp_read(cs40l50, CS40L50_DSP1_HALO_STATE, &val);
> +		if (!error && val < 0xFFFF && val >= CS40L50_HALO_STATE_BOOT_DONE)
> +			break;
> +
> +		usleep_range(CS40L50_DSP_POLL_US, CS40L50_DSP_POLL_US + 100);
> +	}
> +
> +	if (i == CS40L50_DSP_TIMEOUT_COUNT) {
> +		dev_err(cs40l50->dev, "Firmware boot failed: %d, halo state = %#x\n", error, val);
> +		goto err;
> +	}
> +
> +	cs40l50->vibe_workqueue = alloc_ordered_workqueue("cs40l50_workqueue", WQ_HIGHPRI);
> +	if (!cs40l50->vibe_workqueue) {
> +		error = -ENOMEM;
> +		goto err;
> +	}
> +
> +	INIT_WORK(&cs40l50->vibe_start_work, cs40l50_vibe_start_worker);
> +	INIT_WORK(&cs40l50->vibe_stop_work, cs40l50_vibe_stop_worker);
> +
> +	error = cs40l50_cs_dsp_init(cs40l50);
> +	if (error)
> +		goto err;
> +
> +	error = cs40l50_input_init(cs40l50);
> +	if (error)
> +		goto err;
> +
> +	error = cs40l50_patch_firmware(cs40l50);
> +	if (error)
> +		goto err;
> +
> +	error = cs40l50_pseq_init(cs40l50);
> +	if (error)
> +		goto err;
> +
> +	error = cs40l50_config_bst(cs40l50);
> +	if (error)
> +		goto err;
> +
> +	error = devm_regmap_add_irq_chip(cs40l50->dev, cs40l50->regmap, cs40l50->irq,
> +			IRQF_ONESHOT | IRQF_SHARED | IRQF_TRIGGER_LOW, 0, &cs40l50_regmap_irq_chip,
> +			&cs40l50->irq_data);
> +	if (error) {
> +		dev_err(cs40l50->dev, "Failed to register IRQ chip: %d\n", error);
> +		goto err;
> +	}
> +
> +	for (i = 0; i < ARRAY_SIZE(cs40l50_irqs); i++) {
> +		irq = regmap_irq_get_virq(cs40l50->irq_data, cs40l50_irqs[i].irq);
> +		if (irq < 0) {
> +			dev_err(cs40l50->dev, "Failed to get %s\n", cs40l50_irqs[i].name);
> +			error = irq;
> +			goto err;
> +		}
> +
> +		error = devm_request_threaded_irq(cs40l50->dev, irq, NULL, cs40l50_irqs[i].handler,
> +				IRQF_ONESHOT | IRQF_SHARED | IRQF_TRIGGER_LOW,
> +						cs40l50_irqs[i].name, cs40l50);
> +		if (error) {
> +			dev_err(cs40l50->dev, "Failed to request IRQ %s: %d\n",
> +					cs40l50_irqs[i].name, error);
> +			goto err;
> +		}
> +	}
> +
> +	return 0;
> +
> +err:
> +	cs40l50_remove(cs40l50);
> +
> +	return error;
> +}
> +EXPORT_SYMBOL_GPL(cs40l50_probe);
> +
> +int cs40l50_remove(struct cs40l50_private *cs40l50)
> +{
> +	disable_irq(cs40l50->irq);
> +
> +	if (cs40l50->dsp.booted)
> +		cs_dsp_power_down(&cs40l50->dsp);
> +	if (&cs40l50->dsp)
> +		cs_dsp_remove(&cs40l50->dsp);
> +
> +	if (cs40l50->vibe_workqueue) {
> +		flush_workqueue(cs40l50->vibe_workqueue);
> +		destroy_workqueue(cs40l50->vibe_workqueue);
> +	}
> +
> +	gpiod_set_value_cansleep(cs40l50->reset_gpio, 1);
> +	regulator_bulk_disable(ARRAY_SIZE(cs40l50_supplies), cs40l50_supplies);
> +
> +	return 0;
> +}
> +EXPORT_SYMBOL_GPL(cs40l50_remove);
> +
> +MODULE_DESCRIPTION("CS40L50 Advanced Haptic Driver");
> +MODULE_AUTHOR("James Ogletree, Cirrus Logic Inc. <james.ogletree@cirrus.com>");
> +MODULE_LICENSE("GPL");

I don't think this is a module.

Best regards,
Krzysztof
James Ogletree Aug. 9, 2023, 6:02 p.m. UTC | #2
> On Aug 9, 2023, at 1:47 AM, Krzysztof Kozlowski <krzysztof.kozlowski@linaro.org> wrote:
> 
> On 08/08/2023 19:25, James Ogletree wrote:
> 
>> + goto err;
>> + }
>> +
>> + cs40l50->reset_gpio = devm_gpiod_get_optional(cs40l50->dev, "reset", GPIOD_OUT_HIGH);
>> + if (IS_ERR(cs40l50->reset_gpio)) {
>> + error = PTR_ERR(cs40l50->reset_gpio);
>> + goto err;
>> + }
>> +
>> + usleep_range(CS40L50_MIN_RESET_PULSE_WIDTH_US, CS40L50_MIN_RESET_PULSE_WIDTH_US + 100);
>> +
>> + gpiod_set_value_cansleep(cs40l50->reset_gpio, 0);
>> +
>> + usleep_range(CS40L50_CP_READY_DELAY_US, CS40L50_CP_READY_DELAY_US + 1000);
>> +
>> + for (i = 0; i < CS40L50_DSP_TIMEOUT_COUNT; i++) {
>> + error = cs40l50_dsp_read(cs40l50, CS40L50_DSP1_HALO_STATE, &val);
>> + if (!error && val < 0xFFFF && val >= CS40L50_HALO_STATE_BOOT_DONE)
>> + break;
>> +
>> + usleep_range(CS40L50_DSP_POLL_US, CS40L50_DSP_POLL_US + 100);
>> + }
>> +
>> + if (i == CS40L50_DSP_TIMEOUT_COUNT) {
>> + dev_err(cs40l50->dev, "Firmware boot failed: %d, halo state = %#x\n", error, val);
>> + goto err;
>> + }
>> +
>> + cs40l50->vibe_workqueue = alloc_ordered_workqueue("cs40l50_workqueue", WQ_HIGHPRI);
>> + if (!cs40l50->vibe_workqueue) {
>> + error = -ENOMEM;
>> + goto err;
>> + }
>> +
>> + INIT_WORK(&cs40l50->vibe_start_work, cs40l50_vibe_start_worker);
>> + INIT_WORK(&cs40l50->vibe_stop_work, cs40l50_vibe_stop_worker);
>> +
>> + error = cs40l50_cs_dsp_init(cs40l50);
>> + if (error)
>> + goto err;
>> +
>> + error = cs40l50_input_init(cs40l50);
>> + if (error)
>> + goto err;
>> +
>> + error = cs40l50_patch_firmware(cs40l50);
>> + if (error)
>> + goto err;
>> +
>> + error = cs40l50_pseq_init(cs40l50);
>> + if (error)
>> + goto err;
>> +
>> + error = cs40l50_config_bst(cs40l50);
>> + if (error)
>> + goto err;
>> +
>> + error = devm_regmap_add_irq_chip(cs40l50->dev, cs40l50->regmap, cs40l50->irq,
>> + IRQF_ONESHOT | IRQF_SHARED | IRQF_TRIGGER_LOW, 0, &cs40l50_regmap_irq_chip,
>> + &cs40l50->irq_data);
>> + if (error) {
>> + dev_err(cs40l50->dev, "Failed to register IRQ chip: %d\n", error);
>> + goto err;
>> + }
>> +
>> + for (i = 0; i < ARRAY_SIZE(cs40l50_irqs); i++) {
>> + irq = regmap_irq_get_virq(cs40l50->irq_data, cs40l50_irqs[i].irq);
>> + if (irq < 0) {
>> + dev_err(cs40l50->dev, "Failed to get %s\n", cs40l50_irqs[i].name);
>> + error = irq;
>> + goto err;
>> + }
>> +
>> + error = devm_request_threaded_irq(cs40l50->dev, irq, NULL, cs40l50_irqs[i].handler,
>> + IRQF_ONESHOT | IRQF_SHARED | IRQF_TRIGGER_LOW,
>> + cs40l50_irqs[i].name, cs40l50);
>> + if (error) {
>> + dev_err(cs40l50->dev, "Failed to request IRQ %s: %d\n",
>> + cs40l50_irqs[i].name, error);
>> + goto err;
>> + }
>> + }
>> +
>> + return 0;
>> +
>> +err:
>> + cs40l50_remove(cs40l50);
>> +
>> + return error;
>> +}
>> +EXPORT_SYMBOL_GPL(cs40l50_probe);
>> +
>> +int cs40l50_remove(struct cs40l50_private *cs40l50)
>> +{
>> + disable_irq(cs40l50->irq);
>> +
>> + if (cs40l50->dsp.booted)
>> + cs_dsp_power_down(&cs40l50->dsp);
>> + if (&cs40l50->dsp)
>> + cs_dsp_remove(&cs40l50->dsp);
>> +
>> + if (cs40l50->vibe_workqueue) {
>> + flush_workqueue(cs40l50->vibe_workqueue);
>> + destroy_workqueue(cs40l50->vibe_workqueue);
>> + }
>> +
>> + gpiod_set_value_cansleep(cs40l50->reset_gpio, 1);
>> + regulator_bulk_disable(ARRAY_SIZE(cs40l50_supplies), cs40l50_supplies);
>> +
>> + return 0;
>> +}
>> +EXPORT_SYMBOL_GPL(cs40l50_remove);
>> +
>> +MODULE_DESCRIPTION("CS40L50 Advanced Haptic Driver");
>> +MODULE_AUTHOR("James Ogletree, Cirrus Logic Inc. <james.ogletree@cirrus.com>");
>> +MODULE_LICENSE("GPL");
> 
> I don't think this is a module.

It can be compiled as a module with CONFIG_INPUT_CS40L50=m. However, there is a
typo in the Kconfig entry description: the module will be called “cs40l50” not “cs40l50-core”.
That will be fixed. 

Best,

James
Krzysztof Kozlowski Aug. 10, 2023, 6:12 a.m. UTC | #3
On 09/08/2023 20:02, James Ogletree wrote:
>>> + if (cs40l50->vibe_workqueue) {
>>> + flush_workqueue(cs40l50->vibe_workqueue);
>>> + destroy_workqueue(cs40l50->vibe_workqueue);
>>> + }
>>> +
>>> + gpiod_set_value_cansleep(cs40l50->reset_gpio, 1);
>>> + regulator_bulk_disable(ARRAY_SIZE(cs40l50_supplies), cs40l50_supplies);
>>> +
>>> + return 0;
>>> +}
>>> +EXPORT_SYMBOL_GPL(cs40l50_remove);
>>> +
>>> +MODULE_DESCRIPTION("CS40L50 Advanced Haptic Driver");
>>> +MODULE_AUTHOR("James Ogletree, Cirrus Logic Inc. <james.ogletree@cirrus.com>");
>>> +MODULE_LICENSE("GPL");
>>
>> I don't think this is a module.
> 
> It can be compiled as a module with CONFIG_INPUT_CS40L50=m. However, there is a
> typo in the Kconfig entry description: the module will be called “cs40l50” not “cs40l50-core”.
> That will be fixed. 

Really, *this* unit file can be compiled as module? Where is the
module_xxx_driver() then?

Best regards,
Krzysztof
James Ogletree Aug. 14, 2023, 9:44 p.m. UTC | #4
> On Aug 10, 2023, at 1:12 AM, Krzysztof Kozlowski <krzysztof.kozlowski@linaro.org> wrote:
> 
> On 09/08/2023 20:02, James Ogletree wrote:
>>>> + if (cs40l50->vibe_workqueue) {
>>>> + flush_workqueue(cs40l50->vibe_workqueue);
>>>> + destroy_workqueue(cs40l50->vibe_workqueue);
>>>> + }
>>>> +
>>>> + gpiod_set_value_cansleep(cs40l50->reset_gpio, 1);
>>>> + regulator_bulk_disable(ARRAY_SIZE(cs40l50_supplies), cs40l50_supplies);
>>>> +
>>>> + return 0;
>>>> +}
>>>> +EXPORT_SYMBOL_GPL(cs40l50_remove);
>>>> +
>>>> +MODULE_DESCRIPTION("CS40L50 Advanced Haptic Driver");
>>>> +MODULE_AUTHOR("James Ogletree, Cirrus Logic Inc. <james.ogletree@cirrus.com>");
>>>> +MODULE_LICENSE("GPL");
>>> 
>>> I don't think this is a module.
>> 
>> It can be compiled as a module with CONFIG_INPUT_CS40L50=m. However, there is a
>> typo in the Kconfig entry description: the module will be called “cs40l50” not “cs40l50-core”.
>> That will be fixed. 
> 
> Really, *this* unit file can be compiled as module? Where is the
> module_xxx_driver() then?
> 

As I understand it, the “module_XXX_driver()” has nothing to do with whether or not the file is built as a module, rather it just provides code to attach the driver to a bus. But this is a bus-agnostic, separate module (not a driver in itself) that provides implementation to cs40l26-i2c or cs40l26-spi drivers, which do contain that macro as they should. This doesn’t appear to be an uncommon pattern.

Regards,

James
Krzysztof Kozlowski Aug. 15, 2023, 5:08 a.m. UTC | #5
On 14/08/2023 23:44, James Ogletree wrote:

>>>> I don't think this is a module.
>>>
>>> It can be compiled as a module with CONFIG_INPUT_CS40L50=m. However, there is a
>>> typo in the Kconfig entry description: the module will be called “cs40l50” not “cs40l50-core”.
>>> That will be fixed. 
>>
>> Really, *this* unit file can be compiled as module? Where is the
>> module_xxx_driver() then?
>>
> 
> As I understand it, the “module_XXX_driver()” has nothing to do with whether or not the file is built as a module, rather it just provides code to attach the driver to a bus. But this is a bus-agnostic, separate module (not a driver in itself) that provides implementation to cs40l26-i2c or cs40l26-spi drivers, which do contain that macro as they should. This doesn’t appear to be an uncommon pattern.


I see now in the Makefile that it can be indeed built as module and
compiled on its own. It's fine then.

Best regards,
Krzysztof
diff mbox series

Patch

diff --git a/MAINTAINERS b/MAINTAINERS
index 016e5ff3b831..f1d913166166 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -2089,6 +2089,8 @@  L:	patches@Qopensource.cirrus.com
 S:	Supported
 T:	git https://github.com/CirrusLogic/linux-drivers.git
 F:	Documentation/devicetree/bindings/input/cirrus,cs40l50.yaml
+F:	drivers/input/misc/cs40l*
+F:	include/linux/input/cs40l*
 
 ARM/CLKDEV SUPPORT
 M:	Russell King <linux@armlinux.org.uk>
diff --git a/drivers/input/misc/Kconfig b/drivers/input/misc/Kconfig
index c47eecc6f83b..fa0bdd23f87d 100644
--- a/drivers/input/misc/Kconfig
+++ b/drivers/input/misc/Kconfig
@@ -140,6 +140,39 @@  config INPUT_BMA150
 	  To compile this driver as a module, choose M here: the
 	  module will be called bma150.
 
+config INPUT_CS40L50
+	tristate "Cirrus Logic CS40L50 Haptic Driver support"
+	select CS_DSP
+	select REGMAP_IRQ
+	help
+	  Say Y here to enable support for CS40L50 boosted haptic
+	  driver.
+
+	  To compile the driver as a module choose M here: the module
+	  will be called cs40l50_core.
+
+config INPUT_CS40L50_I2C
+	tristate "Support I2C bus connection"
+	depends on INPUT_CS40L50 && I2C
+	select REGMAP_I2C
+	help
+	  Say Y here if you have Cirrus Logic's CS40L50 connected
+	  to an I2C bus.
+
+	  To compile the driver as a module choose M here: the
+	  module will be called cs40l50_i2c
+
+config INPUT_CS40L50_SPI
+	tristate "Support SPI bus connection"
+	depends on INPUT_CS40L50 && SPI
+	select REGMAP_SPI
+	help
+	  Say Y here if you have Cirrus Logic's CS40L50 connected
+	  to a SPI bus.
+
+	  To compile the driver as a module choose M here: the
+	  module will be called cs40l50_spi.
+
 config INPUT_E3X0_BUTTON
 	tristate "NI Ettus Research USRP E3xx Button support."
 	default n
diff --git a/drivers/input/misc/Makefile b/drivers/input/misc/Makefile
index 04296a4abe8e..77f34a33c364 100644
--- a/drivers/input/misc/Makefile
+++ b/drivers/input/misc/Makefile
@@ -28,6 +28,9 @@  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_CS40L50)		+= cs40l50.o
+obj-$(CONFIG_INPUT_CS40L50_I2C)		+= cs40l50-i2c.o
+obj-$(CONFIG_INPUT_CS40L50_SPI)		+= cs40l50-spi.o
 obj-$(CONFIG_INPUT_DA7280_HAPTICS)	+= da7280.o
 obj-$(CONFIG_INPUT_DA9052_ONKEY)	+= da9052_onkey.o
 obj-$(CONFIG_INPUT_DA9055_ONKEY)	+= da9055_onkey.o
diff --git a/drivers/input/misc/cs40l50-i2c.c b/drivers/input/misc/cs40l50-i2c.c
new file mode 100644
index 000000000000..bab07debdaa9
--- /dev/null
+++ b/drivers/input/misc/cs40l50-i2c.c
@@ -0,0 +1,67 @@ 
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * CS40L50 Advanced Haptic Driver with Waveform Memory DSP
+ * and Closed-Loop Algorithms
+ *
+ * Copyright 2023 Cirrus Logic, Inc.
+ *
+ */
+#include <linux/i2c.h>
+#include <linux/input/cs40l50.h>
+
+static const struct i2c_device_id cs40l50_id_i2c[] = {
+	{"cs40l50", 0},
+	{}
+};
+MODULE_DEVICE_TABLE(i2c, cs40l50_id_i2c);
+
+static const struct of_device_id cs40l50_of_match[] = {
+	{ .compatible = "cirrus,cs40l50" },
+	{}
+};
+MODULE_DEVICE_TABLE(of, cs40l50_of_match);
+
+static int cs40l50_i2c_probe(struct i2c_client *client)
+{
+	struct device *dev = &client->dev;
+	struct cs40l50_private *cs40l50;
+
+	cs40l50 = devm_kzalloc(dev, sizeof(struct cs40l50_private), GFP_KERNEL);
+	if (!cs40l50)
+		return -ENOMEM;
+
+	i2c_set_clientdata(client, cs40l50);
+
+	cs40l50->regmap = devm_regmap_init_i2c(client, &cs40l50_regmap);
+	if (IS_ERR(cs40l50->regmap))
+		return dev_err_probe(cs40l50->dev, PTR_ERR(cs40l50->regmap),
+				"Failed to initialize register map\n");
+
+	cs40l50->dev = dev;
+	cs40l50->irq = client->irq;
+
+	return cs40l50_probe(cs40l50);
+}
+
+static void cs40l50_i2c_remove(struct i2c_client *client)
+{
+	struct cs40l50_private *cs40l50 = i2c_get_clientdata(client);
+
+	cs40l50_remove(cs40l50);
+}
+
+static struct i2c_driver cs40l50_i2c_driver = {
+	.driver = {
+		.name = "cs40l50",
+		.of_match_table = cs40l50_of_match,
+	},
+	.id_table = cs40l50_id_i2c,
+	.probe_new = cs40l50_i2c_probe,
+	.remove = cs40l50_i2c_remove,
+};
+
+module_i2c_driver(cs40l50_i2c_driver);
+
+MODULE_DESCRIPTION("CS40L50 I2C Driver");
+MODULE_AUTHOR("James Ogletree, Cirrus Logic Inc. <james.ogletree@cirrus.com>");
+MODULE_LICENSE("GPL");
diff --git a/drivers/input/misc/cs40l50-spi.c b/drivers/input/misc/cs40l50-spi.c
new file mode 100644
index 000000000000..dbaa185b6bbd
--- /dev/null
+++ b/drivers/input/misc/cs40l50-spi.c
@@ -0,0 +1,67 @@ 
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * CS40L50 Advanced Haptic Driver with Waveform Memory DSP
+ * and Closed-Loop Algorithms
+ *
+ * Copyright 2023 Cirrus Logic, Inc.
+ *
+ */
+#include <linux/spi/spi.h>
+#include <linux/input/cs40l50.h>
+
+static const struct spi_device_id cs40l50_id_spi[] = {
+	{"cs40l50", 0},
+	{}
+};
+MODULE_DEVICE_TABLE(spi, cs40l50_id_spi);
+
+static const struct of_device_id cs40l50_of_match[] = {
+	{ .compatible = "cirrus,cs40l50" },
+	{}
+};
+MODULE_DEVICE_TABLE(of, cs40l50_of_match);
+
+static int cs40l50_spi_probe(struct spi_device *spi)
+{
+	struct cs40l50_private *cs40l50;
+	struct device *dev = &spi->dev;
+
+	cs40l50 = devm_kzalloc(dev, sizeof(struct cs40l50_private), GFP_KERNEL);
+	if (!cs40l50)
+		return -ENOMEM;
+
+	spi_set_drvdata(spi, cs40l50);
+
+	cs40l50->regmap = devm_regmap_init_spi(spi, &cs40l50_regmap);
+	if (IS_ERR(cs40l50->regmap))
+		return dev_err_probe(cs40l50->dev, PTR_ERR(cs40l50->regmap),
+					"Failed to initialize register map\n");
+
+	cs40l50->dev = dev;
+	cs40l50->irq = spi->irq;
+
+	return cs40l50_probe(cs40l50);
+}
+
+static void cs40l50_spi_remove(struct spi_device *spi)
+{
+	struct cs40l50_private *cs40l50 = spi_get_drvdata(spi);
+
+	cs40l50_remove(cs40l50);
+}
+
+static struct spi_driver cs40l50_spi_driver = {
+	.driver = {
+		.name = "cs40l50",
+		.of_match_table = cs40l50_of_match,
+	},
+	.id_table = cs40l50_id_spi,
+	.probe = cs40l50_spi_probe,
+	.remove = cs40l50_spi_remove,
+};
+
+module_spi_driver(cs40l50_spi_driver);
+
+MODULE_DESCRIPTION("CS40L50 SPI Driver");
+MODULE_AUTHOR("James Ogletree, Cirrus Logic Inc. <james.ogletree@cirrus.com>");
+MODULE_LICENSE("GPL");
diff --git a/drivers/input/misc/cs40l50.c b/drivers/input/misc/cs40l50.c
new file mode 100644
index 000000000000..17c8d756b432
--- /dev/null
+++ b/drivers/input/misc/cs40l50.c
@@ -0,0 +1,1013 @@ 
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * CS40L50 Advanced Haptic Driver with Waveform Memory DSP
+ * and Closed-Loop Algorithms
+ *
+ * Copyright 2023 Cirrus Logic, Inc.
+ *
+ */
+#include <linux/firmware.h>
+#include <linux/i2c.h>
+#include <linux/interrupt.h>
+#include <linux/kernel.h>
+#include <linux/string.h>
+#include <linux/module.h>
+#include <linux/bitfield.h>
+#include <linux/input/cs40l50.h>
+
+const struct regmap_config cs40l50_regmap = {
+	.reg_bits = 32,
+	.val_bits = 32,
+	.reg_stride = 4,
+	.reg_format_endian = REGMAP_ENDIAN_BIG,
+	.val_format_endian = REGMAP_ENDIAN_BIG,
+};
+EXPORT_SYMBOL_GPL(cs40l50_regmap);
+
+static struct regulator_bulk_data cs40l50_supplies[] = {
+	{ .supply = "VP" },
+	{ .supply = "VIO" },
+	{ .supply = "VA" },
+};
+
+static int cs40l50_dsp_write(struct cs40l50_private *cs40l50, u32 write_reg, u32 write_val)
+{
+	int error = 0, i;
+
+	for (i = 0; i < CS40L50_DSP_TIMEOUT_COUNT; i++) {
+		error = regmap_write(cs40l50->regmap, write_reg, write_val);
+		if (!error)
+			break;
+
+		usleep_range(CS40L50_DSP_POLL_US, CS40L50_DSP_POLL_US + 100);
+	}
+
+	if (i == CS40L50_DSP_TIMEOUT_COUNT) {
+		dev_err(cs40l50->dev, "Error writing %#X to %#X: %d", write_val, write_reg, error);
+		return error;
+	}
+
+	return 0;
+}
+
+static int cs40l50_dsp_read(struct cs40l50_private *cs40l50, u32 read_reg, u32 *read_val)
+{
+	int error = 0, i;
+
+	for (i = 0; i < CS40L50_DSP_TIMEOUT_COUNT; i++) {
+		error = regmap_read(cs40l50->regmap, read_reg, read_val);
+		if (!error)
+			break;
+
+		usleep_range(CS40L50_DSP_POLL_US, CS40L50_DSP_POLL_US + 100);
+	}
+
+	if (i == CS40L50_DSP_TIMEOUT_COUNT) {
+		dev_err(cs40l50->dev, "Error reading %#X: %d", read_reg, error);
+		return error;
+	}
+
+	return 0;
+}
+
+static int cs40l50_pseq_write(struct cs40l50_private *cs40l50, u32 addr, u32 data)
+{
+	u8 operation = 0;
+	struct cs40l50_pseq_op *op_new, *op_end;
+	int error;
+
+	op_new = devm_kzalloc(cs40l50->dev, sizeof(struct cs40l50_pseq_op), GFP_KERNEL);
+	if (!op_new)
+		return -ENOMEM;
+
+	op_new->words[0] = (addr >> CS40L50_PSEQ_WRITE_FULL_UPPER_ADDR_SHIFT);
+	op_new->words[1] = ((addr & CS40L50_PSEQ_WRITE_FULL_LOWER_ADDR_MASK) <<
+			CS40L50_PSEQ_WRITE_FULL_LOWER_ADDR_SHIFT) |
+			(data >> CS40L50_PSEQ_WRITE_FULL_UPPER_DATA_SHIFT);
+	op_new->words[2] = data & CS40L50_PSEQ_WRITE_FULL_LOWER_DATA_MASK;
+
+	list_for_each_entry(op_end, &cs40l50->pseq_op_head, list) {
+		operation = op_end->operation;
+		if (operation == CS40L50_PSEQ_OP_END)
+			break;
+	}
+
+	if (operation != CS40L50_PSEQ_OP_END) {
+		error = -ENOENT;
+		goto op_new_free;
+	}
+
+	if ((CS40L50_PSEQ_MAX_WORDS - op_end->offset) < ARRAY_SIZE(op_new->words)) {
+		error = -ENOSPC;
+		goto op_new_free;
+	}
+
+	op_new->offset = op_end->offset;
+	op_end->offset += (ARRAY_SIZE(op_new->words) * sizeof(u32));
+
+	error = regmap_bulk_write(cs40l50->regmap, CS40L50_DSP1_POWER_ON_SEQ + op_new->offset,
+			op_new->words, ARRAY_SIZE(op_new->words));
+	if (error)
+		goto op_new_free;
+
+	error = cs40l50_dsp_write(cs40l50, CS40L50_DSP1_POWER_ON_SEQ + op_end->offset,
+			op_end->words[0]);
+	if (error)
+		goto op_new_free;
+
+	list_add(&op_new->list, &cs40l50->pseq_op_head);
+
+	return 0;
+
+op_new_free:
+	devm_kfree(cs40l50->dev, op_new);
+
+	return error;
+}
+
+static struct cs40l50_effect *cs40l50_find_effect(struct cs40l50_private *cs40l50, int id)
+{
+	struct cs40l50_effect *effect;
+
+	if (!list_empty(&cs40l50->effect_head)) {
+		list_for_each_entry(effect, &cs40l50->effect_head, list) {
+			if (effect->id == id)
+				return effect;
+		}
+	}
+
+	return NULL;
+}
+
+static int cs40l50_owt_upload(struct cs40l50_private *cs40l50, s16 *in_data, u32 in_data_nibbles)
+{
+	size_t in_data_bytes = 2 * in_data_nibbles;
+	struct cs40l50_owt_header header;
+	u32 wt_offset, wt_size_words;
+	int error;
+	u8 *data;
+
+	error = cs40l50_dsp_read(cs40l50, CS40L50_DSP1_OWT_NEXT, &wt_offset);
+	if (error)
+		return error;
+
+	error = cs40l50_dsp_read(cs40l50, CS40L50_DSP1_OWT_SIZE, &wt_size_words);
+	if (error)
+		return error;
+
+	if ((wt_size_words * sizeof(u32)) < CS40L50_WT_HEADER_PWLE_SIZE + in_data_bytes)
+		return -ENOSPC;
+
+	data = kcalloc(CS40L50_WT_HEADER_PWLE_SIZE + in_data_bytes, sizeof(u8), GFP_KERNEL);
+	if (IS_ERR_OR_NULL(data))
+		return -ENOMEM;
+
+	header.wvfrm_type = in_data[0] == CS40L50_PCM_ID ? CS40L50_WT_TYPE_PCM :
+			CS40L50_WT_TYPE_PWLE;
+	header.offset = CS40L50_WT_HEADER_OFFSET;
+	header.data_size = in_data_bytes / sizeof(u32);
+
+	memcpy(data, &header, sizeof(struct cs40l50_owt_header));
+	memcpy(data + (CS40L50_WT_HEADER_OFFSET * sizeof(u32)), in_data, in_data_bytes);
+
+	error = regmap_bulk_write(cs40l50->regmap, CS40L50_DSP1_OWT_BASE +
+			(wt_offset * sizeof(u32)), data,
+			CS40L50_WT_HEADER_PWLE_SIZE + in_data_bytes);
+	if (error)
+		goto err_free;
+
+	error = cs40l50_dsp_write(cs40l50, CS40L50_DSP_VIRTUAL1_MBOX_1, CS40L50_MBOX_CMD_OWT_PUSH);
+err_free:
+	kfree(data);
+
+	return error;
+}
+
+static int cs40l50_add_effect(struct input_dev *dev, struct ff_effect *effect,
+		struct ff_effect *old)
+{
+	struct cs40l50_private *cs40l50 = input_get_drvdata(dev);
+	bool is_new = false;
+	struct cs40l50_effect *new_effect;
+	u32 nwaves, base_index, max_index;
+	s16 *raw_custom_data;
+	int error;
+
+	if (effect->type != FF_PERIODIC || effect->u.periodic.waveform != FF_CUSTOM) {
+		dev_err(cs40l50->dev, "Type (%#X) or waveform (%#X) unsupported\n",
+				effect->type, effect->u.periodic.waveform);
+		return -EINVAL;
+	}
+
+	raw_custom_data = kcalloc(effect->u.periodic.custom_len, sizeof(s16), GFP_KERNEL);
+	if (IS_ERR_OR_NULL(raw_custom_data))
+		return -ENOMEM;
+
+	error = copy_from_user(raw_custom_data, effect->u.periodic.custom_data,
+			sizeof(s16) * effect->u.periodic.custom_len);
+	if (error)
+		goto out_free;
+
+	mutex_lock(&cs40l50->lock);
+
+	new_effect = cs40l50_find_effect(cs40l50, effect->id);
+	if (!new_effect) {
+		is_new = true;
+		new_effect = kzalloc(sizeof(struct cs40l50_effect), GFP_KERNEL);
+		if (IS_ERR_OR_NULL(new_effect)) {
+			error = -ENOMEM;
+			goto err_mutex;
+		}
+	}
+
+	if (effect->u.periodic.custom_len > CS40L50_CUSTOM_DATA_SIZE) {
+		error = cs40l50_dsp_read(cs40l50, CS40L50_NUM_OF_OWT_WAVES,
+				&new_effect->trigger_index);
+		if (error)
+			goto err_free;
+
+		new_effect->wvfrm_bank = CS40L50_WVFRM_BANK_OWT;
+		base_index = CS40L50_OWT_INDEX_START;
+		max_index = CS40L50_OWT_INDEX_END;
+	} else {
+		new_effect->trigger_index = (raw_custom_data[1] & 0xffffu);
+		new_effect->wvfrm_bank = (raw_custom_data[0] & 0xffffu);
+
+		if (new_effect->wvfrm_bank == CS40L50_WVFRM_BANK_ROM) {
+			base_index = CS40L50_ROM_BANK_INDEX_START;
+			max_index = CS40L50_ROM_BANK_INDEX_END;
+		} else if (new_effect->wvfrm_bank == CS40L50_WVFRM_BANK_RAM) {
+			error = cs40l50_dsp_read(cs40l50, CS40L50_NUM_OF_WAVES, &nwaves);
+			if (error)
+				goto err_free;
+
+			base_index = CS40L50_RAM_BANK_INDEX_START;
+			max_index = base_index + nwaves - 1;
+		} else {
+			dev_err(cs40l50->dev, "Invalid waveform bank\n");
+			error = -EINVAL;
+			goto err_free;
+		}
+	}
+
+	new_effect->trigger_index += base_index;
+
+	if (new_effect->trigger_index > max_index) {
+		dev_err(cs40l50->dev, "Index %#X out of bounds\n", new_effect->trigger_index);
+		error = -EINVAL;
+		goto err_free;
+	}
+
+	if (effect->trigger.button) {
+		new_effect->mapping = CS40L50_DSP1_GPIO_BASE + sizeof(u32) *
+				FIELD_GET(CS40L50_BTN_NUM_MASK, effect->trigger.button) * 2 -
+				FIELD_GET(CS40L50_BTN_EDGE_MASK, effect->trigger.button);
+
+		error = cs40l50_dsp_write(cs40l50, new_effect->mapping, effect->trigger.button);
+		if (error)
+			goto err_free;
+	}
+
+	if (new_effect->wvfrm_bank == CS40L50_WVFRM_BANK_OWT) {
+		error = cs40l50_owt_upload(cs40l50, raw_custom_data,
+				effect->u.periodic.custom_len);
+		if (error)
+			goto err_free;
+	}
+
+err_free:
+	if (is_new)
+		error ? kfree(new_effect) : list_add(&new_effect->list, &cs40l50->effect_head);
+err_mutex:
+	mutex_unlock(&cs40l50->lock);
+out_free:
+	kfree(raw_custom_data);
+
+	return error ? -EFAULT : 0;
+}
+
+
+static void cs40l50_vibe_start_worker(struct work_struct *work)
+{
+	struct cs40l50_private *cs40l50 =
+			container_of(work, struct cs40l50_private, vibe_start_work);
+	struct cs40l50_effect *effect;
+
+	mutex_lock(&cs40l50->lock);
+
+	effect = cs40l50_find_effect(cs40l50, cs40l50->trigger_effect->id);
+	if (effect)
+		cs40l50_dsp_write(cs40l50, CS40L50_DSP_VIRTUAL1_MBOX_1, effect->trigger_index);
+
+	mutex_unlock(&cs40l50->lock);
+}
+
+static void cs40l50_vibe_stop_worker(struct work_struct *work)
+{
+	struct cs40l50_private *cs40l50 =
+			container_of(work, struct cs40l50_private, vibe_stop_work);
+
+	mutex_lock(&cs40l50->lock);
+
+	cs40l50_dsp_write(cs40l50, CS40L50_DSP_VIRTUAL1_MBOX_1, CS40L50_MBOX_CMD_STOP_PLAYBACK);
+
+	mutex_unlock(&cs40l50->lock);
+}
+
+static int cs40l50_playback_effect(struct input_dev *dev, int effect_id, int val)
+{
+	struct cs40l50_private *cs40l50 = input_get_drvdata(dev);
+
+	cs40l50->trigger_effect = &dev->ff->effects[effect_id];
+
+	if (val > 0)
+		queue_work(cs40l50->vibe_workqueue, &cs40l50->vibe_start_work);
+	else
+		queue_work(cs40l50->vibe_workqueue, &cs40l50->vibe_stop_work);
+
+	return 0;
+}
+
+static int cs40l50_erase_effect(struct input_dev *dev, int effect_id)
+{
+	int error = 0;
+	struct cs40l50_private *cs40l50 = input_get_drvdata(dev);
+	struct cs40l50_effect *effect, *effect_owt;
+
+	mutex_lock(&cs40l50->lock);
+
+	effect = cs40l50_find_effect(cs40l50, dev->ff->effects[effect_id].id);
+	if (!effect) {
+		error = -EINVAL;
+		goto err_mutex;
+	}
+
+	if (effect->mapping != CS40L50_GPIO_MAPPING_INVALID) {
+		error = cs40l50_dsp_write(cs40l50, effect->mapping, CS40L50_GPI_DISABLE);
+		if (error)
+			goto err_mutex;
+	}
+
+	if (effect->wvfrm_bank == CS40L50_WVFRM_BANK_OWT) {
+		error = cs40l50_dsp_write(cs40l50, CS40L50_DSP_VIRTUAL1_MBOX_1,
+				CS40L50_MBOX_CMD_OWT_DELETE | effect->trigger_index);
+		if (error)
+			goto err_mutex;
+
+		list_for_each_entry(effect_owt, &cs40l50->effect_head, list) {
+			if (effect_owt->wvfrm_bank == CS40L50_WVFRM_BANK_OWT &&
+					effect_owt->trigger_index > effect->trigger_index)
+				effect_owt->trigger_index--;
+		}
+	}
+
+	list_del(&effect->list);
+	kfree(effect);
+err_mutex:
+	mutex_unlock(&cs40l50->lock);
+
+	return error;
+}
+
+static int cs40l50_mailbox_read_next(struct cs40l50_private *cs40l50, u32 *val)
+{
+	u32 wt_ptr, rd_ptr;
+	int error = 0;
+
+	error = cs40l50_dsp_read(cs40l50, CS40L50_DSP1_MBOX_QUEUE_WT, &wt_ptr);
+	if (error)
+		return error;
+
+	error = cs40l50_dsp_read(cs40l50, CS40L50_DSP1_MBOX_QUEUE_RD, &rd_ptr);
+	if (error)
+		return error;
+
+	if (wt_ptr == rd_ptr) {
+		*val = 0;
+		return 0;
+	}
+
+	error = cs40l50_dsp_read(cs40l50, rd_ptr, val);
+	if (error)
+		return error;
+
+	rd_ptr += sizeof(u32);
+	if (rd_ptr > CS40L50_MBOX_QUEUE_END)
+		rd_ptr = CS40L50_MBOX_QUEUE_BASE;
+
+	return cs40l50_dsp_write(cs40l50, CS40L50_DSP1_MBOX_QUEUE_RD, rd_ptr);
+}
+
+static int cs40l50_handle_f0_est_done(struct cs40l50_private *cs40l50)
+{
+	int error;
+	u32 f_zero;
+
+	error = cs40l50_dsp_read(cs40l50, CS40L50_DSP1_F0_ESTIMATION, &f_zero);
+	if (error)
+		return error;
+
+	return cs40l50_dsp_write(cs40l50, CS40L50_DSP1_F0_STORED, f_zero);
+}
+
+static int cs40l50_handle_redc_est_done(struct cs40l50_private *cs40l50)
+{
+	int integer, fractional, error;
+	u32 redc;
+
+	error = cs40l50_dsp_read(cs40l50, CS40L50_DSP1_RE_EST_STATUS, &redc);
+	if (error)
+		return error;
+
+	error = cs40l50_dsp_write(cs40l50, CS40L50_DSP1_REDC_ESTIMATION, redc);
+	if (error)
+		return error;
+
+	/* Convert from Q8.15 to (Q7.17 * 29/240) */
+	integer = (redc >> 15) & 0xFF;
+	fractional = (redc & 0x7FFF) * 4;
+
+	return cs40l50_dsp_write(cs40l50, CS40L50_DSP1_REDC_STORED,
+		(((integer << 17) | fractional) * 29 / 240) & GENMASK(23, 0));
+}
+
+static int cs40l50_error_release(struct cs40l50_private *cs40l50)
+{
+	int error;
+
+	error = cs40l50_dsp_write(cs40l50, CS40L50_ERROR_RELEASE, CS40L50_ERROR_RELEASE_MASK);
+	if (error)
+		return error;
+
+	return cs40l50_dsp_write(cs40l50, CS40L50_ERROR_RELEASE, 0);
+}
+
+static irqreturn_t cs40l50_handle_mbox_buffer(int irq, void *data)
+{
+	struct cs40l50_private *cs40l50 = data;
+	int error = 0;
+	u32 val;
+
+	mutex_lock(&cs40l50->lock);
+
+	while (!cs40l50_mailbox_read_next(cs40l50, &val)) {
+		switch (val) {
+		case 0:
+			goto out_irq;
+		case CS40L50_DSP_MBOX_HAPTIC_TRIGGER_GPIO:
+			dev_dbg(cs40l50->dev, "Mailbox: TRIGGER_GPIO\n");
+			break;
+		case CS40L50_DSP_MBOX_HAPTIC_TRIGGER_MBOX:
+			dev_dbg(cs40l50->dev, "Mailbox: TRIGGER_MBOX\n");
+			break;
+		case CS40L50_DSP_MBOX_HAPTIC_TRIGGER_I2S:
+			dev_dbg(cs40l50->dev, "Mailbox: TRIGGER_I2S\n");
+			break;
+		case CS40L50_DSP_MBOX_HAPTIC_COMPLETE_MBOX:
+			dev_dbg(cs40l50->dev, "Mailbox: COMPLETE_MBOX\n");
+			break;
+		case CS40L50_DSP_MBOX_HAPTIC_COMPLETE_GPIO:
+			dev_dbg(cs40l50->dev, "Mailbox: COMPLETE_GPIO\n");
+			break;
+		case CS40L50_DSP_MBOX_HAPTIC_COMPLETE_I2S:
+			dev_dbg(cs40l50->dev, "Mailbox: COMPLETE_I2S\n");
+			break;
+		case CS40L50_DSP_MBOX_F0_EST_START:
+			dev_dbg(cs40l50->dev, "Mailbox: F0_EST_START\n");
+			break;
+		case CS40L50_DSP_MBOX_F0_EST_DONE:
+			dev_dbg(cs40l50->dev, "Mailbox: F0_EST_DONE\n");
+			error = cs40l50_handle_f0_est_done(cs40l50);
+			if (error)
+				goto out_irq;
+			break;
+		case CS40L50_DSP_MBOX_REDC_EST_START:
+			dev_dbg(cs40l50->dev, "Mailbox: REDC_EST_START\n");
+			break;
+		case CS40L50_DSP_MBOX_REDC_EST_DONE:
+			dev_dbg(cs40l50->dev, "Mailbox: REDC_EST_DONE\n");
+			error = cs40l50_handle_redc_est_done(cs40l50);
+			if (error)
+				goto out_irq;
+			break;
+		case CS40L50_DSP_MBOX_LE_EST_START:
+			dev_dbg(cs40l50->dev, "Mailbox: LE_EST_START\n");
+			break;
+		case CS40L50_DSP_MBOX_LE_EST_DONE:
+			dev_dbg(cs40l50->dev, "Mailbox: LE_EST_DONE\n");
+			break;
+		case CS40L50_DSP_MBOX_AWAKE:
+			dev_dbg(cs40l50->dev, "Mailbox: AWAKE\n");
+			break;
+		case CS40L50_DSP_MBOX_INIT:
+			dev_dbg(cs40l50->dev, "Mailbox: INIT\n");
+			break;
+		case CS40L50_DSP_MBOX_ACK:
+			dev_dbg(cs40l50->dev, "Mailbox: ACK\n");
+			break;
+		case CS40L50_DSP_MBOX_ERR_EVENT_UNMAPPED:
+			dev_err(cs40l50->dev, "Unmapped event\n");
+			break;
+		case CS40L50_DSP_MBOX_ERR_EVENT_MODIFY:
+			dev_err(cs40l50->dev, "Attempt to modify event index failed\n");
+			break;
+		case CS40L50_DSP_MBOX_ERR_NULLPTR:
+			dev_err(cs40l50->dev, "Null pointer\n");
+			break;
+		case CS40L50_DSP_MBOX_ERR_BRAKING:
+			dev_err(cs40l50->dev, "Braking not in progress\n");
+			break;
+		case CS40L50_DSP_MBOX_ERR_INVAL_SRC:
+			dev_err(cs40l50->dev, "Suspend/resume invalid source\n");
+			break;
+		case CS40L50_DSP_MBOX_ERR_ENABLE_RANGE:
+			dev_err(cs40l50->dev, "GPIO enable out of range\n");
+			break;
+		case CS40L50_DSP_MBOX_ERR_GPIO_UNMAPPED:
+			dev_err(cs40l50->dev, "GPIO not mapped\n");
+			break;
+		case CS40L50_DSP_MBOX_ERR_ISR_RANGE:
+			dev_err(cs40l50->dev, "GPIO ISR out of range\n");
+			break;
+		case CS40L50_DSP_MBOX_PERMANENT_SHORT:
+			dev_crit(cs40l50->dev, "Permanent short detected\n");
+			break;
+		case CS40L50_DSP_MBOX_RUNTIME_SHORT:
+			dev_err(cs40l50->dev, "Runtime short detected\n");
+			error = cs40l50_error_release(cs40l50);
+			if (error)
+				goto out_irq;
+			break;
+		default:
+			dev_err(cs40l50->dev, "Mailbox value %#X not recognized\n", val);
+			error = -EINVAL;
+			goto out_irq;
+		}
+	}
+
+	error = -EIO;
+
+out_irq:
+	mutex_unlock(&cs40l50->lock);
+
+	return IRQ_RETVAL(!error);
+}
+
+
+static irqreturn_t cs40l50_vddb_uv(int irq, void *data)
+{
+	struct cs40l50_private *cs40l50 = data;
+
+	dev_err(cs40l50->dev, "VDD_B undervoltage error\n");
+
+	return IRQ_RETVAL(!cs40l50_error_release(cs40l50));
+}
+
+static irqreturn_t cs40l50_boost_current_limit(int irq, void *data)
+{
+	struct cs40l50_private *cs40l50 = data;
+
+	dev_err(cs40l50->dev, "Boost current limit engagement\n");
+
+	return IRQ_RETVAL(!cs40l50_error_release(cs40l50));
+}
+
+static irqreturn_t cs40l50_boost_short(int irq, void *data)
+{
+	struct cs40l50_private *cs40l50 = data;
+
+	dev_err(cs40l50->dev, "Boost inductor short error\n");
+
+	return IRQ_RETVAL(!cs40l50_error_release(cs40l50));
+}
+
+static irqreturn_t cs40l50_boost_uv(int irq, void *data)
+{
+	struct cs40l50_private *cs40l50 = data;
+
+	dev_err(cs40l50->dev, "Boost undervoltage error\n");
+
+	return IRQ_RETVAL(!cs40l50_error_release(cs40l50));
+}
+
+static irqreturn_t cs40l50_overtemperature(int irq, void *data)
+{
+	struct cs40l50_private *cs40l50 = data;
+
+	dev_err(cs40l50->dev, "Overtemperature error\n");
+
+	return IRQ_RETVAL(!cs40l50_error_release(cs40l50));
+}
+
+static irqreturn_t cs40l50_amp_short(int irq, void *data)
+{
+	struct cs40l50_private *cs40l50 = data;
+
+	dev_err(cs40l50->dev, "Amp short error\n");
+
+	return IRQ_RETVAL(!cs40l50_error_release(cs40l50));
+}
+
+static irqreturn_t cs40l50_global_error(int irq, void *data)
+{
+	struct cs40l50_private *cs40l50 = data;
+
+	dev_err(cs40l50->dev, "MSM chip error\n");
+
+	return IRQ_RETVAL(!cs40l50_error_release(cs40l50));
+}
+
+static const struct cs40l50_irq cs40l50_irqs[] = {
+	CS40L50_IRQ(CS40L50_AMP_SHORT_IRQ, "Amp short", cs40l50_amp_short),
+	CS40L50_IRQ(CS40L50_VIRT2_MBOX_IRQ, "Firmware interrupt", cs40l50_handle_mbox_buffer),
+	CS40L50_IRQ(CS40L50_TEMP_ERR_IRQ, "Overtemperature", cs40l50_overtemperature),
+	CS40L50_IRQ(CS40L50_BST_UVP_IRQ, "Boost undervoltage", cs40l50_boost_uv),
+	CS40L50_IRQ(CS40L50_BST_SHORT_IRQ, "Boost short", cs40l50_boost_short),
+	CS40L50_IRQ(CS40L50_BST_ILIMIT_IRQ, "Boost current limit", cs40l50_boost_current_limit),
+	CS40L50_IRQ(CS40L50_UVLO_VDDBATT_IRQ, "VDD_B undervoltage", cs40l50_vddb_uv),
+	CS40L50_IRQ(CS40L50_GLOBAL_ERROR_IRQ, "Global error", cs40l50_global_error),
+};
+
+static const struct regmap_irq cs40l50_reg_irqs[] = {
+	CS40L50_REG_IRQ(IRQ1_INT_1, AMP_SHORT),
+	CS40L50_REG_IRQ(IRQ1_INT_2, VIRT2_MBOX),
+	CS40L50_REG_IRQ(IRQ1_INT_8, TEMP_ERR),
+	CS40L50_REG_IRQ(IRQ1_INT_9, BST_UVP),
+	CS40L50_REG_IRQ(IRQ1_INT_9, BST_SHORT),
+	CS40L50_REG_IRQ(IRQ1_INT_9, BST_ILIMIT),
+	CS40L50_REG_IRQ(IRQ1_INT_10, UVLO_VDDBATT),
+	CS40L50_REG_IRQ(IRQ1_INT_18, GLOBAL_ERROR),
+};
+
+static const struct regmap_irq_chip cs40l50_regmap_irq_chip = {
+	.name = "cs40l50 IRQ1 Controller",
+	.status_base = CS40L50_IRQ1_INT_1,
+	.mask_base = CS40L50_IRQ1_MASK_1,
+	.ack_base = CS40L50_IRQ1_INT_1,
+	.num_regs = 19,
+	.irqs = cs40l50_reg_irqs,
+	.num_irqs = ARRAY_SIZE(cs40l50_reg_irqs),
+	.runtime_pm = true,
+};
+
+static const struct reg_sequence cs40l50_ext_bst_seq[] = {
+	{ CS40L50_BLOCK_ENABLES, CS40L50_MON_ENABLE },
+	{ CS40L50_MON_VALUE_CTRL, CS40L50_VBST_VDD_AMP },
+	{ CS40L50_TEST_KEY, CS40L50_KEY_SET_1 },
+	{ CS40L50_TEST_KEY, CS40L50_KEY_SET_2 },
+	{ CS40L50_TEST_VIS_SPARE, CS40L50_TEMP_SENSOR_3 },
+	{ CS40L50_MON_ERROR_OVERIDE, CS40L50_TEMP_ERR_THLD },
+	{ CS40L50_STATUS, CS40L50_TEMP_WARN_THLD },
+	{ CS40L50_TEST_CONFIG, CS40L50_TEMP_FILT_DEBOUNCE_OFF },
+	{ CS40L50_TEMP_WARN_CONFIG, CS40L50_TEMP_WARN_ATK_VOL },
+};
+
+static int cs40l50_config_bst(struct cs40l50_private *cs40l50)
+{
+	int error, i;
+
+	if (device_property_present(cs40l50->dev, "cirrus,external-boost")) {
+		error = regmap_multi_reg_write(cs40l50->regmap, cs40l50_ext_bst_seq,
+				ARRAY_SIZE(cs40l50_ext_bst_seq));
+		if (error)
+			return error;
+
+		for (i = 0; i < ARRAY_SIZE(cs40l50_ext_bst_seq); i++) {
+			error = cs40l50_pseq_write(cs40l50, cs40l50_ext_bst_seq[i].reg,
+					cs40l50_ext_bst_seq[i].def);
+			if (error)
+				return error;
+		}
+
+		return 0;
+	}
+
+	/* Internal boost config */
+	return cs40l50_dsp_write(cs40l50, CS40L50_BST_LPMODE_SEL, CS40L50_DCM_LOW_POWER);
+}
+
+static int cs40l50_patch_firmware(struct cs40l50_private *cs40l50)
+{
+	const struct firmware *bin = NULL, *wmfw = NULL;
+	int error = 0;
+
+	if (request_firmware(&bin, "cs40l50.bin", cs40l50->dev))
+		dev_info(cs40l50->dev, "Could not request wavetable file: %d\n", error);
+
+	if (request_firmware(&wmfw, "cs40l50.wmfw", cs40l50->dev))
+		dev_info(cs40l50->dev, "Could not request firmware patch file: %d\n", error);
+
+	if (wmfw) {
+		error = cs40l50_dsp_write(cs40l50, CS40L50_CCM_CORE_CONTROL,
+				CS40L50_DSP1_CLOCK_DISABLE);
+		if (error)
+			goto err_fw;
+
+		error = cs40l50_dsp_write(cs40l50, CS40L50_RAM_INIT,
+				CS40L50_DSP1_RAM_INIT_FLAG);
+		if (error)
+			goto err_fw;
+
+		error = cs40l50_dsp_write(cs40l50, CS40L50_PWRMGT_CTL,
+				CS40L50_DSP1_MEM_RDY_HW);
+		if (error)
+			goto err_fw;
+	}
+
+	mutex_lock(&cs40l50->lock);
+
+	cs_dsp_power_up(&cs40l50->dsp, wmfw, "cs40l50.wmfw", bin, "cs40l50.bin", "cs40l50");
+
+	mutex_unlock(&cs40l50->lock);
+
+	if (wmfw)
+		error = cs40l50_dsp_write(cs40l50, CS40L50_CCM_CORE_CONTROL,
+				CS40L50_DSP1_CLOCK_ENABLE);
+
+err_fw:
+	release_firmware(wmfw);
+	release_firmware(bin);
+
+	return error;
+}
+
+static int cs40l50_pseq_init(struct cs40l50_private *cs40l50)
+{
+	struct cs40l50_pseq_op *pseq_op;
+	int error, i, num_words;
+	u8 operation;
+	u32 *words;
+
+	INIT_LIST_HEAD(&cs40l50->pseq_op_head);
+
+	words = kcalloc(CS40L50_PSEQ_MAX_WORDS, sizeof(u32), GFP_KERNEL);
+	if (IS_ERR_OR_NULL(words))
+		return -ENOMEM;
+
+	error = regmap_bulk_read(cs40l50->regmap, CS40L50_DSP1_POWER_ON_SEQ, words,
+			CS40L50_PSEQ_MAX_WORDS);
+	if (error)
+		goto err_free;
+
+	for (i = 0; i < CS40L50_PSEQ_MAX_WORDS; i++)
+		words[i] = be32_to_cpu(words[i]);
+
+	for (i = 0; i < CS40L50_PSEQ_MAX_WORDS; i += num_words) {
+		operation = (words[i] & CS40L50_PSEQ_OP_MASK) >> CS40L50_PSEQ_OP_SHIFT;
+
+		switch (operation) {
+		case CS40L50_PSEQ_OP_END:
+			num_words = CS40L50_PSEQ_OP_END_WORDS;
+			break;
+		case CS40L50_PSEQ_OP_WRITE_ADDR8:
+		case CS40L50_PSEQ_OP_WRITE_H16:
+		case CS40L50_PSEQ_OP_WRITE_L16:
+			num_words = CS40L50_PSEQ_OP_WRITE_X16_WORDS;
+			break;
+		case CS40L50_PSEQ_OP_WRITE_FULL:
+			num_words = CS40L50_PSEQ_OP_WRITE_FULL_WORDS;
+			break;
+		default:
+			dev_err(cs40l50->dev, "Invalid OP code 0x%02X\n", operation);
+			error = -EINVAL;
+			goto err_free;
+		}
+
+		pseq_op = devm_kzalloc(cs40l50->dev, sizeof(struct cs40l50_pseq_op), GFP_KERNEL);
+		if (!pseq_op) {
+			error = -ENOMEM;
+			goto err_free;
+		}
+
+		memcpy(pseq_op->words, &words[i], num_words * sizeof(u32));
+		pseq_op->size = num_words;
+		pseq_op->offset = i * sizeof(u32);
+		pseq_op->operation = operation;
+		list_add(&pseq_op->list, &cs40l50->pseq_op_head);
+
+		if (operation == CS40L50_PSEQ_OP_END)
+			break;
+	}
+
+	if (operation != CS40L50_PSEQ_OP_END) {
+		dev_err(cs40l50->dev, "PSEQ_END_OF_SCRIPT not found\n");
+		error = -ENOENT;
+	}
+
+err_free:
+	kfree(words);
+
+	return error;
+}
+
+static int cs40l50_input_init(struct cs40l50_private *cs40l50)
+{
+	int error;
+	u32 val;
+
+	error = cs40l50_dsp_read(cs40l50, CS40L50_DEVID, &val);
+	if (error)
+		return error;
+
+	if (val != CS40L50_DEVID_A) {
+		dev_err(cs40l50->dev, "Invalid device ID: %#010X\n", val);
+		return -EINVAL;
+	}
+
+	cs40l50->input->id.product = val & 0xFFFF;
+
+	error = cs40l50_dsp_read(cs40l50, CS40L50_REVID, &val);
+	if (error)
+		return error;
+
+	if (val != CS40L50_REVID_B0) {
+		dev_err(cs40l50->dev, "Invalid device revision: %#04X\n", val);
+		return -EINVAL;
+	}
+
+	cs40l50->input->id.version = val;
+
+	cs40l50->input = devm_input_allocate_device(cs40l50->dev);
+	if (IS_ERR_OR_NULL(cs40l50->input))
+		return cs40l50->input ? PTR_ERR(cs40l50->input) : -ENOMEM;
+
+	cs40l50->input->name = "cs40l50_input";
+	input_set_drvdata(cs40l50->input, cs40l50);
+	input_set_capability(cs40l50->input, EV_FF, FF_PERIODIC);
+	input_set_capability(cs40l50->input, EV_FF, FF_CUSTOM);
+
+	error = input_ff_create(cs40l50->input, FF_MAX_EFFECTS);
+	if (error)
+		return error;
+
+	clear_bit(FF_RUMBLE, cs40l50->input->ffbit);
+
+	cs40l50->input->ff->upload = cs40l50_add_effect;
+	cs40l50->input->ff->playback = cs40l50_playback_effect;
+	cs40l50->input->ff->erase = cs40l50_erase_effect;
+
+	INIT_LIST_HEAD(&cs40l50->effect_head);
+
+	return input_register_device(cs40l50->input);
+}
+
+static const struct cs_dsp_client_ops cs40l50_cs_dsp_client_ops;
+
+static const struct cs_dsp_region cs40l50_dsp_regions[] = {
+	{ .type = WMFW_HALO_PM_PACKED,		.base = CS40L50_DSP1_PMEM_0 },
+	{ .type = WMFW_HALO_XM_PACKED,		.base = CS40L50_DSP1_XMEM_PACKED_0 },
+	{ .type = WMFW_HALO_YM_PACKED,		.base = CS40L50_DSP1_YMEM_PACKED_0 },
+	{ .type = WMFW_ADSP2_XM,		.base = CS40L50_DSP1_XMEM_UNPACKED24_0 },
+	{ .type = WMFW_ADSP2_YM,		.base = CS40L50_DSP1_YMEM_UNPACKED24_0 },
+};
+
+static int cs40l50_cs_dsp_init(struct cs40l50_private *cs40l50)
+{
+	cs40l50->dsp.num = 1;
+	cs40l50->dsp.type = WMFW_HALO;
+	cs40l50->dsp.dev = cs40l50->dev;
+	cs40l50->dsp.regmap = cs40l50->regmap;
+	cs40l50->dsp.base = CS40L50_DSP1_CORE_BASE;
+	cs40l50->dsp.base_sysinfo = CS40L50_DSP1_SYS_INFO_ID;
+	cs40l50->dsp.mem = cs40l50_dsp_regions;
+	cs40l50->dsp.num_mems = ARRAY_SIZE(cs40l50_dsp_regions);
+	cs40l50->dsp.lock_regions = 0xFFFFFFFF;
+	cs40l50->dsp.no_core_startstop = true;
+	cs40l50->dsp.client_ops = &cs40l50_cs_dsp_client_ops;
+
+	return cs_dsp_halo_init(&cs40l50->dsp);
+}
+
+int cs40l50_probe(struct cs40l50_private *cs40l50)
+{
+	int error, i, irq;
+	u32 val;
+
+	mutex_init(&cs40l50->lock);
+
+	error = devm_regulator_bulk_get(cs40l50->dev, ARRAY_SIZE(cs40l50_supplies),
+			cs40l50_supplies);
+	if (error) {
+		dev_err(cs40l50->dev, "Failed to request supplies\n");
+		goto err;
+	}
+
+	error = regulator_bulk_enable(ARRAY_SIZE(cs40l50_supplies), cs40l50_supplies);
+	if (error) {
+		dev_err(cs40l50->dev, "Failed to enable supplies\n");
+		goto err;
+	}
+
+	cs40l50->reset_gpio = devm_gpiod_get_optional(cs40l50->dev, "reset", GPIOD_OUT_HIGH);
+	if (IS_ERR(cs40l50->reset_gpio)) {
+		error = PTR_ERR(cs40l50->reset_gpio);
+		goto err;
+	}
+
+	usleep_range(CS40L50_MIN_RESET_PULSE_WIDTH_US, CS40L50_MIN_RESET_PULSE_WIDTH_US + 100);
+
+	gpiod_set_value_cansleep(cs40l50->reset_gpio, 0);
+
+	usleep_range(CS40L50_CP_READY_DELAY_US, CS40L50_CP_READY_DELAY_US + 1000);
+
+	for (i = 0; i < CS40L50_DSP_TIMEOUT_COUNT; i++) {
+		error = cs40l50_dsp_read(cs40l50, CS40L50_DSP1_HALO_STATE, &val);
+		if (!error && val < 0xFFFF && val >= CS40L50_HALO_STATE_BOOT_DONE)
+			break;
+
+		usleep_range(CS40L50_DSP_POLL_US, CS40L50_DSP_POLL_US + 100);
+	}
+
+	if (i == CS40L50_DSP_TIMEOUT_COUNT) {
+		dev_err(cs40l50->dev, "Firmware boot failed: %d, halo state = %#x\n", error, val);
+		goto err;
+	}
+
+	cs40l50->vibe_workqueue = alloc_ordered_workqueue("cs40l50_workqueue", WQ_HIGHPRI);
+	if (!cs40l50->vibe_workqueue) {
+		error = -ENOMEM;
+		goto err;
+	}
+
+	INIT_WORK(&cs40l50->vibe_start_work, cs40l50_vibe_start_worker);
+	INIT_WORK(&cs40l50->vibe_stop_work, cs40l50_vibe_stop_worker);
+
+	error = cs40l50_cs_dsp_init(cs40l50);
+	if (error)
+		goto err;
+
+	error = cs40l50_input_init(cs40l50);
+	if (error)
+		goto err;
+
+	error = cs40l50_patch_firmware(cs40l50);
+	if (error)
+		goto err;
+
+	error = cs40l50_pseq_init(cs40l50);
+	if (error)
+		goto err;
+
+	error = cs40l50_config_bst(cs40l50);
+	if (error)
+		goto err;
+
+	error = devm_regmap_add_irq_chip(cs40l50->dev, cs40l50->regmap, cs40l50->irq,
+			IRQF_ONESHOT | IRQF_SHARED | IRQF_TRIGGER_LOW, 0, &cs40l50_regmap_irq_chip,
+			&cs40l50->irq_data);
+	if (error) {
+		dev_err(cs40l50->dev, "Failed to register IRQ chip: %d\n", error);
+		goto err;
+	}
+
+	for (i = 0; i < ARRAY_SIZE(cs40l50_irqs); i++) {
+		irq = regmap_irq_get_virq(cs40l50->irq_data, cs40l50_irqs[i].irq);
+		if (irq < 0) {
+			dev_err(cs40l50->dev, "Failed to get %s\n", cs40l50_irqs[i].name);
+			error = irq;
+			goto err;
+		}
+
+		error = devm_request_threaded_irq(cs40l50->dev, irq, NULL, cs40l50_irqs[i].handler,
+				IRQF_ONESHOT | IRQF_SHARED | IRQF_TRIGGER_LOW,
+						cs40l50_irqs[i].name, cs40l50);
+		if (error) {
+			dev_err(cs40l50->dev, "Failed to request IRQ %s: %d\n",
+					cs40l50_irqs[i].name, error);
+			goto err;
+		}
+	}
+
+	return 0;
+
+err:
+	cs40l50_remove(cs40l50);
+
+	return error;
+}
+EXPORT_SYMBOL_GPL(cs40l50_probe);
+
+int cs40l50_remove(struct cs40l50_private *cs40l50)
+{
+	disable_irq(cs40l50->irq);
+
+	if (cs40l50->dsp.booted)
+		cs_dsp_power_down(&cs40l50->dsp);
+	if (&cs40l50->dsp)
+		cs_dsp_remove(&cs40l50->dsp);
+
+	if (cs40l50->vibe_workqueue) {
+		flush_workqueue(cs40l50->vibe_workqueue);
+		destroy_workqueue(cs40l50->vibe_workqueue);
+	}
+
+	gpiod_set_value_cansleep(cs40l50->reset_gpio, 1);
+	regulator_bulk_disable(ARRAY_SIZE(cs40l50_supplies), cs40l50_supplies);
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(cs40l50_remove);
+
+MODULE_DESCRIPTION("CS40L50 Advanced Haptic Driver");
+MODULE_AUTHOR("James Ogletree, Cirrus Logic Inc. <james.ogletree@cirrus.com>");
+MODULE_LICENSE("GPL");
diff --git a/include/linux/input/cs40l50.h b/include/linux/input/cs40l50.h
new file mode 100644
index 000000000000..f2c8a101fa00
--- /dev/null
+++ b/include/linux/input/cs40l50.h
@@ -0,0 +1,321 @@ 
+/* SPDX-License-Identifier: GPL-2.0
+ *
+ * CS40L50 Advanced Haptic Driver with Waveform Memory DSP
+ * and Closed-Loop Algorithms
+ *
+ * Copyright 2023 Cirrus Logic, Inc.
+ *
+ */
+#ifndef __CS40L50_H__
+#define __CS40L50_H__
+
+#include <linux/bitops.h>
+#include <linux/gpio/consumer.h>
+#include <linux/input.h>
+#include <linux/regmap.h>
+#include <linux/interrupt.h>
+#include <linux/firmware/cirrus/cs_dsp.h>
+#include <linux/firmware/cirrus/wmfw.h>
+
+/* Boost */
+#define CS40L50_TEST_KEY			0x40
+#define CS40L50_BLOCK_ENABLES			0x2018
+#define CS40L50_ERROR_RELEASE			0x2034
+#define CS40L50_PWRMGT_CTL			0x2900
+#define CS40L50_BST_LPMODE_SEL			0x3810
+#define CS40L50_STATUS				0x4200
+#define CS40L50_MON_ERROR_OVERIDE		0x4220
+#define CS40L50_TEST_CONFIG			0x4240
+#define CS40L50_MON_VALUE_CTRL			0x4404
+#define CS40L50_TEST_VIS_SPARE			0x5C00
+#define CS40L50_TEMP_WARN_CONFIG		0x6024
+#define CS40L50_KEY_SET_1		0x55
+#define CS40L50_KEY_SET_2		0xAA
+#define CS40L50_TEMP_SENSOR_3		0x400
+#define CS40L50_DCM_LOW_POWER		0x1
+#define CS40L50_MON_ENABLE		0x3201
+#define CS40L50_VBST_VDD_AMP		0x1000000
+#define CS40L50_TEMP_ERR_THLD		0x8000007D
+#define CS40L50_TEMP_WARN_THLD		0x8
+#define CS40L50_TEMP_FILT_DEBOUNCE_OFF	0x510002B5
+#define CS40L50_TEMP_WARN_ATK_VOL	0x522303
+
+/* Interrupts */
+#define CS40L50_IRQ1_INT_1			0xE010
+#define CS40L50_IRQ1_INT_2			0xE014
+#define CS40L50_IRQ1_INT_3			0xE018
+#define CS40L50_IRQ1_INT_4			0xE01C
+#define CS40L50_IRQ1_INT_8			0xE02C
+#define CS40L50_IRQ1_INT_9			0xE030
+#define CS40L50_IRQ1_INT_10			0xE034
+#define CS40L50_IRQ1_INT_18			0xE054
+#define CS40L50_IRQ1_INT_19			0xE058
+#define CS40L50_IRQ1_MASK_1			0xE090
+#define CS40L50_IRQ1_MASK_2			0xE094
+#define CS40L50_IRQ1_MASK_3			0xE098
+#define CS40L50_IRQ1_MASK_8			0xE0AC
+#define CS40L50_IRQ1_MASK_9			0xE0B0
+#define CS40L50_IRQ1_MASK_10			0xE0B4
+#define CS40L50_IRQ1_MASK_18			0xE0D4
+#define CS40L50_IRQ1_MASK_19			0xE0D8
+#define CS40L50_IRQ1_STS_1			0xE210
+#define CS40L50_IRQ1_STS_2			0xE214
+#define CS40L50_IRQ1_STS_3			0xE218
+#define CS40L50_IRQ1_STS_8			0xE22C
+#define CS40L50_IRQ1_STS_9			0xE230
+#define CS40L50_IRQ1_STS_10			0xE234
+#define CS40L50_IRQ1_STS_18			0xE254
+#define CS40L50_IRQ1_STS_19			0xE258
+#define CS40L50_AMP_SHORT_MASK		BIT(31)
+#define CS40L50_VIRT2_MBOX_MASK		BIT(21)
+#define CS40L50_TEMP_ERR_MASK		BIT(31)
+#define CS40L50_BST_UVP_MASK		BIT(6)
+#define CS40L50_BST_SHORT_MASK		BIT(7)
+#define CS40L50_BST_ILIMIT_MASK		BIT(8)
+#define CS40L50_UVLO_VDDBATT_MASK	BIT(16)
+#define CS40L50_GLOBAL_ERROR_MASK	BIT(15)
+#define CS40L50_ERROR_RELEASE_MASK	BIT(11)
+#define CS40L50_BOOT_DONE_MASK		BIT(1)
+#define CS40L50_IRQ(_irq, _name, _hand)	\
+	{				\
+		.irq = _irq,		\
+		.name = _name,		\
+		.handler = _hand,	\
+	}
+#define CS40L50_REG_IRQ(_reg, _irq)					\
+	[CS40L50_ ## _irq ## _IRQ] = {					\
+		.reg_offset = (CS40L50_ ## _reg) - CS40L50_IRQ1_INT_1,	\
+		.mask = CS40L50_ ## _irq ## _MASK			\
+	}
+
+/* Mailbox Inbound Commands */
+#define CS40L50_RAM_BANK_INDEX_START		0x1000000
+#define CS40L50_RAM_BANK_INDEX_END		0x100007F
+#define CS40L50_OWT_INDEX_START			0x1400000
+#define CS40L50_OWT_INDEX_END			0x140007F
+#define CS40L50_ROM_BANK_INDEX_START		0x1800000
+#define CS40L50_ROM_BANK_INDEX_END		0x180001A
+#define CS40L50_OTP_BUZZ			0x1800080
+#define CS40L50_RAM_BUZZ_INDEX_START		0x1800081
+#define CS40L50_RAM_BUZZ_INDEX_END		0x1800085
+#define CS40L50_MBOX_CMD_HIBERNATE		0x2000001
+#define CS40L50_MBOX_CMD_WAKEUP			0x2000002
+#define CS40L50_MBOX_CMD_PREVENT_HIBERNATE	0x2000003
+#define CS40L50_MBOX_CMD_ALLOW_HIBERNATION	0x2000004
+#define CS40L50_MBOX_CMD_SHUTDOWN		0x2000005
+#define CS40L50_MBOX_CMD_DURATION_REPORT	0x3000001
+#define CS40L50_MBOX_CMD_START_I2S		0x3000002
+#define CS40L50_MBOX_CMD_STOP_I2S		0x3000003
+#define CS40L50_MBOX_CMD_HAPTICS_LOGGER_RESET	0x3000004
+#define CS40L50_MBOX_CMD_OWT_PUSH		0x3000008
+#define CS40L50_MBOX_CMD_OWT_RESET		0x3000009
+#define CS40L50_MBOX_CMD_DVL_REINIT		0x300000A
+#define CS40L50_MBOX_CMD_STOP_PLAYBACK		0x5000000
+#define CS40L50_MBOX_CMD_F0_EST			0x7000001
+#define CS40L50_MBOX_CMD_REDC_EST		0x7000002
+#define CS40L50_MBOX_CMD_LE_EST			0x7000004
+#define CS40L50_MBOX_CMD_OWT_DELETE		0xD000000
+
+/* Mailbox Outbound Commands */
+#define CS40L50_MBOX_QUEUE_BASE				0x11004
+#define CS40L50_MBOX_QUEUE_END				0x1101C
+#define CS40L50_DSP_VIRTUAL1_MBOX_1			0x11020
+#define CS40L50_DSP1_MBOX_QUEUE_WT			0x28042C8
+#define CS40L50_DSP1_MBOX_QUEUE_RD			0x28042CC
+#define CS40L50_DSP_MBOX_HAPTIC_COMPLETE_MBOX	0x1000000
+#define CS40L50_DSP_MBOX_HAPTIC_COMPLETE_GPIO	0x1000001
+#define CS40L50_DSP_MBOX_HAPTIC_COMPLETE_I2S	0x1000002
+#define CS40L50_DSP_MBOX_HAPTIC_TRIGGER_MBOX	0x1000010
+#define CS40L50_DSP_MBOX_HAPTIC_TRIGGER_GPIO	0x1000011
+#define CS40L50_DSP_MBOX_HAPTIC_TRIGGER_I2S	0x1000012
+#define CS40L50_DSP_MBOX_INIT			0x2000000
+#define CS40L50_DSP_MBOX_AWAKE			0x2000002
+#define CS40L50_DSP_MBOX_F0_EST_START		0x7000011
+#define CS40L50_DSP_MBOX_F0_EST_DONE		0x7000021
+#define CS40L50_DSP_MBOX_REDC_EST_START		0x7000012
+#define CS40L50_DSP_MBOX_REDC_EST_DONE		0x7000022
+#define CS40L50_DSP_MBOX_LE_EST_START		0x7000014
+#define CS40L50_DSP_MBOX_LE_EST_DONE		0x7000024
+#define CS40L50_DSP_MBOX_ACK			0xA000000
+#define CS40L50_DSP_MBOX_PANIC			0xC000000
+#define CS40L50_DSP_MBOX_WATERMARK		0xD000000
+#define CS40L50_DSP_MBOX_ERR_EVENT_UNMAPPED	0xC0004B3
+#define CS40L50_DSP_MBOX_ERR_EVENT_MODIFY	0xC0004B4
+#define CS40L50_DSP_MBOX_ERR_NULLPTR		0xC0006A5
+#define CS40L50_DSP_MBOX_ERR_BRAKING		0xC0006A8
+#define CS40L50_DSP_MBOX_ERR_INVAL_SRC		0xC0006AC
+#define CS40L50_DSP_MBOX_ERR_ENABLE_RANGE	0xC000836
+#define CS40L50_DSP_MBOX_ERR_GPIO_UNMAPPED	0xC000837
+#define CS40L50_DSP_MBOX_ERR_ISR_RANGE		0xC000838
+#define CS40L50_DSP_MBOX_PERMANENT_SHORT	0xC000C1C
+#define CS40L50_DSP_MBOX_RUNTIME_SHORT		0xC000C1D
+
+/* DSP */
+#define CS40L50_DSP1_XMEM_PACKED_0		0x2000000
+#define CS40L50_DSP1_XMEM_UNPACKED32_0		0x2400000
+#define CS40L50_DSP1_SYS_INFO_ID		0x25E0000
+#define CS40L50_DSP1_XMEM_UNPACKED24_0		0x2800000
+#define CS40L50_NUM_OF_WAVES			0x280CB4C
+#define CS40L50_DSP1_CORE_BASE			0x2B80000
+#define CS40L50_DSP1_SCRATCH1			0x2B805C0
+#define CS40L50_DSP1_SCRATCH2			0x2B805C8
+#define CS40L50_DSP1_SCRATCH3			0x2B805D0
+#define CS40L50_DSP1_SCRATCH4			0x2B805D8
+#define CS40L50_CCM_CORE_CONTROL		0x2BC1000
+#define CS40L50_RAM_INIT			0x28021DC
+#define CS40L50_DSP1_YMEM_PACKED_0		0x2C00000
+#define CS40L50_DSP1_YMEM_UNPACKED32_0		0x3000000
+#define CS40L50_DSP1_YMEM_UNPACKED24_0		0x3400000
+#define CS40L50_DSP1_PMEM_0			0x3800000
+#define CS40L50_DSP1_PMEM_5114			0x3804FE8
+#define CS40L50_DSP1_MEM_RDY_HW		0x2
+#define CS40L50_DSP1_RAM_INIT_FLAG	0x1
+#define CS40L50_DSP1_CLOCK_DISABLE	0x80
+#define CS40L50_DSP1_CLOCK_ENABLE	0x281
+#define CS40L50_DSP_POLL_US		1000
+#define CS40L50_DSP_TIMEOUT_COUNT	100
+#define CS40L50_MBOX_POLL_US		5000
+
+/* Calibration */
+#define CS40L50_DSP1_REDC_ESTIMATION		0x2802F7C
+#define CS40L50_DSP1_F0_ESTIMATION		0x2802F84
+#define CS40L50_DSP1_DYNAMIC_F0_ENABLE		0x2802F8C
+#define CS40L50_DSP1_DYNAMIC_F0_THRESHOLD	0x2802F90
+#define CS40L50_DSP1_F0_STORED			0x2805C00
+#define CS40L50_DSP1_REDC_STORED		0x2805C04
+#define CS40L50_DSP1_RE_EST_STATUS		0x3401B40
+#define CS40L50_REDC_EST_DELAY_US	30000
+#define CS40L50_F0_EST_DELAY_US		10000
+
+/* OWT */
+#define CS40L50_DSP1_OWT_SIZE			0x2805C38
+#define CS40L50_DSP1_OWT_NEXT			0x2805C3C
+#define CS40L50_NUM_OF_OWT_WAVES		0x2805C40
+#define CS40L50_DSP1_OWT_BASE			0x2805C34
+#define CS40L50_MAX_PWLE_SECTIONS	126
+#define CS40L50_CUSTOM_DATA_SIZE	2
+#define CS40L50_WT_HEADER_PWLE_SIZE	12
+#define CS40L50_WT_HEADER_DEFAULT_FLAGS	0x0000
+#define CS40L50_WT_HEADER_OFFSET	3
+#define CS40L50_WT_TYPE_PCM		8
+#define CS40L50_WT_TYPE_PWLE		12
+#define CS40L50_PCM_ID			0x0000
+
+/* GPIO */
+#define CS40L50_DSP1_GPIO_BASE			0x2804140
+#define CS40L50_GPIO_MAPPING_INVALID	0
+#define CS40L50_GPI_DISABLE		0x1FF
+#define CS40L50_BTN_NUM_MASK		GENMASK(14, 12)
+#define CS40L50_BTN_EDGE_MASK		BIT(15)
+
+/* State */
+#define CS40L50_DSP1_HALO_STATE				0x28021E0
+#define CS40L50_HALO_STATE_BOOT_DONE		2
+#define CS40L50_MIN_RESET_PULSE_WIDTH_US	1100
+#define CS40L50_CP_READY_DELAY_US		2200
+
+/* Device */
+#define CS40L50_DEVID			0x0
+#define CS40L50_REVID			0x4
+#define CS40L50_DEVID_A		0x40A50
+#define CS40L50_REVID_B0	0xB0
+
+/* Write sequencer */
+#define CS40L50_DSP1_POWER_ON_SEQ				0x280C320
+#define CS40L50_PSEQ_MAX_WORDS				200
+#define CS40L50_PSEQ_OP_MASK				GENMASK(23, 16)
+#define CS40L50_PSEQ_OP_SHIFT				16
+#define CS40L50_PSEQ_OP_WRITE_ADDR8			0x02
+#define CS40L50_PSEQ_OP_WRITE_L16			0x04
+#define CS40L50_PSEQ_OP_WRITE_H16			0x05
+#define CS40L50_PSEQ_OP_WRITE_FULL			0x00
+#define CS40L50_PSEQ_OP_WRITE_FULL_WORDS		3
+#define CS40L50_PSEQ_OP_WRITE_X16_WORDS			2
+#define CS40L50_PSEQ_OP_END				0xFF
+#define CS40L50_PSEQ_OP_END_WORDS			1
+#define CS40L50_PSEQ_WRITE_FULL_LOWER_ADDR_SHIFT	8
+#define CS40L50_PSEQ_WRITE_FULL_UPPER_ADDR_SHIFT	16
+#define CS40L50_PSEQ_WRITE_FULL_LOWER_ADDR_MASK		GENMASK(15, 0)
+#define CS40L50_PSEQ_WRITE_FULL_UPPER_DATA_SHIFT	24
+#define CS40L50_PSEQ_WRITE_FULL_LOWER_DATA_MASK		GENMASK(23, 0)
+
+enum cs40l50_wvfrm_bank {
+	CS40L50_WVFRM_BANK_RAM,
+	CS40L50_WVFRM_BANK_ROM,
+	CS40L50_WVFRM_BANK_OWT,
+	CS40L50_WVFRM_BANK_BUZ,
+	CS40L50_WVFRM_BANK_NUM,
+};
+
+enum cs40l50_irq_list {
+	CS40L50_GLOBAL_ERROR_IRQ,
+	CS40L50_UVLO_VDDBATT_IRQ,
+	CS40L50_BST_ILIMIT_IRQ,
+	CS40L50_BST_SHORT_IRQ,
+	CS40L50_BST_UVP_IRQ,
+	CS40L50_TEMP_ERR_IRQ,
+	CS40L50_VIRT2_MBOX_IRQ,
+	CS40L50_AMP_SHORT_IRQ,
+	CS40L50_NUM_IRQ
+};
+
+union cs40l50_buzzgen {
+	u32 words[3];
+	struct {
+		u32 freq;
+		u32 level;
+		u32 duration;
+	} params;
+};
+
+struct cs40l50_owt_header {
+	u32 wvfrm_type;
+	u32 offset;
+	u32 data_size;
+};
+
+struct cs40l50_irq {
+	int irq;
+	const char *name;
+	irqreturn_t (*handler)(int irq, void *data);
+};
+
+struct cs40l50_effect {
+	int id;
+	u32 trigger_index;
+	enum cs40l50_wvfrm_bank wvfrm_bank;
+	u32 mapping;
+	struct list_head list;
+};
+
+struct cs40l50_pseq_op {
+	u16 offset;
+	u8 operation;
+	u32 words[3];
+	u32 size;
+	struct list_head list;
+};
+
+struct cs40l50_private {
+	struct device *dev;
+	struct regmap *regmap;
+	struct cs_dsp dsp;
+	struct mutex lock;
+	struct gpio_desc *reset_gpio;
+	struct input_dev *input;
+	struct list_head effect_head;
+	struct ff_effect *trigger_effect;
+	struct workqueue_struct *vibe_workqueue;
+	struct work_struct vibe_start_work;
+	struct work_struct vibe_stop_work;
+	struct regmap_irq_chip_data *irq_data;
+	struct list_head pseq_op_head;
+	int irq;
+};
+
+int cs40l50_probe(struct cs40l50_private *cs40l50);
+int cs40l50_remove(struct cs40l50_private *cs40l50);
+
+extern const struct regmap_config cs40l50_regmap;
+
+#endif /* __CS40L50_H__ */