Input: cs40l50 - Add support for the CS40L50 haptic driver
authorJames Ogletree <jogletre@opensource.cirrus.com>
Thu, 20 Jun 2024 16:17:44 +0000 (16:17 +0000)
committerLee Jones <lee@kernel.org>
Fri, 28 Jun 2024 14:36:11 +0000 (15:36 +0100)
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 <jogletre@opensource.cirrus.com>
Reviewed-by: Jeff LaBundy <jeff@labundy.com>
Acked-by: Dmitry Torokhov <dmitry.torokhov@gmail.com>
Link: https://lore.kernel.org/r/20240620161745.2312359-5-jogletre@opensource.cirrus.com
Signed-off-by: Lee Jones <lee@kernel.org>
MAINTAINERS
drivers/input/misc/Kconfig
drivers/input/misc/Makefile
drivers/input/misc/cs40l50-vibra.c [new file with mode: 0644]

index 5c776ac417b9c4adcc63e53d35b897e3e637d14a..2cc63d886e592816c4efd69075e144603886d5c6 100644 (file)
@@ -5213,6 +5213,7 @@ M:        Ben Bright <ben.bright@cirrus.com>
 L:     patches@opensource.cirrus.com
 S:     Supported
 F:     Documentation/devicetree/bindings/input/cirrus,cs40l50.yaml
+F:     drivers/input/misc/cs40l*
 F:     drivers/mfd/cs40l*
 F:     include/linux/mfd/cs40l*
 
index 6ba984d7f0b18694f48160c4628575af1c8ea252..ee45dbb0636e998436c6cd34f4e210fe93b95169 100644 (file)
@@ -140,6 +140,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
index 04296a4abe8e87ac41873d4451abc8e3c00d02a5..88279de6d3d5b435c1d45e8f2764fc7d52004961 100644 (file)
@@ -28,6 +28,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
 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 (file)
