diff options
| author | Lee Jones <lee@kernel.org> | 2024-07-04 17:06:36 +0100 |
|---|---|---|
| committer | Lee Jones <lee@kernel.org> | 2024-07-04 17:06:36 +0100 |
| commit | 2d21e9745f7b76fa32f941aa9df876b8c4bda1ca (patch) | |
| tree | 2c46f6edaf9fe9a8c3ac1266263c35c9a2c2fe6f | |
| parent | ecad8fb868aad03a3caf1d6e03a6a55fdd93370a (diff) | |
| parent | c486def5b3ba6c55294cee9abc7396d9dc18f223 (diff) | |
| download | linux-2d21e9745f7b76fa32f941aa9df876b8c4bda1ca.tar.gz linux-2d21e9745f7b76fa32f941aa9df876b8c4bda1ca.tar.bz2 linux-2d21e9745f7b76fa32f941aa9df876b8c4bda1ca.zip | |
Merge branch 'ib-mfd-firmware-input-sound-soc-6.11' into ibs-for-mfd-merged
| -rw-r--r-- | Documentation/devicetree/bindings/input/cirrus,cs40l50.yaml | 68 | ||||
| -rw-r--r-- | MAINTAINERS | 12 | ||||
| -rw-r--r-- | drivers/firmware/cirrus/cs_dsp.c | 278 | ||||
| -rw-r--r-- | drivers/input/misc/Kconfig | 10 | ||||
| -rw-r--r-- | drivers/input/misc/Makefile | 1 | ||||
| -rw-r--r-- | drivers/input/misc/cs40l50-vibra.c | 555 | ||||
| -rw-r--r-- | drivers/mfd/Kconfig | 30 | ||||
| -rw-r--r-- | drivers/mfd/Makefile | 4 | ||||
| -rw-r--r-- | drivers/mfd/cs40l50-core.c | 570 | ||||
| -rw-r--r-- | drivers/mfd/cs40l50-i2c.c | 68 | ||||
| -rw-r--r-- | drivers/mfd/cs40l50-spi.c | 68 | ||||
| -rw-r--r-- | include/linux/firmware/cirrus/cs_dsp.h | 27 | ||||
| -rw-r--r-- | include/linux/mfd/cs40l50.h | 137 | ||||
| -rw-r--r-- | sound/soc/codecs/Kconfig | 11 | ||||
| -rw-r--r-- | sound/soc/codecs/Makefile | 2 | ||||
| -rw-r--r-- | sound/soc/codecs/cs40l50-codec.c | 307 |
16 files changed, 2148 insertions, 0 deletions
diff --git a/Documentation/devicetree/bindings/input/cirrus,cs40l50.yaml b/Documentation/devicetree/bindings/input/cirrus,cs40l50.yaml new file mode 100644 index 000000000000..89bd06864bd4 --- /dev/null +++ b/Documentation/devicetree/bindings/input/cirrus,cs40l50.yaml @@ -0,0 +1,68 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/input/cirrus,cs40l50.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Cirrus Logic CS40L50 Advanced Haptic Driver + +maintainers: + - James Ogletree <jogletre@opensource.cirrus.com> + +description: + CS40L50 is a haptic driver with waveform memory, + integrated DSP, and closed-loop algorithms. + +properties: + compatible: + enum: + - cirrus,cs40l50 + + reg: + maxItems: 1 + + interrupts: + maxItems: 1 + + reset-gpios: + maxItems: 1 + + vdd-a-supply: + description: Power supply for internal analog circuits. + + vdd-p-supply: + description: Power supply for always-on circuits. + + vdd-io-supply: + description: Power supply for digital input/output. + + vdd-b-supply: + description: Power supply for the boost converter. + +required: + - compatible + - reg + - interrupts + - reset-gpios + - vdd-io-supply + +additionalProperties: false + +examples: + - | + #include <dt-bindings/gpio/gpio.h> + #include <dt-bindings/interrupt-controller/irq.h> + + i2c { + #address-cells = <1>; + #size-cells = <0>; + + haptic-driver@34 { + compatible = "cirrus,cs40l50"; + reg = <0x34>; + interrupt-parent = <&gpio>; + interrupts = <113 IRQ_TYPE_LEVEL_LOW>; + reset-gpios = <&gpio 112 GPIO_ACTIVE_LOW>; + vdd-io-supply = <&vreg>; + }; + }; diff --git a/MAINTAINERS b/MAINTAINERS index 003118d088f0..d402ef49e2a4 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -5211,6 +5211,18 @@ F: sound/pci/hda/hda_component* F: sound/pci/hda/hda_cs_dsp_ctl.* F: sound/soc/codecs/cs* +CIRRUS LOGIC HAPTIC DRIVERS +M: James Ogletree <jogletre@opensource.cirrus.com> +M: Fred Treven <fred.treven@cirrus.com> +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* +F: sound/soc/codecs/cs40l* + CIRRUS LOGIC DSP FIRMWARE DRIVER M: Simon Trimmer <simont@opensource.cirrus.com> M: Charles Keepax <ckeepax@opensource.cirrus.com> diff --git a/drivers/firmware/cirrus/cs_dsp.c b/drivers/firmware/cirrus/cs_dsp.c index 0d139e4de37c..85ade75fce32 100644 --- a/drivers/firmware/cirrus/cs_dsp.c +++ b/drivers/firmware/cirrus/cs_dsp.c @@ -275,6 +275,12 @@ #define HALO_MPU_VIO_ERR_SRC_MASK 0x00007fff #define HALO_MPU_VIO_ERR_SRC_SHIFT 0 +/* + * Write Sequence + */ +#define WSEQ_OP_MAX_WORDS 3 +#define WSEQ_END_OF_SCRIPT 0xFFFFFF + struct cs_dsp_ops { bool (*validate_version)(struct cs_dsp *dsp, unsigned int version); unsigned int (*parse_sizes)(struct cs_dsp *dsp, @@ -3398,6 +3404,278 @@ int cs_dsp_chunk_read(struct cs_dsp_chunk *ch, int nbits) } EXPORT_SYMBOL_NS_GPL(cs_dsp_chunk_read, FW_CS_DSP); + +struct cs_dsp_wseq_op { + struct list_head list; + u32 address; + u32 data; + u16 offset; + u8 operation; +}; + +static void cs_dsp_wseq_clear(struct cs_dsp *dsp, struct cs_dsp_wseq *wseq) +{ + struct cs_dsp_wseq_op *op, *op_tmp; + + list_for_each_entry_safe(op, op_tmp, &wseq->ops, list) { + list_del(&op->list); + devm_kfree(dsp->dev, op); + } +} + +static int cs_dsp_populate_wseq(struct cs_dsp *dsp, struct cs_dsp_wseq *wseq) +{ + struct cs_dsp_wseq_op *op = NULL; + struct cs_dsp_chunk chunk; + u8 *words; + int ret; + + if (!wseq->ctl) { + cs_dsp_err(dsp, "No control for write sequence\n"); + return -EINVAL; + } + + words = kzalloc(wseq->ctl->len, GFP_KERNEL); + if (!words) + return -ENOMEM; + + ret = cs_dsp_coeff_read_ctrl(wseq->ctl, 0, words, wseq->ctl->len); + if (ret) { + cs_dsp_err(dsp, "Failed to read %s: %d\n", wseq->ctl->subname, ret); + goto err_free; + } + + INIT_LIST_HEAD(&wseq->ops); + + chunk = cs_dsp_chunk(words, wseq->ctl->len); + + while (!cs_dsp_chunk_end(&chunk)) { + op = devm_kzalloc(dsp->dev, sizeof(*op), GFP_KERNEL); + if (!op) { + ret = -ENOMEM; + goto err_free; + } + + op->offset = cs_dsp_chunk_bytes(&chunk); + op->operation = cs_dsp_chunk_read(&chunk, 8); + + switch (op->operation) { + case CS_DSP_WSEQ_END: + op->data = WSEQ_END_OF_SCRIPT; + break; + case CS_DSP_WSEQ_UNLOCK: + op->data = cs_dsp_chunk_read(&chunk, 16); + break; + case CS_DSP_WSEQ_ADDR8: + op->address = cs_dsp_chunk_read(&chunk, 8); + op->data = cs_dsp_chunk_read(&chunk, 32); + break; + case CS_DSP_WSEQ_H16: + case CS_DSP_WSEQ_L16: + op->address = cs_dsp_chunk_read(&chunk, 24); + op->data = cs_dsp_chunk_read(&chunk, 16); + break; + case CS_DSP_WSEQ_FULL: + op->address = cs_dsp_chunk_read(&chunk, 32); + op->data = cs_dsp_chunk_read(&chunk, 32); + break; + default: + ret = -EINVAL; + cs_dsp_err(dsp, "Unsupported op: %X\n", op->operation); + devm_kfree(dsp->dev, op); + goto err_free; + } + + list_add_tail(&op->list, &wseq->ops); + + if (op->operation == CS_DSP_WSEQ_END) + break; + } + + if (op && op->operation != CS_DSP_WSEQ_END) { + cs_dsp_err(dsp, "%s missing end terminator\n", wseq->ctl->subname); + ret = -ENOENT; + } + +err_free: + kfree(words); + + return ret; +} + +/** + * cs_dsp_wseq_init() - Initialize write sequences contained within the loaded DSP firmware + * @dsp: Pointer to DSP structure + * @wseqs: List of write sequences to initialize + * @num_wseqs: Number of write sequences to initialize + * + * Return: Zero for success, a negative number on error. + */ +int cs_dsp_wseq_init(struct cs_dsp *dsp, struct cs_dsp_wseq *wseqs, unsigned int num_wseqs) +{ + int i, ret; + + lockdep_assert_held(&dsp->pwr_lock); + + for (i = 0; i < num_wseqs; i++) { + ret = cs_dsp_populate_wseq(dsp, &wseqs[i]); + if (ret) { + cs_dsp_wseq_clear(dsp, &wseqs[i]); + return ret; + } + } + + return 0; +} +EXPORT_SYMBOL_NS_GPL(cs_dsp_wseq_init, FW_CS_DSP); + +static struct cs_dsp_wseq_op *cs_dsp_wseq_find_op(u32 addr, u8 op_code, + struct list_head *wseq_ops) +{ + struct cs_dsp_wseq_op *op; + + list_for_each_entry(op, wseq_ops, list) { + if (op->operation == op_code && op->address == addr) + return op; + } + + return NULL; +} + +/** + * cs_dsp_wseq_write() - Add or update an entry in a write sequence + * @dsp: Pointer to a DSP structure + * @wseq: Write sequence to write to + * @addr: Address of the register to be written to + * @data: Data to be written + * @op_code: The type of operation of the new entry + * @update: If true, searches for the first entry in the write sequence with + * the same address and op_code, and replaces it. If false, creates a new entry + * at the tail + * + * This function formats register address and value pairs into the format + * required for write sequence entries, and either updates or adds the + * new entry into the write sequence. + * + * If update is set to true and no matching entry is found, it will add a new entry. + * + * Return: Zero for success, a negative number on error. + */ +int cs_dsp_wseq_write(struct cs_dsp *dsp, struct cs_dsp_wseq *wseq, + u32 addr, u32 data, u8 op_code, bool update) +{ + struct cs_dsp_wseq_op *op_end, *op_new = NULL; + u32 words[WSEQ_OP_MAX_WORDS]; + struct cs_dsp_chunk chunk; + int new_op_size, ret; + + if (update) + op_new = cs_dsp_wseq_find_op(addr, op_code, &wseq->ops); + + /* If entry to update is not found, treat it as a new operation */ + if (!op_new) { + op_end = cs_dsp_wseq_find_op(0, CS_DSP_WSEQ_END, &wseq->ops); + if (!op_end) { + cs_dsp_err(dsp, "Missing terminator for %s\n", wseq->ctl->subname); + return -EINVAL; + } + + op_new = devm_kzalloc(dsp->dev, sizeof(*op_new), GFP_KERNEL); + if (!op_new) + return -ENOMEM; + + op_new->operation = op_code; + op_new->address = addr; + op_new->offset = op_end->offset; + update = false; + } + + op_new->data = data; + + chunk = cs_dsp_chunk(words, sizeof(words)); + cs_dsp_chunk_write(&chunk, 8, op_new->operation); + + switch (op_code) { + case CS_DSP_WSEQ_FULL: + cs_dsp_chunk_write(&chunk, 32, op_new->address); + cs_dsp_chunk_write(&chunk, 32, op_new->data); + break; + case CS_DSP_WSEQ_L16: + case CS_DSP_WSEQ_H16: + cs_dsp_chunk_write(&chunk, 24, op_new->address); + cs_dsp_chunk_write(&chunk, 16, op_new->data); + break; + default: + ret = -EINVAL; + cs_dsp_err(dsp, "Operation %X not supported\n", op_code); + goto op_new_free; + } + + new_op_size = cs_dsp_chunk_bytes(&chunk); + + if (!update) { + if (wseq->ctl->len - op_end->offset < new_op_size) { + cs_dsp_err(dsp, "Not enough memory in %s for entry\n", wseq->ctl->subname); + ret = -E2BIG; + goto op_new_free; + } + + op_end->offset += new_op_size; + + ret = cs_dsp_coeff_write_ctrl(wseq->ctl, op_end->offset / sizeof(u32), + &op_end->data, sizeof(u32)); + if (ret) + goto op_new_free; + + list_add_tail(&op_new->list, &op_end->list); + } + + ret = cs_dsp_coeff_write_ctrl(wseq->ctl, op_new->offset / sizeof(u32), + words, new_op_size); + if (ret) + goto op_new_free; + + return 0; + +op_new_free: + devm_kfree(dsp->dev, op_new); + + return ret; +} +EXPORT_SYMBOL_NS_GPL(cs_dsp_wseq_write, FW_CS_DSP); + +/** + * cs_dsp_wseq_multi_write() - Add or update multiple entries in a write sequence + * @dsp: Pointer to a DSP structure + * @wseq: Write sequence to write to + * @reg_seq: List of address-data pairs + * @num_regs: Number of address-data pairs + * @op_code: The types of operations of the new entries + * @update: If true, searches for the first entry in the write sequence with + * the same address and op_code, and replaces it. If false, creates a new entry + * at the tail + * + * This function calls cs_dsp_wseq_write() for multiple address-data pairs. + * + * Return: Zero for success, a negative number on error. + */ +int cs_dsp_wseq_multi_write(struct cs_dsp *dsp, struct cs_dsp_wseq *wseq, + const struct reg_sequence *reg_seq, int num_regs, + u8 op_code, bool update) +{ + int i, ret; + + for (i = 0; i < num_regs; i++) { + ret = cs_dsp_wseq_write(dsp, wseq, reg_seq[i].reg, + reg_seq[i].def, op_code, update); + if (ret) + return ret; + } + + return 0; +} +EXPORT_SYMBOL_NS_GPL(cs_dsp_wseq_multi_write, FW_CS_DSP); + MODULE_DESCRIPTION("Cirrus Logic DSP Support"); MODULE_AUTHOR("Simon Trimmer <simont@opensource.cirrus.com>"); MODULE_LICENSE("GPL v2"); diff --git a/drivers/input/misc/Kconfig b/drivers/input/misc/Kconfig index 6ba984d7f0b1..ee45dbb0636e 100644 --- a/drivers/input/misc/Kconfig +++ b/drivers/input/misc/Kconfig @@ -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 diff --git a/drivers/input/misc/Makefile b/drivers/input/misc/Makefile index 04296a4abe8e..88279de6d3d5 100644 --- a/drivers/input/misc/Makefile +++ b/drivers/input/misc/Makefile @@ -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 index 000000000000..03bdb7c26ec0 --- /dev/null +++ b/drivers/input/misc/cs40l50-vibra.c @@ -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"); diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig index 266b4f54af60..c09403ea408e 100644 --- a/drivers/mfd/Kconfig +++ b/drivers/mfd/Kconfig @@ -2243,6 +2243,36 @@ config MCP_UCB1200_TS endmenu +config MFD_CS40L50_CORE + tristate + select MFD_CORE + select FW_CS_DS |
