[PATCH v4 4/4] Input: cs40l50 - Add support for the CS40L50 haptic driver

From: James Ogletree
Date: Wed Oct 18 2023 - 13:58:22 EST


From: James Ogletree <james.ogletree@xxxxxxxxxx>

Introduce support for Cirrus Logic Device CS40L50: a
haptic driver with waveform memory, integrated DSP,
and closed-loop algorithms.

The input driver provides the interface for control of
haptic effects through the device.

Signed-off-by: James Ogletree <james.ogletree@xxxxxxxxxx>
---
MAINTAINERS | 1 +
drivers/input/misc/Kconfig | 10 +
drivers/input/misc/Makefile | 1 +
drivers/input/misc/cs40l50-vibra.c | 353 +++++++++++++++++++++++++++++
4 files changed, 365 insertions(+)
create mode 100644 drivers/input/misc/cs40l50-vibra.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 08e1e9695d49..24a00d8e5c1c 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -4971,6 +4971,7 @@ L: patches@xxxxxxxxxxxxxxxxxxxxx
S: Supported
F: Documentation/devicetree/bindings/input/cirrus,cs40l50.yaml
F: drivers/input/misc/cirrus*
+F: drivers/input/misc/cs40l*
F: drivers/mfd/cs40l*
F: include/linux/input/cirrus*
F: include/linux/mfd/cs40l*
diff --git a/drivers/input/misc/Kconfig b/drivers/input/misc/Kconfig
index 9f088900f863..938090648126 100644
--- a/drivers/input/misc/Kconfig
+++ b/drivers/input/misc/Kconfig
@@ -129,6 +129,16 @@ config INPUT_BMA150
To compile this driver as a module, choose M here: the
module will be called bma150.

