Introduce support for Cirrus Logic Device CS40L50: a
haptics driver with waveform memory DSP and closed-loop
algorithms.
Signed-off-by: James Ogletree <[email protected]>
---
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: [email protected]
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 <[email protected]>
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. <[email protected]>");
+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. <[email protected]>");
+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. <[email protected]>");
+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__ */
--
2.25.1
On 09/08/2023 15:53, James Ogletree wrote:
> Introduce support for Cirrus Logic Device CS40L50: a
> haptics driver with waveform memory DSP and closed-loop
> algorithms.
> +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);
This is a friendly reminder during the review process.
It seems my previous comments were not fully addressed. Maybe my
feedback got lost between the quotes, maybe you just forgot to apply it.
Please go back to the previous discussion and either implement all
requested changes or keep discussing them.
Thank you.
Best regards,
Krzysztof