index 0000000..03bdb7c
--- /dev/null
@@ -0,0 +1,555 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * CS40L50 Advanced Haptic Driver with waveform memory,
+ * integrated DSP, and closed-loop algorithms
+ *
+ * Copyright 2024 Cirrus Logic, Inc.
+ *
+ * Author: James Ogletree <james.ogletree@cirrus.com>
+ */
+
+#include <linux/bitfield.h>
+#include <linux/input.h>
+#include <linux/mfd/cs40l50.h>
+#include <linux/platform_device.h>
+#include <linux/pm_runtime.h>
+
+/* Wavetables */
+#define CS40L50_RAM_INDEX_START                0x1000000
+#define CS40L50_RAM_INDEX_END          0x100007F
+#define CS40L50_RTH_INDEX_START                0x1400000
+#define CS40L50_RTH_INDEX_END          0x1400001
+#define CS40L50_ROM_INDEX_START                0x1800000
+#define CS40L50_ROM_INDEX_END          0x180001A
+#define CS40L50_TYPE_PCM               8
+#define CS40L50_TYPE_PWLE              12
+#define CS40L50_PCM_ID                 0x0
+#define CS40L50_OWT_CUSTOM_DATA_SIZE   2
+#define CS40L50_CUSTOM_DATA_MASK       0xFFFFU
+
+/* DSP */
+#define CS40L50_GPIO_BASE              0x2804140
+#define CS40L50_OWT_BASE               0x2805C34
+#define CS40L50_OWT_SIZE               0x2805C38
+#define CS40L50_OWT_NEXT               0x2805C3C
+#define CS40L50_EFFECTS_MAX            1
+
+/* GPIO */
+#define CS40L50_GPIO_NUM_MASK          GENMASK(14, 12)
+#define CS40L50_GPIO_EDGE_MASK         BIT(15)
+#define CS40L50_GPIO_MAPPING_NONE      0
+#define CS40L50_GPIO_DISABLE           0x1FF
+
+enum cs40l50_bank_type {
+       CS40L50_WVFRM_BANK_RAM,
+       CS40L50_WVFRM_BANK_ROM,
+       CS40L50_WVFRM_BANK_OWT,
+       CS40L50_WVFRM_BANK_NUM,
+};
+
+/* Describes an area in DSP memory populated by effects */
+struct cs40l50_bank {
+       enum cs40l50_bank_type type;
+       u32 base_index;
+       u32 max_index;
+};
+
+struct cs40l50_effect {
+       enum cs40l50_bank_type type;
+       struct list_head list;
+       u32 gpio_reg;
+       u32 index;
+       int id;
+};
+
+/* Describes haptic interface of loaded DSP firmware */
+struct cs40l50_vibra_dsp {
+       struct cs40l50_bank *banks;
+       u32 gpio_base_reg;
+       u32 owt_offset_reg;
+       u32 owt_size_reg;
+       u32 owt_base_reg;
+       u32 push_owt_cmd;
+       u32 delete_owt_cmd;
+       u32 stop_cmd;
+       int (*write)(struct device *dev, struct regmap *regmap, u32 val);
+};
+
+/* Describes configuration and state of haptic operations */
+struct cs40l50_vibra {
+       struct device *dev;
+       struct regmap *regmap;
+       struct input_dev *input;
+       struct workqueue_struct *vib_wq;
+       struct list_head effect_head;
+       struct cs40l50_vibra_dsp dsp;
+};
+
+struct cs40l50_work {
+       struct cs40l50_vibra *vib;
+       struct ff_effect *effect;
+       struct work_struct work;
+       s16 *custom_data;
+       int custom_len;
+       int count;
+       int error;
+};
+
+static struct cs40l50_bank cs40l50_banks[] = {
+       {
+               .type =         CS40L50_WVFRM_BANK_RAM,
+               .base_index =   CS40L50_RAM_INDEX_START,
+               .max_index =    CS40L50_RAM_INDEX_END,
+       },
+       {
+               .type =         CS40L50_WVFRM_BANK_ROM,
+               .base_index =   CS40L50_ROM_INDEX_START,
+               .max_index =    CS40L50_ROM_INDEX_END,
+       },
+       {
+               .type =         CS40L50_WVFRM_BANK_OWT,
+               .base_index =   CS40L50_RTH_INDEX_START,
+               .max_index =    CS40L50_RTH_INDEX_END,
+       },
+};
+
+static struct cs40l50_vibra_dsp cs40l50_dsp = {
+       .banks =                cs40l50_banks,
+       .gpio_base_reg =        CS40L50_GPIO_BASE,
+       .owt_base_reg =         CS40L50_OWT_BASE,
+       .owt_offset_reg =       CS40L50_OWT_NEXT,
+       .owt_size_reg =         CS40L50_OWT_SIZE,
+       .push_owt_cmd =         CS40L50_OWT_PUSH,
+       .delete_owt_cmd =       CS40L50_OWT_DELETE,
+       .stop_cmd =             CS40L50_STOP_PLAYBACK,
+       .write =                cs40l50_dsp_write,
+};
+
+static struct cs40l50_effect *cs40l50_find_effect(int id, struct list_head *effect_head)
+{
+       struct cs40l50_effect *effect;
+
+       list_for_each_entry(effect, effect_head, list)
+               if (effect->id == id)
+                       return effect;
+
+       return NULL;
+}
+
+static int cs40l50_effect_bank_set(struct cs40l50_work *work_data,
+                                  struct cs40l50_effect *effect)
+{
+       s16 bank_type = work_data->custom_data[0] & CS40L50_CUSTOM_DATA_MASK;
+
+       if (bank_type >= CS40L50_WVFRM_BANK_NUM) {
+               dev_err(work_data->vib->dev, "Invalid bank (%d)\n", bank_type);
+               return -EINVAL;
+       }
+
+       if (work_data->custom_len > CS40L50_OWT_CUSTOM_DATA_SIZE)
+               effect->type = CS40L50_WVFRM_BANK_OWT;
+       else
+               effect->type = bank_type;
+
+       return 0;
+}
+
+static int cs40l50_effect_index_set(struct cs40l50_work *work_data,
+                                   struct cs40l50_effect *effect)
+{
+       struct cs40l50_vibra *vib = work_data->vib;
+       struct cs40l50_effect *owt_effect;
+       u32 base_index, max_index;
+
+       base_index = vib->dsp.banks[effect->type].base_index;
+       max_index = vib->dsp.banks[effect->type].max_index;
+
+       effect->index = base_index;
+
+       switch (effect->type) {
+       case CS40L50_WVFRM_BANK_OWT:
+               list_for_each_entry(owt_effect, &vib->effect_head, list)
+                       if (owt_effect->type == CS40L50_WVFRM_BANK_OWT)
+                               effect->index++;
+               break;
+       case CS40L50_WVFRM_BANK_ROM:
+       case CS40L50_WVFRM_BANK_RAM:
+               effect->index += work_data->custom_data[1] & CS40L50_CUSTOM_DATA_MASK;
+               break;
+       default:
+               dev_err(vib->dev, "Bank type %d not supported\n", effect->type);
+               return -EINVAL;
+       }
+
+       if (effect->index > max_index || effect->index < base_index) {
+               dev_err(vib->dev, "Index out of bounds: %u\n", effect->index);
+               return -ENOSPC;
+       }
+
+       return 0;
+}
+
+static int cs40l50_effect_gpio_mapping_set(struct cs40l50_work *work_data,
+                                          struct cs40l50_effect *effect)
+{
+       u16 gpio_edge, gpio_num, button = work_data->effect->trigger.button;
+       struct cs40l50_vibra *vib = work_data->vib;
+
+       if (button) {
+               gpio_num = FIELD_GET(CS40L50_GPIO_NUM_MASK, button);
+               gpio_edge = FIELD_GET(CS40L50_GPIO_EDGE_MASK, button);
+               effect->gpio_reg = vib->dsp.gpio_base_reg + (gpio_num * 8) - gpio_edge;
+
+               return regmap_write(vib->regmap, effect->gpio_reg, button);
+       }
+
+       effect->gpio_reg = CS40L50_GPIO_MAPPING_NONE;
+
+       return 0;
+}
+
+struct cs40l50_owt_header {
+       u32 type;
+       u32 data_words;
+       u32 offset;
+} __packed;
+
+static int cs40l50_upload_owt(struct cs40l50_work *work_data)
+{
+       u8 *new_owt_effect_data __free(kfree) = NULL;
+       struct cs40l50_vibra *vib = work_data->vib;
+       size_t len = work_data->custom_len * 2;
+       struct cs40l50_owt_header header;
+       u32 offset, size;
+       int error;
+
+       error = regmap_read(vib->regmap, vib->dsp.owt_size_reg, &size);
+       if (error)
+               return error;
+
+       if ((size * sizeof(u32)) < sizeof(header) + len) {
+               dev_err(vib->dev, "No space in open wavetable for effect\n");
+               return -ENOSPC;
+       }
+
+       header.type = work_data->custom_data[0] == CS40L50_PCM_ID ? CS40L50_TYPE_PCM :
+                                                                   CS40L50_TYPE_PWLE;
+       header.offset = sizeof(header) / sizeof(u32);
+       header.data_words = len / sizeof(u32);
+
+       new_owt_effect_data = kmalloc(sizeof(header) + len, GFP_KERNEL);
+
+       memcpy(new_owt_effect_data, &header, sizeof(header));
+       memcpy(new_owt_effect_data + sizeof(header), work_data->custom_data, len);
+
+       error = regmap_read(vib->regmap, vib->dsp.owt_offset_reg, &offset);
+       if (error)
+               return error;
+
+       error = regmap_bulk_write(vib->regmap, vib->dsp.owt_base_reg +
+                                 (offset * sizeof(u32)), new_owt_effect_data,
+                                 sizeof(header) + len);
+       if (error)
+               return error;
+
+       error = vib->dsp.write(vib->dev, vib->regmap, vib->dsp.push_owt_cmd);
+       if (error)
+               return error;
+
+       return 0;
+}
+
+static void cs40l50_add_worker(struct work_struct *work)
+{
+       struct cs40l50_work *work_data = container_of(work, struct cs40l50_work, work);
+       struct cs40l50_vibra *vib = work_data->vib;
+       struct cs40l50_effect *effect;
+       bool is_new = false;
+       int error;
+
+       error = pm_runtime_resume_and_get(vib->dev);
+       if (error)
+               goto err_exit;
+
+       /* Update effect if already uploaded, otherwise create new effect */
+       effect = cs40l50_find_effect(work_data->effect->id, &vib->effect_head);
+       if (!effect) {
+               effect = kzalloc(sizeof(*effect), GFP_KERNEL);
+               if (!effect) {
+                       error = -ENOMEM;
+                       goto err_pm;
+               }
+
+               effect->id = work_data->effect->id;
+               is_new = true;
+       }
+
+       error = cs40l50_effect_bank_set(work_data, effect);
+       if (error)
+               goto err_free;
+
+       error = cs40l50_effect_index_set(work_data, effect);
+       if (error)
+               goto err_free;
+
+       error = cs40l50_effect_gpio_mapping_set(work_data, effect);
+       if (error)
+               goto err_free;
+
+       if (effect->type == CS40L50_WVFRM_BANK_OWT)
+               error = cs40l50_upload_owt(work_data);
+err_free:
+       if (is_new) {
+               if (error)
+                       kfree(effect);
+               else
+                       list_add(&effect->list, &vib->effect_head);
+       }
+err_pm:
+       pm_runtime_mark_last_busy(vib->dev);
+       pm_runtime_put_autosuspend(vib->dev);
+err_exit:
+       work_data->error = error;
+}
+
+static int cs40l50_add(struct input_dev *dev, struct ff_effect *effect,
+                      struct ff_effect *old)
+{
+       struct ff_periodic_effect *periodic = &effect->u.periodic;
+       struct cs40l50_vibra *vib = input_get_drvdata(dev);
+       struct cs40l50_work work_data;
+
+       if (effect->type != FF_PERIODIC || periodic->waveform != FF_CUSTOM) {
+               dev_err(vib->dev, "Type (%#X) or waveform (%#X) unsupported\n",
+                       effect->type, periodic->waveform);
+               return -EINVAL;
+       }
+
+       work_data.custom_data = memdup_array_user(effect->u.periodic.custom_data,
+                                                 effect->u.periodic.custom_len,
+                                                 sizeof(s16));
+       if (IS_ERR(work_data.custom_data))
+               return PTR_ERR(work_data.custom_data);
+
+       work_data.custom_len = effect->u.periodic.custom_len;
+       work_data.vib = vib;
+       work_data.effect = effect;
+       INIT_WORK(&work_data.work, cs40l50_add_worker);
+
+       /* Push to the workqueue to serialize with playbacks */
+       queue_work(vib->vib_wq, &work_data.work);
+       flush_work(&work_data.work);
+
+       kfree(work_data.custom_data);
+
+       return work_data.error;
+}
+
+static void cs40l50_start_worker(struct work_struct *work)
+{
+       struct cs40l50_work *work_data = container_of(work, struct cs40l50_work, work);
+       struct cs40l50_vibra *vib = work_data->vib;
+       struct cs40l50_effect *start_effect;
+
+       if (pm_runtime_resume_and_get(vib->dev) < 0)
+               goto err_free;
+
+       start_effect = cs40l50_find_effect(work_data->effect->id, &vib->effect_head);
+       if (start_effect) {
+               while (--work_data->count >= 0) {
+                       vib->dsp.write(vib->dev, vib->regmap, start_effect->index);
+                       usleep_range(work_data->effect->replay.length,
+                                    work_data->effect->replay.length + 100);
+               }
+       } else {
+               dev_err(vib->dev, "Effect to play not found\n");
+       }
+
+       pm_runtime_mark_last_busy(vib->dev);
+       pm_runtime_put_autosuspend(vib->dev);
+err_free:
+       kfree(work_data);
+}
+
+static void cs40l50_stop_worker(struct work_struct *work)
+{
+       struct cs40l50_work *work_data = container_of(work, struct cs40l50_work, work);
+       struct cs40l50_vibra *vib = work_data->vib;
+
+       if (pm_runtime_resume_and_get(vib->dev) < 0)
+               return;
+
+       vib->dsp.write(vib->dev, vib->regmap, vib->dsp.stop_cmd);
+
+       pm_runtime_mark_last_busy(vib->dev);
+       pm_runtime_put_autosuspend(vib->dev);
+
+       kfree(work_data);
+}
+
+static int cs40l50_playback(struct input_dev *dev, int effect_id, int val)
+{
+       struct cs40l50_vibra *vib = input_get_drvdata(dev);
+       struct cs40l50_work *work_data;
+
+       work_data = kzalloc(sizeof(*work_data), GFP_ATOMIC);
+       if (!work_data)
+               return -ENOMEM;
+
+       work_data->vib = vib;
+
+       if (val > 0) {
+               work_data->effect = &dev->ff->effects[effect_id];
+               work_data->count = val;
+               INIT_WORK(&work_data->work, cs40l50_start_worker);
+       } else {
+               /* Stop the amplifier as device drives only one effect */
+               INIT_WORK(&work_data->work, cs40l50_stop_worker);
+       }
+
+       queue_work(vib->vib_wq, &work_data->work);
+
+       return 0;
+}
+
+static void cs40l50_erase_worker(struct work_struct *work)
+{
+       struct cs40l50_work *work_data = container_of(work, struct cs40l50_work, work);
+       struct cs40l50_effect *erase_effect, *owt_effect;
+       struct cs40l50_vibra *vib = work_data->vib;
+       int error;
+
+       error = pm_runtime_resume_and_get(vib->dev);
+       if (error)
+               goto err_exit;
+
+       erase_effect = cs40l50_find_effect(work_data->effect->id, &vib->effect_head);
+       if (!erase_effect) {
+               dev_err(vib->dev, "Effect to erase not found\n");
+               error = -EINVAL;
+               goto err_pm;
+       }
+
+       if (erase_effect->gpio_reg != CS40L50_GPIO_MAPPING_NONE) {
+               error = regmap_write(vib->regmap, erase_effect->gpio_reg,
+                                    CS40L50_GPIO_DISABLE);
+               if (error)
+                       goto err_pm;
+       }
+
+       if (erase_effect->type == CS40L50_WVFRM_BANK_OWT) {
+               error = vib->dsp.write(vib->dev, vib->regmap,
+                                      vib->dsp.delete_owt_cmd |
+                                      (erase_effect->index & 0xFF));
+               if (error)
+                       goto err_pm;
+
+               list_for_each_entry(owt_effect, &vib->effect_head, list)
+                       if (owt_effect->type == CS40L50_WVFRM_BANK_OWT &&
+                           owt_effect->index > erase_effect->index)
+                               owt_effect->index--;
+       }
+
+       list_del(&erase_effect->list);
+       kfree(erase_effect);
+err_pm:
+       pm_runtime_mark_last_busy(vib->dev);
+       pm_runtime_put_autosuspend(vib->dev);
+err_exit:
+       work_data->error = error;
+}
+
+static int cs40l50_erase(struct input_dev *dev, int effect_id)
+{
+       struct cs40l50_vibra *vib = input_get_drvdata(dev);
+       struct cs40l50_work work_data;
+
+       work_data.vib = vib;
+       work_data.effect = &dev->ff->effects[effect_id];
+
+       INIT_WORK(&work_data.work, cs40l50_erase_worker);
+
+       /* Push to workqueue to serialize with playbacks */
+       queue_work(vib->vib_wq, &work_data.work);
+       flush_work(&work_data.work);
+
+       return work_data.error;
+}
+
+static void cs40l50_remove_wq(void *data)
+{
+       flush_workqueue(data);
+       destroy_workqueue(data);
+}
+
+static int cs40l50_vibra_probe(struct platform_device *pdev)
+{
+       struct cs40l50 *cs40l50 = dev_get_drvdata(pdev->dev.parent);
+       struct cs40l50_vibra *vib;
+       int error;
+
+       vib = devm_kzalloc(pdev->dev.parent, sizeof(*vib), GFP_KERNEL);
+       if (!vib)
+               return -ENOMEM;
+
+       vib->dev = cs40l50->dev;
+       vib->regmap = cs40l50->regmap;
+       vib->dsp = cs40l50_dsp;
+
+       vib->input = devm_input_allocate_device(vib->dev);
+       if (!vib->input)
+               return -ENOMEM;
+
+       vib->input->id.product = cs40l50->devid;
+       vib->input->id.version = cs40l50->revid;
+       vib->input->name = "cs40l50_vibra";
+
+       input_set_drvdata(vib->input, vib);
+       input_set_capability(vib->input, EV_FF, FF_PERIODIC);
+       input_set_capability(vib->input, EV_FF, FF_CUSTOM);
+
+       error = input_ff_create(vib->input, CS40L50_EFFECTS_MAX);
+       if (error) {
+               dev_err(vib->dev, "Failed to create input device\n");
+               return error;
+       }
+
+       vib->input->ff->upload = cs40l50_add;
+       vib->input->ff->playback = cs40l50_playback;
+       vib->input->ff->erase = cs40l50_erase;
+
+       INIT_LIST_HEAD(&vib->effect_head);
+
+       vib->vib_wq = alloc_ordered_workqueue("vib_wq", WQ_HIGHPRI);
+       if (!vib->vib_wq)
+               return -ENOMEM;
+
+       error = devm_add_action_or_reset(vib->dev, cs40l50_remove_wq, vib->vib_wq);
+       if (error)
+               return error;
+
+       error = input_register_device(vib->input);
+       if (error)
+               return error;
+
+       return 0;
+}
+
+static const struct platform_device_id cs40l50_vibra_id_match[] = {
+       { "cs40l50-vibra", },
+       {}
+};
+MODULE_DEVICE_TABLE(platform, cs40l50_vibra_id_match);
+
+static struct platform_driver cs40l50_vibra_driver = {
+       .probe          = cs40l50_vibra_probe,
+       .id_table       = cs40l50_vibra_id_match,
+       .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@cirrus.com>");
+MODULE_LICENSE("GPL");