| // 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) | |
| { | |
| u32 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 (%u)\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); | |
| if (!new_owt_effect_data) | |
| return -ENOMEM; | |
| 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_obj(*effect); | |
| 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_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; | |
| } | |
| if (periodic->custom_len < CS40L50_OWT_CUSTOM_DATA_SIZE) { | |
| dev_err(vib->dev, "Invalid custom data length (%u)\n", | |
| periodic->custom_len); | |
| 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_ONSTACK(&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); | |
| destroy_work_on_stack(&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_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_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_obj(*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_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_ONSTACK(&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); | |
| destroy_work_on_stack(&work_data.work); | |
| return work_data.error; | |
| } | |
| static void cs40l50_remove_wq(void *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"); |