From: James Ogletree <james.ogletree@xxxxxxxxxx> Introduce the cirrus haptics library which factors out common haptics operations used by Cirrus Logic Input drivers. Signed-off-by: James Ogletree <james.ogletree@xxxxxxxxxx> --- MAINTAINERS | 2 + drivers/input/misc/cirrus_haptics.c | 586 +++++++++++++++++++++++++++ include/linux/input/cirrus_haptics.h | 121 ++++++ 3 files changed, 709 insertions(+) create mode 100644 drivers/input/misc/cirrus_haptics.c create mode 100644 include/linux/input/cirrus_haptics.h diff --git a/MAINTAINERS b/MAINTAINERS index 28f0ca9324b3..57daf77bf550 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -4970,6 +4970,8 @@ M: Ben Bright <ben.bright@xxxxxxxxxx> L: patches@xxxxxxxxxxxxxxxxxxxxx S: Supported F: Documentation/devicetree/bindings/input/cirrus,cs40l50.yaml +F: drivers/input/misc/cirrus* +F: include/linux/input/cirrus* CIRRUS LOGIC DSP FIRMWARE DRIVER M: Simon Trimmer <simont@xxxxxxxxxxxxxxxxxxxxx> diff --git a/drivers/input/misc/cirrus_haptics.c b/drivers/input/misc/cirrus_haptics.c new file mode 100644 index 000000000000..7e539cd45167 --- /dev/null +++ b/drivers/input/misc/cirrus_haptics.c @@ -0,0 +1,586 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Helper functions for dealing with wavetable + * formats and DSP interfaces used by Cirrus + * haptic drivers. + * + * Copyright 2023 Cirrus Logic, Inc. + */ + +#include <linux/firmware/cirrus/cs_dsp.h> +#include <linux/input.h> +#include <linux/input/cirrus_haptics.h> +#include <linux/pm_runtime.h> + +static int cs_hap_pseq_init(struct cs_hap *haptics) +{ + struct cs_hap_pseq_op *op; + int error, i, num_words; + u8 operation; + u32 *words; + + if (!haptics->dsp.pseq_size || !haptics->dsp.pseq_reg) + return 0; + + INIT_LIST_HEAD(&haptics->pseq_head); + + words = kcalloc(haptics->dsp.pseq_size, sizeof(u32), GFP_KERNEL); + if (!words) + return -ENOMEM; + + error = regmap_bulk_read(haptics->regmap, haptics->dsp.pseq_reg, + words, haptics->dsp.pseq_size); + if (error) + goto err_free; + + for (i = 0; i < haptics->dsp.pseq_size; i += num_words) { + operation = FIELD_GET(PSEQ_OP_MASK, words[i]); + switch (operation) { + case PSEQ_OP_END: + case PSEQ_OP_WRITE_UNLOCK: + num_words = PSEQ_OP_END_WORDS; + break; + case PSEQ_OP_WRITE_ADDR8: + case PSEQ_OP_WRITE_H16: + case PSEQ_OP_WRITE_L16: + num_words = PSEQ_OP_WRITE_X16_WORDS; + break; + case PSEQ_OP_WRITE_FULL: + num_words = PSEQ_OP_WRITE_FULL_WORDS; + break; + default: + error = -EINVAL; + dev_err(haptics->dev, "Unsupported op: %u\n", operation); + goto err_free; + } + + op = devm_kzalloc(haptics->dev, sizeof(*op), GFP_KERNEL); + if (!op) { + error = -ENOMEM; + goto err_free; + } + + op->size = num_words * sizeof(u32); + memcpy(op->words, &words[i], op->size); + op->offset = i * sizeof(u32); + op->operation = operation; + list_add(&op->list, &haptics->pseq_head); + + if (operation == PSEQ_OP_END) + break; + } + + if (operation != PSEQ_OP_END) + error = -ENOENT; + +err_free: + kfree(words); + + return error; +} + +static int cs_hap_pseq_find_end(struct cs_hap *haptics, + struct cs_hap_pseq_op **op_end) +{ + u8 operation = PSEQ_OP_WRITE_FULL; + struct cs_hap_pseq_op *op; + + list_for_each_entry(op, &haptics->pseq_head, list) { + operation = op->operation; + if (operation == PSEQ_OP_END) + break; + } + + if (operation != PSEQ_OP_END) { + dev_err(haptics->dev, "Missing PSEQ list terminator\n"); + return -ENOENT; + } + + *op_end = op; + + return 0; +} + +static struct cs_hap_pseq_op *cs_hap_pseq_find_op(struct cs_hap_pseq_op *match_op, + struct list_head *pseq_head) +{ + struct cs_hap_pseq_op *op; + + list_for_each_entry(op, pseq_head, list) { + if (op->operation == PSEQ_OP_END) + break; + if (op->operation != match_op->operation || + op->words[0] != match_op->words[0]) + continue; + switch (op->operation) { + case PSEQ_OP_WRITE_FULL: + if (FIELD_GET(GENMASK(23, 8), op->words[1]) == + FIELD_GET(GENMASK(23, 8), match_op->words[1])) + return op; + break; + case PSEQ_OP_WRITE_H16: + case PSEQ_OP_WRITE_L16: + if (FIELD_GET(GENMASK(23, 16), op->words[1]) == + FIELD_GET(GENMASK(23, 16), match_op->words[1])) + return op; + break; + default: + break; + } + } + + return NULL; +} + +int cs_hap_pseq_write(struct cs_hap *haptics, u32 addr, + u32 data, bool update, u8 op_code) +{ + struct cs_hap_pseq_op *op, *op_end, *op_new; + struct cs_dsp_chunk ch; + u32 pseq_bytes; + int error; + + op_new = devm_kzalloc(haptics->dev, sizeof(*op_new), GFP_KERNEL); + if (!op_new) + return -ENOMEM; + + op_new->operation = op_code; + + ch = cs_dsp_chunk((void *) op_new->words, + PSEQ_OP_WRITE_FULL_WORDS * sizeof(u32)); + cs_dsp_chunk_write(&ch, 8, op_code); + switch (op_code) { + case PSEQ_OP_WRITE_FULL: + cs_dsp_chunk_write(&ch, 32, addr); + cs_dsp_chunk_write(&ch, 32, data); + break; + case PSEQ_OP_WRITE_L16: + case PSEQ_OP_WRITE_H16: + cs_dsp_chunk_write(&ch, 24, addr); + cs_dsp_chunk_write(&ch, 16, data); + break; + default: + error = -EINVAL; + goto op_new_free; + } + + op_new->size = cs_dsp_chunk_bytes(&ch); + + if (update) { + op = cs_hap_pseq_find_op(op_new, &haptics->pseq_head); + if (!op) { + error = -EINVAL; + goto op_new_free; + } + } + + error = cs_hap_pseq_find_end(haptics, &op_end); + if (error) + goto op_new_free; + + pseq_bytes = haptics->dsp.pseq_size * sizeof(u32); + + if (pseq_bytes - op_end->offset < op_new->size) { + error = -ENOMEM; + goto op_new_free; + } + + if (update) { + op_new->offset = op->offset; + } else { + op_new->offset = op_end->offset; + op_end->offset += op_new->size; + } + + error = regmap_raw_write(haptics->regmap, haptics->dsp.pseq_reg + + op_new->offset, op_new->words, op_new->size); + if (error) + goto op_new_free; + + if (update) { + list_replace(&op->list, &op_new->list); + } else { + error = regmap_raw_write(haptics->regmap, haptics->dsp.pseq_reg + + op_end->offset, op_end->words, + op_end->size); + if (error) + goto op_new_free; + + list_add(&op_new->list, &haptics->pseq_head); + } + + return 0; + +op_new_free: + devm_kfree(haptics->dev, op_new); + + return error; +} +EXPORT_SYMBOL_GPL(cs_hap_pseq_write); + +int cs_hap_pseq_multi_write(struct cs_hap *haptics, + const struct reg_sequence *reg_seq, + int num_regs, bool update, u8 op_code) +{ + int error, i; + + for (i = 0; i < num_regs; i++) { + error = cs_hap_pseq_write(haptics, reg_seq[i].reg, + reg_seq[i].def, update, op_code); + if (error) + return error; + } + + return 0; +} +EXPORT_SYMBOL_GPL(cs_hap_pseq_multi_write); + +static struct cs_hap_effect *cs_hap_find_effect(int id, + struct list_head *effect_head) +{ + struct cs_hap_effect *effect; + + list_for_each_entry(effect, effect_head, list) + if (effect->id == id) + return effect; + + return NULL; +} + +static int cs_hap_effect_bank_set(struct cs_hap *haptics, + struct cs_hap_effect *effect, + struct ff_periodic_effect add_effect) +{ + s16 bank = add_effect.custom_data[0] & 0xffffu; + unsigned int len = add_effect.custom_len; + + if (bank >= WVFRM_BANK_NUM) { + dev_err(haptics->dev, "Invalid waveform bank: %d\n", bank); + return -EINVAL; + } + + effect->bank = len > CUSTOM_DATA_SIZE ? WVFRM_BANK_OWT : bank; + + return 0; +} + +static int cs_hap_effect_mapping_set(struct cs_hap *haptics, u16 button, + struct cs_hap_effect *effect) +{ + u32 gpio_num, gpio_edge; + + if (button) { + gpio_num = FIELD_GET(BTN_NUM_MASK, button); + gpio_edge = FIELD_GET(BTN_EDGE_MASK, button); + effect->mapping = haptics->dsp.gpio_base_reg + + (gpio_num * 8) - gpio_edge; + + return regmap_write(haptics->regmap, effect->mapping, button); + } + + effect->mapping = GPIO_MAPPING_INVALID; + + return 0; +} + +static int cs_hap_effect_index_set(struct cs_hap *haptics, + struct cs_hap_effect *effect, + struct ff_periodic_effect add_effect) +{ + struct cs_hap_effect *owt_effect; + u32 base_index, max_index; + + base_index = haptics->banks[effect->bank].base_index; + max_index = haptics->banks[effect->bank].max_index; + + effect->index = base_index; + + switch (effect->bank) { + case WVFRM_BANK_OWT: + list_for_each_entry(owt_effect, &haptics->effect_head, list) + if (owt_effect->bank == WVFRM_BANK_OWT) + effect->index++; + break; + case WVFRM_BANK_ROM: + case WVFRM_BANK_RAM: + effect->index += add_effect.custom_data[1] & 0xffffu; + break; + default: + dev_err(haptics->dev, "Bank not supported: %d\n", effect->bank); + return -EINVAL; + } + + if (effect->index > max_index || effect->index < base_index) { + dev_err(haptics->dev, "Index out of bounds: %u\n", effect->index); + return -ENOSPC; + } + + return 0; +} + +static int cs_hap_upload_pwle(struct cs_hap *haptics, + struct cs_hap_effect *effect, + struct ff_periodic_effect add_effect) +{ + u32 len, wt_offset, wt_size_words; + struct cs_hap_pwle_header header; + u8 *out_data; + int error; + + error = regmap_read(haptics->regmap, haptics->dsp.owt_offset_reg, + &wt_offset); + if (error) + return error; + + error = regmap_read(haptics->regmap, haptics->dsp.owt_size_reg, + &wt_size_words); + if (error) + return error; + + len = 2 * add_effect.custom_len; + + if ((wt_size_words * sizeof(u32)) < OWT_HEADER_SIZE + len) + return -ENOSPC; + + out_data = kzalloc(OWT_HEADER_SIZE + len, GFP_KERNEL); + if (!out_data) + return -ENOMEM; + + header.type = add_effect.custom_data[0] == PCM_ID ? OWT_TYPE_PCM : + OWT_TYPE_PWLE; + header.offset = OWT_HEADER_SIZE / sizeof(u32); + header.data_words = len / sizeof(u32); + + memcpy(out_data, &header, sizeof(header)); + memcpy(out_data + OWT_HEADER_SIZE, add_effect.custom_data, len); + + error = regmap_bulk_write(haptics->regmap, haptics->dsp.owt_base_reg + + (wt_offset * sizeof(u32)), out_data, + OWT_HEADER_SIZE + len); + if (error) + goto err_free; + + error = regmap_write(haptics->regmap, haptics->dsp.mailbox_reg, + haptics->dsp.push_owt_cmd); + +err_free: + kfree(out_data); + + return error; +} + +static void cs_hap_add_worker(struct work_struct *work) +{ + struct cs_hap *haptics = container_of(work, struct cs_hap, + add_work); + struct ff_effect add_effect = haptics->add_effect; + bool is_new = false; + struct cs_hap_effect *effect; + int error; + + if (haptics->runtime_pm) { + error = pm_runtime_resume_and_get(haptics->dev); + if (error < 0) { + haptics->add_error = error; + return; + } + } + + mutex_lock(&haptics->lock); + + effect = cs_hap_find_effect(add_effect.id, &haptics->effect_head); + if (!effect) { + effect = kzalloc(sizeof(*effect), GFP_KERNEL); + if (!effect) { + error = -ENOMEM; + goto err_mutex; + } + effect->id = add_effect.id; + is_new = true; + } + + error = cs_hap_effect_bank_set(haptics, effect, add_effect.u.periodic); + if (error) + goto err_free; + + error = cs_hap_effect_index_set(haptics, effect, add_effect.u.periodic); + if (error) + goto err_free; + + error = cs_hap_effect_mapping_set(haptics, add_effect.trigger.button, + effect); + if (error) + goto err_free; + + if (effect->bank == WVFRM_BANK_OWT) + error = cs_hap_upload_pwle(haptics, effect, + add_effect.u.periodic); + +err_free: + if (is_new) { + if (error) + kfree(effect); + else + list_add(&effect->list, &haptics->effect_head); + } + +err_mutex: + mutex_unlock(&haptics->lock); + + if (haptics->runtime_pm) { + pm_runtime_mark_last_busy(haptics->dev); + pm_runtime_put_autosuspend(haptics->dev); + } + + haptics->add_error = error; +} + +static void cs_hap_erase_worker(struct work_struct *work) +{ + struct cs_hap *haptics = container_of(work, struct cs_hap, + erase_work); + int error = 0; + struct cs_hap_effect *owt_effect, *erase_effect; + + if (haptics->runtime_pm) { + error = pm_runtime_resume_and_get(haptics->dev); + if (error < 0) { + haptics->erase_error = error; + return; + } + } + + mutex_lock(&haptics->lock); + + erase_effect = cs_hap_find_effect(haptics->erase_effect->id, + &haptics->effect_head); + if (!erase_effect) { + dev_err(haptics->dev, "Effect to erase does not exist\n"); + error = -EINVAL; + goto err_mutex; + } + + if (erase_effect->mapping != GPIO_MAPPING_INVALID) { + error = regmap_write(haptics->regmap, erase_effect->mapping, + GPIO_DISABLE); + if (error) + goto err_mutex; + } + + if (erase_effect->bank == WVFRM_BANK_OWT) { + error = regmap_write(haptics->regmap, haptics->dsp.mailbox_reg, + haptics->dsp.delete_owt_cmd | + erase_effect->index); + if (error) + goto err_mutex; + + list_for_each_entry(owt_effect, &haptics->effect_head, list) + if (owt_effect->bank == WVFRM_BANK_OWT && + owt_effect->index > erase_effect->index) + owt_effect->index--; + } + + list_del(&erase_effect->list); + kfree(erase_effect); + +err_mutex: + mutex_unlock(&haptics->lock); + + if (haptics->runtime_pm) { + pm_runtime_mark_last_busy(haptics->dev); + pm_runtime_put_autosuspend(haptics->dev); + } + + haptics->erase_error = error; +} + +static void cs_hap_vibe_start_worker(struct work_struct *work) +{ + struct cs_hap *haptics = container_of(work, struct cs_hap, + vibe_start_work); + struct cs_hap_effect *effect; + int error; + + if (haptics->runtime_pm) { + error = pm_runtime_resume_and_get(haptics->dev); + if (error < 0) { + haptics->start_error = error; + return; + } + } + + mutex_lock(&haptics->lock); + + effect = cs_hap_find_effect(haptics->start_effect->id, + &haptics->effect_head); + if (effect) { + error = regmap_write(haptics->regmap, haptics->dsp.mailbox_reg, + effect->index); + } else { + dev_err(haptics->dev, "Effect to start does not exist\n"); + error = -EINVAL; + } + + mutex_unlock(&haptics->lock); + + if (haptics->runtime_pm) { + pm_runtime_mark_last_busy(haptics->dev); + pm_runtime_put_autosuspend(haptics->dev); + } + + haptics->start_error = error; +} + +static void cs_hap_vibe_stop_worker(struct work_struct *work) +{ + struct cs_hap *haptics = container_of(work, struct cs_hap, + vibe_stop_work); + int error; + + if (haptics->runtime_pm) { + error = pm_runtime_resume_and_get(haptics->dev); + if (error < 0) { + haptics->stop_error = error; + return; + } + } + + mutex_lock(&haptics->lock); + error = regmap_write(haptics->regmap, haptics->dsp.mailbox_reg, + haptics->dsp.stop_cmd); + mutex_unlock(&haptics->lock); + + if (haptics->runtime_pm) { + pm_runtime_mark_last_busy(haptics->dev); + pm_runtime_put_autosuspend(haptics->dev); + } + + haptics->stop_error = error; +} + +int cs_hap_init(struct cs_hap *haptics) +{ + haptics->vibe_wq = alloc_ordered_workqueue("vibe_wq", 0); + if (!haptics->vibe_wq) + return -ENOMEM; + + mutex_init(&haptics->lock); + + INIT_WORK(&haptics->vibe_start_work, cs_hap_vibe_start_worker); + INIT_WORK(&haptics->vibe_stop_work, cs_hap_vibe_stop_worker); + INIT_WORK(&haptics->erase_work, cs_hap_erase_worker); + INIT_WORK(&haptics->add_work, cs_hap_add_worker); + + return cs_hap_pseq_init(haptics); +} +EXPORT_SYMBOL_GPL(cs_hap_init); + +void cs_hap_remove(struct cs_hap *haptics) +{ + flush_workqueue(haptics->vibe_wq); + destroy_workqueue(haptics->vibe_wq); +} +EXPORT_SYMBOL_GPL(cs_hap_remove); + +MODULE_DESCRIPTION("Cirrus Logic Haptics Support"); +MODULE_LICENSE("GPL"); diff --git a/include/linux/input/cirrus_haptics.h b/include/linux/input/cirrus_haptics.h new file mode 100644 index 000000000000..42f6afed7944 --- /dev/null +++ b/include/linux/input/cirrus_haptics.h @@ -0,0 +1,121 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Helper functions for dealing with wavetable + * formats and DSP interfaces used by Cirrus + * haptic drivers. + * + * Copyright 2023 Cirrus Logic, Inc. + */ + +#ifndef __CIRRUS_HAPTICS_H +#define __CIRRUS_HAPTICS_H + +#include <linux/property.h> +#include <linux/regmap.h> + +/* Power-on write sequencer */ +#define PSEQ_OP_MASK GENMASK(23, 16) +#define PSEQ_OP_SHIFT 16 +#define PSEQ_OP_WRITE_FULL_WORDS 3 +#define PSEQ_OP_WRITE_X16_WORDS 2 +#define PSEQ_OP_END_WORDS 1 +#define PSEQ_OP_WRITE_FULL 0x00 +#define PSEQ_OP_WRITE_ADDR8 0x02 +#define PSEQ_OP_WRITE_L16 0x04 +#define PSEQ_OP_WRITE_H16 0x05 +#define PSEQ_OP_WRITE_UNLOCK 0xFD +#define PSEQ_OP_END 0xFF + +/* Open wavetable */ +#define OWT_HEADER_SIZE 12 +#define OWT_TYPE_PCM 8 +#define OWT_TYPE_PWLE 12 +#define PCM_ID 0x0 +#define CUSTOM_DATA_SIZE 2 + +/* GPIO */ +#define BTN_NUM_MASK GENMASK(14, 12) +#define BTN_EDGE_MASK BIT(15) +#define GPIO_MAPPING_INVALID 0 +#define GPIO_DISABLE 0x1FF + +enum cs_hap_bank_type { + WVFRM_BANK_RAM, + WVFRM_BANK_ROM, + WVFRM_BANK_OWT, + WVFRM_BANK_NUM, +}; + +struct cs_hap_pseq_op { + struct list_head list; + u32 words[3]; + u16 offset; + u8 operation; + u8 size; +}; + +struct cs_hap_effect { + enum cs_hap_bank_type bank; + struct list_head list; + u32 mapping; + u32 index; + int id; +}; + +struct cs_hap_pwle_header { + u32 type; + u32 data_words; + u32 offset; +} __packed; + +struct cs_hap_bank { + enum cs_hap_bank_type bank; + u32 base_index; + u32 max_index; +}; + +struct cs_hap_dsp { + u32 gpio_base_reg; + u32 owt_offset_reg; + u32 owt_size_reg; + u32 owt_base_reg; + u32 mailbox_reg; + u32 pseq_reg; + u32 push_owt_cmd; + u32 delete_owt_cmd; + u32 stop_cmd; + u32 pseq_size; +}; + +struct cs_hap { + struct regmap *regmap; + struct mutex lock; + struct device *dev; + struct list_head pseq_head; + struct cs_hap_bank *banks; + struct cs_hap_dsp dsp; + struct workqueue_struct *vibe_wq; + struct work_struct vibe_start_work; + struct work_struct vibe_stop_work; + struct work_struct erase_work; + struct work_struct add_work; + struct ff_effect *start_effect; + struct ff_effect *erase_effect; + struct ff_effect add_effect; + struct list_head effect_head; + int erase_error; + int start_error; + int stop_error; + int add_error; + bool runtime_pm; +}; + +int cs_hap_pseq_write(struct cs_hap *haptics, u32 addr, + u32 data, bool update, u8 op_code); +int cs_hap_pseq_multi_write(struct cs_hap *haptics, + const struct reg_sequence *reg_seq, + int num_regs, bool update, u8 op_code); +int cs_hap_init(struct cs_hap *haptics); +void cs_hap_remove(struct cs_hap *haptics); + +#endif -- 2.25.1