Merge branch 'ib-mfd-regulator-pm8008-6.11' into ibs-for-mfd-merged
diff --git a/Documentation/devicetree/bindings/input/cirrus,cs40l50.yaml b/Documentation/devicetree/bindings/input/cirrus,cs40l50.yaml
new file mode 100644
index 0000000..89bd068
--- /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/Documentation/devicetree/bindings/mfd/marvell,88pm886-a1.yaml b/Documentation/devicetree/bindings/mfd/marvell,88pm886-a1.yaml
new file mode 100644
index 0000000..d6a71c9
--- /dev/null
+++ b/Documentation/devicetree/bindings/mfd/marvell,88pm886-a1.yaml
@@ -0,0 +1,76 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/mfd/marvell,88pm886-a1.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Marvell 88PM886 PMIC core
+
+maintainers:
+ - Karel Balej <balejk@matfyz.cz>
+
+description:
+ Marvell 88PM886 is a PMIC providing several functions such as onkey,
+ regulators or battery and charger.
+
+properties:
+ compatible:
+ const: marvell,88pm886-a1
+
+ reg:
+ maxItems: 1
+
+ interrupts:
+ maxItems: 1
+
+ wakeup-source: true
+
+ regulators:
+ type: object
+ additionalProperties: false
+ patternProperties:
+ "^(ldo(1[0-6]|[1-9])|buck[1-5])$":
+ type: object
+ $ref: /schemas/regulator/regulator.yaml#
+ description: LDO or buck regulator.
+ unevaluatedProperties: false
+
+required:
+ - compatible
+ - reg
+ - interrupts
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/interrupt-controller/irq.h>
+ i2c {
+ #address-cells = <1>;
+ #size-cells = <0>;
+ pmic@30 {
+ compatible = "marvell,88pm886-a1";
+ reg = <0x30>;
+ interrupts = <0 4 IRQ_TYPE_LEVEL_HIGH>;
+ interrupt-parent = <&gic>;
+ wakeup-source;
+
+ regulators {
+ ldo2: ldo2 {
+ regulator-min-microvolt = <3100000>;
+ regulator-max-microvolt = <3300000>;
+ };
+
+ ldo15: ldo15 {
+ regulator-min-microvolt = <3300000>;
+ regulator-max-microvolt = <3300000>;
+ };
+
+ buck2: buck2 {
+ regulator-min-microvolt = <1800000>;
+ regulator-max-microvolt = <1800000>;
+ };
+ };
+ };
+ };
+...
diff --git a/Documentation/devicetree/bindings/mfd/syscon.yaml b/Documentation/devicetree/bindings/mfd/syscon.yaml
index 7ed12a9..d331bb2 100644
--- a/Documentation/devicetree/bindings/mfd/syscon.yaml
+++ b/Documentation/devicetree/bindings/mfd/syscon.yaml
@@ -57,7 +57,6 @@
- hisilicon,pcie-sas-subctrl
- hisilicon,peri-subctrl
- hpe,gxp-sysreg
- - intel,lgm-syscon
- loongson,ls1b-syscon
- loongson,ls1c-syscon
- marvell,armada-3700-cpu-misc
@@ -71,7 +70,6 @@
- mediatek,mt8173-pctl-a-syscfg
- mediatek,mt8365-syscfg
- microchip,lan966x-cpu-syscon
- - microchip,sparx5-cpu-syscon
- mstar,msc313-pmsleep
- nuvoton,ma35d1-sys
- nuvoton,wpcm450-shm
@@ -90,7 +88,6 @@
- ti,am62-usb-phy-ctrl
- ti,am62p-cpsw-mac-efuse
- ti,am654-dss-oldi-io-ctrl
- - ti,am654-serdes-ctrl
- ti,j784s4-pcie-ctrl
- const: syscon
@@ -112,11 +109,6 @@
resets:
maxItems: 1
- hwlocks:
- maxItems: 1
- description:
- Reference to a phandle of a hardware spinlock provider node.
-
required:
- compatible
- reg
diff --git a/Documentation/devicetree/bindings/soc/intel/intel,lgm-syscon.yaml b/Documentation/devicetree/bindings/soc/intel/intel,lgm-syscon.yaml
new file mode 100644
index 0000000..6951d55
--- /dev/null
+++ b/Documentation/devicetree/bindings/soc/intel/intel,lgm-syscon.yaml
@@ -0,0 +1,57 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/soc/intel/intel,lgm-syscon.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Intel Lightning Mountain(LGM) Syscon
+
+maintainers:
+ - Chuanhua Lei <lchuanhua@maxlinear.com>
+ - Rahul Tanwar <rtanwar@maxlinear.com>
+
+properties:
+ compatible:
+ items:
+ - const: intel,lgm-syscon
+ - const: syscon
+
+ reg:
+ maxItems: 1
+
+ ranges: true
+
+ "#address-cells":
+ const: 1
+
+ "#size-cells":
+ const: 1
+
+patternProperties:
+ "^emmc-phy@[0-9a-f]+$":
+ $ref: /schemas/phy/intel,lgm-emmc-phy.yaml#
+
+required:
+ - compatible
+ - reg
+ - "#address-cells"
+ - "#size-cells"
+
+additionalProperties: false
+
+examples:
+ - |
+ chiptop@e0200000 {
+ compatible = "intel,lgm-syscon", "syscon";
+ reg = <0xe0200000 0x100>;
+ ranges = <0x0 0xe0200000 0x100>;
+ #address-cells = <1>;
+ #size-cells = <1>;
+
+ emmc-phy@a8 {
+ compatible = "intel,lgm-emmc-phy";
+ reg = <0x00a8 0x10>;
+ clocks = <&emmc>;
+ #phy-cells = <0>;
+ };
+ };
diff --git a/Documentation/devicetree/bindings/soc/microchip/microchip,sparx5-cpu-syscon.yaml b/Documentation/devicetree/bindings/soc/microchip/microchip,sparx5-cpu-syscon.yaml
new file mode 100644
index 0000000..1f0b542
--- /dev/null
+++ b/Documentation/devicetree/bindings/soc/microchip/microchip,sparx5-cpu-syscon.yaml
@@ -0,0 +1,49 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/soc/microchip/microchip,sparx5-cpu-syscon.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Microchip Sparx5 CPU Syscon
+
+maintainers:
+ - Lars Povlsen <lars.povlsen@microchip.com>
+
+properties:
+ compatible:
+ items:
+ - const: microchip,sparx5-cpu-syscon
+ - const: syscon
+ - const: simple-mfd
+
+ reg:
+ maxItems: 1
+
+ mux-controller:
+ $ref: /schemas/mux/reg-mux.yaml#
+
+required:
+ - compatible
+ - reg
+ - mux-controller
+
+additionalProperties: false
+
+examples:
+ - |
+ soc {
+ #address-cells = <2>;
+ #size-cells = <1>;
+
+ syscon@600000000 {
+ compatible = "microchip,sparx5-cpu-syscon", "syscon",
+ "simple-mfd";
+ reg = <0x6 0x00000000 0xd0>;
+
+ mux: mux-controller {
+ compatible = "mmio-mux";
+ #mux-control-cells = <1>;
+ mux-reg-masks = <0x88 0xf0>;
+ };
+ };
+ };
diff --git a/Documentation/devicetree/bindings/soc/sprd/sprd,sc9863a-glbregs.yaml b/Documentation/devicetree/bindings/soc/sprd/sprd,sc9863a-glbregs.yaml
new file mode 100644
index 0000000..49add56
--- /dev/null
+++ b/Documentation/devicetree/bindings/soc/sprd/sprd,sc9863a-glbregs.yaml
@@ -0,0 +1,55 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/soc/sprd/sprd,sc9863a-glbregs.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: SC9863A Syscon
+
+maintainers:
+ - Orson Zhai <orsonzhai@gmail.com>
+ - Baolin Wang <baolin.wang7@gmail.com>
+ - Chunyan Zhang <zhang.lyra@gmail.com>
+
+properties:
+ compatible:
+ items:
+ - const: sprd,sc9863a-glbregs
+ - const: syscon
+ - const: simple-mfd
+
+ reg:
+ maxItems: 1
+
+ ranges: true
+
+ "#address-cells":
+ const: 1
+
+ "#size-cells":
+ const: 1
+
+patternProperties:
+ "@[0-9a-f]+$":
+ $ref: /schemas/clock/sprd,sc9863a-clk.yaml
+ description: Clock controllers
+
+additionalProperties: false
+
+examples:
+ - |
+ syscon@20e00000 {
+ compatible = "sprd,sc9863a-glbregs", "syscon", "simple-mfd";
+ reg = <0x20e00000 0x4000>;
+ ranges = <0 0x20e00000 0x4000>;
+ #address-cells = <1>;
+ #size-cells = <1>;
+
+ apahb_gate: apahb-gate@0 {
+ compatible = "sprd,sc9863a-apahb-gate";
+ reg = <0x0 0x1020>;
+ #clock-cells = <1>;
+ };
+ };
+
+...
diff --git a/Documentation/devicetree/bindings/soc/ti/ti,am654-serdes-ctrl.yaml b/Documentation/devicetree/bindings/soc/ti/ti,am654-serdes-ctrl.yaml
new file mode 100644
index 0000000..a10a3b8
--- /dev/null
+++ b/Documentation/devicetree/bindings/soc/ti/ti,am654-serdes-ctrl.yaml
@@ -0,0 +1,42 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/soc/ti/ti,am654-serdes-ctrl.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Texas Instruments AM654 Serdes Control Syscon
+
+maintainers:
+ - Nishanth Menon <nm@ti.com>
+
+properties:
+ compatible:
+ items:
+ - const: ti,am654-serdes-ctrl
+ - const: syscon
+
+ reg:
+ maxItems: 1
+
+ mux-controller:
+ $ref: /schemas/mux/reg-mux.yaml#
+
+required:
+ - compatible
+ - reg
+ - mux-controller
+
+additionalProperties: false
+
+examples:
+ - |
+ clock@4080 {
+ compatible = "ti,am654-serdes-ctrl", "syscon";
+ reg = <0x4080 0x4>;
+
+ mux-controller {
+ compatible = "mmio-mux";
+ #mux-control-cells = <1>;
+ mux-reg-masks = <0x0 0x3>; /* lane select */
+ };
+ };
diff --git a/MAINTAINERS b/MAINTAINERS
index d6c9016..5cf839b 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -5135,6 +5135,11 @@
F: Documentation/devicetree/bindings/sound/google,cros-ec-codec.yaml
F: sound/soc/codecs/cros_ec_codec.*
+CHROMEOS EC LED DRIVER
+M: Thomas Weißschuh <thomas@weissschuh.net>
+S: Maintained
+F: drivers/leds/leds-cros_ec.c
+
CHROMEOS EC SUBDRIVERS
M: Benson Leung <bleung@chromium.org>
R: Guenter Roeck <groeck@chromium.org>
@@ -5206,6 +5211,18 @@
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>
@@ -13295,6 +13312,15 @@
F: include/linux/dsa/mv88e6xxx.h
F: include/linux/platform_data/mv88e6xxx.h
+MARVELL 88PM886 PMIC DRIVER
+M: Karel Balej <balejk@matfyz.cz>
+S: Maintained
+F: Documentation/devicetree/bindings/mfd/marvell,88pm886-a1.yaml
+F: drivers/input/misc/88pm886-onkey.c
+F: drivers/mfd/88pm886.c
+F: drivers/regulators/88pm886-regulator.c
+F: include/linux/mfd/88pm886.h
+
MARVELL ARMADA 3700 PHY DRIVERS
M: Miquel Raynal <miquel.raynal@bootlin.com>
S: Maintained
diff --git a/drivers/firmware/cirrus/cs_dsp.c b/drivers/firmware/cirrus/cs_dsp.c
index 0d139e4..85ade75 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/88pm886-onkey.c b/drivers/input/misc/88pm886-onkey.c
new file mode 100644
index 0000000..284ff51
--- /dev/null
+++ b/drivers/input/misc/88pm886-onkey.c
@@ -0,0 +1,98 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <linux/input.h>
+#include <linux/interrupt.h>
+#include <linux/irq.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+
+#include <linux/mfd/88pm886.h>
+
+struct pm886_onkey {
+ struct input_dev *idev;
+ struct pm886_chip *chip;
+};
+
+static irqreturn_t pm886_onkey_irq_handler(int irq, void *data)
+{
+ struct pm886_onkey *onkey = data;
+ struct regmap *regmap = onkey->chip->regmap;
+ struct input_dev *idev = onkey->idev;
+ struct device *parent = idev->dev.parent;
+ unsigned int val;
+ int err;
+
+ err = regmap_read(regmap, PM886_REG_STATUS1, &val);
+ if (err) {
+ dev_err(parent, "Failed to read status: %d\n", err);
+ return IRQ_NONE;
+ }
+ val &= PM886_ONKEY_STS1;
+
+ input_report_key(idev, KEY_POWER, val);
+ input_sync(idev);
+
+ return IRQ_HANDLED;
+}
+
+static int pm886_onkey_probe(struct platform_device *pdev)
+{
+ struct pm886_chip *chip = dev_get_drvdata(pdev->dev.parent);
+ struct device *dev = &pdev->dev;
+ struct pm886_onkey *onkey;
+ struct input_dev *idev;
+ int irq, err;
+
+ onkey = devm_kzalloc(dev, sizeof(*onkey), GFP_KERNEL);
+ if (!onkey)
+ return -ENOMEM;
+
+ onkey->chip = chip;
+
+ irq = platform_get_irq(pdev, 0);
+ if (irq < 0)
+ return dev_err_probe(dev, irq, "Failed to get IRQ\n");
+
+ idev = devm_input_allocate_device(dev);
+ if (!idev) {
+ dev_err(dev, "Failed to allocate input device\n");
+ return -ENOMEM;
+ }
+ onkey->idev = idev;
+
+ idev->name = "88pm886-onkey";
+ idev->phys = "88pm886-onkey/input0";
+ idev->id.bustype = BUS_I2C;
+
+ input_set_capability(idev, EV_KEY, KEY_POWER);
+
+ err = devm_request_threaded_irq(dev, irq, NULL, pm886_onkey_irq_handler,
+ IRQF_ONESHOT | IRQF_NO_SUSPEND, "onkey",
+ onkey);
+ if (err)
+ return dev_err_probe(dev, err, "Failed to request IRQ\n");
+
+ err = input_register_device(idev);
+ if (err)
+ return dev_err_probe(dev, err, "Failed to register input device\n");
+
+ return 0;
+}
+
+static const struct platform_device_id pm886_onkey_id_table[] = {
+ { "88pm886-onkey", },
+ { }
+};
+MODULE_DEVICE_TABLE(platform, pm886_onkey_id_table);
+
+static struct platform_driver pm886_onkey_driver = {
+ .driver = {
+ .name = "88pm886-onkey",
+ },
+ .probe = pm886_onkey_probe,
+ .id_table = pm886_onkey_id_table,
+};
+module_platform_driver(pm886_onkey_driver);
+
+MODULE_DESCRIPTION("Marvell 88PM886 onkey driver");
+MODULE_AUTHOR("Karel Balej <balejk@matfyz.cz>");
+MODULE_LICENSE("GPL");
diff --git a/drivers/input/misc/Kconfig b/drivers/input/misc/Kconfig
index 6ba984d..6a852c7 100644
--- a/drivers/input/misc/Kconfig
+++ b/drivers/input/misc/Kconfig
@@ -33,6 +33,13 @@
To compile this driver as a module, choose M here: the module
will be called 88pm80x_onkey.
+config INPUT_88PM886_ONKEY
+ tristate "Marvell 88PM886 onkey support"
+ depends on MFD_88PM886_PMIC
+ help
+ Support the onkey of Marvell 88PM886 PMIC as an input device
+ reporting power button status.
+
config INPUT_AB8500_PONKEY
tristate "AB8500 Pon (PowerOn) Key"
depends on AB8500_CORE
@@ -140,6 +147,16 @@
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 04296a4..4f7f736 100644
--- a/drivers/input/misc/Makefile
+++ b/drivers/input/misc/Makefile
@@ -7,6 +7,7 @@
obj-$(CONFIG_INPUT_88PM860X_ONKEY) += 88pm860x_onkey.o
obj-$(CONFIG_INPUT_88PM80X_ONKEY) += 88pm80x_onkey.o
+obj-$(CONFIG_INPUT_88PM886_ONKEY) += 88pm886-onkey.o
obj-$(CONFIG_INPUT_AB8500_PONKEY) += ab8500-ponkey.o
obj-$(CONFIG_INPUT_AD714X) += ad714x.o
obj-$(CONFIG_INPUT_AD714X_I2C) += ad714x-i2c.o
@@ -28,6 +29,7 @@
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 0000000..03bdb7c
--- /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/leds/Kconfig b/drivers/leds/Kconfig
index 05e6af8..aa2fec9 100644
--- a/drivers/leds/Kconfig
+++ b/drivers/leds/Kconfig
@@ -179,6 +179,21 @@
To compile this driver as a module, choose M here: the module
will be called leds-cr0014114.
+config LEDS_CROS_EC
+ tristate "LED Support for ChromeOS EC"
+ depends on MFD_CROS_EC_DEV
+ depends on LEDS_CLASS_MULTICOLOR
+ select LEDS_TRIGGERS
+ default MFD_CROS_EC_DEV
+ help
+ This option enables support for LEDs managed by ChromeOS ECs.
+ All LEDs exposed by the EC are supported in multicolor mode.
+ A hardware trigger to switch back to the automatic behaviour is
+ provided.
+
+ To compile this driver as a module, choose M here: the module
+ will be called leds-cros_ec.
+
config LEDS_EL15203000
tristate "LED Support for Crane EL15203000"
depends on LEDS_CLASS
diff --git a/drivers/leds/Makefile b/drivers/leds/Makefile
index effdfc6..3491904 100644
--- a/drivers/leds/Makefile
+++ b/drivers/leds/Makefile
@@ -26,6 +26,7 @@
obj-$(CONFIG_LEDS_COBALT_QUBE) += leds-cobalt-qube.o
obj-$(CONFIG_LEDS_COBALT_RAQ) += leds-cobalt-raq.o
obj-$(CONFIG_LEDS_CPCAP) += leds-cpcap.o
+obj-$(CONFIG_LEDS_CROS_EC) += leds-cros_ec.o
obj-$(CONFIG_LEDS_DA903X) += leds-da903x.o
obj-$(CONFIG_LEDS_DA9052) += leds-da9052.o
obj-$(CONFIG_LEDS_GPIO) += leds-gpio.o
diff --git a/drivers/leds/led-class-multicolor.c b/drivers/leds/led-class-multicolor.c
index ec62a48..584e378 100644
--- a/drivers/leds/led-class-multicolor.c
+++ b/drivers/leds/led-class-multicolor.c
@@ -101,7 +101,7 @@ static ssize_t multi_index_show(struct device *dev,
for (i = 0; i < mcled_cdev->num_colors; i++) {
index = mcled_cdev->subled_info[i].color_index;
- len += sprintf(buf + len, "%s", led_colors[index]);
+ len += sprintf(buf + len, "%s", led_get_color_name(index));
if (i < mcled_cdev->num_colors - 1)
len += sprintf(buf + len, " ");
}
diff --git a/drivers/leds/led-class.c b/drivers/leds/led-class.c
index 24fcff6..2f08c20 100644
--- a/drivers/leds/led-class.c
+++ b/drivers/leds/led-class.c
@@ -503,6 +503,11 @@ int led_classdev_register_ext(struct device *parent,
ret = led_classdev_next_name(proposed_name, final_name, sizeof(final_name));
if (ret < 0)
return ret;
+ else if (ret && led_cdev->flags & LED_REJECT_NAME_CONFLICT)
+ return -EEXIST;
+ else if (ret)
+ dev_warn(parent, "Led %s renamed to %s due to name collision\n",
+ proposed_name, final_name);
if (led_cdev->color >= LED_COLOR_ID_MAX)
dev_warn(parent, "LED %s color identifier out of range\n", final_name);
@@ -518,10 +523,6 @@ int led_classdev_register_ext(struct device *parent,
if (init_data && init_data->fwnode)
device_set_node(led_cdev->dev, init_data->fwnode);
- if (ret)
- dev_warn(parent, "Led %s renamed to %s due to name collision",
- proposed_name, dev_name(led_cdev->dev));
-
if (led_cdev->flags & LED_BRIGHT_HW_CHANGED) {
ret = led_add_brightness_hw_changed(led_cdev);
if (ret) {
diff --git a/drivers/leds/led-core.c b/drivers/leds/led-core.c
index 89c9806..f2cea4e 100644
--- a/drivers/leds/led-core.c
+++ b/drivers/leds/led-core.c
@@ -25,7 +25,7 @@ EXPORT_SYMBOL_GPL(leds_list_lock);
LIST_HEAD(leds_list);
EXPORT_SYMBOL_GPL(leds_list);
-const char * const led_colors[LED_COLOR_ID_MAX] = {
+static const char * const led_colors[LED_COLOR_ID_MAX] = {
[LED_COLOR_ID_WHITE] = "white",
[LED_COLOR_ID_RED] = "red",
[LED_COLOR_ID_GREEN] = "green",
@@ -42,7 +42,6 @@ const char * const led_colors[LED_COLOR_ID_MAX] = {
[LED_COLOR_ID_CYAN] = "cyan",
[LED_COLOR_ID_LIME] = "lime",
};
-EXPORT_SYMBOL_GPL(led_colors);
static int __led_set_brightness(struct led_classdev *led_cdev, unsigned int value)
{
@@ -534,6 +533,15 @@ int led_compose_name(struct device *dev, struct led_init_data *init_data,
}
EXPORT_SYMBOL_GPL(led_compose_name);
+const char *led_get_color_name(u8 color_id)
+{
+ if (color_id >= ARRAY_SIZE(led_colors))
+ return NULL;
+
+ return led_colors[color_id];
+}
+EXPORT_SYMBOL_GPL(led_get_color_name);
+
enum led_default_state led_init_default_state_get(struct fwnode_handle *fwnode)
{
const char *state = NULL;
diff --git a/drivers/leds/leds-cros_ec.c b/drivers/leds/leds-cros_ec.c
new file mode 100644
index 0000000..275522b
--- /dev/null
+++ b/drivers/leds/leds-cros_ec.c
@@ -0,0 +1,277 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * ChromeOS EC LED Driver
+ *
+ * Copyright (C) 2024 Thomas Weißschuh <linux@weissschuh.net>
+ */
+
+#include <linux/device.h>
+#include <linux/leds.h>
+#include <linux/led-class-multicolor.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/platform_data/cros_ec_commands.h>
+#include <linux/platform_data/cros_ec_proto.h>
+
+static const char * const cros_ec_led_functions[] = {
+ [EC_LED_ID_BATTERY_LED] = LED_FUNCTION_CHARGING,
+ [EC_LED_ID_POWER_LED] = LED_FUNCTION_POWER,
+ [EC_LED_ID_ADAPTER_LED] = "adapter",
+ [EC_LED_ID_LEFT_LED] = "left",
+ [EC_LED_ID_RIGHT_LED] = "right",
+ [EC_LED_ID_RECOVERY_HW_REINIT_LED] = "recovery-hw-reinit",
+ [EC_LED_ID_SYSRQ_DEBUG_LED] = "sysrq-debug",
+};
+
+static_assert(ARRAY_SIZE(cros_ec_led_functions) == EC_LED_ID_COUNT);
+
+static const int cros_ec_led_to_linux_id[] = {
+ [EC_LED_COLOR_RED] = LED_COLOR_ID_RED,
+ [EC_LED_COLOR_GREEN] = LED_COLOR_ID_GREEN,
+ [EC_LED_COLOR_BLUE] = LED_COLOR_ID_BLUE,
+ [EC_LED_COLOR_YELLOW] = LED_COLOR_ID_YELLOW,
+ [EC_LED_COLOR_WHITE] = LED_COLOR_ID_WHITE,
+ [EC_LED_COLOR_AMBER] = LED_COLOR_ID_AMBER,
+};
+
+static_assert(ARRAY_SIZE(cros_ec_led_to_linux_id) == EC_LED_COLOR_COUNT);
+
+static const int cros_ec_linux_to_ec_id[] = {
+ [LED_COLOR_ID_RED] = EC_LED_COLOR_RED,
+ [LED_COLOR_ID_GREEN] = EC_LED_COLOR_GREEN,
+ [LED_COLOR_ID_BLUE] = EC_LED_COLOR_BLUE,
+ [LED_COLOR_ID_YELLOW] = EC_LED_COLOR_YELLOW,
+ [LED_COLOR_ID_WHITE] = EC_LED_COLOR_WHITE,
+ [LED_COLOR_ID_AMBER] = EC_LED_COLOR_AMBER,
+};
+
+struct cros_ec_led_priv {
+ struct led_classdev_mc led_mc_cdev;
+ struct cros_ec_device *cros_ec;
+ enum ec_led_id led_id;
+};
+
+static inline struct cros_ec_led_priv *cros_ec_led_cdev_to_priv(struct led_classdev *led_cdev)
+{
+ return container_of(lcdev_to_mccdev(led_cdev), struct cros_ec_led_priv, led_mc_cdev);
+}
+
+union cros_ec_led_cmd_data {
+ struct ec_params_led_control req;
+ struct ec_response_led_control resp;
+} __packed;
+
+static int cros_ec_led_send_cmd(struct cros_ec_device *cros_ec,
+ union cros_ec_led_cmd_data *arg)
+{
+ int ret;
+ struct {
+ struct cros_ec_command msg;
+ union cros_ec_led_cmd_data data;
+ } __packed buf = {
+ .msg = {
+ .version = 1,
+ .command = EC_CMD_LED_CONTROL,
+ .insize = sizeof(arg->resp),
+ .outsize = sizeof(arg->req),
+ },
+ .data.req = arg->req
+ };
+
+ ret = cros_ec_cmd_xfer_status(cros_ec, &buf.msg);
+ if (ret < 0)
+ return ret;
+
+ arg->resp = buf.data.resp;
+
+ return 0;
+}
+
+static int cros_ec_led_trigger_activate(struct led_classdev *led_cdev)
+{
+ struct cros_ec_led_priv *priv = cros_ec_led_cdev_to_priv(led_cdev);
+ union cros_ec_led_cmd_data arg = {};
+
+ arg.req.led_id = priv->led_id;
+ arg.req.flags = EC_LED_FLAGS_AUTO;
+
+ return cros_ec_led_send_cmd(priv->cros_ec, &arg);
+}
+
+static struct led_hw_trigger_type cros_ec_led_trigger_type;
+
+static struct led_trigger cros_ec_led_trigger = {
+ .name = "chromeos-auto",
+ .trigger_type = &cros_ec_led_trigger_type,
+ .activate = cros_ec_led_trigger_activate,
+};
+
+static int cros_ec_led_brightness_set_blocking(struct led_classdev *led_cdev,
+ enum led_brightness brightness)
+{
+ struct cros_ec_led_priv *priv = cros_ec_led_cdev_to_priv(led_cdev);
+ union cros_ec_led_cmd_data arg = {};
+ enum ec_led_colors led_color;
+ struct mc_subled *subled;
+ size_t i;
+
+ led_mc_calc_color_components(&priv->led_mc_cdev, brightness);
+
+ arg.req.led_id = priv->led_id;
+
+ for (i = 0; i < priv->led_mc_cdev.num_colors; i++) {
+ subled = &priv->led_mc_cdev.subled_info[i];
+ led_color = cros_ec_linux_to_ec_id[subled->color_index];
+ arg.req.brightness[led_color] = subled->brightness;
+ }
+
+ return cros_ec_led_send_cmd(priv->cros_ec, &arg);
+}
+
+static int cros_ec_led_count_subleds(struct device *dev,
+ struct ec_response_led_control *resp,
+ unsigned int *max_brightness)
+{
+ unsigned int range, common_range = 0;
+ int num_subleds = 0;
+ size_t i;
+
+ for (i = 0; i < EC_LED_COLOR_COUNT; i++) {
+ range = resp->brightness_range[i];
+
+ if (!range)
+ continue;
+
+ num_subleds++;
+
+ if (!common_range)
+ common_range = range;
+
+ if (common_range != range) {
+ /* The multicolor LED API expects a uniform max_brightness */
+ dev_err(dev, "Inconsistent LED brightness values\n");
+ return -EINVAL;
+ }
+ }
+
+ if (!num_subleds)
+ return -EINVAL;
+
+ *max_brightness = common_range;
+ return num_subleds;
+}
+
+static const char *cros_ec_led_get_color_name(struct led_classdev_mc *led_mc_cdev)
+{
+ int color;
+
+ if (led_mc_cdev->num_colors == 1)
+ color = led_mc_cdev->subled_info[0].color_index;
+ else
+ color = LED_COLOR_ID_MULTI;
+
+ return led_get_color_name(color);
+}
+
+static int cros_ec_led_probe_one(struct device *dev, struct cros_ec_device *cros_ec,
+ enum ec_led_id id)
+{
+ union cros_ec_led_cmd_data arg = {};
+ struct cros_ec_led_priv *priv;
+ struct led_classdev *led_cdev;
+ struct mc_subled *subleds;
+ int i, ret, num_subleds;
+ size_t subled;
+
+ arg.req.led_id = id;
+ arg.req.flags = EC_LED_FLAGS_QUERY;
+ ret = cros_ec_led_send_cmd(cros_ec, &arg);
+ if (ret == -EINVAL)
+ return 0; /* Unknown LED, skip */
+ if (ret == -EOPNOTSUPP)
+ return -ENODEV;
+ if (ret < 0)
+ return ret;
+
+ priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+ if (!priv)
+ return -ENOMEM;
+
+ num_subleds = cros_ec_led_count_subleds(dev, &arg.resp,
+ &priv->led_mc_cdev.led_cdev.max_brightness);
+ if (num_subleds < 0)
+ return num_subleds;
+
+ priv->cros_ec = cros_ec;
+ priv->led_id = id;
+
+ subleds = devm_kcalloc(dev, num_subleds, sizeof(*subleds), GFP_KERNEL);
+ if (!subleds)
+ return -ENOMEM;
+
+ subled = 0;
+ for (i = 0; i < EC_LED_COLOR_COUNT; i++) {
+ if (!arg.resp.brightness_range[i])
+ continue;
+
+ subleds[subled].color_index = cros_ec_led_to_linux_id[i];
+ if (subled == 0)
+ subleds[subled].intensity = 100;
+ subled++;
+ }
+
+ priv->led_mc_cdev.subled_info = subleds;
+ priv->led_mc_cdev.num_colors = num_subleds;
+
+ led_cdev = &priv->led_mc_cdev.led_cdev;
+ led_cdev->brightness_set_blocking = cros_ec_led_brightness_set_blocking;
+ led_cdev->trigger_type = &cros_ec_led_trigger_type;
+ led_cdev->default_trigger = cros_ec_led_trigger.name;
+ led_cdev->hw_control_trigger = cros_ec_led_trigger.name;
+
+ led_cdev->name = devm_kasprintf(dev, GFP_KERNEL, "chromeos:%s:%s",
+ cros_ec_led_get_color_name(&priv->led_mc_cdev),
+ cros_ec_led_functions[id]);
+ if (!led_cdev->name)
+ return -ENOMEM;
+
+ return devm_led_classdev_multicolor_register(dev, &priv->led_mc_cdev);
+}
+
+static int cros_ec_led_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct cros_ec_dev *ec_dev = dev_get_drvdata(dev->parent);
+ struct cros_ec_device *cros_ec = ec_dev->ec_dev;
+ int i, ret = 0;
+
+ ret = devm_led_trigger_register(dev, &cros_ec_led_trigger);
+ if (ret)
+ return ret;
+
+ for (i = 0; i < EC_LED_ID_COUNT; i++) {
+ ret = cros_ec_led_probe_one(dev, cros_ec, i);
+ if (ret)
+ break;
+ }
+
+ return ret;
+}
+
+static const struct platform_device_id cros_ec_led_id[] = {
+ { "cros-ec-led", 0 },
+ {}
+};
+
+static struct platform_driver cros_ec_led_driver = {
+ .driver.name = "cros-ec-led",
+ .probe = cros_ec_led_probe,
+ .id_table = cros_ec_led_id,
+};
+module_platform_driver(cros_ec_led_driver);
+
+MODULE_DEVICE_TABLE(platform, cros_ec_led_id);
+MODULE_DESCRIPTION("ChromeOS EC LED Driver");
+MODULE_AUTHOR("Thomas Weißschuh <linux@weissschuh.net");
+MODULE_LICENSE("GPL");
diff --git a/drivers/leds/leds.h b/drivers/leds/leds.h
index 1138e2a..d7999e7 100644
--- a/drivers/leds/leds.h
+++ b/drivers/leds/leds.h
@@ -30,6 +30,5 @@ ssize_t led_trigger_write(struct file *filp, struct kobject *kobj,
extern struct rw_semaphore leds_list_lock;
extern struct list_head leds_list;
-extern const char * const led_colors[LED_COLOR_ID_MAX];
#endif /* __LEDS_H_INCLUDED */
diff --git a/drivers/mfd/88pm886.c b/drivers/mfd/88pm886.c
new file mode 100644
index 0000000..dbe9efc
--- /dev/null
+++ b/drivers/mfd/88pm886.c
@@ -0,0 +1,148 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <linux/i2c.h>
+#include <linux/mfd/core.h>
+#include <linux/module.h>
+#include <linux/notifier.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/reboot.h>
+#include <linux/regmap.h>
+
+#include <linux/mfd/88pm886.h>
+
+static const struct regmap_config pm886_regmap_config = {
+ .reg_bits = 8,
+ .val_bits = 8,
+ .max_register = PM886_REG_RTC_SPARE6,
+};
+
+static struct regmap_irq pm886_regmap_irqs[] = {
+ REGMAP_IRQ_REG(PM886_IRQ_ONKEY, 0, PM886_INT_ENA1_ONKEY),
+};
+
+static struct regmap_irq_chip pm886_regmap_irq_chip = {
+ .name = "88pm886",
+ .irqs = pm886_regmap_irqs,
+ .num_irqs = ARRAY_SIZE(pm886_regmap_irqs),
+ .num_regs = 4,
+ .status_base = PM886_REG_INT_STATUS1,
+ .ack_base = PM886_REG_INT_STATUS1,
+ .unmask_base = PM886_REG_INT_ENA_1,
+};
+
+static struct resource pm886_onkey_resources[] = {
+ DEFINE_RES_IRQ_NAMED(PM886_IRQ_ONKEY, "88pm886-onkey"),
+};
+
+static struct mfd_cell pm886_devs[] = {
+ MFD_CELL_RES("88pm886-onkey", pm886_onkey_resources),
+ MFD_CELL_NAME("88pm886-regulator"),
+};
+
+static int pm886_power_off_handler(struct sys_off_data *sys_off_data)
+{
+ struct pm886_chip *chip = sys_off_data->cb_data;
+ struct regmap *regmap = chip->regmap;
+ struct device *dev = &chip->client->dev;
+ int err;
+
+ err = regmap_update_bits(regmap, PM886_REG_MISC_CONFIG1, PM886_SW_PDOWN, PM886_SW_PDOWN);
+ if (err) {
+ dev_err(dev, "Failed to power off the device: %d\n", err);
+ return NOTIFY_BAD;
+ }
+ return NOTIFY_DONE;
+}
+
+static int pm886_setup_irq(struct pm886_chip *chip,
+ struct regmap_irq_chip_data **irq_data)
+{
+ struct regmap *regmap = chip->regmap;
+ struct device *dev = &chip->client->dev;
+ int err;
+
+ /* Set interrupt clearing mode to clear on write. */
+ err = regmap_update_bits(regmap, PM886_REG_MISC_CONFIG2,
+ PM886_INT_INV | PM886_INT_CLEAR | PM886_INT_MASK_MODE,
+ PM886_INT_WC);
+ if (err) {
+ dev_err(dev, "Failed to set interrupt clearing mode: %d\n", err);
+ return err;
+ }
+
+ err = devm_regmap_add_irq_chip(dev, regmap, chip->client->irq,
+ IRQF_ONESHOT, 0, &pm886_regmap_irq_chip,
+ irq_data);
+ if (err) {
+ dev_err(dev, "Failed to request IRQ: %d\n", err);
+ return err;
+ }
+
+ return 0;
+}
+
+static int pm886_probe(struct i2c_client *client)
+{
+ struct regmap_irq_chip_data *irq_data;
+ struct device *dev = &client->dev;
+ struct pm886_chip *chip;
+ struct regmap *regmap;
+ unsigned int chip_id;
+ int err;
+
+ chip = devm_kzalloc(dev, sizeof(*chip), GFP_KERNEL);
+ if (!chip)
+ return -ENOMEM;
+
+ chip->client = client;
+ chip->chip_id = (uintptr_t)device_get_match_data(dev);
+ i2c_set_clientdata(client, chip);
+
+ regmap = devm_regmap_init_i2c(client, &pm886_regmap_config);
+ if (IS_ERR(regmap))
+ return dev_err_probe(dev, PTR_ERR(regmap), "Failed to initialize regmap\n");
+ chip->regmap = regmap;
+
+ err = regmap_read(regmap, PM886_REG_ID, &chip_id);
+ if (err)
+ return dev_err_probe(dev, err, "Failed to read chip ID\n");
+
+ if (chip->chip_id != chip_id)
+ return dev_err_probe(dev, -EINVAL, "Unsupported chip: 0x%x\n", chip_id);
+
+ err = pm886_setup_irq(chip, &irq_data);
+ if (err)
+ return err;
+
+ err = devm_mfd_add_devices(dev, PLATFORM_DEVID_NONE, pm886_devs, ARRAY_SIZE(pm886_devs),
+ NULL, 0, regmap_irq_get_domain(irq_data));
+ if (err)
+ return dev_err_probe(dev, err, "Failed to add devices\n");
+
+ err = devm_register_power_off_handler(dev, pm886_power_off_handler, chip);
+ if (err)
+ return dev_err_probe(dev, err, "Failed to register power off handler\n");
+
+ device_init_wakeup(dev, device_property_read_bool(dev, "wakeup-source"));
+
+ return 0;
+}
+
+static const struct of_device_id pm886_of_match[] = {
+ { .compatible = "marvell,88pm886-a1", .data = (void *)PM886_A1_CHIP_ID },
+ { }
+};
+MODULE_DEVICE_TABLE(of, pm886_of_match);
+
+static struct i2c_driver pm886_i2c_driver = {
+ .driver = {
+ .name = "88pm886",
+ .of_match_table = pm886_of_match,
+ },
+ .probe = pm886_probe,
+};
+module_i2c_driver(pm886_i2c_driver);
+
+MODULE_DESCRIPTION("Marvell 88PM886 PMIC driver");
+MODULE_AUTHOR("Karel Balej <balejk@matfyz.cz>");
+MODULE_LICENSE("GPL");
diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig
index 6b220df..abf745b 100644
--- a/drivers/mfd/Kconfig
+++ b/drivers/mfd/Kconfig
@@ -794,6 +794,18 @@
select individual components like voltage regulators, RTC and
battery-charger under the corresponding menus.
+config MFD_88PM886_PMIC
+ bool "Marvell 88PM886 PMIC"
+ depends on I2C=y
+ depends on OF
+ select REGMAP_I2C
+ select REGMAP_IRQ
+ select MFD_CORE
+ help
+ This enables support for Marvell 88PM886 Power Management IC.
+ This includes the I2C driver and the core APIs _only_, you have to
+ select individual components like onkey under the corresponding menus.
+
config MFD_MAX14577
tristate "Maxim Semiconductor MAX14577/77836 MUIC + Charger Support"
depends on I2C
@@ -2244,6 +2256,36 @@
endmenu
+config MFD_CS40L50_CORE
+ tristate
+ select MFD_CORE
+ select FW_CS_DSP
+ select REGMAP_IRQ
+
+config MFD_CS40L50_I2C
+ tristate "Cirrus Logic CS40L50 (I2C)"
+ select REGMAP_I2C
+ select MFD_CS40L50_CORE
+ depends on I2C
+ help
+ Select this to support the Cirrus Logic CS40L50 Haptic
+ Driver over I2C.
+
+ This driver can be built as a module. If built as a module it will be
+ called "cs40l50-i2c".
+
+config MFD_CS40L50_SPI
+ tristate "Cirrus Logic CS40L50 (SPI)"
+ select REGMAP_SPI
+ select MFD_CS40L50_CORE
+ depends on SPI
+ help
+ Select this to support the Cirrus Logic CS40L50 Haptic
+ Driver over SPI.
+
+ This driver can be built as a module. If built as a module it will be
+ called "cs40l50-spi".
+
config MFD_VEXPRESS_SYSREG
tristate "Versatile Express System Registers"
depends on VEXPRESS_CONFIG && GPIOLIB
diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile
index c66f07e..a02d17c 100644
--- a/drivers/mfd/Makefile
+++ b/drivers/mfd/Makefile
@@ -7,6 +7,7 @@
obj-$(CONFIG_MFD_88PM860X) += 88pm860x.o
obj-$(CONFIG_MFD_88PM800) += 88pm800.o 88pm80x.o
obj-$(CONFIG_MFD_88PM805) += 88pm805.o 88pm80x.o
+obj-$(CONFIG_MFD_88PM886_PMIC) += 88pm886.o
obj-$(CONFIG_MFD_ACT8945A) += act8945a.o
obj-$(CONFIG_MFD_SM501) += sm501.o
obj-$(CONFIG_ARCH_BCM2835) += bcm2835-pm.o
@@ -88,6 +89,10 @@
obj-$(CONFIG_MFD_MADERA_I2C) += madera-i2c.o
obj-$(CONFIG_MFD_MADERA_SPI) += madera-spi.o
+obj-$(CONFIG_MFD_CS40L50_CORE) += cs40l50-core.o
+obj-$(CONFIG_MFD_CS40L50_I2C) += cs40l50-i2c.o
+obj-$(CONFIG_MFD_CS40L50_SPI) += cs40l50-spi.o
+
obj-$(CONFIG_TPS6105X) += tps6105x.o
obj-$(CONFIG_TPS65010) += tps65010.o
obj-$(CONFIG_TPS6507X) += tps6507x.o
diff --git a/drivers/mfd/cros_ec_dev.c b/drivers/mfd/cros_ec_dev.c
index a52d59c..af698e2 100644
--- a/drivers/mfd/cros_ec_dev.c
+++ b/drivers/mfd/cros_ec_dev.c
@@ -99,6 +99,14 @@ static const struct mfd_cell cros_ec_wdt_cells[] = {
{ .name = "cros-ec-wdt", }
};
+static const struct mfd_cell cros_ec_led_cells[] = {
+ { .name = "cros-ec-led", },
+};
+
+static const struct mfd_cell cros_ec_keyboard_leds_cells[] = {
+ { .name = "cros-keyboard-leds", },
+};
+
static const struct cros_feature_to_cells cros_subdevices[] = {
{
.id = EC_FEATURE_CEC,
@@ -125,6 +133,16 @@ static const struct cros_feature_to_cells cros_subdevices[] = {
.mfd_cells = cros_ec_wdt_cells,
.num_cells = ARRAY_SIZE(cros_ec_wdt_cells),
},
+ {
+ .id = EC_FEATURE_LED,
+ .mfd_cells = cros_ec_led_cells,
+ .num_cells = ARRAY_SIZE(cros_ec_led_cells),
+ },
+ {
+ .id = EC_FEATURE_PWM_KEYB,
+ .mfd_cells = cros_ec_keyboard_leds_cells,
+ .num_cells = ARRAY_SIZE(cros_ec_keyboard_leds_cells),
+ },
};
static const struct mfd_cell cros_ec_platform_cells[] = {
diff --git a/drivers/mfd/cs40l50-core.c b/drivers/mfd/cs40l50-core.c
new file mode 100644
index 0000000..26e7a76
--- /dev/null
+++ b/drivers/mfd/cs40l50-core.c
@@ -0,0 +1,570 @@
+// 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/firmware/cirrus/cs_dsp.h>
+#include <linux/firmware/cirrus/wmfw.h>
+#include <linux/mfd/core.h>
+#include <linux/mfd/cs40l50.h>
+#include <linux/pm_runtime.h>
+#include <linux/regulator/consumer.h>
+
+static const struct mfd_cell cs40l50_devs[] = {
+ { .name = "cs40l50-codec", },
+ { .name = "cs40l50-vibra", },
+};
+
+const struct regmap_config cs40l50_regmap = {
+ .reg_bits = 32,
+ .reg_stride = 4,
+ .val_bits = 32,
+ .reg_format_endian = REGMAP_ENDIAN_BIG,
+ .val_format_endian = REGMAP_ENDIAN_BIG,
+};
+EXPORT_SYMBOL_GPL(cs40l50_regmap);
+
+static const char * const cs40l50_supplies[] = {
+ "vdd-io",
+};
+
+static const struct regmap_irq cs40l50_reg_irqs[] = {
+ REGMAP_IRQ_REG(CS40L50_DSP_QUEUE_IRQ, CS40L50_IRQ1_INT_2_OFFSET,
+ CS40L50_DSP_QUEUE_MASK),
+ REGMAP_IRQ_REG(CS40L50_AMP_SHORT_IRQ, CS40L50_IRQ1_INT_1_OFFSET,
+ CS40L50_AMP_SHORT_MASK),
+ REGMAP_IRQ_REG(CS40L50_TEMP_ERR_IRQ, CS40L50_IRQ1_INT_8_OFFSET,
+ CS40L50_TEMP_ERR_MASK),
+ REGMAP_IRQ_REG(CS40L50_BST_UVP_IRQ, CS40L50_IRQ1_INT_9_OFFSET,
+ CS40L50_BST_UVP_MASK),
+ REGMAP_IRQ_REG(CS40L50_BST_SHORT_IRQ, CS40L50_IRQ1_INT_9_OFFSET,
+ CS40L50_BST_SHORT_MASK),
+ REGMAP_IRQ_REG(CS40L50_BST_ILIMIT_IRQ, CS40L50_IRQ1_INT_9_OFFSET,
+ CS40L50_BST_ILIMIT_MASK),
+ REGMAP_IRQ_REG(CS40L50_UVLO_VDDBATT_IRQ, CS40L50_IRQ1_INT_10_OFFSET,
+ CS40L50_UVLO_VDDBATT_MASK),
+ REGMAP_IRQ_REG(CS40L50_GLOBAL_ERROR_IRQ, CS40L50_IRQ1_INT_18_OFFSET,
+ CS40L50_GLOBAL_ERROR_MASK),
+};
+
+static struct regmap_irq_chip cs40l50_irq_chip = {
+ .name = "cs40l50",
+ .status_base = CS40L50_IRQ1_INT_1,
+ .mask_base = CS40L50_IRQ1_MASK_1,
+ .ack_base = CS40L50_IRQ1_INT_1,
+ .num_regs = 22,
+ .irqs = cs40l50_reg_irqs,
+ .num_irqs = ARRAY_SIZE(cs40l50_reg_irqs),
+ .runtime_pm = true,
+};
+
+int cs40l50_dsp_write(struct device *dev, struct regmap *regmap, u32 val)
+{
+ int i, ret;
+ u32 ack;
+
+ /* Device NAKs if hibernating, so optionally retry */
+ for (i = 0; i < CS40L50_DSP_TIMEOUT_COUNT; i++) {
+ ret = regmap_write(regmap, CS40L50_DSP_QUEUE, val);
+ if (!ret)
+ break;
+
+ usleep_range(CS40L50_DSP_POLL_US, CS40L50_DSP_POLL_US + 100);
+ }
+
+ /* If the write never took place, no need to check for the ACK */
+ if (i == CS40L50_DSP_TIMEOUT_COUNT) {
+ dev_err(dev, "Timed out writing %#X to DSP: %d\n", val, ret);
+ return ret;
+ }
+
+ ret = regmap_read_poll_timeout(regmap, CS40L50_DSP_QUEUE, ack, !ack,
+ CS40L50_DSP_POLL_US,
+ CS40L50_DSP_POLL_US * CS40L50_DSP_TIMEOUT_COUNT);
+ if (ret)
+ dev_err(dev, "DSP failed to ACK %#X: %d\n", val, ret);
+
+ return ret;
+}
+EXPORT_SYMBOL_GPL(cs40l50_dsp_write);
+
+static const struct cs_dsp_region cs40l50_dsp_regions[] = {
+ { .type = WMFW_HALO_PM_PACKED, .base = CS40L50_PMEM_0 },
+ { .type = WMFW_HALO_XM_PACKED, .base = CS40L50_XMEM_PACKED_0 },
+ { .type = WMFW_HALO_YM_PACKED, .base = CS40L50_YMEM_PACKED_0 },
+ { .type = WMFW_ADSP2_XM, .base = CS40L50_XMEM_UNPACKED24_0 },
+ { .type = WMFW_ADSP2_YM, .base = CS40L50_YMEM_UNPACKED24_0 },
+};
+
+static const struct reg_sequence cs40l50_internal_vamp_config[] = {
+ { CS40L50_BST_LPMODE_SEL, CS40L50_DCM_LOW_POWER },
+ { CS40L50_BLOCK_ENABLES2, CS40L50_OVERTEMP_WARN },
+};
+
+static const struct reg_sequence cs40l50_irq_mask_override[] = {
+ { CS40L50_IRQ1_MASK_2, CS40L50_IRQ_MASK_2_OVERRIDE },
+ { CS40L50_IRQ1_MASK_20, CS40L50_IRQ_MASK_20_OVERRIDE },
+};
+
+static int cs40l50_wseq_init(struct cs40l50 *cs40l50)
+{
+ struct cs_dsp *dsp = &cs40l50->dsp;
+
+ cs40l50->wseqs[CS40L50_STANDBY].ctl = cs_dsp_get_ctl(dsp, "STANDBY_SEQUENCE",
+ WMFW_ADSP2_XM,
+ CS40L50_PM_ALGO);
+ if (!cs40l50->wseqs[CS40L50_STANDBY].ctl) {
+ dev_err(cs40l50->dev, "Control not found for standby sequence\n");
+ return -ENOENT;
+ }
+
+ cs40l50->wseqs[CS40L50_ACTIVE].ctl = cs_dsp_get_ctl(dsp, "ACTIVE_SEQUENCE",
+ WMFW_ADSP2_XM,
+ CS40L50_PM_ALGO);
+ if (!cs40l50->wseqs[CS40L50_ACTIVE].ctl) {
+ dev_err(cs40l50->dev, "Control not found for active sequence\n");
+ return -ENOENT;
+ }
+
+ cs40l50->wseqs[CS40L50_PWR_ON].ctl = cs_dsp_get_ctl(dsp, "PM_PWR_ON_SEQ",
+ WMFW_ADSP2_XM,
+ CS40L50_PM_ALGO);
+ if (!cs40l50->wseqs[CS40L50_PWR_ON].ctl) {
+ dev_err(cs40l50->dev, "Control not found for power-on sequence\n");
+ return -ENOENT;
+ }
+
+ return cs_dsp_wseq_init(&cs40l50->dsp, cs40l50->wseqs, ARRAY_SIZE(cs40l50->wseqs));
+}
+
+static int cs40l50_dsp_config(struct cs40l50 *cs40l50)
+{
+ int ret;
+
+ /* Configure internal V_AMP supply */
+ ret = regmap_multi_reg_write(cs40l50->regmap, cs40l50_internal_vamp_config,
+ ARRAY_SIZE(cs40l50_internal_vamp_config));
+ if (ret)
+ return ret;
+
+ ret = cs_dsp_wseq_multi_write(&cs40l50->dsp, &cs40l50->wseqs[CS40L50_PWR_ON],
+ cs40l50_internal_vamp_config, CS_DSP_WSEQ_FULL,
+ ARRAY_SIZE(cs40l50_internal_vamp_config), false);
+ if (ret)
+ return ret;
+
+ /* Override firmware defaults for IRQ masks */
+ ret = regmap_multi_reg_write(cs40l50->regmap, cs40l50_irq_mask_override,
+ ARRAY_SIZE(cs40l50_irq_mask_override));
+ if (ret)
+ return ret;
+
+ return cs_dsp_wseq_multi_write(&cs40l50->dsp, &cs40l50->wseqs[CS40L50_PWR_ON],
+ cs40l50_irq_mask_override, CS_DSP_WSEQ_FULL,
+ ARRAY_SIZE(cs40l50_irq_mask_override), false);
+}
+
+static int cs40l50_dsp_post_run(struct cs_dsp *dsp)
+{
+ struct cs40l50 *cs40l50 = container_of(dsp, struct cs40l50, dsp);
+ int ret;
+
+ ret = cs40l50_wseq_init(cs40l50);
+ if (ret)
+ return ret;
+
+ ret = cs40l50_dsp_config(cs40l50);
+ if (ret) {
+ dev_err(cs40l50->dev, "Failed to configure DSP: %d\n", ret);
+ return ret;
+ }
+
+ ret = devm_mfd_add_devices(cs40l50->dev, PLATFORM_DEVID_NONE, cs40l50_devs,
+ ARRAY_SIZE(cs40l50_devs), NULL, 0, NULL);
+ if (ret)
+ dev_err(cs40l50->dev, "Failed to add child devices: %d\n", ret);
+
+ return ret;
+}
+
+static const struct cs_dsp_client_ops client_ops = {
+ .post_run = cs40l50_dsp_post_run,
+};
+
+static void cs40l50_dsp_remove(void *data)
+{
+ cs_dsp_remove(data);
+}
+
+static int cs40l50_dsp_init(struct cs40l50 *cs40l50)
+{
+ int ret;
+
+ 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 = &client_ops;
+
+ ret = cs_dsp_halo_init(&cs40l50->dsp);
+ if (ret)
+ return ret;
+
+ return devm_add_action_or_reset(cs40l50->dev, cs40l50_dsp_remove,
+ &cs40l50->dsp);
+}
+
+static int cs40l50_reset_dsp(struct cs40l50 *cs40l50)
+{
+ int ret;
+
+ mutex_lock(&cs40l50->lock);
+
+ if (cs40l50->dsp.running)
+ cs_dsp_stop(&cs40l50->dsp);
+
+ if (cs40l50->dsp.booted)
+ cs_dsp_power_down(&cs40l50->dsp);
+
+ ret = cs40l50_dsp_write(cs40l50->dev, cs40l50->regmap, CS40L50_SHUTDOWN);
+ if (ret)
+ goto err_mutex;
+
+ ret = cs_dsp_power_up(&cs40l50->dsp, cs40l50->fw, "cs40l50.wmfw",
+ cs40l50->bin, "cs40l50.bin", "cs40l50");
+ if (ret)
+ goto err_mutex;
+
+ ret = cs40l50_dsp_write(cs40l50->dev, cs40l50->regmap, CS40L50_SYSTEM_RESET);
+ if (ret)
+ goto err_mutex;
+
+ ret = cs40l50_dsp_write(cs40l50->dev, cs40l50->regmap, CS40L50_PREVENT_HIBER);
+ if (ret)
+ goto err_mutex;
+
+ ret = cs_dsp_run(&cs40l50->dsp);
+err_mutex:
+ mutex_unlock(&cs40l50->lock);
+
+ return ret;
+}
+
+static void cs40l50_dsp_power_down(void *data)
+{
+ cs_dsp_power_down(data);
+}
+
+static void cs40l50_dsp_stop(void *data)
+{
+ cs_dsp_stop(data);
+}
+
+static void cs40l50_dsp_bringup(const struct firmware *bin, void *context)
+{
+ struct cs40l50 *cs40l50 = context;
+ u32 nwaves;
+ int ret;
+
+ /* Wavetable is optional; bringup DSP regardless */
+ cs40l50->bin = bin;
+
+ ret = cs40l50_reset_dsp(cs40l50);
+ if (ret) {
+ dev_err(cs40l50->dev, "Failed to reset DSP: %d\n", ret);
+ goto err_fw;
+ }
+
+ ret = regmap_read(cs40l50->regmap, CS40L50_NUM_WAVES, &nwaves);
+ if (ret)
+ goto err_fw;
+
+ dev_info(cs40l50->dev, "%u RAM effects loaded\n", nwaves);
+
+ /* Add teardown actions for first-time bringup */
+ ret = devm_add_action_or_reset(cs40l50->dev, cs40l50_dsp_power_down,
+ &cs40l50->dsp);
+ if (ret) {
+ dev_err(cs40l50->dev, "Failed to add power down action: %d\n", ret);
+ goto err_fw;
+ }
+
+ ret = devm_add_action_or_reset(cs40l50->dev, cs40l50_dsp_stop, &cs40l50->dsp);
+ if (ret)
+ dev_err(cs40l50->dev, "Failed to add stop action: %d\n", ret);
+err_fw:
+ release_firmware(cs40l50->bin);
+ release_firmware(cs40l50->fw);
+}
+
+static void cs40l50_request_firmware(const struct firmware *fw, void *context)
+{
+ struct cs40l50 *cs40l50 = context;
+ int ret;
+
+ if (!fw) {
+ dev_err(cs40l50->dev, "No firmware file found\n");
+ return;
+ }
+
+ cs40l50->fw = fw;
+
+ ret = request_firmware_nowait(THIS_MODULE, FW_ACTION_UEVENT, CS40L50_WT,
+ cs40l50->dev, GFP_KERNEL, cs40l50,
+ cs40l50_dsp_bringup);
+ if (ret) {
+ dev_err(cs40l50->dev, "Failed to request %s: %d\n", CS40L50_WT, ret);
+ release_firmware(cs40l50->fw);
+ }
+}
+
+struct cs40l50_irq {
+ const char *name;
+ int virq;
+};
+
+static struct cs40l50_irq cs40l50_irqs[] = {
+ { "DSP", },
+ { "Global", },
+ { "Boost UVLO", },
+ { "Boost current limit", },
+ { "Boost short", },
+ { "Boost undervolt", },
+ { "Overtemp", },
+ { "Amp short", },
+};
+
+static const struct reg_sequence cs40l50_err_rls[] = {
+ { CS40L50_ERR_RLS, CS40L50_GLOBAL_ERR_RLS_SET },
+ { CS40L50_ERR_RLS, CS40L50_GLOBAL_ERR_RLS_CLEAR },
+};
+
+static irqreturn_t cs40l50_hw_err(int irq, void *data)
+{
+ struct cs40l50 *cs40l50 = data;
+ int ret = 0, i;
+
+ mutex_lock(&cs40l50->lock);
+
+ /* Log hardware interrupt and execute error release sequence */
+ for (i = 1; i < ARRAY_SIZE(cs40l50_irqs); i++) {
+ if (cs40l50_irqs[i].virq == irq) {
+ dev_err(cs40l50->dev, "%s error\n", cs40l50_irqs[i].name);
+ ret = regmap_multi_reg_write(cs40l50->regmap, cs40l50_err_rls,
+ ARRAY_SIZE(cs40l50_err_rls));
+ break;
+ }
+ }
+
+ mutex_unlock(&cs40l50->lock);
+ return IRQ_RETVAL(!ret);
+}
+
+static irqreturn_t cs40l50_dsp_queue(int irq, void *data)
+{
+ struct cs40l50 *cs40l50 = data;
+ u32 rd_ptr, val, wt_ptr;
+ int ret = 0;
+
+ mutex_lock(&cs40l50->lock);
+
+ /* Read from DSP queue, log, and update read pointer */
+ while (!ret) {
+ ret = regmap_read(cs40l50->regmap, CS40L50_DSP_QUEUE_WT, &wt_ptr);
+ if (ret)
+ break;
+
+ ret = regmap_read(cs40l50->regmap, CS40L50_DSP_QUEUE_RD, &rd_ptr);
+ if (ret)
+ break;
+
+ /* Check if queue is empty */
+ if (wt_ptr == rd_ptr)
+ break;
+
+ ret = regmap_read(cs40l50->regmap, rd_ptr, &val);
+ if (ret)
+ break;
+
+ dev_dbg(cs40l50->dev, "DSP payload: %#X", val);
+
+ rd_ptr += sizeof(u32);
+
+ if (rd_ptr > CS40L50_DSP_QUEUE_END)
+ rd_ptr = CS40L50_DSP_QUEUE_BASE;
+
+ ret = regmap_write(cs40l50->regmap, CS40L50_DSP_QUEUE_RD, rd_ptr);
+ }
+
+ mutex_unlock(&cs40l50->lock);
+
+ return IRQ_RETVAL(!ret);
+}
+
+static int cs40l50_irq_init(struct cs40l50 *cs40l50)
+{
+ int ret, i, virq;
+
+ ret = devm_regmap_add_irq_chip(cs40l50->dev, cs40l50->regmap, cs40l50->irq,
+ IRQF_ONESHOT | IRQF_SHARED, 0,
+ &cs40l50_irq_chip, &cs40l50->irq_data);
+ if (ret) {
+ dev_err(cs40l50->dev, "Failed adding IRQ chip\n");
+ return ret;
+ }
+
+ for (i = 0; i < ARRAY_SIZE(cs40l50_irqs); i++) {
+ virq = regmap_irq_get_virq(cs40l50->irq_data, i);
+ if (virq < 0) {
+ dev_err(cs40l50->dev, "Failed getting virq for %s\n",
+ cs40l50_irqs[i].name);
+ return virq;
+ }
+
+ cs40l50_irqs[i].virq = virq;
+
+ /* Handle DSP and hardware interrupts separately */
+ ret = devm_request_threaded_irq(cs40l50->dev, virq, NULL,
+ i ? cs40l50_hw_err : cs40l50_dsp_queue,
+ IRQF_ONESHOT | IRQF_SHARED,
+ cs40l50_irqs[i].name, cs40l50);
+ if (ret) {
+ return dev_err_probe(cs40l50->dev, ret,
+ "Failed requesting %s IRQ\n",
+ cs40l50_irqs[i].name);
+ }
+ }
+
+ return 0;
+}
+
+static int cs40l50_get_model(struct cs40l50 *cs40l50)
+{
+ int ret;
+
+ ret = regmap_read(cs40l50->regmap, CS40L50_DEVID, &cs40l50->devid);
+ if (ret)
+ return ret;
+
+ if (cs40l50->devid != CS40L50_DEVID_A)
+ return -EINVAL;
+
+ ret = regmap_read(cs40l50->regmap, CS40L50_REVID, &cs40l50->revid);
+ if (ret)
+ return ret;
+
+ if (cs40l50->revid < CS40L50_REVID_B0)
+ return -EINVAL;
+
+ dev_dbg(cs40l50->dev, "Cirrus Logic CS40L50 rev. %02X\n", cs40l50->revid);
+
+ return 0;
+}
+
+static int cs40l50_pm_runtime_setup(struct device *dev)
+{
+ int ret;
+
+ pm_runtime_set_autosuspend_delay(dev, CS40L50_AUTOSUSPEND_MS);
+ pm_runtime_use_autosuspend(dev);
+ pm_runtime_get_noresume(dev);
+ ret = pm_runtime_set_active(dev);
+ if (ret)
+ return ret;
+
+ return devm_pm_runtime_enable(dev);
+}
+
+int cs40l50_probe(struct cs40l50 *cs40l50)
+{
+ struct device *dev = cs40l50->dev;
+ int ret;
+
+ mutex_init(&cs40l50->lock);
+
+ cs40l50->reset_gpio = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_HIGH);
+ if (IS_ERR(cs40l50->reset_gpio))
+ return dev_err_probe(dev, PTR_ERR(cs40l50->reset_gpio),
+ "Failed getting reset GPIO\n");
+
+ ret = devm_regulator_bulk_get_enable(dev, ARRAY_SIZE(cs40l50_supplies),
+ cs40l50_supplies);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed getting supplies\n");
+
+ /* Ensure minimum reset pulse width */
+ usleep_range(CS40L50_RESET_PULSE_US, CS40L50_RESET_PULSE_US + 100);
+
+ gpiod_set_value_cansleep(cs40l50->reset_gpio, 0);
+
+ /* Wait for control port to be ready */
+ usleep_range(CS40L50_CP_READY_US, CS40L50_CP_READY_US + 100);
+
+ ret = cs40l50_get_model(cs40l50);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to get part number\n");
+
+ ret = cs40l50_dsp_init(cs40l50);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to initialize DSP\n");
+
+ ret = cs40l50_pm_runtime_setup(dev);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to initialize runtime PM\n");
+
+ ret = cs40l50_irq_init(cs40l50);
+ if (ret)
+ return ret;
+
+ ret = request_firmware_nowait(THIS_MODULE, FW_ACTION_UEVENT, CS40L50_FW,
+ dev, GFP_KERNEL, cs40l50, cs40l50_request_firmware);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to request %s\n", CS40L50_FW);
+
+ pm_runtime_mark_last_busy(dev);
+ pm_runtime_put_autosuspend(dev);
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(cs40l50_probe);
+
+int cs40l50_remove(struct cs40l50 *cs40l50)
+{
+ gpiod_set_value_cansleep(cs40l50->reset_gpio, 1);
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(cs40l50_remove);
+
+static int cs40l50_runtime_suspend(struct device *dev)
+{
+ struct cs40l50 *cs40l50 = dev_get_drvdata(dev);
+
+ return regmap_write(cs40l50->regmap, CS40L50_DSP_QUEUE, CS40L50_ALLOW_HIBER);
+}
+
+static int cs40l50_runtime_resume(struct device *dev)
+{
+ struct cs40l50 *cs40l50 = dev_get_drvdata(dev);
+
+ return cs40l50_dsp_write(dev, cs40l50->regmap, CS40L50_PREVENT_HIBER);
+}
+
+EXPORT_GPL_DEV_PM_OPS(cs40l50_pm_ops) = {
+ RUNTIME_PM_OPS(cs40l50_runtime_suspend, cs40l50_runtime_resume, NULL)
+};
+
+MODULE_DESCRIPTION("CS40L50 Advanced Haptic Driver");
+MODULE_AUTHOR("James Ogletree, Cirrus Logic Inc. <james.ogletree@cirrus.com>");
+MODULE_LICENSE("GPL");
+MODULE_IMPORT_NS(FW_CS_DSP);
diff --git a/drivers/mfd/cs40l50-i2c.c b/drivers/mfd/cs40l50-i2c.c
new file mode 100644
index 0000000..639be74
--- /dev/null
+++ b/drivers/mfd/cs40l50-i2c.c
@@ -0,0 +1,68 @@
+// 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/i2c.h>
+#include <linux/mfd/cs40l50.h>
+
+static int cs40l50_i2c_probe(struct i2c_client *i2c)
+{
+ struct cs40l50 *cs40l50;
+
+ cs40l50 = devm_kzalloc(&i2c->dev, sizeof(*cs40l50), GFP_KERNEL);
+ if (!cs40l50)
+ return -ENOMEM;
+
+ i2c_set_clientdata(i2c, cs40l50);
+
+ cs40l50->dev = &i2c->dev;
+ cs40l50->irq = i2c->irq;
+
+ cs40l50->regmap = devm_regmap_init_i2c(i2c, &cs40l50_regmap);
+ if (IS_ERR(cs40l50->regmap))
+ return dev_err_probe(cs40l50->dev, PTR_ERR(cs40l50->regmap),
+ "Failed to initialize register map\n");
+
+ return cs40l50_probe(cs40l50);
+}
+
+static void cs40l50_i2c_remove(struct i2c_client *i2c)
+{
+ struct cs40l50 *cs40l50 = i2c_get_clientdata(i2c);
+
+ cs40l50_remove(cs40l50);
+}
+
+static const struct i2c_device_id cs40l50_id_i2c[] = {
+ { "cs40l50" },
+ {}
+};
+MODULE_DEVICE_TABLE(i2c, cs40l50_id_i2c);
+
+static const struct of_device_id cs40l50_of_match[] = {
+ { .compatible = "cirrus,cs40l50" },
+ {}
+};
+MODULE_DEVICE_TABLE(of, cs40l50_of_match);
+
+static struct i2c_driver cs40l50_i2c_driver = {
+ .driver = {
+ .name = "cs40l50",
+ .of_match_table = cs40l50_of_match,
+ .pm = pm_ptr(&cs40l50_pm_ops),
+ },
+ .id_table = cs40l50_id_i2c,
+ .probe = cs40l50_i2c_probe,
+ .remove = cs40l50_i2c_remove,
+};
+module_i2c_driver(cs40l50_i2c_driver);
+
+MODULE_DESCRIPTION("CS40L50 I2C Driver");
+MODULE_AUTHOR("James Ogletree, Cirrus Logic Inc. <james.ogletree@cirrus.com>");
+MODULE_LICENSE("GPL");
diff --git a/drivers/mfd/cs40l50-spi.c b/drivers/mfd/cs40l50-spi.c
new file mode 100644
index 0000000..53526b5
--- /dev/null
+++ b/drivers/mfd/cs40l50-spi.c
@@ -0,0 +1,68 @@
+// 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/mfd/cs40l50.h>
+#include <linux/spi/spi.h>
+
+static int cs40l50_spi_probe(struct spi_device *spi)
+{
+ struct cs40l50 *cs40l50;
+
+ cs40l50 = devm_kzalloc(&spi->dev, sizeof(*cs40l50), GFP_KERNEL);
+ if (!cs40l50)
+ return -ENOMEM;
+
+ spi_set_drvdata(spi, cs40l50);
+
+ cs40l50->dev = &spi->dev;
+ cs40l50->irq = spi->irq;
+
+ cs40l50->regmap = devm_regmap_init_spi(spi, &cs40l50_regmap);
+ if (IS_ERR(cs40l50->regmap))
+ return dev_err_probe(cs40l50->dev, PTR_ERR(cs40l50->regmap),
+ "Failed to initialize register map\n");
+
+ return cs40l50_probe(cs40l50);
+}
+
+static void cs40l50_spi_remove(struct spi_device *spi)
+{
+ struct cs40l50 *cs40l50 = spi_get_drvdata(spi);
+
+ cs40l50_remove(cs40l50);
+}
+
+static const struct spi_device_id cs40l50_id_spi[] = {
+ { "cs40l50" },
+ {}
+};
+MODULE_DEVICE_TABLE(spi, cs40l50_id_spi);
+
+static const struct of_device_id cs40l50_of_match[] = {
+ { .compatible = "cirrus,cs40l50" },
+ {}
+};
+MODULE_DEVICE_TABLE(of, cs40l50_of_match);
+
+static struct spi_driver cs40l50_spi_driver = {
+ .driver = {
+ .name = "cs40l50",
+ .of_match_table = cs40l50_of_match,
+ .pm = pm_ptr(&cs40l50_pm_ops),
+ },
+ .id_table = cs40l50_id_spi,
+ .probe = cs40l50_spi_probe,
+ .remove = cs40l50_spi_remove,
+};
+module_spi_driver(cs40l50_spi_driver);
+
+MODULE_DESCRIPTION("CS40L50 SPI Driver");
+MODULE_AUTHOR("James Ogletree, Cirrus Logic Inc. <james.ogletree@cirrus.com>");
+MODULE_LICENSE("GPL");
diff --git a/drivers/platform/chrome/Kconfig b/drivers/platform/chrome/Kconfig
index 073616b..7dbeb78 100644
--- a/drivers/platform/chrome/Kconfig
+++ b/drivers/platform/chrome/Kconfig
@@ -150,7 +150,7 @@
config CROS_KBD_LED_BACKLIGHT
tristate "Backlight LED support for Chrome OS keyboards"
- depends on LEDS_CLASS && (ACPI || CROS_EC)
+ depends on LEDS_CLASS && (ACPI || CROS_EC || MFD_CROS_EC_DEV)
help
This option enables support for the keyboard backlight LEDs on
select Chrome OS systems.
diff --git a/drivers/platform/chrome/cros_kbd_led_backlight.c b/drivers/platform/chrome/cros_kbd_led_backlight.c
index b83e4f3..78097c8 100644
--- a/drivers/platform/chrome/cros_kbd_led_backlight.c
+++ b/drivers/platform/chrome/cros_kbd_led_backlight.c
@@ -9,6 +9,7 @@
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/leds.h>
+#include <linux/mfd/core.h>
#include <linux/mod_devicetable.h>
#include <linux/module.h>
#include <linux/of.h>
@@ -194,13 +195,46 @@ static const __maybe_unused struct keyboard_led_drvdata keyboard_led_drvdata_ec_
#endif /* IS_ENABLED(CONFIG_CROS_EC) */
+#if IS_ENABLED(CONFIG_MFD_CROS_EC_DEV)
+static int keyboard_led_init_ec_pwm_mfd(struct platform_device *pdev)
+{
+ struct cros_ec_dev *ec_dev = dev_get_drvdata(pdev->dev.parent);
+ struct cros_ec_device *cros_ec = ec_dev->ec_dev;
+ struct keyboard_led *keyboard_led = platform_get_drvdata(pdev);
+
+ keyboard_led->ec = cros_ec;
+
+ return 0;
+}
+
+static const struct keyboard_led_drvdata keyboard_led_drvdata_ec_pwm_mfd = {
+ .init = keyboard_led_init_ec_pwm_mfd,
+ .brightness_set_blocking = keyboard_led_set_brightness_ec_pwm,
+ .brightness_get = keyboard_led_get_brightness_ec_pwm,
+ .max_brightness = KEYBOARD_BACKLIGHT_MAX,
+};
+
+#else /* IS_ENABLED(CONFIG_MFD_CROS_EC_DEV) */
+
+static const struct keyboard_led_drvdata keyboard_led_drvdata_ec_pwm_mfd = {};
+
+#endif /* IS_ENABLED(CONFIG_MFD_CROS_EC_DEV) */
+
+static int keyboard_led_is_mfd_device(struct platform_device *pdev)
+{
+ return IS_ENABLED(CONFIG_MFD_CROS_EC_DEV) && mfd_get_cell(pdev);
+}
+
static int keyboard_led_probe(struct platform_device *pdev)
{
const struct keyboard_led_drvdata *drvdata;
struct keyboard_led *keyboard_led;
int error;
- drvdata = device_get_match_data(&pdev->dev);
+ if (keyboard_led_is_mfd_device(pdev))
+ drvdata = &keyboard_led_drvdata_ec_pwm_mfd;
+ else
+ drvdata = device_get_match_data(&pdev->dev);
if (!drvdata)
return -EINVAL;
@@ -216,13 +250,15 @@ static int keyboard_led_probe(struct platform_device *pdev)
}
keyboard_led->cdev.name = "chromeos::kbd_backlight";
- keyboard_led->cdev.flags |= LED_CORE_SUSPENDRESUME;
+ keyboard_led->cdev.flags |= LED_CORE_SUSPENDRESUME | LED_REJECT_NAME_CONFLICT;
keyboard_led->cdev.max_brightness = drvdata->max_brightness;
keyboard_led->cdev.brightness_set = drvdata->brightness_set;
keyboard_led->cdev.brightness_set_blocking = drvdata->brightness_set_blocking;
keyboard_led->cdev.brightness_get = drvdata->brightness_get;
error = devm_led_classdev_register(&pdev->dev, &keyboard_led->cdev);
+ if (error == -EEXIST) /* Already bound via other mechanism */
+ return -ENODEV;
if (error)
return error;
diff --git a/drivers/regulator/88pm886-regulator.c b/drivers/regulator/88pm886-regulator.c
new file mode 100644
index 0000000..a38bd4f
--- /dev/null
+++ b/drivers/regulator/88pm886-regulator.c
@@ -0,0 +1,392 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <linux/i2c.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+#include <linux/regulator/driver.h>
+
+#include <linux/mfd/88pm886.h>
+
+static const struct regmap_config pm886_regulator_regmap_config = {
+ .reg_bits = 8,
+ .val_bits = 8,
+ .max_register = PM886_REG_BUCK5_VOUT,
+};
+
+static const struct regulator_ops pm886_ldo_ops = {
+ .list_voltage = regulator_list_voltage_table,
+ .map_voltage = regulator_map_voltage_iterate,
+ .set_voltage_sel = regulator_set_voltage_sel_regmap,
+ .get_voltage_sel = regulator_get_voltage_sel_regmap,
+ .enable = regulator_enable_regmap,
+ .disable = regulator_disable_regmap,
+ .is_enabled = regulator_is_enabled_regmap,
+};
+
+static const struct regulator_ops pm886_buck_ops = {
+ .list_voltage = regulator_list_voltage_linear_range,
+ .map_voltage = regulator_map_voltage_linear_range,
+ .set_voltage_sel = regulator_set_voltage_sel_regmap,
+ .get_voltage_sel = regulator_get_voltage_sel_regmap,
+ .enable = regulator_enable_regmap,
+ .disable = regulator_disable_regmap,
+ .is_enabled = regulator_is_enabled_regmap,
+};
+
+static const unsigned int pm886_ldo_volt_table1[] = {
+ 1700000, 1800000, 1900000, 2500000, 2800000, 2900000, 3100000, 3300000,
+};
+
+static const unsigned int pm886_ldo_volt_table2[] = {
+ 1200000, 1250000, 1700000, 1800000, 1850000, 1900000, 2500000, 2600000,
+ 2700000, 2750000, 2800000, 2850000, 2900000, 3000000, 3100000, 3300000,
+};
+
+static const unsigned int pm886_ldo_volt_table3[] = {
+ 1700000, 1800000, 1900000, 2000000, 2100000, 2500000, 2700000, 2800000,
+};
+
+static const struct linear_range pm886_buck_volt_ranges1[] = {
+ REGULATOR_LINEAR_RANGE(600000, 0, 79, 12500),
+ REGULATOR_LINEAR_RANGE(1600000, 80, 84, 50000),
+};
+
+static const struct linear_range pm886_buck_volt_ranges2[] = {
+ REGULATOR_LINEAR_RANGE(600000, 0, 79, 12500),
+ REGULATOR_LINEAR_RANGE(1600000, 80, 114, 50000),
+};
+
+static struct regulator_desc pm886_regulators[] = {
+ {
+ .name = "LDO1",
+ .regulators_node = "regulators",
+ .of_match = "ldo1",
+ .ops = &pm886_ldo_ops,
+ .type = REGULATOR_VOLTAGE,
+ .enable_reg = PM886_REG_LDO_EN1,
+ .enable_mask = BIT(0),
+ .volt_table = pm886_ldo_volt_table1,
+ .n_voltages = ARRAY_SIZE(pm886_ldo_volt_table1),
+ .vsel_reg = PM886_REG_LDO1_VOUT,
+ .vsel_mask = PM886_LDO_VSEL_MASK,
+ },
+ {
+ .name = "LDO2",
+ .regulators_node = "regulators",
+ .of_match = "ldo2",
+ .ops = &pm886_ldo_ops,
+ .type = REGULATOR_VOLTAGE,
+ .enable_reg = PM886_REG_LDO_EN1,
+ .enable_mask = BIT(1),
+ .volt_table = pm886_ldo_volt_table1,
+ .n_voltages = ARRAY_SIZE(pm886_ldo_volt_table1),
+ .vsel_reg = PM886_REG_LDO2_VOUT,
+ .vsel_mask = PM886_LDO_VSEL_MASK,
+ },
+ {
+ .name = "LDO3",
+ .regulators_node = "regulators",
+ .of_match = "ldo3",
+ .ops = &pm886_ldo_ops,
+ .type = REGULATOR_VOLTAGE,
+ .enable_reg = PM886_REG_LDO_EN1,
+ .enable_mask = BIT(2),
+ .volt_table = pm886_ldo_volt_table1,
+ .n_voltages = ARRAY_SIZE(pm886_ldo_volt_table1),
+ .vsel_reg = PM886_REG_LDO3_VOUT,
+ .vsel_mask = PM886_LDO_VSEL_MASK,
+ },
+ {
+ .name = "LDO4",
+ .regulators_node = "regulators",
+ .of_match = "ldo4",
+ .ops = &pm886_ldo_ops,
+ .type = REGULATOR_VOLTAGE,
+ .enable_reg = PM886_REG_LDO_EN1,
+ .enable_mask = BIT(3),
+ .volt_table = pm886_ldo_volt_table2,
+ .n_voltages = ARRAY_SIZE(pm886_ldo_volt_table2),
+ .vsel_reg = PM886_REG_LDO4_VOUT,
+ .vsel_mask = PM886_LDO_VSEL_MASK,
+ },
+ {
+ .name = "LDO5",
+ .regulators_node = "regulators",
+ .of_match = "ldo5",
+ .ops = &pm886_ldo_ops,
+ .type = REGULATOR_VOLTAGE,
+ .enable_reg = PM886_REG_LDO_EN1,
+ .enable_mask = BIT(4),
+ .volt_table = pm886_ldo_volt_table2,
+ .n_voltages = ARRAY_SIZE(pm886_ldo_volt_table2),
+ .vsel_reg = PM886_REG_LDO5_VOUT,
+ .vsel_mask = PM886_LDO_VSEL_MASK,
+ },
+ {
+ .name = "LDO6",
+ .regulators_node = "regulators",
+ .of_match = "ldo6",
+ .ops = &pm886_ldo_ops,
+ .type = REGULATOR_VOLTAGE,
+ .enable_reg = PM886_REG_LDO_EN1,
+ .enable_mask = BIT(5),
+ .volt_table = pm886_ldo_volt_table2,
+ .n_voltages = ARRAY_SIZE(pm886_ldo_volt_table2),
+ .vsel_reg = PM886_REG_LDO6_VOUT,
+ .vsel_mask = PM886_LDO_VSEL_MASK,
+ },
+ {
+ .name = "LDO7",
+ .regulators_node = "regulators",
+ .of_match = "ldo7",
+ .ops = &pm886_ldo_ops,
+ .type = REGULATOR_VOLTAGE,
+ .enable_reg = PM886_REG_LDO_EN1,
+ .enable_mask = BIT(6),
+ .volt_table = pm886_ldo_volt_table2,
+ .n_voltages = ARRAY_SIZE(pm886_ldo_volt_table2),
+ .vsel_reg = PM886_REG_LDO7_VOUT,
+ .vsel_mask = PM886_LDO_VSEL_MASK,
+ },
+ {
+ .name = "LDO8",
+ .regulators_node = "regulators",
+ .of_match = "ldo8",
+ .ops = &pm886_ldo_ops,
+ .type = REGULATOR_VOLTAGE,
+ .enable_reg = PM886_REG_LDO_EN1,
+ .enable_mask = BIT(7),
+ .volt_table = pm886_ldo_volt_table2,
+ .n_voltages = ARRAY_SIZE(pm886_ldo_volt_table2),
+ .vsel_reg = PM886_REG_LDO8_VOUT,
+ .vsel_mask = PM886_LDO_VSEL_MASK,
+ },
+ {
+ .name = "LDO9",
+ .regulators_node = "regulators",
+ .of_match = "ldo9",
+ .ops = &pm886_ldo_ops,
+ .type = REGULATOR_VOLTAGE,
+ .enable_reg = PM886_REG_LDO_EN2,
+ .enable_mask = BIT(0),
+ .volt_table = pm886_ldo_volt_table2,
+ .n_voltages = ARRAY_SIZE(pm886_ldo_volt_table2),
+ .vsel_reg = PM886_REG_LDO9_VOUT,
+ .vsel_mask = PM886_LDO_VSEL_MASK,
+ },
+ {
+ .name = "LDO10",
+ .regulators_node = "regulators",
+ .of_match = "ldo10",
+ .ops = &pm886_ldo_ops,
+ .type = REGULATOR_VOLTAGE,
+ .enable_reg = PM886_REG_LDO_EN2,
+ .enable_mask = BIT(1),
+ .volt_table = pm886_ldo_volt_table2,
+ .n_voltages = ARRAY_SIZE(pm886_ldo_volt_table2),
+ .vsel_reg = PM886_REG_LDO10_VOUT,
+ .vsel_mask = PM886_LDO_VSEL_MASK,
+ },
+ {
+ .name = "LDO11",
+ .regulators_node = "regulators",
+ .of_match = "ldo11",
+ .ops = &pm886_ldo_ops,
+ .type = REGULATOR_VOLTAGE,
+ .enable_reg = PM886_REG_LDO_EN2,
+ .enable_mask = BIT(2),
+ .volt_table = pm886_ldo_volt_table2,
+ .n_voltages = ARRAY_SIZE(pm886_ldo_volt_table2),
+ .vsel_reg = PM886_REG_LDO11_VOUT,
+ .vsel_mask = PM886_LDO_VSEL_MASK,
+ },
+ {
+ .name = "LDO12",
+ .regulators_node = "regulators",
+ .of_match = "ldo12",
+ .ops = &pm886_ldo_ops,
+ .type = REGULATOR_VOLTAGE,
+ .enable_reg = PM886_REG_LDO_EN2,
+ .enable_mask = BIT(3),
+ .volt_table = pm886_ldo_volt_table2,
+ .n_voltages = ARRAY_SIZE(pm886_ldo_volt_table2),
+ .vsel_reg = PM886_REG_LDO12_VOUT,
+ .vsel_mask = PM886_LDO_VSEL_MASK,
+ },
+ {
+ .name = "LDO13",
+ .regulators_node = "regulators",
+ .of_match = "ldo13",
+ .ops = &pm886_ldo_ops,
+ .type = REGULATOR_VOLTAGE,
+ .enable_reg = PM886_REG_LDO_EN2,
+ .enable_mask = BIT(4),
+ .volt_table = pm886_ldo_volt_table2,
+ .n_voltages = ARRAY_SIZE(pm886_ldo_volt_table2),
+ .vsel_reg = PM886_REG_LDO13_VOUT,
+ .vsel_mask = PM886_LDO_VSEL_MASK,
+ },
+ {
+ .name = "LDO14",
+ .regulators_node = "regulators",
+ .of_match = "ldo14",
+ .ops = &pm886_ldo_ops,
+ .type = REGULATOR_VOLTAGE,
+ .enable_reg = PM886_REG_LDO_EN2,
+ .enable_mask = BIT(5),
+ .volt_table = pm886_ldo_volt_table2,
+ .n_voltages = ARRAY_SIZE(pm886_ldo_volt_table2),
+ .vsel_reg = PM886_REG_LDO14_VOUT,
+ .vsel_mask = PM886_LDO_VSEL_MASK,
+ },
+ {
+ .name = "LDO15",
+ .regulators_node = "regulators",
+ .of_match = "ldo15",
+ .ops = &pm886_ldo_ops,
+ .type = REGULATOR_VOLTAGE,
+ .enable_reg = PM886_REG_LDO_EN2,
+ .enable_mask = BIT(6),
+ .volt_table = pm886_ldo_volt_table2,
+ .n_voltages = ARRAY_SIZE(pm886_ldo_volt_table2),
+ .vsel_reg = PM886_REG_LDO15_VOUT,
+ .vsel_mask = PM886_LDO_VSEL_MASK,
+ },
+ {
+ .name = "LDO16",
+ .regulators_node = "regulators",
+ .of_match = "ldo16",
+ .ops = &pm886_ldo_ops,
+ .type = REGULATOR_VOLTAGE,
+ .enable_reg = PM886_REG_LDO_EN2,
+ .enable_mask = BIT(7),
+ .volt_table = pm886_ldo_volt_table3,
+ .n_voltages = ARRAY_SIZE(pm886_ldo_volt_table3),
+ .vsel_reg = PM886_REG_LDO16_VOUT,
+ .vsel_mask = PM886_LDO_VSEL_MASK,
+ },
+ {
+ .name = "buck1",
+ .regulators_node = "regulators",
+ .of_match = "buck1",
+ .ops = &pm886_buck_ops,
+ .type = REGULATOR_VOLTAGE,
+ .n_voltages = 85,
+ .linear_ranges = pm886_buck_volt_ranges1,
+ .n_linear_ranges = ARRAY_SIZE(pm886_buck_volt_ranges1),
+ .vsel_reg = PM886_REG_BUCK1_VOUT,
+ .vsel_mask = PM886_BUCK_VSEL_MASK,
+ .enable_reg = PM886_REG_BUCK_EN,
+ .enable_mask = BIT(0),
+ },
+ {
+ .name = "buck2",
+ .regulators_node = "regulators",
+ .of_match = "buck2",
+ .ops = &pm886_buck_ops,
+ .type = REGULATOR_VOLTAGE,
+ .n_voltages = 115,
+ .linear_ranges = pm886_buck_volt_ranges2,
+ .n_linear_ranges = ARRAY_SIZE(pm886_buck_volt_ranges2),
+ .vsel_reg = PM886_REG_BUCK2_VOUT,
+ .vsel_mask = PM886_BUCK_VSEL_MASK,
+ .enable_reg = PM886_REG_BUCK_EN,
+ .enable_mask = BIT(1),
+ },
+ {
+ .name = "buck3",
+ .regulators_node = "regulators",
+ .of_match = "buck3",
+ .ops = &pm886_buck_ops,
+ .type = REGULATOR_VOLTAGE,
+ .n_voltages = 115,
+ .linear_ranges = pm886_buck_volt_ranges2,
+ .n_linear_ranges = ARRAY_SIZE(pm886_buck_volt_ranges2),
+ .vsel_reg = PM886_REG_BUCK3_VOUT,
+ .vsel_mask = PM886_BUCK_VSEL_MASK,
+ .enable_reg = PM886_REG_BUCK_EN,
+ .enable_mask = BIT(2),
+ },
+ {
+ .name = "buck4",
+ .regulators_node = "regulators",
+ .of_match = "buck4",
+ .ops = &pm886_buck_ops,
+ .type = REGULATOR_VOLTAGE,
+ .n_voltages = 115,
+ .linear_ranges = pm886_buck_volt_ranges2,
+ .n_linear_ranges = ARRAY_SIZE(pm886_buck_volt_ranges2),
+ .vsel_reg = PM886_REG_BUCK4_VOUT,
+ .vsel_mask = PM886_BUCK_VSEL_MASK,
+ .enable_reg = PM886_REG_BUCK_EN,
+ .enable_mask = BIT(3),
+ },
+ {
+ .name = "buck5",
+ .regulators_node = "regulators",
+ .of_match = "buck5",
+ .ops = &pm886_buck_ops,
+ .type = REGULATOR_VOLTAGE,
+ .n_voltages = 115,
+ .linear_ranges = pm886_buck_volt_ranges2,
+ .n_linear_ranges = ARRAY_SIZE(pm886_buck_volt_ranges2),
+ .vsel_reg = PM886_REG_BUCK5_VOUT,
+ .vsel_mask = PM886_BUCK_VSEL_MASK,
+ .enable_reg = PM886_REG_BUCK_EN,
+ .enable_mask = BIT(4),
+ },
+};
+
+static int pm886_regulator_probe(struct platform_device *pdev)
+{
+ struct pm886_chip *chip = dev_get_drvdata(pdev->dev.parent);
+ struct regulator_config rcfg = { };
+ struct device *dev = &pdev->dev;
+ struct regulator_desc *rdesc;
+ struct regulator_dev *rdev;
+ struct i2c_client *page;
+ struct regmap *regmap;
+
+ page = devm_i2c_new_dummy_device(dev, chip->client->adapter,
+ chip->client->addr + PM886_PAGE_OFFSET_REGULATORS);
+ if (IS_ERR(page))
+ return dev_err_probe(dev, PTR_ERR(page),
+ "Failed to initialize regulators client\n");
+
+ regmap = devm_regmap_init_i2c(page, &pm886_regulator_regmap_config);
+ if (IS_ERR(regmap))
+ return dev_err_probe(dev, PTR_ERR(regmap),
+ "Failed to initialize regulators regmap\n");
+ rcfg.regmap = regmap;
+
+ rcfg.dev = dev->parent;
+
+ for (int i = 0; i < ARRAY_SIZE(pm886_regulators); i++) {
+ rdesc = &pm886_regulators[i];
+ rdev = devm_regulator_register(dev, rdesc, &rcfg);
+ if (IS_ERR(rdev))
+ return dev_err_probe(dev, PTR_ERR(rdev),
+ "Failed to register %s\n", rdesc->name);
+ }
+
+ return 0;
+}
+
+static const struct platform_device_id pm886_regulator_id_table[] = {
+ { "88pm886-regulator", },
+ { }
+};
+MODULE_DEVICE_TABLE(platform, pm886_regulator_id_table);
+
+static struct platform_driver pm886_regulator_driver = {
+ .driver = {
+ .name = "88pm886-regulator",
+ },
+ .probe = pm886_regulator_probe,
+ .id_table = pm886_regulator_id_table,
+};
+module_platform_driver(pm886_regulator_driver);
+
+MODULE_DESCRIPTION("Marvell 88PM886 PMIC regulator driver");
+MODULE_AUTHOR("Karel Balej <balejk@matfyz.cz>");
+MODULE_LICENSE("GPL");
diff --git a/drivers/regulator/Kconfig b/drivers/regulator/Kconfig
index 17982e1..8385b57 100644
--- a/drivers/regulator/Kconfig
+++ b/drivers/regulator/Kconfig
@@ -91,6 +91,12 @@
help
This driver supports 88PM8607 voltage regulator chips.
+config REGULATOR_88PM886
+ tristate "Marvell 88PM886 voltage regulators"
+ depends on MFD_88PM886_PMIC
+ help
+ This driver implements support for Marvell 88PM886 voltage regulators.
+
config REGULATOR_ACT8865
tristate "Active-semi act8865 voltage regulator"
depends on I2C
diff --git a/drivers/regulator/Makefile b/drivers/regulator/Makefile
index ca4d09c..4087765e 100644
--- a/drivers/regulator/Makefile
+++ b/drivers/regulator/Makefile
@@ -14,6 +14,7 @@
obj-$(CONFIG_REGULATOR_88PG86X) += 88pg86x.o
obj-$(CONFIG_REGULATOR_88PM800) += 88pm800-regulator.o
obj-$(CONFIG_REGULATOR_88PM8607) += 88pm8607.o
+obj-$(CONFIG_REGULATOR_88PM886) += 88pm886-regulator.o
obj-$(CONFIG_REGULATOR_CROS_EC) += cros-ec-regulator.o
obj-$(CONFIG_REGULATOR_CPCAP) += cpcap-regulator.o
obj-$(CONFIG_REGULATOR_AAT2870) += aat2870-regulator.o
diff --git a/include/linux/firmware/cirrus/cs_dsp.h b/include/linux/firmware/cirrus/cs_dsp.h
index 82687e0..c28a6b9 100644
--- a/include/linux/firmware/cirrus/cs_dsp.h
+++ b/include/linux/firmware/cirrus/cs_dsp.h
@@ -42,6 +42,16 @@
#define CS_DSP_ACKED_CTL_MIN_VALUE 0
#define CS_DSP_ACKED_CTL_MAX_VALUE 0xFFFFFF
+/*
+ * Write sequence operation codes
+ */
+#define CS_DSP_WSEQ_FULL 0x00
+#define CS_DSP_WSEQ_ADDR8 0x02
+#define CS_DSP_WSEQ_L16 0x04
+#define CS_DSP_WSEQ_H16 0x05
+#define CS_DSP_WSEQ_UNLOCK 0xFD
+#define CS_DSP_WSEQ_END 0xFF
+
/**
* struct cs_dsp_region - Describes a logical memory region in DSP address space
* @type: Memory region type
@@ -259,6 +269,23 @@ struct cs_dsp_alg_region *cs_dsp_find_alg_region(struct cs_dsp *dsp,
const char *cs_dsp_mem_region_name(unsigned int type);
/**
+ * struct cs_dsp_wseq - Describes a write sequence
+ * @ctl: Write sequence cs_dsp control
+ * @ops: Operations contained within
+ */
+struct cs_dsp_wseq {
+ struct cs_dsp_coeff_ctl *ctl;
+ struct list_head ops;
+};
+
+int cs_dsp_wseq_init(struct cs_dsp *dsp, struct cs_dsp_wseq *wseqs, unsigned int num_wseqs);
+int cs_dsp_wseq_write(struct cs_dsp *dsp, struct cs_dsp_wseq *wseq, u32 addr, u32 data,
+ u8 op_code, bool update);
+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);
+
+/**
* struct cs_dsp_chunk - Describes a buffer holding data formatted for the DSP
* @data: Pointer to underlying buffer memory
* @max: Pointer to end of the buffer memory
diff --git a/include/linux/leds.h b/include/linux/leds.h
index 6300313..99f8758 100644
--- a/include/linux/leds.h
+++ b/include/linux/leds.h
@@ -107,6 +107,7 @@ struct led_classdev {
#define LED_BRIGHT_HW_CHANGED BIT(21)
#define LED_RETAIN_AT_SHUTDOWN BIT(22)
#define LED_INIT_DEFAULT_TRIGGER BIT(23)
+#define LED_REJECT_NAME_CONFLICT BIT(24)
/* set_brightness_work / blink_timer flags, atomic, private. */
unsigned long work_flags;
@@ -428,6 +429,16 @@ int led_compose_name(struct device *dev, struct led_init_data *init_data,
char *led_classdev_name);
/**
+ * led_get_color_name - get string representation of color ID
+ * @color_id: The LED_COLOR_ID_* constant
+ *
+ * Get the string name of a LED_COLOR_ID_* constant.
+ *
+ * Returns: A string constant or NULL on an invalid ID.
+ */
+const char *led_get_color_name(u8 color_id);
+
+/**
* led_sysfs_is_disabled - check if LED sysfs interface is disabled
* @led_cdev: the LED to query
*
diff --git a/include/linux/mfd/88pm886.h b/include/linux/mfd/88pm886.h
new file mode 100644
index 0000000..133aa30
--- /dev/null
+++ b/include/linux/mfd/88pm886.h
@@ -0,0 +1,69 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef __MFD_88PM886_H
+#define __MFD_88PM886_H
+
+#include <linux/i2c.h>
+#include <linux/regmap.h>
+
+#define PM886_A1_CHIP_ID 0xa1
+
+#define PM886_IRQ_ONKEY 0
+
+#define PM886_PAGE_OFFSET_REGULATORS 1
+
+#define PM886_REG_ID 0x00
+
+#define PM886_REG_STATUS1 0x01
+#define PM886_ONKEY_STS1 BIT(0)
+
+#define PM886_REG_INT_STATUS1 0x05
+
+#define PM886_REG_INT_ENA_1 0x0a
+#define PM886_INT_ENA1_ONKEY BIT(0)
+
+#define PM886_REG_MISC_CONFIG1 0x14
+#define PM886_SW_PDOWN BIT(5)
+
+#define PM886_REG_MISC_CONFIG2 0x15
+#define PM886_INT_INV BIT(0)
+#define PM886_INT_CLEAR BIT(1)
+#define PM886_INT_RC 0x00
+#define PM886_INT_WC BIT(1)
+#define PM886_INT_MASK_MODE BIT(2)
+
+#define PM886_REG_RTC_SPARE6 0xef
+
+#define PM886_REG_BUCK_EN 0x08
+#define PM886_REG_LDO_EN1 0x09
+#define PM886_REG_LDO_EN2 0x0a
+#define PM886_REG_LDO1_VOUT 0x20
+#define PM886_REG_LDO2_VOUT 0x26
+#define PM886_REG_LDO3_VOUT 0x2c
+#define PM886_REG_LDO4_VOUT 0x32
+#define PM886_REG_LDO5_VOUT 0x38
+#define PM886_REG_LDO6_VOUT 0x3e
+#define PM886_REG_LDO7_VOUT 0x44
+#define PM886_REG_LDO8_VOUT 0x4a
+#define PM886_REG_LDO9_VOUT 0x50
+#define PM886_REG_LDO10_VOUT 0x56
+#define PM886_REG_LDO11_VOUT 0x5c
+#define PM886_REG_LDO12_VOUT 0x62
+#define PM886_REG_LDO13_VOUT 0x68
+#define PM886_REG_LDO14_VOUT 0x6e
+#define PM886_REG_LDO15_VOUT 0x74
+#define PM886_REG_LDO16_VOUT 0x7a
+#define PM886_REG_BUCK1_VOUT 0xa5
+#define PM886_REG_BUCK2_VOUT 0xb3
+#define PM886_REG_BUCK3_VOUT 0xc1
+#define PM886_REG_BUCK4_VOUT 0xcf
+#define PM886_REG_BUCK5_VOUT 0xdd
+
+#define PM886_LDO_VSEL_MASK 0x0f
+#define PM886_BUCK_VSEL_MASK 0x7f
+
+struct pm886_chip {
+ struct i2c_client *client;
+ unsigned int chip_id;
+ struct regmap *regmap;
+};
+#endif /* __MFD_88PM886_H */
diff --git a/include/linux/mfd/cs40l50.h b/include/linux/mfd/cs40l50.h
new file mode 100644
index 0000000..e5dc498
--- /dev/null
+++ b/include/linux/mfd/cs40l50.h
@@ -0,0 +1,137 @@
+/* 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>
+ */
+
+#ifndef __MFD_CS40L50_H__
+#define __MFD_CS40L50_H__
+
+#include <linux/firmware/cirrus/cs_dsp.h>
+#include <linux/gpio/consumer.h>
+#include <linux/pm.h>
+#include <linux/regmap.h>
+
+/* Power Supply Configuration */
+#define CS40L50_BLOCK_ENABLES2 0x201C
+#define CS40L50_ERR_RLS 0x2034
+#define CS40L50_BST_LPMODE_SEL 0x3810
+#define CS40L50_DCM_LOW_POWER 0x1
+#define CS40L50_OVERTEMP_WARN 0x4000010
+
+/* Interrupts */
+#define CS40L50_IRQ1_INT_1 0xE010
+#define CS40L50_IRQ1_BASE CS40L50_IRQ1_INT_1
+#define CS40L50_IRQ1_INT_2 0xE014
+#define CS40L50_IRQ1_INT_8 0xE02C
+#define CS40L50_IRQ1_INT_9 0xE030
+#define CS40L50_IRQ1_INT_10 0xE034
+#define CS40L50_IRQ1_INT_18 0xE054
+#define CS40L50_IRQ1_MASK_1 0xE090
+#define CS40L50_IRQ1_MASK_2 0xE094
+#define CS40L50_IRQ1_MASK_20 0xE0DC
+#define CS40L50_IRQ1_INT_1_OFFSET (CS40L50_IRQ1_INT_1 - CS40L50_IRQ1_BASE)
+#define CS40L50_IRQ1_INT_2_OFFSET (CS40L50_IRQ1_INT_2 - CS40L50_IRQ1_BASE)
+#define CS40L50_IRQ1_INT_8_OFFSET (CS40L50_IRQ1_INT_8 - CS40L50_IRQ1_BASE)
+#define CS40L50_IRQ1_INT_9_OFFSET (CS40L50_IRQ1_INT_9 - CS40L50_IRQ1_BASE)
+#define CS40L50_IRQ1_INT_10_OFFSET (CS40L50_IRQ1_INT_10 - CS40L50_IRQ1_BASE)
+#define CS40L50_IRQ1_INT_18_OFFSET (CS40L50_IRQ1_INT_18 - CS40L50_IRQ1_BASE)
+#define CS40L50_IRQ_MASK_2_OVERRIDE 0xFFDF7FFF
+#define CS40L50_IRQ_MASK_20_OVERRIDE 0x15C01000
+#define CS40L50_AMP_SHORT_MASK BIT(31)
+#define CS40L50_DSP_QUEUE_MASK BIT(21)
+#define CS40L50_TEMP_ERR_MASK BIT(31)
+#define CS40L50_BST_UVP_MASK BIT(6)
+#define CS40L50_BST_SHORT_MASK BIT(7)
+#define CS40L50_BST_ILIMIT_MASK BIT(18)
+#define CS40L50_UVLO_VDDBATT_MASK BIT(16)
+#define CS40L50_GLOBAL_ERROR_MASK BIT(15)
+
+enum cs40l50_irq_list {
+ CS40L50_DSP_QUEUE_IRQ,
+ CS40L50_GLOBAL_ERROR_IRQ,
+ CS40L50_UVLO_VDDBATT_IRQ,
+ CS40L50_BST_ILIMIT_IRQ,
+ CS40L50_BST_SHORT_IRQ,
+ CS40L50_BST_UVP_IRQ,
+ CS40L50_TEMP_ERR_IRQ,
+ CS40L50_AMP_SHORT_IRQ,
+};
+
+/* DSP */
+#define CS40L50_XMEM_PACKED_0 0x2000000
+#define CS40L50_XMEM_UNPACKED24_0 0x2800000
+#define CS40L50_SYS_INFO_ID 0x25E0000
+#define CS40L50_DSP_QUEUE_WT 0x28042C8
+#define CS40L50_DSP_QUEUE_RD 0x28042CC
+#define CS40L50_NUM_WAVES 0x2805C18
+#define CS40L50_CORE_BASE 0x2B80000
+#define CS40L50_YMEM_PACKED_0 0x2C00000
+#define CS40L50_YMEM_UNPACKED24_0 0x3400000
+#define CS40L50_PMEM_0 0x3800000
+#define CS40L50_DSP_POLL_US 1000
+#define CS40L50_DSP_TIMEOUT_COUNT 100
+#define CS40L50_RESET_PULSE_US 2200
+#define CS40L50_CP_READY_US 3100
+#define CS40L50_AUTOSUSPEND_MS 2000
+#define CS40L50_PM_ALGO 0x9F206
+#define CS40L50_GLOBAL_ERR_RLS_SET BIT(11)
+#define CS40L50_GLOBAL_ERR_RLS_CLEAR 0
+
+enum cs40l50_wseqs {
+ CS40L50_PWR_ON,
+ CS40L50_STANDBY,
+ CS40L50_ACTIVE,
+ CS40L50_NUM_WSEQS,
+};
+
+/* DSP Queue */
+#define CS40L50_DSP_QUEUE_BASE 0x11004
+#define CS40L50_DSP_QUEUE_END 0x1101C
+#define CS40L50_DSP_QUEUE 0x11020
+#define CS40L50_PREVENT_HIBER 0x2000003
+#define CS40L50_ALLOW_HIBER 0x2000004
+#define CS40L50_SHUTDOWN 0x2000005
+#define CS40L50_SYSTEM_RESET 0x2000007
+#define CS40L50_START_I2S 0x3000002
+#define CS40L50_OWT_PUSH 0x3000008
+#define CS40L50_STOP_PLAYBACK 0x5000000
+#define CS40L50_OWT_DELETE 0xD000000
+
+/* Firmware files */
+#define CS40L50_FW "cs40l50.wmfw"
+#define CS40L50_WT "cs40l50.bin"
+
+/* Device */
+#define CS40L50_DEVID 0x0
+#define CS40L50_REVID 0x4
+#define CS40L50_DEVID_A 0x40A50
+#define CS40L50_REVID_B0 0xB0
+
+struct cs40l50 {
+ struct device *dev;
+ struct regmap *regmap;
+ struct mutex lock;
+ struct cs_dsp dsp;
+ struct gpio_desc *reset_gpio;
+ struct regmap_irq_chip_data *irq_data;
+ const struct firmware *fw;
+ const struct firmware *bin;
+ struct cs_dsp_wseq wseqs[CS40L50_NUM_WSEQS];
+ int irq;
+ u32 devid;
+ u32 revid;
+};
+
+int cs40l50_dsp_write(struct device *dev, struct regmap *regmap, u32 val);
+int cs40l50_probe(struct cs40l50 *cs40l50);
+int cs40l50_remove(struct cs40l50 *cs40l50);
+
+extern const struct regmap_config cs40l50_regmap;
+extern const struct dev_pm_ops cs40l50_pm_ops;
+
+#endif /* __MFD_CS40L50_H__ */
diff --git a/sound/soc/codecs/Kconfig b/sound/soc/codecs/Kconfig
index 4afc43d..91b502f 100644
--- a/sound/soc/codecs/Kconfig
+++ b/sound/soc/codecs/Kconfig
@@ -75,6 +75,7 @@
imply SND_SOC_CS35L56_I2C
imply SND_SOC_CS35L56_SPI
imply SND_SOC_CS35L56_SDW
+ imply SND_SOC_CS40L50
imply SND_SOC_CS42L42
imply SND_SOC_CS42L42_SDW
imply SND_SOC_CS42L43
@@ -847,6 +848,16 @@
help
Enable support for Cirrus Logic CS35L56 boosted amplifier with SoundWire control
+config SND_SOC_CS40L50
+ tristate "Cirrus Logic CS40L50 CODEC"
+ depends on MFD_CS40L50_CORE
+ help
+ This option enables support for I2S streaming to Cirrus Logic CS40L50.
+
+ CS40L50 is a haptic driver with waveform memory, an integrated
+ DSP, and closed-loop algorithms. If built as a module, it will be
+ called snd-soc-cs40l50.
+
config SND_SOC_CS42L42_CORE
tristate
diff --git a/sound/soc/codecs/Makefile b/sound/soc/codecs/Makefile
index b4df221..3afd7c1 100644
--- a/sound/soc/codecs/Makefile
+++ b/sound/soc/codecs/Makefile
@@ -78,6 +78,7 @@
snd-soc-cs35l56-i2c-y := cs35l56-i2c.o
snd-soc-cs35l56-spi-y := cs35l56-spi.o
snd-soc-cs35l56-sdw-y := cs35l56-sdw.o
+snd-soc-cs40l50-objs := cs40l50-codec.o
snd-soc-cs42l42-y := cs42l42.o
snd-soc-cs42l42-i2c-y := cs42l42-i2c.o
snd-soc-cs42l42-sdw-y := cs42l42-sdw.o
@@ -475,6 +476,7 @@
obj-$(CONFIG_SND_SOC_CS35L56_I2C) += snd-soc-cs35l56-i2c.o
obj-$(CONFIG_SND_SOC_CS35L56_SPI) += snd-soc-cs35l56-spi.o
obj-$(CONFIG_SND_SOC_CS35L56_SDW) += snd-soc-cs35l56-sdw.o
+obj-$(CONFIG_SND_SOC_CS40L50) += snd-soc-cs40l50.o
obj-$(CONFIG_SND_SOC_CS42L42_CORE) += snd-soc-cs42l42.o
obj-$(CONFIG_SND_SOC_CS42L42) += snd-soc-cs42l42-i2c.o
obj-$(CONFIG_SND_SOC_CS42L42_SDW) += snd-soc-cs42l42-sdw.o
diff --git a/sound/soc/codecs/cs40l50-codec.c b/sound/soc/codecs/cs40l50-codec.c
new file mode 100644
index 0000000..aa629ef
--- /dev/null
+++ b/sound/soc/codecs/cs40l50-codec.c
@@ -0,0 +1,307 @@
+// 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/mfd/cs40l50.h>
+#include <sound/pcm_params.h>
+#include <sound/soc.h>
+
+#define CS40L50_REFCLK_INPUT 0x2C04
+#define CS40L50_ASP_CONTROL2 0x4808
+#define CS40L50_ASP_DATA_CONTROL5 0x4840
+
+/* PLL Config */
+#define CS40L50_PLL_REFCLK_BCLK 0x0
+#define CS40L50_PLL_REFCLK_MCLK 0x5
+#define CS40L50_PLL_REEFCLK_MCLK_CFG 0x00
+#define CS40L50_PLL_REFCLK_LOOP_MASK BIT(11)
+#define CS40L50_PLL_REFCLK_OPEN_LOOP 1
+#define CS40L50_PLL_REFCLK_CLOSED_LOOP 0
+#define CS40L50_PLL_REFCLK_LOOP_SHIFT 11
+#define CS40L50_PLL_REFCLK_FREQ_MASK GENMASK(10, 5)
+#define CS40L50_PLL_REFCLK_FREQ_SHIFT 5
+#define CS40L50_PLL_REFCLK_SEL_MASK GENMASK(2, 0)
+#define CS40L50_BCLK_RATIO_DEFAULT 32
+
+/* ASP Config */
+#define CS40L50_ASP_RX_WIDTH_SHIFT 24
+#define CS40L50_ASP_RX_WIDTH_MASK GENMASK(31, 24)
+#define CS40L50_ASP_RX_WL_MASK GENMASK(5, 0)
+#define CS40L50_ASP_FSYNC_INV_MASK BIT(2)
+#define CS40L50_ASP_BCLK_INV_MASK BIT(6)
+#define CS40L50_ASP_FMT_MASK GENMASK(10, 8)
+#define CS40L50_ASP_FMT_I2S 0x2
+
+struct cs40l50_pll_config {
+ unsigned int freq;
+ unsigned int cfg;
+};
+
+struct cs40l50_codec {
+ struct device *dev;
+ struct regmap *regmap;
+ unsigned int daifmt;
+ unsigned int bclk_ratio;
+ unsigned int rate;
+};
+
+static const struct cs40l50_pll_config cs40l50_pll_cfg[] = {
+ { 32768, 0x00 },
+ { 1536000, 0x1B },
+ { 3072000, 0x21 },
+ { 6144000, 0x28 },
+ { 9600000, 0x30 },
+ { 12288000, 0x33 },
+};
+
+static int cs40l50_get_clk_config(const unsigned int freq, unsigned int *cfg)
+{
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(cs40l50_pll_cfg); i++) {
+ if (cs40l50_pll_cfg[i].freq == freq) {
+ *cfg = cs40l50_pll_cfg[i].cfg;
+ return 0;
+ }
+ }
+
+ return -EINVAL;
+}
+
+static int cs40l50_swap_ext_clk(struct cs40l50_codec *codec, const unsigned int clk_src)
+{
+ unsigned int cfg;
+ int ret;
+
+ switch (clk_src) {
+ case CS40L50_PLL_REFCLK_BCLK:
+ ret = cs40l50_get_clk_config(codec->bclk_ratio * codec->rate, &cfg);
+ if (ret)
+ return ret;
+ break;
+ case CS40L50_PLL_REFCLK_MCLK:
+ cfg = CS40L50_PLL_REEFCLK_MCLK_CFG;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ ret = regmap_update_bits(codec->regmap, CS40L50_REFCLK_INPUT,
+ CS40L50_PLL_REFCLK_LOOP_MASK,
+ CS40L50_PLL_REFCLK_OPEN_LOOP <<
+ CS40L50_PLL_REFCLK_LOOP_SHIFT);
+ if (ret)
+ return ret;
+
+ ret = regmap_update_bits(codec->regmap, CS40L50_REFCLK_INPUT,
+ CS40L50_PLL_REFCLK_FREQ_MASK |
+ CS40L50_PLL_REFCLK_SEL_MASK,
+ (cfg << CS40L50_PLL_REFCLK_FREQ_SHIFT) | clk_src);
+ if (ret)
+ return ret;
+
+ return regmap_update_bits(codec->regmap, CS40L50_REFCLK_INPUT,
+ CS40L50_PLL_REFCLK_LOOP_MASK,
+ CS40L50_PLL_REFCLK_CLOSED_LOOP <<
+ CS40L50_PLL_REFCLK_LOOP_SHIFT);
+}
+
+static int cs40l50_clk_en(struct snd_soc_dapm_widget *w,
+ struct snd_kcontrol *kcontrol,
+ int event)
+{
+ struct snd_soc_component *comp = snd_soc_dapm_to_component(w->dapm);
+ struct cs40l50_codec *codec = snd_soc_component_get_drvdata(comp);
+ int ret;
+
+ switch (event) {
+ case SND_SOC_DAPM_POST_PMU:
+ ret = cs40l50_dsp_write(codec->dev, codec->regmap, CS40L50_STOP_PLAYBACK);
+ if (ret)
+ return ret;
+
+ ret = cs40l50_dsp_write(codec->dev, codec->regmap, CS40L50_START_I2S);
+ if (ret)
+ return ret;
+
+ ret = cs40l50_swap_ext_clk(codec, CS40L50_PLL_REFCLK_BCLK);
+ if (ret)
+ return ret;
+ break;
+ case SND_SOC_DAPM_PRE_PMD:
+ ret = cs40l50_swap_ext_clk(codec, CS40L50_PLL_REFCLK_MCLK);
+ if (ret)
+ return ret;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static const struct snd_soc_dapm_widget cs40l50_dapm_widgets[] = {
+ SND_SOC_DAPM_SUPPLY_S("ASP PLL", 0, SND_SOC_NOPM, 0, 0, cs40l50_clk_en,
+ SND_SOC_DAPM_POST_PMU | SND_SOC_DAPM_PRE_PMD),
+ SND_SOC_DAPM_AIF_IN("ASPRX1", NULL, 0, SND_SOC_NOPM, 0, 0),
+ SND_SOC_DAPM_AIF_IN("ASPRX2", NULL, 0, SND_SOC_NOPM, 0, 0),
+ SND_SOC_DAPM_OUTPUT("OUT"),
+};
+
+static const struct snd_soc_dapm_route cs40l50_dapm_routes[] = {
+ { "ASP Playback", NULL, "ASP PLL" },
+ { "ASPRX1", NULL, "ASP Playback" },
+ { "ASPRX2", NULL, "ASP Playback" },
+
+ { "OUT", NULL, "ASPRX1" },
+ { "OUT", NULL, "ASPRX2" },
+};
+
+static int cs40l50_set_dai_fmt(struct snd_soc_dai *codec_dai, unsigned int fmt)
+{
+ struct cs40l50_codec *codec = snd_soc_component_get_drvdata(codec_dai->component);
+
+ if ((fmt & SND_SOC_DAIFMT_MASTER_MASK) != SND_SOC_DAIFMT_CBC_CFC)
+ return -EINVAL;
+
+ switch (fmt & SND_SOC_DAIFMT_INV_MASK) {
+ case SND_SOC_DAIFMT_NB_NF:
+ codec->daifmt = 0;
+ break;
+ case SND_SOC_DAIFMT_NB_IF:
+ codec->daifmt = CS40L50_ASP_FSYNC_INV_MASK;
+ break;
+ case SND_SOC_DAIFMT_IB_NF:
+ codec->daifmt = CS40L50_ASP_BCLK_INV_MASK;
+ break;
+ case SND_SOC_DAIFMT_IB_IF:
+ codec->daifmt = CS40L50_ASP_FSYNC_INV_MASK | CS40L50_ASP_BCLK_INV_MASK;
+ break;
+ default:
+ dev_err(codec->dev, "Invalid clock invert\n");
+ return -EINVAL;
+ }
+
+ switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) {
+ case SND_SOC_DAIFMT_I2S:
+ codec->daifmt |= FIELD_PREP(CS40L50_ASP_FMT_MASK, CS40L50_ASP_FMT_I2S);
+ break;
+ default:
+ dev_err(codec->dev, "Unsupported DAI format\n");
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int cs40l50_hw_params(struct snd_pcm_substream *substream,
+ struct snd_pcm_hw_params *params,
+ struct snd_soc_dai *dai)
+{
+ struct cs40l50_codec *codec = snd_soc_component_get_drvdata(dai->component);
+ unsigned int asp_rx_wl = params_width(params);
+ int ret;
+
+ codec->rate = params_rate(params);
+
+ ret = regmap_update_bits(codec->regmap, CS40L50_ASP_DATA_CONTROL5,
+ CS40L50_ASP_RX_WL_MASK, asp_rx_wl);
+ if (ret)
+ return ret;
+
+ codec->daifmt |= (asp_rx_wl << CS40L50_ASP_RX_WIDTH_SHIFT);
+
+ return regmap_update_bits(codec->regmap, CS40L50_ASP_CONTROL2,
+ CS40L50_ASP_FSYNC_INV_MASK |
+ CS40L50_ASP_BCLK_INV_MASK |
+ CS40L50_ASP_FMT_MASK |
+ CS40L50_ASP_RX_WIDTH_MASK, codec->daifmt);
+}
+
+static int cs40l50_set_dai_bclk_ratio(struct snd_soc_dai *dai, unsigned int ratio)
+{
+ struct cs40l50_codec *codec = snd_soc_component_get_drvdata(dai->component);
+
+ codec->bclk_ratio = ratio;
+
+ return 0;
+}
+
+static const struct snd_soc_dai_ops cs40l50_dai_ops = {
+ .set_fmt = cs40l50_set_dai_fmt,
+ .set_bclk_ratio = cs40l50_set_dai_bclk_ratio,
+ .hw_params = cs40l50_hw_params,
+};
+
+static struct snd_soc_dai_driver cs40l50_dai[] = {
+ {
+ .name = "cs40l50-pcm",
+ .id = 0,
+ .playback = {
+ .stream_name = "ASP Playback",
+ .channels_min = 1,
+ .channels_max = 2,
+ .rates = SNDRV_PCM_RATE_48000,
+ .formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S24_LE,
+ },
+ .ops = &cs40l50_dai_ops,
+ },
+};
+
+static int cs40l50_codec_probe(struct snd_soc_component *component)
+{
+ struct cs40l50_codec *codec = snd_soc_component_get_drvdata(component);
+
+ codec->bclk_ratio = CS40L50_BCLK_RATIO_DEFAULT;
+
+ return 0;
+}
+
+static const struct snd_soc_component_driver soc_codec_dev_cs40l50 = {
+ .probe = cs40l50_codec_probe,
+ .dapm_widgets = cs40l50_dapm_widgets,
+ .num_dapm_widgets = ARRAY_SIZE(cs40l50_dapm_widgets),
+ .dapm_routes = cs40l50_dapm_routes,
+ .num_dapm_routes = ARRAY_SIZE(cs40l50_dapm_routes),
+};
+
+static int cs40l50_codec_driver_probe(struct platform_device *pdev)
+{
+ struct cs40l50 *cs40l50 = dev_get_drvdata(pdev->dev.parent);
+ struct cs40l50_codec *codec;
+
+ codec = devm_kzalloc(&pdev->dev, sizeof(*codec), GFP_KERNEL);
+ if (!codec)
+ return -ENOMEM;
+
+ codec->regmap = cs40l50->regmap;
+ codec->dev = &pdev->dev;
+
+ return devm_snd_soc_register_component(&pdev->dev, &soc_codec_dev_cs40l50,
+ cs40l50_dai, ARRAY_SIZE(cs40l50_dai));
+}
+
+static const struct platform_device_id cs40l50_id[] = {
+ { "cs40l50-codec", },
+ {}
+};
+MODULE_DEVICE_TABLE(platform, cs40l50_id);
+
+static struct platform_driver cs40l50_codec_driver = {
+ .probe = cs40l50_codec_driver_probe,
+ .id_table = cs40l50_id,
+ .driver = {
+ .name = "cs40l50-codec",
+ },
+};
+module_platform_driver(cs40l50_codec_driver);
+
+MODULE_DESCRIPTION("ASoC CS40L50 driver");
+MODULE_AUTHOR("James Ogletree <james.ogletree@cirrus.com>");
+MODULE_LICENSE("GPL");