+config INPUT_CS40L50_VIBRA
+ tristate "CS40L50 Haptic Driver support"
+ depends on MFD_CS40L50_CORE
+ help
+ Say Y here to enable support for Cirrus Logic's CS40L50
+ haptic driver.
+
+ To compile this driver as a module, choose M here: the
+ module will be called cs40l50-vibra.
+
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 6abefc41037b..6b653ed2124f 100644
--- a/drivers/input/misc/Makefile
+++ b/drivers/input/misc/Makefile
@@ -27,6 +27,7 @@ obj-$(CONFIG_INPUT_CMA3000) += cma3000_d0x.o
obj-$(CONFIG_INPUT_CMA3000_I2C) += cma3000_d0x_i2c.o
obj-$(CONFIG_INPUT_COBALT_BTNS) += cobalt_btns.o
obj-$(CONFIG_INPUT_CPCAP_PWRBUTTON) += cpcap-pwrbutton.o
+obj-$(CONFIG_INPUT_CS40L50_VIBRA) += cs40l50-vibra.o cirrus_haptics.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-vibra.c b/drivers/input/misc/cs40l50-vibra.c
new file mode 100644
index 000000000000..3b3e4cb10de0
--- /dev/null
+++ b/drivers/input/misc/cs40l50-vibra.c
@@ -0,0 +1,353 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * CS40L50 Advanced Haptic Driver with waveform memory,
+ * integrated DSP, and closed-loop algorithms
+ *
+ * Copyright 2023 Cirrus Logic, Inc.
+ *
+ */
+
+#include <linux/firmware/cirrus/wmfw.h>
+#include <linux/mfd/cs40l50.h>
+#include <linux/platform_device.h>
+#include <linux/property.h>
+
+static int cs40l50_add(struct input_dev *dev,
+ struct ff_effect *effect,
+ struct ff_effect *old)
+{
+ struct cs40l50_private *cs40l50 = input_get_drvdata(dev);
+ u32 len = effect->u.periodic.custom_len;
+
+ 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;
+ }
+
+ memcpy(&cs40l50->haptics.add_effect, effect, sizeof(struct ff_effect));
+
+ cs40l50->haptics.add_effect.u.periodic.custom_data = kcalloc(len,
+ sizeof(s16),
+ GFP_KERNEL);
+ if (!cs40l50->haptics.add_effect.u.periodic.custom_data)
+ return -ENOMEM;
+
+ if (copy_from_user(cs40l50->haptics.add_effect.u.periodic.custom_data,
+ effect->u.periodic.custom_data, sizeof(s16) * len)) {
+ cs40l50->haptics.add_error = -EFAULT;
+ goto out_free;
+ }
+
+ queue_work(cs40l50->haptics.vibe_wq, &cs40l50->haptics.add_work);
+ flush_work(&cs40l50->haptics.add_work);
+
+out_free:
+ kfree(cs40l50->haptics.add_effect.u.periodic.custom_data);
+ cs40l50->haptics.add_effect.u.periodic.custom_data = NULL;
+
+ return cs40l50->haptics.add_error;
+}
+
+static int cs40l50_playback(struct input_dev *dev, int effect_id, int val)
+{
+ struct cs40l50_private *cs40l50 = input_get_drvdata(dev);
+
+ if (val > 0) {
+ cs40l50->haptics.start_effect = &dev->ff->effects[effect_id];
+ queue_work(cs40l50->haptics.vibe_wq,
+ &cs40l50->haptics.vibe_start_work);
+ } else {
+ queue_work(cs40l50->haptics.vibe_wq,
+ &cs40l50->haptics.vibe_stop_work);
+ }
+
+ return 0;
+}
+
+static int cs40l50_erase(struct input_dev *dev, int effect_id)
+{
+ struct cs40l50_private *cs40l50 = input_get_drvdata(dev);
+
+ cs40l50->haptics.erase_effect = &dev->ff->effects[effect_id];
+
+ queue_work(cs40l50->haptics.vibe_wq, &cs40l50->haptics.erase_work);
+ flush_work(&cs40l50->haptics.erase_work);
+
+ return cs40l50->haptics.erase_error;
+}
+
+static const struct reg_sequence cs40l50_int_vamp_seq[] = {
+ { CS40L50_BST_LPMODE_SEL, CS40L50_DCM_LOW_POWER },
+ { CS40L50_BLOCK_ENABLES2, CS40L50_OVERTEMP_WARN },
+};
+
+static const struct reg_sequence cs40l50_irq_mask_seq[] = {
+ { CS40L50_IRQ1_MASK_2, CS40L50_IRQ_MASK_2_OVERRIDE },
+ { CS40L50_IRQ1_MASK_20, CS40L50_IRQ_MASK_20_OVERRIDE },
+};
+
+static int cs40l50_hw_init(struct cs40l50_private *cs40l50)
+{
+ int error;
+
+ error = regmap_multi_reg_write(cs40l50->regmap,
+ cs40l50_int_vamp_seq,
+ ARRAY_SIZE(cs40l50_int_vamp_seq));
+ if (error)
+ return error;
+
+ error = cs_hap_pseq_multi_write(&cs40l50->haptics,
+ cs40l50_int_vamp_seq,
+ ARRAY_SIZE(cs40l50_int_vamp_seq),
+ false, PSEQ_OP_WRITE_FULL);
+ if (error)
+ return error;
+
+ error = regmap_multi_reg_write(cs40l50->regmap, cs40l50_irq_mask_seq,
+ ARRAY_SIZE(cs40l50_irq_mask_seq));
+ if (error)
+ return error;
+
+ return cs_hap_pseq_multi_write(&cs40l50->haptics, cs40l50_irq_mask_seq,
+ ARRAY_SIZE(cs40l50_irq_mask_seq), false,
+ PSEQ_OP_WRITE_FULL);
+}
+
+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_CORE_BASE;
+ cs40l50->dsp.base_sysinfo = CS40L50_SYS_INFO_ID;
+ cs40l50->dsp.mem = cs40l50_dsp_regions;
+ cs40l50->dsp.num_mems = ARRAY_SIZE(cs40l50_dsp_regions);
+ cs40l50->dsp.no_core_startstop = true;
+ cs40l50->dsp.client_ops = &cs40l50_cs_dsp_client_ops;
+
+ return cs_dsp_halo_init(&cs40l50->dsp);
+}
+
+static struct cs_hap_bank cs40l50_banks[] = {
+ {
+ .bank = WVFRM_BANK_RAM,
+ .base_index = CS40L50_RAM_BANK_INDEX_START,
+ .max_index = CS40L50_RAM_BANK_INDEX_START,
+ },
+ {
+ .bank = WVFRM_BANK_ROM,
+ .base_index = CS40L50_ROM_BANK_INDEX_START,
+ .max_index = CS40L50_ROM_BANK_INDEX_END,
+ },
+ {
+ .bank = WVFRM_BANK_OWT,
+ .base_index = CS40L50_RTH_INDEX_START,
+ .max_index = CS40L50_RTH_INDEX_END,
+ },
+};
+
+static int cs40l50_cs_hap_init(struct cs40l50_private *cs40l50)
+{
+ cs40l50->haptics.regmap = cs40l50->regmap;
+ cs40l50->haptics.dev = cs40l50->dev;
+ cs40l50->haptics.banks = cs40l50_banks;
+ cs40l50->haptics.dsp.gpio_base_reg = CS40L50_GPIO_BASE;
+ cs40l50->haptics.dsp.owt_base_reg = CS40L50_OWT_BASE;
+ cs40l50->haptics.dsp.owt_offset_reg = CS40L50_OWT_NEXT;
+ cs40l50->haptics.dsp.owt_size_reg = CS40L50_OWT_SIZE;
+ cs40l50->haptics.dsp.mailbox_reg = CS40L50_DSP_MBOX;
+ cs40l50->haptics.dsp.pseq_reg = CS40L50_POWER_ON_SEQ;
+ cs40l50->haptics.dsp.push_owt_cmd = CS40L50_OWT_PUSH;
+ cs40l50->haptics.dsp.delete_owt_cmd = CS40L50_OWT_DELETE;
+ cs40l50->haptics.dsp.stop_cmd = CS40L50_STOP_PLAYBACK;
+ cs40l50->haptics.dsp.pseq_size = CS40L50_PSEQ_SIZE;
+ cs40l50->haptics.runtime_pm = true;
+
+ return cs_hap_init(&cs40l50->haptics);
+}
+
+static void cs40l50_upload_wt(const struct firmware *bin, void *context)
+{
+ struct cs40l50_private *cs40l50 = context;
+ u32 nwaves;
+
+ mutex_lock(&cs40l50->lock);
+
+ if (cs40l50->wmfw) {
+ if (regmap_write(cs40l50->regmap, CS40L50_CCM_CORE_CONTROL,
+ CS40L50_CLOCK_DISABLE))
+ goto err_mutex;
+
+ if (regmap_write(cs40l50->regmap, CS40L50_RAM_INIT,
+ CS40L50_RAM_INIT_FLAG))
+ goto err_mutex;
+
+ if (regmap_write(cs40l50->regmap, CS40L50_PWRMGT_CTL,
+ CS40L50_MEM_RDY_HW))
+ goto err_mutex;
+ }
+
+ cs_dsp_power_up(&cs40l50->dsp, cs40l50->wmfw, "cs40l50.wmfw",
+ bin, "cs40l50.bin", "cs40l50");
+
+ if (cs40l50->wmfw) {
+ if (regmap_write(cs40l50->regmap, CS40L50_CCM_CORE_CONTROL,
+ CS40L50_CLOCK_ENABLE))
+ goto err_mutex;
+ }
+
+ if (regmap_read(cs40l50->regmap, CS40L50_NUM_OF_WAVES, &nwaves))
+ goto err_mutex;
+
+ cs40l50->haptics.banks[WVFRM_BANK_RAM].max_index += (nwaves - 1);
+
+err_mutex:
+ mutex_unlock(&cs40l50->lock);
+ release_firmware(bin);
+ release_firmware(cs40l50->wmfw);
+}
+
+static void cs40l50_upload_patch(const struct firmware *wmfw, void *context)
+{
+ struct cs40l50_private *cs40l50 = context;
+
+ cs40l50->wmfw = wmfw;
+
+ if (request_firmware_nowait(THIS_MODULE, FW_ACTION_UEVENT, CS40L50_WT,
+ cs40l50->dev, GFP_KERNEL,
+ cs40l50, cs40l50_upload_wt))
+ release_firmware(cs40l50->wmfw);
+}
+
+static int cs40l50_input_init(struct cs40l50_private *cs40l50)
+{
+ int error;
+
+ cs40l50->input = devm_input_allocate_device(cs40l50->dev);
+ if (!cs40l50->input)
+ return -ENOMEM;
+
+ cs40l50->input->id.product = cs40l50->devid & 0xFFFF;
+ cs40l50->input->id.version = cs40l50->revid;
+ cs40l50->input->name = "cs40l50_vibra";
+
+ 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;
+
+ cs40l50->input->ff->upload = cs40l50_add;
+ cs40l50->input->ff->playback = cs40l50_playback;
+ cs40l50->input->ff->erase = cs40l50_erase;
+
+ INIT_LIST_HEAD(&cs40l50->haptics.effect_head);
+
+ error = input_register_device(cs40l50->input);
+ if (error)
+ goto err_free_dev;
+
+ return cs40l50_cs_hap_init(cs40l50);
+
+err_free_dev:
+ input_free_device(cs40l50->input);
+ return error;
+}
+static int cs40l50_vibra_probe(struct platform_device *pdev)
+{
+ struct cs40l50_private *cs40l50 = dev_get_drvdata(pdev->dev.parent);
+ int error;
+
+ error = cs40l50_input_init(cs40l50);
+ if (error)
+ return error;
+
+ error = cs40l50_hw_init(cs40l50);
+ if (error)
+ goto err_input;
+
+ error = cs40l50_cs_dsp_init(cs40l50);
+ if (error)
+ goto err_input;
+
+ error = request_firmware_nowait(THIS_MODULE, FW_ACTION_UEVENT,
+ CS40L50_FW, cs40l50->dev,
+ GFP_KERNEL, cs40l50,
+ cs40l50_upload_patch);
+ if (error)
+ goto err_dsp;
+
+ return 0;
+
+err_dsp:
+ cs_dsp_remove(&cs40l50->dsp);
+err_input:
+ input_unregister_device(cs40l50->input);
+ cs_hap_remove(&cs40l50->haptics);
+
+ return error;
+}
+
+static int cs40l50_vibra_remove(struct platform_device *pdev)
+{
+ struct cs40l50_private *cs40l50 = dev_get_drvdata(pdev->dev.parent);
+
+ input_unregister_device(cs40l50->input);
+ cs_hap_remove(&cs40l50->haptics);
+
+ if (cs40l50->dsp.booted)
+ cs_dsp_power_down(&cs40l50->dsp);
+ if (&cs40l50->dsp)
+ cs_dsp_remove(&cs40l50->dsp);
+
+ return 0;
+}
+
+static const struct platform_device_id cs40l50_id_vibra[] = {
+ {"cs40l50-vibra", },
+ {}
+};
+MODULE_DEVICE_TABLE(platform, cs40l50_id_vibra);
+
+static struct platform_driver cs40l50_vibra_driver = {
+ .probe = cs40l50_vibra_probe,
+ .remove = cs40l50_vibra_remove,
+ .id_table = cs40l50_id_vibra,
+ .driver = {
+ .name = "cs40l50-vibra",
+ },
+};
+module_platform_driver(cs40l50_vibra_driver);
+
+MODULE_DESCRIPTION("CS40L50 Advanced Haptic Driver");
+MODULE_AUTHOR("James Ogletree, Cirrus Logic Inc. <james.ogletree@xxxxxxxxxx>");
+MODULE_LICENSE("GPL");
--
2.25.1