* [PATCH v4 0/3] input: misc: Add an initial driver for haptics inside Qcom PMIH010x PMIC
@ 2026-07-17 7:28 Fenglin Wu
2026-07-17 7:28 ` [PATCH v4 1/3] dt-bindings: input: Add Qualcomm SPMI PMIC haptics Fenglin Wu
` (4 more replies)
0 siblings, 5 replies; 8+ messages in thread
From: Fenglin Wu @ 2026-07-17 7:28 UTC (permalink / raw)
To: linux-arm-msm, Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Lee Jones, Stephen Boyd, Bjorn Andersson,
Konrad Dybcio
Cc: David Collins, Subbaraman Narayanamurthy, Kamal Wadhwa, kernel,
linux-input, devicetree, linux-kernel, Fenglin Wu,
Krzysztof Kozlowski
Dependencies:
- [patch 2/3] depends on [patch 1/3] and they should be applied together
Qualcomm PMIH0108 PMIC has a haptics module inside and it could drive
a LRA actuator with several play modes, including: DIRECT_PLAY, FIFO,
PAT_MEM, SWR, etc. Add an initial driver to support two of the play
modes using the input force-feedback framework:
-- FF_CONSTANT effect for DIRECT_PLAY mode which drives sinusoidual
waveforms with fixed period and amplitude, which would generate
a constant vibration effect on the LRA actuator.
-- FF_PERIODIC effect with FF_CUSTOM for FIFO streaming mode, which
can play an arbitrary waveform composed of a sequence of 8-bit
samples at a configurable play rate.
Also, add the device node in the existing pmih0108 dtsi files, and enble
the haptics device for several boards by updating the vmax and
lra-period sttings according to the LRA components that mounted on each
of them.
Signed-off-by: Fenglin Wu <fenglin.wu@oss.qualcomm.com>
---
Changes in v4:
Fixed Sashiko AI review comments in the driver:
- In haptics_start_fifo(), remove the goto and do the clear in the error
paths directly, also add a comment to explain that disabling the IRQ
synchronously won't cause a deadlock.
- When data refilling failed in IRQ handler, stop the play and disable
the IRQ to avoid the potential IRQ storming issue
- in play_work(), check if the effect_id in the request is the one which
under playing before stopping the play.
- Link to v3: https://patch.msgid.link/20260713-qcom-spmi-haptics-v3-0-c931bb7cb94f@oss.qualcomm.com
Also, add trailers in the binding changes.
Changes in v3:
In the binding:
- Removed the ref for qcom,vmax-microvolt as the property with standard
unit already has a ref in dtschema
- Added 'qcom,pmih0108-haptics' as a device-specific compatible
In the driver, fixed Sashiko AI review comments with below changes:
- Added a list to queue and serialize all of the request, which helps to
avoid the races between playback(), which is protected by evdev's event_lock,
and play_work(), which is protected by play_lock.
- Changed to use guard(mutex) or scoped_guard(mutex, ) for cleaner mutex logic
usages, and update protection section to prevent race conditions.
- Added runtime pm control in haptics_stop() function, use it as an unified interface
under the guard of 'play_lock' in play_work()/erase()/remove()/suspend(),
to ensure the runtime pm control correctness in race conditions.
- Removed unnecessary stop play sequence in fifo_empty_irq() as the HW would
automatically stop after the FIFO samples are played out.
- Added a common interface haptics_clear_effect() to clear the FIFO data with gaurd
of 'fifo_lock', and use it before upload() and in erase() to prevent race
condition.
- used __free() for safe memory cleanup
- Checked play_rate against negative value when loading FF_PERIOD effect
- Limited the custom_data length to 48K prevent potential OOM
- Link to v2: https://patch.msgid.link/20260624-qcom-spmi-haptics-v2-0-b9118e60f3e3@oss.qualcomm.com
Changes in v2:
Dropped dtsi change and I will resend them after the driver and binding changes get accepted.
Updated haptics binding and addressed review comments from Krzysztof and Konrad:
- Extended the description to clarify the 'PAT_MEM' mode (not yet supported in the driver)
by comparing it with the 'FIFO' mode.
- Updated the compatible string to 'qcom,spmi-haptics' to match the file name and removed
the PMIC wildcard.
- Simplified register names to 'cfg' and 'ptn'.
- Corrected the unit naming for the 'qcom,vmax-microvolt' property.
- Added an additional clarification for the 'qcom,lra-period-us' property.
Updated the driver to address review comments from Konrad and Julian:
- In haptics_write_fifo_chunk(), separated variable declaration and assignment, and added
comments explaining the 4-byte and 1-byte FIFO writes.
- Replaced manual 'x * n / d' calculations with mult_frac().
- Switched to disable_irq() to prevent late IRQs after device removal.
- Replaced property reads with device_property_read_u32().
- Remove the 'INPUT' dependency in Kconfig
Updated the driver to address feedback from Sashiko AI:
- Guarded pm_runtime_resume()/suspend() with 'pm_ref_held' to prevent runtime PM reference leaks.
- Replaced spinlock with a mutex to protect FIFO data during playback and avoid calling
sleepable regmap APIs under spinlock.
- Adjusted suspend/remove() sequence to stop playback before canceling work, and freed
FIFO buffers to prevent potential memory leaks.
- In FF_PERIODIC handling, allocated 'fifo_data' before assigning data to ensure its
consistency with 'data_len'.
- Registered the input device after enabling runtime PM.
- Unify to use 'h->dev' pointer in probe()
- Link to v1: https://patch.msgid.link/20260616-qcom-spmi-haptics-v1-0-d24e422de6b4@oss.qualcomm.com
---
Fenglin Wu (3):
dt-bindings: input: Add Qualcomm SPMI PMIC haptics
dt-bindings: mfd: qcom,spmi-pmic: Document haptics device
input: misc: Add Qualcomm SPMI PMIC haptics driver
.../bindings/input/qcom,spmi-haptics.yaml | 136 +++
.../devicetree/bindings/mfd/qcom,spmi-pmic.yaml | 4 +
drivers/input/misc/Kconfig | 11 +
drivers/input/misc/Makefile | 1 +
drivers/input/misc/qcom-spmi-haptics.c | 1178 ++++++++++++++++++++
5 files changed, 1330 insertions(+)
---
base-commit: 66725039f7090afe14c31bd259e2059a68f04023
change-id: 20260616-qcom-spmi-haptics-3cc97e7b232e
Best regards,
--
Fenglin Wu <fenglin.wu@oss.qualcomm.com>
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v4 1/3] dt-bindings: input: Add Qualcomm SPMI PMIC haptics
2026-07-17 7:28 [PATCH v4 0/3] input: misc: Add an initial driver for haptics inside Qcom PMIH010x PMIC Fenglin Wu
@ 2026-07-17 7:28 ` Fenglin Wu
2026-07-17 7:29 ` [PATCH v4 2/3] dt-bindings: mfd: qcom,spmi-pmic: Document haptics device Fenglin Wu
` (3 subsequent siblings)
4 siblings, 0 replies; 8+ messages in thread
From: Fenglin Wu @ 2026-07-17 7:28 UTC (permalink / raw)
To: linux-arm-msm, Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Lee Jones, Stephen Boyd, Bjorn Andersson,
Konrad Dybcio
Cc: David Collins, Subbaraman Narayanamurthy, Kamal Wadhwa, kernel,
linux-input, devicetree, linux-kernel, Fenglin Wu,
Krzysztof Kozlowski
Add binding document for the haptics module inside Qualcomm PMIC
PMIH0108.
Assisted-by: Claude:claude-4-8-opus
Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski@oss.qualcomm.com>
Signed-off-by: Fenglin Wu <fenglin.wu@oss.qualcomm.com>
---
.../bindings/input/qcom,spmi-haptics.yaml | 136 +++++++++++++++++++++
1 file changed, 136 insertions(+)
diff --git a/Documentation/devicetree/bindings/input/qcom,spmi-haptics.yaml b/Documentation/devicetree/bindings/input/qcom,spmi-haptics.yaml
new file mode 100644
index 000000000000..b1a69e9886ee
--- /dev/null
+++ b/Documentation/devicetree/bindings/input/qcom,spmi-haptics.yaml
@@ -0,0 +1,136 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/input/qcom,spmi-haptics.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Haptics device inside Qualcomm Technologies, Inc. PMIC
+
+maintainers:
+ - Fenglin Wu <fenglin.wu@oss.qualcomm.com>
+
+description: |
+ Certain Qualcomm PMICs integrate a haptics module, such as the HAP530_HV haptics
+ module in the PMIH0108 PMIC, which drives an LRA (Linear Resonant Actuator) with
+ an output voltage up to 10 V. Several play modes are supported in HAP530_HV:
+
+ DIRECT_PLAY: The hardware outputs sinusoidal waveforms whose period is
+ defined by qcom,lra-period-us and whose peak voltage is defined by
+ qcom,vmax-microvolt. The driving amplitude can be scaled in the range
+ [0, 255] via a single register byte. Hardware-based LRA auto-resonance
+ tracking is enabled by default in this mode, allowing the haptics engine
+ to follow the actual resonant frequency of the LRA and update the driving
+ period accordingly to achieve stronger vibration magnitude.
+
+ FIFO: The hardware can play an arbitrary waveform composed of a sequence
+ of 8-bit samples at a configurable play rate. Samples are pre-filled
+ into the internal FIFO memory of the haptics module and continuously
+ replenished via the FIFO-empty IRQ until all samples have been played.
+ An 8K-byte FIFO memory bank is available in the HAP530_HV haptics module,
+ shared between the FIFO and PAT_MEM play modes. The memory partition
+ between the two modes is configurable via registers, and FIFO mode always
+ uses the 1st partition starting from offset 0.
+
+ PAT_MEM: This mode is very similar to FIFO streaming mode but without the
+ data refilling capability. It is designed mainly for short, latency-critical
+ vibrations. The memory space for PAT_MEM mode must be reserved for dedicated
+ usage, and the waveform data should be preloaded and remain unchanged
+ thereafter. The haptics module can play the waveform data from the memory
+ region specified by the PAT_MEM play start address and length registers.
+
+ In either FIFO mode or PAT_MEM mode, the following play rates are supported:
+ -- 0(T_LRA): each FIFO byte drives one full sinusoidal cycle with the
+ period defined in qcom,lra-period-us.
+ -- 1/2/3(T_LRA_DIV_2/4/8): each FIFO byte drives a half/quarter/eighth
+ sinusoidal cycle with the period defined in qcom,lra-period-us.
+ -- 4/5/6(T_LRA_X_2/4/8): each FIFO byte drives 2/4/8 sinusoidal cycles
+ with the period defined in qcom,lra-period-us.
+ -- 8/9/10/11/12/13(8KHz/16KHz/24KHz/32KHz/44.1KHz/48KHz): the FIFO
+ data is treated as PCM samples and drives the output with an
+ arbitrarily shaped waveform. This mode is typically used to define
+ custom driving waveforms for specific vibration effects such as fast
+ attack, crisp brake, etc.
+
+ The drive voltage in FIFO or PAT_MEM mode can exceed the value defined in
+ qcom,vmax-microvolt to achieve a special vibration effect, but the waveform
+ must be short enough to prevent the LRA from being damaged by operating at
+ an overvoltage.
+
+ Also, hardware-based LRA auto-resonance tracking is normally disabled in
+ FIFO or PAT_MEM mode, as these modes are intended to drive arbitrary
+ waveforms that may not follow the resonant frequency; autonomous hardware
+ resonance correction would interfere with the intended output.
+
+properties:
+ compatible:
+ items:
+ - const: qcom,pmih0108-haptics
+ - const: qcom,spmi-haptics
+
+ reg:
+ items:
+ - description: HAP_CFG module base address
+ - description: HAP_PTN module base address
+
+ reg-names:
+ items:
+ - const: cfg
+ - const: ptn
+
+ interrupts:
+ maxItems: 1
+
+ interrupt-names:
+ items:
+ - const: fifo-empty
+
+ qcom,vmax-microvolt:
+ description:
+ Maximum allowed output driving voltage in microvolts, must be a multiple
+ of 50,000 uV. This is the peak driving voltage in DIRECT_PLAY mode,
+ which outputs sinusoidal waveforms. The value should be equal to the
+ square root of 2 times the Vrms voltage of the LRA.
+ minimum: 50000
+ maximum: 10000000
+ multipleOf: 50000
+
+ qcom,lra-period-us:
+ description:
+ LRA actuator initial resonance period in microseconds
+ (1,000,000 / resonant_freq_hz). Used to configure T_LRA-based play
+ rates and the auto-resonance zero-crossing window. It could be also used
+ as the initial period if the LRA wants to be driven off resonance.
+ minimum: 5
+ maximum: 20475
+ multipleOf: 5
+
+required:
+ - compatible
+ - reg
+ - reg-names
+ - interrupts
+ - interrupt-names
+ - qcom,vmax-microvolt
+ - qcom,lra-period-us
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/interrupt-controller/irq.h>
+
+ pmic {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ haptics@f000 {
+ compatible = "qcom,pmih0108-haptics", "qcom,spmi-haptics";
+ reg = <0xf000>, <0xf100>;
+ reg-names = "cfg", "ptn";
+ interrupts = <0x7 0xf0 0x1 IRQ_TYPE_EDGE_RISING>;
+ interrupt-names = "fifo-empty";
+
+ qcom,vmax-microvolt = <1300000>;
+ qcom,lra-period-us = <5880>;
+ };
+ };
--
2.43.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH v4 2/3] dt-bindings: mfd: qcom,spmi-pmic: Document haptics device
2026-07-17 7:28 [PATCH v4 0/3] input: misc: Add an initial driver for haptics inside Qcom PMIH010x PMIC Fenglin Wu
2026-07-17 7:28 ` [PATCH v4 1/3] dt-bindings: input: Add Qualcomm SPMI PMIC haptics Fenglin Wu
@ 2026-07-17 7:29 ` Fenglin Wu
2026-07-17 7:29 ` [PATCH v4 3/3] input: misc: Add Qualcomm SPMI PMIC haptics driver Fenglin Wu
` (2 subsequent siblings)
4 siblings, 0 replies; 8+ messages in thread
From: Fenglin Wu @ 2026-07-17 7:29 UTC (permalink / raw)
To: linux-arm-msm, Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Lee Jones, Stephen Boyd, Bjorn Andersson,
Konrad Dybcio
Cc: David Collins, Subbaraman Narayanamurthy, Kamal Wadhwa, kernel,
linux-input, devicetree, linux-kernel, Fenglin Wu,
Krzysztof Kozlowski
Some of the Qualcomm SPMI PMIC has haptics device in it, add it in the
device list.
Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski@oss.qualcomm.com>
Signed-off-by: Fenglin Wu <fenglin.wu@oss.qualcomm.com>
---
Documentation/devicetree/bindings/mfd/qcom,spmi-pmic.yaml | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/Documentation/devicetree/bindings/mfd/qcom,spmi-pmic.yaml b/Documentation/devicetree/bindings/mfd/qcom,spmi-pmic.yaml
index 644c42b5e2e5..773f4cba5935 100644
--- a/Documentation/devicetree/bindings/mfd/qcom,spmi-pmic.yaml
+++ b/Documentation/devicetree/bindings/mfd/qcom,spmi-pmic.yaml
@@ -165,6 +165,10 @@ patternProperties:
type: object
$ref: /schemas/pinctrl/qcom,pmic-gpio.yaml#
+ "^haptics@[0-9a-f]+$":
+ type: object
+ $ref: /schemas/input/qcom,spmi-haptics.yaml#
+
"^led-controller@[0-9a-f]+$":
type: object
$ref: /schemas/leds/qcom,spmi-flash-led.yaml#
--
2.43.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH v4 3/3] input: misc: Add Qualcomm SPMI PMIC haptics driver
2026-07-17 7:28 [PATCH v4 0/3] input: misc: Add an initial driver for haptics inside Qcom PMIH010x PMIC Fenglin Wu
2026-07-17 7:28 ` [PATCH v4 1/3] dt-bindings: input: Add Qualcomm SPMI PMIC haptics Fenglin Wu
2026-07-17 7:29 ` [PATCH v4 2/3] dt-bindings: mfd: qcom,spmi-pmic: Document haptics device Fenglin Wu
@ 2026-07-17 7:29 ` Fenglin Wu
2026-07-17 7:42 ` sashiko-bot
2026-07-17 16:51 ` Dmitry Torokhov
2026-07-17 9:25 ` [PATCH v4 0/3] input: misc: Add an initial driver for haptics inside Qcom PMIH010x PMIC Krzysztof Kozlowski
2026-07-17 18:52 ` Jeff Johnson
4 siblings, 2 replies; 8+ messages in thread
From: Fenglin Wu @ 2026-07-17 7:29 UTC (permalink / raw)
To: linux-arm-msm, Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Lee Jones, Stephen Boyd, Bjorn Andersson,
Konrad Dybcio
Cc: David Collins, Subbaraman Narayanamurthy, Kamal Wadhwa, kernel,
linux-input, devicetree, linux-kernel, Fenglin Wu
Add an initial driver for the Qualcomm PMIH0108 PMIC haptics module,
named as HAP530_HV. This module supports several play modes, including
DIRECT_PLAY, FIFO, PAT_MEM, and SWR, each with distinct data sourcing
and hardware data handling logic. Currently, the driver provides support
for two play modes using the input force-feedback framework: FF_CONSTANT
effect for DIRECT_PLAY mode and FF_PERIODIC effect with FF_CUSTOM
waveform for FIFO mode.
Assisted-by: Claude:claude-4-8-opus
Signed-off-by: Fenglin Wu <fenglin.wu@oss.qualcomm.com>
---
drivers/input/misc/Kconfig | 11 +
drivers/input/misc/Makefile | 1 +
drivers/input/misc/qcom-spmi-haptics.c | 1178 ++++++++++++++++++++++++++++++++
3 files changed, 1190 insertions(+)
diff --git a/drivers/input/misc/Kconfig b/drivers/input/misc/Kconfig
index 1f6c57dba030..4f40940973e4 100644
--- a/drivers/input/misc/Kconfig
+++ b/drivers/input/misc/Kconfig
@@ -236,6 +236,17 @@ config INPUT_PMIC8XXX_PWRKEY
To compile this driver as a module, choose M here: the
module will be called pmic8xxx-pwrkey.
+config INPUT_QCOM_SPMI_HAPTICS
+ tristate "Qualcomm SPMI PMIC haptics support"
+ depends on MFD_SPMI_PMIC
+ help
+ Say Y to enable support for the Qualcomm PMIH0108 SPMI PMIC haptics
+ module. Supports DIRECT_PLAY, FIFO streaming play modes via the
+ Linux input force-feedback framework.
+
+ To compile this driver as a module, choose M here: the module will
+ be called qcom-spmi-haptics.
+
config INPUT_SPARCSPKR
tristate "SPARC Speaker support"
depends on PCI && SPARC64
diff --git a/drivers/input/misc/Makefile b/drivers/input/misc/Makefile
index 2281d6803fce..c5c9aa139a11 100644
--- a/drivers/input/misc/Makefile
+++ b/drivers/input/misc/Makefile
@@ -69,6 +69,7 @@ obj-$(CONFIG_INPUT_PMIC8XXX_PWRKEY) += pmic8xxx-pwrkey.o
obj-$(CONFIG_INPUT_POWERMATE) += powermate.o
obj-$(CONFIG_INPUT_PWM_BEEPER) += pwm-beeper.o
obj-$(CONFIG_INPUT_PWM_VIBRA) += pwm-vibra.o
+obj-$(CONFIG_INPUT_QCOM_SPMI_HAPTICS) += qcom-spmi-haptics.o
obj-$(CONFIG_INPUT_QNAP_MCU) += qnap-mcu-input.o
obj-$(CONFIG_INPUT_RAVE_SP_PWRBUTTON) += rave-sp-pwrbutton.o
obj-$(CONFIG_INPUT_RB532_BUTTON) += rb532_button.o
diff --git a/drivers/input/misc/qcom-spmi-haptics.c b/drivers/input/misc/qcom-spmi-haptics.c
new file mode 100644
index 000000000000..235f77e0229c
--- /dev/null
+++ b/drivers/input/misc/qcom-spmi-haptics.c
@@ -0,0 +1,1178 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+ */
+
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/device.h>
+#include <linux/input.h>
+#include <linux/interrupt.h>
+#include <linux/list.h>
+#include <linux/math64.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+#include <linux/pm_runtime.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+#include <linux/uaccess.h>
+#include <linux/workqueue.h>
+
+/* HAP_CFG register offsets, bit fields, value constants */
+#define HAP_CFG_INT_RT_STS_REG 0x10
+#define FIFO_EMPTY_BIT BIT(1)
+#define HAP_CFG_EN_CTL_REG 0x46
+#define HAPTICS_EN_BIT BIT(7)
+#define HAP_CFG_VMAX_REG 0x48
+#define VMAX_STEP_MV 50
+#define VMAX_MV_MAX 10000
+#define HAP_CFG_SPMI_PLAY_REG 0x4C
+#define PLAY_EN_BIT BIT(7)
+#define PAT_SRC_MASK GENMASK(2, 0)
+#define PAT_SRC_FIFO 0
+#define PAT_SRC_DIRECT_PLAY 1
+#define HAP_CFG_TLRA_OL_HIGH_REG 0x5C
+#define TLRA_OL_MSB_MASK GENMASK(3, 0)
+#define TLRA_STEP_US 5
+#define TLRA_US_MAX 20475
+#define HAP_CFG_TLRA_OL_LOW_REG 0x5D
+#define HAP_CFG_DRV_DUTY_CFG_REG 0x60
+#define ADT_DRV_DUTY_EN_BIT BIT(7)
+#define ADT_BRK_DUTY_EN_BIT BIT(6)
+#define DRV_DUTY_MASK GENMASK(5, 3)
+#define AUTORES_DRV_DUTY_62P5 2
+#define BRK_DUTY_MASK GENMASK(2, 0)
+#define AUTORES_BRK_DUTY_62P5 5
+#define HAP_CFG_ZX_WIND_CFG_REG 0x62
+#define ZX_DEBOUNCE_MASK GENMASK(6, 4)
+#define AUTORES_ZX_DEBOUNCE 3
+#define ZX_WIN_HEIGHT_MASK GENMASK(2, 0)
+#define AUTORES_ZX_WIN_HEIGHT 2
+#define HAP_CFG_AUTORES_CFG_REG 0x63
+#define AUTORES_EN_BIT BIT(7)
+#define AUTORES_EN_DLY_MASK GENMASK(6, 2)
+#define AUTORES_EN_DLY_CYCLES 10
+#define AUTORES_ERR_WIN_MASK GENMASK(1, 0)
+#define AUTORES_ERR_WIN_25PCT 1
+#define HAP_CFG_FAULT_CLR_REG 0x66
+#define ZX_TO_FAULT_CLR_BIT BIT(4)
+#define SC_CLR_BIT BIT(2)
+#define AUTO_RES_ERR_CLR_BIT BIT(1)
+#define HPWR_RDY_FAULT_CLR_BIT BIT(0)
+#define FAULT_CLR_ALL (ZX_TO_FAULT_CLR_BIT | SC_CLR_BIT | \
+ AUTO_RES_ERR_CLR_BIT | HPWR_RDY_FAULT_CLR_BIT)
+#define HAP_CFG_RAMP_DN_CFG2_REG 0x86
+#define AUTORES_PRE_HIZ_DLY_10US 1
+
+/* HAP_PTN register offsets, bit fields, value constants */
+#define HAP_PTN_REVISION2_REG 0x01
+#define HAP_PTN_FIFO_DIN_0_REG 0x20
+#define HAP_PTN_FIFO_PLAY_RATE_REG 0x24
+#define FIFO_PLAY_RATE_MASK GENMASK(3, 0)
+#define HAP_PTN_DIRECT_PLAY_REG 0x26
+#define HAP_PTN_FIFO_EMPTY_CFG_REG 0x2A
+#define FIFO_THRESH_LSB 64
+#define HAP_PTN_FIFO_DIN_1B_REG 0x2C
+#define HAP_PTN_MEM_OP_ACCESS_REG 0x2D
+#define MEM_FLUSH_RELOAD_BIT BIT(0)
+#define HAP_PTN_MMAP_FIFO_REG 0xA0
+#define MMAP_FIFO_EXIST_BIT BIT(7)
+#define MMAP_FIFO_LEN_MASK GENMASK(6, 0)
+#define HAP_PTN_PATX_PLAY_CFG_REG 0xA2
+
+#define HAP530_MEM_TOTAL_BYTES 8192
+#define FIFO_EMPTY_THRESH 280
+#define FIFO_INIT_FILL 320
+
+#define HAPTICS_AUTOSUSPEND_MS 1000
+
+/*
+ * FF_CUSTOM data layout (custom_data[] of type s16):
+ * [0] = play rate (PLAY_RATE_*)
+ * [1] = vmax in mV (0 = use device default from qcom,vmax-microvolt)
+ * [2..N-1] = signed 8-bit PCM samples packed one per s16 (lower byte used)
+ */
+#define CUSTOM_DATA_RATE_IDX 0
+#define CUSTOM_DATA_VMAX_IDX 1
+#define CUSTOM_DATA_SAMPLE_START 2
+#define CUSTOM_DATA_MAX_LEN (48 * 1024)
+
+#define HAPTICS_MAX_EFFECTS 8
+
+enum qcom_haptics_mode {
+ HAPTICS_MODE_NONE,
+ HAPTICS_DIRECT_PLAY,
+ HAPTICS_FIFO,
+};
+
+enum qcom_haptics_play_rate {
+ PLAY_RATE_T_LRA = 0,
+ PLAY_RATE_T_LRA_DIV_2 = 1,
+ PLAY_RATE_T_LRA_DIV_4 = 2,
+ PLAY_RATE_T_LRA_DIV_8 = 3,
+ PLAY_RATE_T_LRA_X_2 = 4,
+ PLAY_RATE_T_LRA_X_4 = 5,
+ PLAY_RATE_T_LRA_X_8 = 6,
+ PLAY_RATE_RESERVED = 7,
+ PLAY_RATE_F_8KHZ = 8,
+ PLAY_RATE_F_16KHZ = 9,
+ PLAY_RATE_F_24KHZ = 10,
+ PLAY_RATE_F_32KHZ = 11,
+ PLAY_RATE_F_44P1KHZ = 12,
+ PLAY_RATE_F_48KHZ = 13,
+ PLAY_RATE_MAX = PLAY_RATE_F_48KHZ,
+};
+
+/**
+ * struct qcom_haptics_effect: A haptics effect
+ * @mode: haptics HW play mode
+ * @vmax_mv: peak voltage of the haptics output waveform
+ * @length_us: vibration play duration
+ * @amplitude: DIRECT_PLAY mode output waveform amplitude
+ * @play_rate: FIFO mode play rate
+ * @fifo_data: 8-bit data samples consumed in FIFO mode
+ * @data_len: length of the FIFO data samples
+ */
+struct qcom_haptics_effect {
+ enum qcom_haptics_mode mode;
+ u32 vmax_mv;
+ u32 length_us;
+
+ u8 amplitude;
+
+ enum qcom_haptics_play_rate play_rate;
+ s8 *fifo_data;
+ u32 data_len;
+};
+
+/**
+ * struct haptics_play_req: A haptics play request
+ * @node: list node of the request
+ * @effect_id: effect index of the request
+ * @play: flag of starting or stopping the play
+ */
+struct haptics_play_req {
+ struct list_head node;
+ int effect_id;
+ bool play;
+};
+
+/**
+ * struct qcom_haptics
+ * @dev: underlying SPMI device
+ * @regmap: regmap for SPMI register access
+ * @input: input device exposing the FF interface
+ * @cfg_base: base address of the CFG peripheral
+ * @ptn_base: base address of the PTN peripheral
+ * @t_lra_us: LRA resonance period in microseconds
+ * @vmax_mv: maximum actuator drive voltage in millivolts
+ * @fifo_len: programmed HW FIFO depth in bytes
+ * @gain: playback gain scaler
+ * @play_work: delayed work that plays the queued requests
+ * @play_lock: mutex lock to serialize playbacks
+ * @play_queue_lock: spinlock protecting @play_queue
+ * @play_queue: The list of pending start/stop requests
+ * @active_effect_id: index into @effects[] currently under play
+ * @active_mode: mode of the effect currently armed in hardware
+ * @fifo_empty_irq: IRQ number for the FIFO-empty interrupt
+ * @pm_ref_held: true while a pm_runtime_get is held
+ * @irq_enabled: true if fifo_empty_irq is enabled
+ * @fifo_lock: mutex protecting the FIFO streaming data
+ * @fifo_data: pointer of the data buffer for FIFO streaming
+ * @data_len: length of the data buffer for current effect
+ * @data_written: number of samples written to the hardware FIFO
+ * @effects: table of the effects
+ */
+struct qcom_haptics {
+ struct device *dev;
+ struct regmap *regmap;
+ struct input_dev *input;
+
+ u32 cfg_base;
+ u32 ptn_base;
+ u32 t_lra_us;
+ u32 vmax_mv;
+ u32 fifo_len;
+ atomic_t gain;
+
+ struct delayed_work play_work;
+ struct mutex play_lock; /* mutex used to serialize playbacks */
+
+ spinlock_t play_queue_lock; /* protects play_queue */
+ struct list_head play_queue;
+
+ int active_effect_id;
+ enum qcom_haptics_mode active_mode;
+
+ int fifo_empty_irq;
+ bool pm_ref_held;
+ bool irq_enabled;
+
+ struct mutex fifo_lock; /* protect the FIFO data during play */
+ const s8 *fifo_data;
+ u32 data_len;
+ u32 data_written;
+
+ struct qcom_haptics_effect effects[HAPTICS_MAX_EFFECTS];
+};
+
+static int cfg_write(struct qcom_haptics *h, u32 off, u32 val)
+{
+ return regmap_write(h->regmap, h->cfg_base + off, val);
+}
+
+static int cfg_update_bits(struct qcom_haptics *h, u32 off, u32 mask, u32 val)
+{
+ return regmap_update_bits(h->regmap, h->cfg_base + off, mask, val);
+}
+
+static int ptn_write(struct qcom_haptics *h, u32 off, u32 val)
+{
+ return regmap_write(h->regmap, h->ptn_base + off, val);
+}
+
+static int ptn_update_bits(struct qcom_haptics *h, u32 off, u32 mask, u32 val)
+{
+ return regmap_update_bits(h->regmap, h->ptn_base + off, mask, val);
+}
+
+static int ptn_bulk_write(struct qcom_haptics *h, u32 off,
+ const void *buf, size_t count)
+{
+ return regmap_bulk_write(h->regmap, h->ptn_base + off, buf, count);
+}
+
+static int haptics_clear_faults(struct qcom_haptics *h)
+{
+ return cfg_write(h, HAP_CFG_FAULT_CLR_REG, FAULT_CLR_ALL);
+}
+
+static int haptics_set_vmax(struct qcom_haptics *h, u32 vmax_mv)
+{
+ return cfg_write(h, HAP_CFG_VMAX_REG, vmax_mv / VMAX_STEP_MV);
+}
+
+static int haptics_config_lra_period(struct qcom_haptics *h)
+{
+ u32 tmp = h->t_lra_us / TLRA_STEP_US;
+ int ret;
+
+ ret = cfg_write(h, HAP_CFG_TLRA_OL_HIGH_REG, (tmp >> 8) & TLRA_OL_MSB_MASK);
+ if (ret)
+ return ret;
+
+ return cfg_write(h, HAP_CFG_TLRA_OL_LOW_REG, tmp & 0xFF);
+}
+
+static int haptics_enable_module(struct qcom_haptics *h, bool enable)
+{
+ return cfg_update_bits(h, HAP_CFG_EN_CTL_REG, HAPTICS_EN_BIT,
+ enable ? HAPTICS_EN_BIT : 0);
+}
+
+static int haptics_configure_autores(struct qcom_haptics *h)
+{
+ int ret;
+
+ /* AUTORES_CFG: enable, 10-cycle delay, 25% error window */
+ ret = cfg_write(h, HAP_CFG_AUTORES_CFG_REG,
+ AUTORES_EN_BIT |
+ FIELD_PREP(AUTORES_EN_DLY_MASK, AUTORES_EN_DLY_CYCLES) |
+ FIELD_PREP(AUTORES_ERR_WIN_MASK, AUTORES_ERR_WIN_25PCT));
+ if (ret)
+ return ret;
+
+ /* DRV_DUTY: adaptive drive/brake duty cycles at 62.5% */
+ ret = cfg_write(h, HAP_CFG_DRV_DUTY_CFG_REG,
+ ADT_DRV_DUTY_EN_BIT | ADT_BRK_DUTY_EN_BIT |
+ FIELD_PREP(DRV_DUTY_MASK, AUTORES_DRV_DUTY_62P5) |
+ FIELD_PREP(BRK_DUTY_MASK, AUTORES_BRK_DUTY_62P5));
+ if (ret)
+ return ret;
+
+ /* Pre-HIZ delay: 10 µs */
+ ret = cfg_write(h, HAP_CFG_RAMP_DN_CFG2_REG, AUTORES_PRE_HIZ_DLY_10US);
+ if (ret)
+ return ret;
+
+ /* Zero-cross window: debounce 3, no hysteresis, height 2 */
+ return cfg_write(h, HAP_CFG_ZX_WIND_CFG_REG,
+ FIELD_PREP(ZX_DEBOUNCE_MASK, AUTORES_ZX_DEBOUNCE) |
+ FIELD_PREP(ZX_WIN_HEIGHT_MASK, AUTORES_ZX_WIN_HEIGHT));
+}
+
+static int haptics_write_fifo_chunk(struct qcom_haptics *h,
+ const s8 *data, u32 len)
+{
+ u32 bulk_len = ALIGN_DOWN(len, 4);
+ int i, ret;
+
+ /*
+ * FIFO data writing supports both 4-byte bulk writes using registers
+ * [HAP_PTN_FIFO_DIN_0_REG ... HAP_PTN_FIFO_DIN_3_REG], and 1-byte writes
+ * using the HAP_PTN_FIFO_DIN_1B_REG register. The 4-byte bulk write is more
+ * efficient, so use 4-byte writes for the initial 4-byte aligned data,
+ * and 1-byte writes for any trailing remainder.
+ */
+ for (i = 0; i < bulk_len; i += 4) {
+ ret = ptn_bulk_write(h, HAP_PTN_FIFO_DIN_0_REG, &data[i], 4);
+ if (ret)
+ return ret;
+ }
+
+ for (; i < len; i++) {
+ ret = ptn_write(h, HAP_PTN_FIFO_DIN_1B_REG, (u8)data[i]);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+static int haptics_configure_fifo_mmap(struct qcom_haptics *h)
+{
+ u32 fifo_len, fifo_units;
+
+ /* Config all memory space for FIFO usage for now */
+ fifo_len = HAP530_MEM_TOTAL_BYTES;
+ fifo_len = ALIGN_DOWN(fifo_len, 64);
+ fifo_units = fifo_len / 64;
+ h->fifo_len = fifo_len;
+
+ return ptn_write(h, HAP_PTN_MMAP_FIFO_REG,
+ MMAP_FIFO_EXIST_BIT |
+ FIELD_PREP(MMAP_FIFO_LEN_MASK, fifo_units - 1));
+}
+
+static u32 haptics_gain_scaled_vmax(struct qcom_haptics *h, u32 vmax_mv)
+{
+ u16 gain = atomic_read(&h->gain);
+ u32 v = mult_frac(vmax_mv, gain, 0xFFFF);
+
+ return max_t(u32, v, VMAX_STEP_MV);
+}
+
+static void haptics_fifo_irq_enable(struct qcom_haptics *h, bool enable)
+{
+ if (h->irq_enabled == enable)
+ return;
+
+ if (enable)
+ enable_irq(h->fifo_empty_irq);
+ else
+ disable_irq(h->fifo_empty_irq);
+
+ h->irq_enabled = enable;
+}
+
+static int haptics_enqueue_play_req(struct qcom_haptics *h, int effect_id, bool play)
+{
+ struct haptics_play_req *req;
+
+ guard(spinlock_irqsave)(&h->play_queue_lock);
+
+ /*
+ * Coalesce with an already queued request for
+ * the same effect instead of appending.
+ */
+ list_for_each_entry(req, &h->play_queue, node) {
+ if (req->effect_id == effect_id) {
+ req->play = play;
+ return 0;
+ }
+ }
+
+ req = kmalloc_obj(*req, GFP_ATOMIC);
+ if (!req)
+ return -ENOMEM;
+
+ req->effect_id = effect_id;
+ req->play = play;
+
+ list_add_tail(&req->node, &h->play_queue);
+
+ return 0;
+}
+
+static struct haptics_play_req *haptics_dequeue_play_req(struct qcom_haptics *h)
+{
+ struct haptics_play_req *req = NULL;
+
+ guard(spinlock_irqsave)(&h->play_queue_lock);
+
+ if (!list_empty(&h->play_queue)) {
+ req = list_first_entry(&h->play_queue, struct haptics_play_req, node);
+ list_del(&req->node);
+ }
+
+ return req;
+}
+
+static bool haptics_queue_pending(struct qcom_haptics *h)
+{
+ guard(spinlock_irqsave)(&h->play_queue_lock);
+
+ return !list_empty(&h->play_queue);
+}
+
+static void haptics_queue_remove_effect(struct qcom_haptics *h, int effect_id)
+{
+ struct haptics_play_req *req, *tmp;
+
+ guard(spinlock_irqsave)(&h->play_queue_lock);
+
+ list_for_each_entry_safe(req, tmp, &h->play_queue, node) {
+ if (req->effect_id == effect_id) {
+ list_del(&req->node);
+ kfree(req);
+ }
+ }
+}
+
+static void haptics_queue_flush(struct qcom_haptics *h)
+{
+ struct haptics_play_req *req, *tmp;
+
+ guard(spinlock_irqsave)(&h->play_queue_lock);
+
+ list_for_each_entry_safe(req, tmp, &h->play_queue, node) {
+ list_del(&req->node);
+ kfree(req);
+ }
+}
+
+static int haptics_start_direct_play(struct qcom_haptics *h, int effect_id)
+{
+ struct qcom_haptics_effect *eff = &h->effects[effect_id];
+ int ret;
+
+ ret = haptics_clear_faults(h);
+ if (ret)
+ return ret;
+
+ /* Enable auto-resonance for DIRECT_PLAY mode */
+ ret = cfg_update_bits(h, HAP_CFG_AUTORES_CFG_REG,
+ AUTORES_EN_BIT, AUTORES_EN_BIT);
+ if (ret)
+ return ret;
+
+ ret = haptics_set_vmax(h, haptics_gain_scaled_vmax(h, h->vmax_mv));
+ if (ret)
+ return ret;
+
+ ret = ptn_write(h, HAP_PTN_DIRECT_PLAY_REG, eff->amplitude);
+ if (ret)
+ return ret;
+
+ return cfg_write(h, HAP_CFG_SPMI_PLAY_REG,
+ PLAY_EN_BIT | FIELD_PREP(PAT_SRC_MASK, PAT_SRC_DIRECT_PLAY));
+}
+
+static int haptics_start_fifo(struct qcom_haptics *h, int effect_id)
+{
+ struct qcom_haptics_effect *eff = &h->effects[effect_id];
+ u32 vmax = eff->vmax_mv ? eff->vmax_mv : h->vmax_mv;
+ u32 init_len;
+ bool data_done;
+ int ret;
+
+ if (!eff->fifo_data || !eff->data_len)
+ return -EINVAL;
+
+ ret = haptics_clear_faults(h);
+ if (ret)
+ return ret;
+
+ /* Disable auto-resonance for FIFO mode */
+ ret = cfg_update_bits(h, HAP_CFG_AUTORES_CFG_REG, AUTORES_EN_BIT, 0);
+ if (ret)
+ return ret;
+
+ ret = haptics_set_vmax(h, haptics_gain_scaled_vmax(h, vmax));
+ if (ret)
+ return ret;
+
+ ret = ptn_update_bits(h, HAP_PTN_FIFO_PLAY_RATE_REG,
+ FIFO_PLAY_RATE_MASK,
+ FIELD_PREP(FIFO_PLAY_RATE_MASK, eff->play_rate));
+ if (ret)
+ return ret;
+
+ /* Flush FIFO before loading new data */
+ ret = ptn_write(h, HAP_PTN_MEM_OP_ACCESS_REG, MEM_FLUSH_RELOAD_BIT);
+ if (ret)
+ return ret;
+ ret = ptn_write(h, HAP_PTN_MEM_OP_ACCESS_REG, 0);
+ if (ret)
+ return ret;
+
+ guard(mutex)(&h->fifo_lock);
+
+ /* Write the initial chunk and initialise streaming state */
+ init_len = min_t(u32, eff->data_len, FIFO_INIT_FILL);
+ ret = haptics_write_fifo_chunk(h, eff->fifo_data, init_len);
+ if (ret)
+ return ret;
+
+ h->fifo_data = eff->fifo_data;
+ h->data_len = eff->data_len;
+ h->data_written = init_len;
+
+ /*
+ * Set empty threshold. When threshold > 0 the hardware fires the
+ * FIFO-empty interrupt when occupancy drops below the threshold,
+ * allowing the driver to refill.
+ */
+ data_done = (h->data_written >= h->data_len);
+ if (!data_done) {
+ ret = ptn_write(h, HAP_PTN_FIFO_EMPTY_CFG_REG,
+ FIFO_EMPTY_THRESH / FIFO_THRESH_LSB);
+ if (ret) {
+ dev_err(h->dev, "set FIFO empty threshold failed, ret=%d\n", ret);
+ h->fifo_data = NULL;
+ return ret;
+ }
+
+ haptics_fifo_irq_enable(h, true);
+ }
+
+ ret = cfg_write(h, HAP_CFG_SPMI_PLAY_REG,
+ PLAY_EN_BIT | FIELD_PREP(PAT_SRC_MASK, PAT_SRC_FIFO));
+ if (ret) {
+ dev_err(h->dev, "trigger FIFO play failed, ret=%d\n", ret);
+ ptn_write(h, HAP_PTN_FIFO_EMPTY_CFG_REG, 0);
+ /*
+ * HW play never started since this SPMI write itself failed,
+ * and the FIFO empty IRQ has never fired, so disabling the
+ * IRQ synchronously here can't deadlock against 'fifo_lock'.
+ */
+ haptics_fifo_irq_enable(h, false);
+ h->fifo_data = NULL;
+ return ret;
+ }
+
+ return 0;
+}
+
+/*
+ * haptics_fifo_empty_irq: Threaded IRQ handler for the FIFO-empty interrupt.
+ *
+ * While a FIFO play is in progress the hardware fires this interrupt when
+ * the number of samples in the FIFO drops below the programmed threshold.
+ * The handler refills the FIFO from the effect's data buffer. When all
+ * samples have been written the threshold is set to zero. The HW would
+ * stop the play automatically after all of the samples in FIFO memory are
+ * played out.
+ */
+static irqreturn_t haptics_fifo_empty_irq(int irq, void *dev_id)
+{
+ struct qcom_haptics *h = dev_id;
+ u32 sts, to_write;
+ int ret;
+
+ ret = regmap_read(h->regmap,
+ h->cfg_base + HAP_CFG_INT_RT_STS_REG, &sts);
+ if (ret || !(sts & FIFO_EMPTY_BIT))
+ return IRQ_HANDLED;
+
+ guard(mutex)(&h->fifo_lock);
+
+ if (!h->fifo_data)
+ return IRQ_HANDLED;
+
+ /* Refill: write the next chunk */
+ to_write = min_t(u32, h->data_len - h->data_written,
+ h->fifo_len - FIFO_EMPTY_THRESH);
+ ret = haptics_write_fifo_chunk(h, &h->fifo_data[h->data_written], to_write);
+ if (ret) {
+ dev_err(h->dev, "refill FIFO samples failed, ret=%d\n", ret);
+ /*
+ * If data refilling is failed,stop the HW play and disable the
+ * IRQ to prevent the FIFO empty IRQ being fired continuously.
+ */
+ cfg_write(h, HAP_CFG_SPMI_PLAY_REG, 0);
+ disable_irq_nosync(h->fifo_empty_irq);
+ h->irq_enabled = false;
+ return IRQ_HANDLED;
+ }
+
+ h->data_written += to_write;
+
+ /* Disable the interrupt after all the data is queued */
+ if (h->data_written >= h->data_len)
+ ptn_write(h, HAP_PTN_FIFO_EMPTY_CFG_REG, 0);
+
+ return IRQ_HANDLED;
+}
+
+/*
+ * haptics_stop_locked: stop play in HW and put runtime PM
+ * @h: haptics device
+ * @put_noidle: a flag to put noidle
+ *
+ * Stop HW play, clear the FIFO data if the active effect was FIFO-mode, and
+ * put runtime PM into either noidle or autosuspend based on put_noidle flag.
+ *
+ * Must be called with play_lock held.
+ */
+static void haptics_stop_locked(struct qcom_haptics *h, bool put_noidle)
+{
+ cfg_write(h, HAP_CFG_SPMI_PLAY_REG, 0);
+
+ if (h->active_mode == HAPTICS_FIFO) {
+ ptn_write(h, HAP_PTN_FIFO_EMPTY_CFG_REG, 0);
+ haptics_fifo_irq_enable(h, false);
+ scoped_guard(mutex, &h->fifo_lock) {
+ h->fifo_data = NULL;
+ }
+ }
+
+ h->active_effect_id = -1;
+ h->active_mode = HAPTICS_MODE_NONE;
+
+ if (h->pm_ref_held) {
+ if (put_noidle)
+ pm_runtime_put_noidle(h->dev);
+ else
+ pm_runtime_put_autosuspend(h->dev);
+
+ h->pm_ref_held = false;
+ }
+}
+
+/*
+ * haptics_start_locked: start to play an effect
+ * @h: haptics device
+ * @effect_id: the index of the effect
+ *
+ * If an effect is currently active, stop it 1st. Acquires a PM ref
+ * if not already held, and then trigger the play based on the
+ * play mode. Update active_effect_id/active_mode on success.
+ *
+ * Must be called with play_lock held.
+ */
+static int haptics_start_locked(struct qcom_haptics *h, int effect_id)
+{
+ int ret;
+
+ if (h->active_effect_id != -1)
+ haptics_stop_locked(h, false);
+
+ if (!h->pm_ref_held) {
+ ret = pm_runtime_resume_and_get(h->dev);
+ if (ret < 0) {
+ dev_err(h->dev, "failed to resume device: %d\n", ret);
+ return ret;
+ }
+
+ h->pm_ref_held = true;
+ }
+
+ switch (h->effects[effect_id].mode) {
+ case HAPTICS_DIRECT_PLAY:
+ ret = haptics_start_direct_play(h, effect_id);
+ break;
+ case HAPTICS_FIFO:
+ ret = haptics_start_fifo(h, effect_id);
+ break;
+ default:
+ ret = -EINVAL;
+ }
+
+ if (ret) {
+ dev_err(h->dev, "failed to start effect %d: %d\n", effect_id, ret);
+ if (h->pm_ref_held) {
+ pm_runtime_put_autosuspend(h->dev);
+ h->pm_ref_held = false;
+ }
+
+ return ret;
+ }
+
+ h->active_effect_id = effect_id;
+ h->active_mode = h->effects[effect_id].mode;
+
+ return 0;
+}
+
+static void haptics_play_work(struct work_struct *work)
+{
+ struct qcom_haptics *h = container_of(to_delayed_work(work),
+ struct qcom_haptics, play_work);
+ struct haptics_play_req *req = haptics_dequeue_play_req(h);
+ bool rearmed = false;
+ u32 length_us;
+ int ret;
+
+ guard(mutex)(&h->play_lock);
+
+ /* No valid request available */
+ if (req == NULL)
+ return;
+
+ if (req->play) {
+ ret = haptics_start_locked(h, req->effect_id);
+ if (ret) {
+ dev_err(h->dev, "play haptics failed, ret=%d\n", ret);
+ } else {
+ /*
+ * Re-arm the work to stop the play or advance
+ * to next play after the play duration
+ */
+ length_us = h->effects[req->effect_id].length_us;
+ if (length_us) {
+ schedule_delayed_work(&h->play_work,
+ usecs_to_jiffies(length_us));
+ rearmed = true;
+ }
+ }
+ } else {
+ if (req->effect_id != h->active_effect_id)
+ dev_warn(h->dev, "effect %d is not under playing\n", req->effect_id);
+ else
+ haptics_stop_locked(h, false);
+ }
+
+ kfree(req);
+
+ /*
+ * Handle the remaining requests if doesn't need to wait
+ */
+ if (!rearmed && haptics_queue_pending(h))
+ schedule_delayed_work(&h->play_work, 0);
+}
+
+static void haptics_clear_effect(struct qcom_haptics *h,
+ struct qcom_haptics_effect *effect)
+{
+ guard(mutex)(&h->fifo_lock);
+
+ if (h->fifo_data == effect->fifo_data)
+ h->fifo_data = NULL;
+
+ kvfree(effect->fifo_data);
+ effect->fifo_data = NULL;
+ effect->data_len = 0;
+}
+
+/*
+ * haptics_fifo_length_us: Calculate the play duration of a FIFO effect.
+ * @h: haptics device pointer
+ * @rate: FIFO data play rate
+ * @data_len: FFO data length
+ *
+ * Each FIFO sample is played out over one play-rate period, so the total
+ * duration is the number of samples times that period:
+ *
+ * length_us = (clk_base * data_len * multiplier) / divider
+ *
+ * For T_LRA-based rates the period derives from the LRA resonance period
+ * (h->t_lra_us): DIV_2/4/8 shorten it, X_2/4/8 lengthen it. For the
+ * kHz-based rates the period is 1000 us / freq_khz.
+ */
+static u32 haptics_fifo_length_us(struct qcom_haptics *h,
+ enum qcom_haptics_play_rate rate,
+ u32 data_len)
+{
+ u32 clk_base = 1000, multiplier = 1, divider = 1;
+
+ switch (rate) {
+ case PLAY_RATE_T_LRA:
+ clk_base = h->t_lra_us;
+ break;
+ case PLAY_RATE_T_LRA_DIV_2:
+ clk_base = h->t_lra_us;
+ divider = 2;
+ break;
+ case PLAY_RATE_T_LRA_DIV_4:
+ clk_base = h->t_lra_us;
+ divider = 4;
+ break;
+ case PLAY_RATE_T_LRA_DIV_8:
+ clk_base = h->t_lra_us;
+ divider = 8;
+ break;
+ case PLAY_RATE_T_LRA_X_2:
+ clk_base = h->t_lra_us;
+ multiplier = 2;
+ break;
+ case PLAY_RATE_T_LRA_X_4:
+ clk_base = h->t_lra_us;
+ multiplier = 4;
+ break;
+ case PLAY_RATE_T_LRA_X_8:
+ clk_base = h->t_lra_us;
+ multiplier = 8;
+ break;
+ case PLAY_RATE_F_8KHZ:
+ divider = 8;
+ break;
+ case PLAY_RATE_F_16KHZ:
+ divider = 16;
+ break;
+ case PLAY_RATE_F_24KHZ:
+ divider = 24;
+ break;
+ case PLAY_RATE_F_32KHZ:
+ divider = 32;
+ break;
+ case PLAY_RATE_F_44P1KHZ:
+ clk_base *= 10;
+ divider = 441;
+ break;
+ case PLAY_RATE_F_48KHZ:
+ divider = 48;
+ break;
+ default:
+ /* Unexpected rate: fall back to the resonance period. */
+ clk_base = h->t_lra_us;
+ break;
+ }
+
+ return div_u64((u64)clk_base * data_len * multiplier, divider);
+}
+
+static int haptics_upload_effect(struct input_dev *dev,
+ struct ff_effect *effect,
+ struct ff_effect *old)
+{
+ struct qcom_haptics *h = input_get_drvdata(dev);
+ struct qcom_haptics_effect *priv;
+ int id = effect->id;
+ u32 data_len, level;
+ s8 *fifo;
+
+ s16 *buf __free(kvfree) = NULL;
+
+ guard(mutex)(&h->play_lock);
+
+ if (id < 0 || id >= HAPTICS_MAX_EFFECTS)
+ return -EINVAL;
+
+ if (id == h->active_effect_id) {
+ dev_err(h->dev, "effect %d is under playing\n", id);
+ return -EBUSY;
+ }
+
+ priv = &h->effects[id];
+
+ switch (effect->type) {
+ case FF_CONSTANT:
+ haptics_clear_effect(h, priv);
+ level = effect->u.constant.level <= 0 ? 0 : effect->u.constant.level;
+ priv->amplitude = (u8)mult_frac(level, 255, 0x7FFF);
+ priv->length_us = effect->replay.length * USEC_PER_MSEC;
+ priv->mode = HAPTICS_DIRECT_PLAY;
+ return 0;
+
+ case FF_PERIODIC:
+ if (effect->u.periodic.waveform != FF_CUSTOM)
+ return -EINVAL;
+ /*
+ * Minimum 3 elements: play-rate code + vmax + at least one sample.
+ * Limit the maximum data length to ~48K so that it can at least
+ * handle ~1s vibration at the fast (48K) play rate.
+ */
+ if (effect->u.periodic.custom_len < 3 ||
+ effect->u.periodic.custom_len > CUSTOM_DATA_MAX_LEN + 2)
+ return -EINVAL;
+
+ buf = vmemdup_array_user(effect->u.periodic.custom_data,
+ effect->u.periodic.custom_len,
+ sizeof(s16));
+ if (IS_ERR(buf))
+ return PTR_ERR(no_free_ptr(buf));
+
+ if (buf[CUSTOM_DATA_RATE_IDX] < 0 ||
+ buf[CUSTOM_DATA_RATE_IDX] > PLAY_RATE_MAX ||
+ buf[CUSTOM_DATA_RATE_IDX] == PLAY_RATE_RESERVED)
+ return -EINVAL;
+
+ data_len = effect->u.periodic.custom_len - CUSTOM_DATA_SAMPLE_START;
+
+ fifo = kvmalloc(data_len, GFP_KERNEL);
+ if (!fifo)
+ return -ENOMEM;
+
+ /* Pack: one s8 sample per s16 slot (lower byte) */
+ for (int i = 0; i < data_len; i++)
+ fifo[i] = (s8)buf[CUSTOM_DATA_SAMPLE_START + i];
+
+ haptics_clear_effect(h, priv);
+
+ scoped_guard(mutex, &h->fifo_lock) {
+ priv->fifo_data = fifo;
+ priv->data_len = data_len;
+ }
+
+ priv->play_rate = (u8)buf[CUSTOM_DATA_RATE_IDX];
+ priv->vmax_mv = (u32)clamp_val(buf[CUSTOM_DATA_VMAX_IDX], 0, VMAX_MV_MAX);
+ priv->length_us = haptics_fifo_length_us(h, priv->play_rate, data_len);
+ priv->mode = HAPTICS_FIFO;
+
+ return 0;
+
+ default:
+ return -EINVAL;
+ }
+}
+
+static int haptics_playback(struct input_dev *dev, int effect_id, int val)
+{
+ struct qcom_haptics *h = input_get_drvdata(dev);
+ int ret;
+
+ ret = haptics_enqueue_play_req(h, effect_id, val > 0);
+ if (ret)
+ return ret;
+
+ if (val > 0)
+ /*
+ * Queue the play. If a duration re-arm is already pending this
+ * is a no-op, so the new play waits for the current effect to
+ * finish before the worker dequeues it.
+ */
+ schedule_delayed_work(&h->play_work, 0);
+ else
+ /*
+ * Run the worker now, cancelling any pending duration re-arm,
+ * so an explicit stop takes effect immediately.
+ */
+ mod_delayed_work(system_percpu_wq, &h->play_work, 0);
+
+ return 0;
+}
+
+static int haptics_erase(struct input_dev *dev, int effect_id)
+{
+ struct qcom_haptics *h = input_get_drvdata(dev);
+ struct qcom_haptics_effect *priv = &h->effects[effect_id];
+
+ guard(mutex)(&h->play_lock);
+
+ haptics_queue_remove_effect(h, effect_id);
+
+ if (h->active_effect_id == effect_id)
+ haptics_stop_locked(h, false);
+
+ haptics_clear_effect(h, priv);
+ priv->mode = HAPTICS_MODE_NONE;
+
+ return 0;
+}
+
+static void haptics_set_gain(struct input_dev *dev, u16 gain)
+{
+ struct qcom_haptics *h = input_get_drvdata(dev);
+
+ atomic_set(&h->gain, gain);
+}
+
+static int qcom_haptics_probe(struct platform_device *pdev)
+{
+ struct qcom_haptics *h;
+ struct input_dev *input;
+ struct ff_device *ff;
+ u32 regs[2], vmax_uv;
+ int ret, irq;
+
+ h = devm_kzalloc(&pdev->dev, sizeof(*h), GFP_KERNEL);
+ if (!h)
+ return -ENOMEM;
+
+ h->dev = &pdev->dev;
+
+ h->regmap = dev_get_regmap(pdev->dev.parent, NULL);
+ if (!h->regmap)
+ return dev_err_probe(h->dev, -ENODEV, "no regmap from parent\n");
+
+ ret = device_property_read_u32_array(h->dev, "reg", regs, ARRAY_SIZE(regs));
+ if (ret)
+ return dev_err_probe(h->dev, ret, "failed to read 'reg' property\n");
+
+ h->cfg_base = regs[0];
+ h->ptn_base = regs[1];
+
+ ret = device_property_read_u32(h->dev, "qcom,lra-period-us", &h->t_lra_us);
+ if (ret)
+ return dev_err_probe(h->dev, ret, "missing qcom,lra-period-us\n");
+
+ h->t_lra_us = clamp(h->t_lra_us, (u32)TLRA_STEP_US, (u32)TLRA_US_MAX);
+
+ ret = device_property_read_u32(h->dev, "qcom,vmax-microvolt", &vmax_uv);
+ if (ret)
+ return dev_err_probe(h->dev, ret, "missing qcom,vmax-microvolt\n");
+
+ h->vmax_mv = clamp(vmax_uv / 1000, (u32)VMAX_STEP_MV, (u32)VMAX_MV_MAX);
+
+ ret = haptics_config_lra_period(h);
+ if (ret)
+ return ret;
+
+ ret = haptics_configure_autores(h);
+ if (ret)
+ return ret;
+
+ ret = haptics_set_vmax(h, h->vmax_mv);
+ if (ret)
+ return ret;
+
+ ret = haptics_configure_fifo_mmap(h);
+ if (ret)
+ return ret;
+
+ mutex_init(&h->play_lock);
+ mutex_init(&h->fifo_lock);
+ spin_lock_init(&h->play_queue_lock);
+ INIT_LIST_HEAD(&h->play_queue);
+ INIT_DELAYED_WORK(&h->play_work, haptics_play_work);
+ atomic_set(&h->gain, 0xFFFF);
+ h->active_effect_id = -1;
+
+ irq = platform_get_irq_byname(pdev, "fifo-empty");
+ if (irq < 0)
+ return dev_err_probe(h->dev, irq, "failed to get fifo-empty IRQ\n");
+
+ ret = devm_request_threaded_irq(h->dev, irq, NULL,
+ haptics_fifo_empty_irq,
+ IRQF_ONESHOT | IRQF_NO_AUTOEN,
+ "qcom-haptics-fifo-empty", h);
+ if (ret)
+ return dev_err_probe(h->dev, ret, "failed to request fifo-empty IRQ\n");
+
+ h->fifo_empty_irq = irq;
+ platform_set_drvdata(pdev, h);
+
+ pm_runtime_use_autosuspend(h->dev);
+ pm_runtime_set_autosuspend_delay(h->dev, HAPTICS_AUTOSUSPEND_MS);
+ ret = devm_pm_runtime_enable(h->dev);
+ if (ret)
+ return dev_err_probe(h->dev, ret, "enable runtime PM failed\n");
+
+ input = devm_input_allocate_device(h->dev);
+ if (!input)
+ return -ENOMEM;
+
+ input->name = "qcom-spmi-haptics";
+ input_set_drvdata(input, h);
+ h->input = input;
+
+ input_set_capability(input, EV_FF, FF_CONSTANT);
+ input_set_capability(input, EV_FF, FF_PERIODIC);
+ input_set_capability(input, EV_FF, FF_CUSTOM);
+ input_set_capability(input, EV_FF, FF_GAIN);
+
+ ret = input_ff_create(input, HAPTICS_MAX_EFFECTS);
+ if (ret)
+ return ret;
+
+ ff = input->ff;
+ ff->upload = haptics_upload_effect;
+ ff->playback = haptics_playback;
+ ff->erase = haptics_erase;
+ ff->set_gain = haptics_set_gain;
+
+ ret = input_register_device(input);
+ if (ret) {
+ input_ff_destroy(input);
+ return dev_err_probe(h->dev, ret, "failed to register input device\n");
+ }
+
+ return 0;
+}
+
+static void qcom_haptics_remove(struct platform_device *pdev)
+{
+ struct qcom_haptics *h = platform_get_drvdata(pdev);
+
+ /*
+ * Unregister the input device explicitly at the beginning
+ * to avoid the input device being used after the resources
+ * are freed.
+ */
+ input_unregister_device(h->input);
+ disable_delayed_work_sync(&h->play_work);
+ scoped_guard(mutex, &h->play_lock) {
+ haptics_queue_flush(h);
+ haptics_stop_locked(h, true);
+ }
+
+ haptics_enable_module(h, false);
+
+ scoped_guard(mutex, &h->fifo_lock) {
+ for (int i = 0; i < HAPTICS_MAX_EFFECTS; i++) {
+ kvfree(h->effects[i].fifo_data);
+ h->effects[i].fifo_data = NULL;
+ }
+ }
+}
+
+static int qcom_haptics_runtime_suspend(struct device *dev)
+{
+ struct qcom_haptics *h = dev_get_drvdata(dev);
+
+ return haptics_enable_module(h, false);
+}
+
+static int qcom_haptics_runtime_resume(struct device *dev)
+{
+ struct qcom_haptics *h = dev_get_drvdata(dev);
+
+ return haptics_enable_module(h, true);
+}
+
+static int qcom_haptics_suspend(struct device *dev)
+{
+ struct qcom_haptics *h = dev_get_drvdata(dev);
+ int ret;
+
+ disable_delayed_work_sync(&h->play_work);
+ scoped_guard(mutex, &h->play_lock) {
+ haptics_queue_flush(h);
+ haptics_stop_locked(h, true);
+ }
+
+ ret = pm_runtime_force_suspend(dev);
+ if (ret)
+ enable_delayed_work(&h->play_work);
+
+ return ret;
+}
+
+static int qcom_haptics_resume(struct device *dev)
+{
+ struct qcom_haptics *h = dev_get_drvdata(dev);
+
+ enable_delayed_work(&h->play_work);
+
+ return pm_runtime_force_resume(dev);
+}
+
+static const struct dev_pm_ops qcom_haptics_pm_ops = {
+ SYSTEM_SLEEP_PM_OPS(qcom_haptics_suspend, qcom_haptics_resume)
+ RUNTIME_PM_OPS(qcom_haptics_runtime_suspend, qcom_haptics_runtime_resume,
+ NULL)
+};
+
+static const struct of_device_id qcom_haptics_of_match[] = {
+ { .compatible = "qcom,spmi-haptics" },
+ {}
+};
+MODULE_DEVICE_TABLE(of, qcom_haptics_of_match);
+
+static struct platform_driver qcom_haptics_driver = {
+ .probe = qcom_haptics_probe,
+ .remove = qcom_haptics_remove,
+ .driver = {
+ .name = "qcom-spmi-haptics",
+ .of_match_table = qcom_haptics_of_match,
+ .pm = pm_ptr(&qcom_haptics_pm_ops),
+ },
+};
+module_platform_driver(qcom_haptics_driver);
+
+MODULE_DESCRIPTION("Qualcomm SPMI PMIC Haptics driver");
+MODULE_LICENSE("GPL");
--
2.43.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* Re: [PATCH v4 3/3] input: misc: Add Qualcomm SPMI PMIC haptics driver
2026-07-17 7:29 ` [PATCH v4 3/3] input: misc: Add Qualcomm SPMI PMIC haptics driver Fenglin Wu
@ 2026-07-17 7:42 ` sashiko-bot
2026-07-17 16:51 ` Dmitry Torokhov
1 sibling, 0 replies; 8+ messages in thread
From: sashiko-bot @ 2026-07-17 7:42 UTC (permalink / raw)
To: Fenglin Wu; +Cc: conor+dt, devicetree, robh, linux-input, dmitry.torokhov
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] Missing stop logic in the haptics worker causes indefinite playback and PM reference leak when effect duration expires.
- [Medium] Explicit input_unregister_device() on a devm-managed device violates input subsystem rules.
- [Medium] Data race on `irq_enabled` causes permanently unbalanced IRQ disable depth on bus errors.
- [Low] Use of undefined macro `kmalloc_obj` causes compilation failure.
--
--- Patch [3]: [PATCH v4 3/3] input: misc: Add Qualcomm SPMI PMIC haptics driver ---
commit e63f14da39f2a372d48e418f9fac8b20cd9d0698
Author: Fenglin Wu <fenglin.wu@oss.qualcomm.com>
input: misc: Add Qualcomm SPMI PMIC haptics driver
Adds an initial driver for the Qualcomm PMIH0108 PMIC haptics module.
Supports DIRECT_PLAY and FIFO modes via the input force-feedback framework.
> diff --git a/drivers/input/misc/qcom-spmi-haptics.c b/drivers/input/misc/qcom-spmi-haptics.c
> new file mode 100644
> index 0000000000000..235f77e0229c5
> --- /dev/null
> +++ b/drivers/input/misc/qcom-spmi-haptics.c
[ ... ]
> +static int haptics_enqueue_play_req(struct qcom_haptics *h, int effect_id, bool play)
> +{
> + struct haptics_play_req *req;
> +
> + guard(spinlock_irqsave)(&h->play_queue_lock);
> +
> + /*
> + * Coalesce with an already queued request for
> + * the same effect instead of appending.
> + */
> + list_for_each_entry(req, &h->play_queue, node) {
> + if (req->effect_id == effect_id) {
> + req->play = play;
> + return 0;
> + }
> + }
> +
> + req = kmalloc_obj(*req, GFP_ATOMIC);
> + if (!req)
> + return -ENOMEM;
[Severity: Low]
Does this code compile successfully? I couldn't find kmalloc_obj() defined in
the standard kernel headers. Should this use kmalloc() or kzalloc() instead?
> +
> + req->effect_id = effect_id;
> + req->play = play;
[ ... ]
> +static irqreturn_t haptics_fifo_empty_irq(int irq, void *dev_id)
> +{
[ ... ]
> + /* Refill: write the next chunk */
> + to_write = min_t(u32, h->data_len - h->data_written,
> + h->fifo_len - FIFO_EMPTY_THRESH);
> + ret = haptics_write_fifo_chunk(h, &h->fifo_data[h->data_written], to_write);
> + if (ret) {
> + dev_err(h->dev, "refill FIFO samples failed, ret=%d\n", ret);
> + /*
> + * If data refilling is failed,stop the HW play and disable the
> + * IRQ to prevent the FIFO empty IRQ being fired continuously.
> + */
> + cfg_write(h, HAP_CFG_SPMI_PLAY_REG, 0);
> + disable_irq_nosync(h->fifo_empty_irq);
> + h->irq_enabled = false;
[Severity: Medium]
Can a data race occur on h->irq_enabled here?
If userspace stops playback concurrently with a bus write failure in
haptics_fifo_empty_irq(), both paths could check and modify h->irq_enabled
without mutual exclusion.
For example, this path:
haptics_fifo_empty_irq()
disable_irq_nosync(h->fifo_empty_irq);
h->irq_enabled = false;
Could race with:
haptics_stop_locked()
haptics_fifo_irq_enable(h, false);
This might result in disable_irq() being called twice while enable_irq() is
only called once, leaving the IRQ permanently disabled and breaking future
FIFO playback.
> + return IRQ_HANDLED;
> + }
> +
> + h->data_written += to_write;
[ ... ]
> +static void haptics_play_work(struct work_struct *work)
> +{
> + struct qcom_haptics *h = container_of(to_delayed_work(work),
> + struct qcom_haptics, play_work);
> + struct haptics_play_req *req = haptics_dequeue_play_req(h);
> + bool rearmed = false;
> + u32 length_us;
> + int ret;
> +
> + guard(mutex)(&h->play_lock);
> +
> + /* No valid request available */
> + if (req == NULL)
> + return;
[Severity: High]
When a finite-length effect expires, schedule_delayed_work() wakes up this
worker, but haptics_dequeue_play_req() returns NULL since the queue is
empty.
Since we return early here when req is NULL, how does the driver stop the
active effect and release the runtime PM reference? This appears to leave
the device vibrating indefinitely.
> +
> + if (req->play) {
> + ret = haptics_start_locked(h, req->effect_id);
[ ... ]
> +static void qcom_haptics_remove(struct platform_device *pdev)
> +{
> + struct qcom_haptics *h = platform_get_drvdata(pdev);
> +
> + /*
> + * Unregister the input device explicitly at the beginning
> + * to avoid the input device being used after the resources
> + * are freed.
> + */
> + input_unregister_device(h->input);
[Severity: Medium]
Is it safe to explicitly unregister this input device here?
The input device is allocated with devm_input_allocate_device() during
probe, which means the input core will automatically handle unregistration
when the provider device is unbound.
Manually calling input_unregister_device() on a devm-managed device can
lead to double-unregistration.
> + disable_delayed_work_sync(&h->play_work);
> + scoped_guard(mutex, &h->play_lock) {
> + haptics_queue_flush(h);
> + haptics_stop_locked(h, true);
> + }
> +
> + haptics_enable_module(h, false);
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260717-qcom-spmi-haptics-v4-0-b0fe0ed30849@oss.qualcomm.com?part=3
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH v4 0/3] input: misc: Add an initial driver for haptics inside Qcom PMIH010x PMIC
2026-07-17 7:28 [PATCH v4 0/3] input: misc: Add an initial driver for haptics inside Qcom PMIH010x PMIC Fenglin Wu
` (2 preceding siblings ...)
2026-07-17 7:29 ` [PATCH v4 3/3] input: misc: Add Qualcomm SPMI PMIC haptics driver Fenglin Wu
@ 2026-07-17 9:25 ` Krzysztof Kozlowski
2026-07-17 18:52 ` Jeff Johnson
4 siblings, 0 replies; 8+ messages in thread
From: Krzysztof Kozlowski @ 2026-07-17 9:25 UTC (permalink / raw)
To: Fenglin Wu, linux-arm-msm, Dmitry Torokhov, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Lee Jones, Stephen Boyd,
Bjorn Andersson, Konrad Dybcio
Cc: David Collins, Subbaraman Narayanamurthy, Kamal Wadhwa, kernel,
linux-input, devicetree, linux-kernel
On 17/07/2026 09:28, Fenglin Wu wrote:
> Dependencies:
> - [patch 2/3] depends on [patch 1/3] and they should be applied together
>
Why are you cc-ing internal kernel@oss.qualcomm.com?
I already asked you TWICE. Both times you ignored that, so that's it.
Best regards,
Krzysztof
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH v4 3/3] input: misc: Add Qualcomm SPMI PMIC haptics driver
2026-07-17 7:29 ` [PATCH v4 3/3] input: misc: Add Qualcomm SPMI PMIC haptics driver Fenglin Wu
2026-07-17 7:42 ` sashiko-bot
@ 2026-07-17 16:51 ` Dmitry Torokhov
1 sibling, 0 replies; 8+ messages in thread
From: Dmitry Torokhov @ 2026-07-17 16:51 UTC (permalink / raw)
To: Fenglin Wu
Cc: linux-arm-msm, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Lee Jones, Stephen Boyd, Bjorn Andersson, Konrad Dybcio,
David Collins, Subbaraman Narayanamurthy, Kamal Wadhwa, kernel,
linux-input, devicetree, linux-kernel
Hi Fenglin,
On Fri, Jul 17, 2026 at 12:29:01AM -0700, Fenglin Wu wrote:
> Add an initial driver for the Qualcomm PMIH0108 PMIC haptics module,
> named as HAP530_HV. This module supports several play modes, including
> DIRECT_PLAY, FIFO, PAT_MEM, and SWR, each with distinct data sourcing
> and hardware data handling logic. Currently, the driver provides support
> for two play modes using the input force-feedback framework: FF_CONSTANT
> effect for DIRECT_PLAY mode and FF_PERIODIC effect with FF_CUSTOM
> waveform for FIFO mode.
>
> Assisted-by: Claude:claude-4-8-opus
> Signed-off-by: Fenglin Wu <fenglin.wu@oss.qualcomm.com>
> ---
> drivers/input/misc/Kconfig | 11 +
> drivers/input/misc/Makefile | 1 +
> drivers/input/misc/qcom-spmi-haptics.c | 1178 ++++++++++++++++++++++++++++++++
> 3 files changed, 1190 insertions(+)
>
> diff --git a/drivers/input/misc/Kconfig b/drivers/input/misc/Kconfig
> index 1f6c57dba030..4f40940973e4 100644
> --- a/drivers/input/misc/Kconfig
> +++ b/drivers/input/misc/Kconfig
> @@ -236,6 +236,17 @@ config INPUT_PMIC8XXX_PWRKEY
> To compile this driver as a module, choose M here: the
> module will be called pmic8xxx-pwrkey.
>
> +config INPUT_QCOM_SPMI_HAPTICS
> + tristate "Qualcomm SPMI PMIC haptics support"
> + depends on MFD_SPMI_PMIC
> + help
> + Say Y to enable support for the Qualcomm PMIH0108 SPMI PMIC haptics
> + module. Supports DIRECT_PLAY, FIFO streaming play modes via the
> + Linux input force-feedback framework.
> +
> + To compile this driver as a module, choose M here: the module will
> + be called qcom-spmi-haptics.
> +
> config INPUT_SPARCSPKR
> tristate "SPARC Speaker support"
> depends on PCI && SPARC64
> diff --git a/drivers/input/misc/Makefile b/drivers/input/misc/Makefile
> index 2281d6803fce..c5c9aa139a11 100644
> --- a/drivers/input/misc/Makefile
> +++ b/drivers/input/misc/Makefile
> @@ -69,6 +69,7 @@ obj-$(CONFIG_INPUT_PMIC8XXX_PWRKEY) += pmic8xxx-pwrkey.o
> obj-$(CONFIG_INPUT_POWERMATE) += powermate.o
> obj-$(CONFIG_INPUT_PWM_BEEPER) += pwm-beeper.o
> obj-$(CONFIG_INPUT_PWM_VIBRA) += pwm-vibra.o
> +obj-$(CONFIG_INPUT_QCOM_SPMI_HAPTICS) += qcom-spmi-haptics.o
> obj-$(CONFIG_INPUT_QNAP_MCU) += qnap-mcu-input.o
> obj-$(CONFIG_INPUT_RAVE_SP_PWRBUTTON) += rave-sp-pwrbutton.o
> obj-$(CONFIG_INPUT_RB532_BUTTON) += rb532_button.o
> diff --git a/drivers/input/misc/qcom-spmi-haptics.c b/drivers/input/misc/qcom-spmi-haptics.c
> new file mode 100644
> index 000000000000..235f77e0229c
> --- /dev/null
> +++ b/drivers/input/misc/qcom-spmi-haptics.c
> @@ -0,0 +1,1178 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
> + */
> +
> +#include <linux/bitfield.h>
> +#include <linux/bits.h>
> +#include <linux/device.h>
> +#include <linux/input.h>
> +#include <linux/interrupt.h>
> +#include <linux/list.h>
> +#include <linux/math64.h>
> +#include <linux/module.h>
> +#include <linux/mutex.h>
> +#include <linux/of.h>
> +#include <linux/platform_device.h>
> +#include <linux/regmap.h>
> +#include <linux/pm_runtime.h>
> +#include <linux/slab.h>
> +#include <linux/spinlock.h>
> +#include <linux/uaccess.h>
> +#include <linux/workqueue.h>
> +
> +/* HAP_CFG register offsets, bit fields, value constants */
> +#define HAP_CFG_INT_RT_STS_REG 0x10
> +#define FIFO_EMPTY_BIT BIT(1)
> +#define HAP_CFG_EN_CTL_REG 0x46
> +#define HAPTICS_EN_BIT BIT(7)
> +#define HAP_CFG_VMAX_REG 0x48
> +#define VMAX_STEP_MV 50
> +#define VMAX_MV_MAX 10000
> +#define HAP_CFG_SPMI_PLAY_REG 0x4C
> +#define PLAY_EN_BIT BIT(7)
> +#define PAT_SRC_MASK GENMASK(2, 0)
> +#define PAT_SRC_FIFO 0
> +#define PAT_SRC_DIRECT_PLAY 1
> +#define HAP_CFG_TLRA_OL_HIGH_REG 0x5C
> +#define TLRA_OL_MSB_MASK GENMASK(3, 0)
> +#define TLRA_STEP_US 5
> +#define TLRA_US_MAX 20475
> +#define HAP_CFG_TLRA_OL_LOW_REG 0x5D
> +#define HAP_CFG_DRV_DUTY_CFG_REG 0x60
> +#define ADT_DRV_DUTY_EN_BIT BIT(7)
> +#define ADT_BRK_DUTY_EN_BIT BIT(6)
> +#define DRV_DUTY_MASK GENMASK(5, 3)
> +#define AUTORES_DRV_DUTY_62P5 2
> +#define BRK_DUTY_MASK GENMASK(2, 0)
> +#define AUTORES_BRK_DUTY_62P5 5
> +#define HAP_CFG_ZX_WIND_CFG_REG 0x62
> +#define ZX_DEBOUNCE_MASK GENMASK(6, 4)
> +#define AUTORES_ZX_DEBOUNCE 3
> +#define ZX_WIN_HEIGHT_MASK GENMASK(2, 0)
> +#define AUTORES_ZX_WIN_HEIGHT 2
> +#define HAP_CFG_AUTORES_CFG_REG 0x63
> +#define AUTORES_EN_BIT BIT(7)
> +#define AUTORES_EN_DLY_MASK GENMASK(6, 2)
> +#define AUTORES_EN_DLY_CYCLES 10
> +#define AUTORES_ERR_WIN_MASK GENMASK(1, 0)
> +#define AUTORES_ERR_WIN_25PCT 1
> +#define HAP_CFG_FAULT_CLR_REG 0x66
> +#define ZX_TO_FAULT_CLR_BIT BIT(4)
> +#define SC_CLR_BIT BIT(2)
> +#define AUTO_RES_ERR_CLR_BIT BIT(1)
> +#define HPWR_RDY_FAULT_CLR_BIT BIT(0)
> +#define FAULT_CLR_ALL (ZX_TO_FAULT_CLR_BIT | SC_CLR_BIT | \
> + AUTO_RES_ERR_CLR_BIT | HPWR_RDY_FAULT_CLR_BIT)
> +#define HAP_CFG_RAMP_DN_CFG2_REG 0x86
> +#define AUTORES_PRE_HIZ_DLY_10US 1
> +
> +/* HAP_PTN register offsets, bit fields, value constants */
> +#define HAP_PTN_REVISION2_REG 0x01
> +#define HAP_PTN_FIFO_DIN_0_REG 0x20
> +#define HAP_PTN_FIFO_PLAY_RATE_REG 0x24
> +#define FIFO_PLAY_RATE_MASK GENMASK(3, 0)
> +#define HAP_PTN_DIRECT_PLAY_REG 0x26
> +#define HAP_PTN_FIFO_EMPTY_CFG_REG 0x2A
> +#define FIFO_THRESH_LSB 64
> +#define HAP_PTN_FIFO_DIN_1B_REG 0x2C
> +#define HAP_PTN_MEM_OP_ACCESS_REG 0x2D
> +#define MEM_FLUSH_RELOAD_BIT BIT(0)
> +#define HAP_PTN_MMAP_FIFO_REG 0xA0
> +#define MMAP_FIFO_EXIST_BIT BIT(7)
> +#define MMAP_FIFO_LEN_MASK GENMASK(6, 0)
> +#define HAP_PTN_PATX_PLAY_CFG_REG 0xA2
> +
> +#define HAP530_MEM_TOTAL_BYTES 8192
> +#define FIFO_EMPTY_THRESH 280
> +#define FIFO_INIT_FILL 320
> +
> +#define HAPTICS_AUTOSUSPEND_MS 1000
> +
> +/*
> + * FF_CUSTOM data layout (custom_data[] of type s16):
> + * [0] = play rate (PLAY_RATE_*)
> + * [1] = vmax in mV (0 = use device default from qcom,vmax-microvolt)
> + * [2..N-1] = signed 8-bit PCM samples packed one per s16 (lower byte used)
> + */
> +#define CUSTOM_DATA_RATE_IDX 0
> +#define CUSTOM_DATA_VMAX_IDX 1
> +#define CUSTOM_DATA_SAMPLE_START 2
> +#define CUSTOM_DATA_MAX_LEN (48 * 1024)
> +
> +#define HAPTICS_MAX_EFFECTS 8
> +
> +enum qcom_haptics_mode {
> + HAPTICS_MODE_NONE,
> + HAPTICS_DIRECT_PLAY,
> + HAPTICS_FIFO,
> +};
> +
> +enum qcom_haptics_play_rate {
> + PLAY_RATE_T_LRA = 0,
> + PLAY_RATE_T_LRA_DIV_2 = 1,
> + PLAY_RATE_T_LRA_DIV_4 = 2,
> + PLAY_RATE_T_LRA_DIV_8 = 3,
> + PLAY_RATE_T_LRA_X_2 = 4,
> + PLAY_RATE_T_LRA_X_4 = 5,
> + PLAY_RATE_T_LRA_X_8 = 6,
> + PLAY_RATE_RESERVED = 7,
> + PLAY_RATE_F_8KHZ = 8,
> + PLAY_RATE_F_16KHZ = 9,
> + PLAY_RATE_F_24KHZ = 10,
> + PLAY_RATE_F_32KHZ = 11,
> + PLAY_RATE_F_44P1KHZ = 12,
> + PLAY_RATE_F_48KHZ = 13,
> + PLAY_RATE_MAX = PLAY_RATE_F_48KHZ,
> +};
> +
> +/**
> + * struct qcom_haptics_effect: A haptics effect
> + * @mode: haptics HW play mode
> + * @vmax_mv: peak voltage of the haptics output waveform
> + * @length_us: vibration play duration
> + * @amplitude: DIRECT_PLAY mode output waveform amplitude
> + * @play_rate: FIFO mode play rate
> + * @fifo_data: 8-bit data samples consumed in FIFO mode
> + * @data_len: length of the FIFO data samples
> + */
> +struct qcom_haptics_effect {
> + enum qcom_haptics_mode mode;
> + u32 vmax_mv;
> + u32 length_us;
> +
> + u8 amplitude;
> +
> + enum qcom_haptics_play_rate play_rate;
> + s8 *fifo_data;
> + u32 data_len;
> +};
> +
> +/**
> + * struct haptics_play_req: A haptics play request
> + * @node: list node of the request
> + * @effect_id: effect index of the request
> + * @play: flag of starting or stopping the play
> + */
> +struct haptics_play_req {
> + struct list_head node;
> + int effect_id;
> + bool play;
> +};
> +
> +/**
> + * struct qcom_haptics
> + * @dev: underlying SPMI device
> + * @regmap: regmap for SPMI register access
> + * @input: input device exposing the FF interface
> + * @cfg_base: base address of the CFG peripheral
> + * @ptn_base: base address of the PTN peripheral
> + * @t_lra_us: LRA resonance period in microseconds
> + * @vmax_mv: maximum actuator drive voltage in millivolts
> + * @fifo_len: programmed HW FIFO depth in bytes
> + * @gain: playback gain scaler
> + * @play_work: delayed work that plays the queued requests
> + * @play_lock: mutex lock to serialize playbacks
> + * @play_queue_lock: spinlock protecting @play_queue
> + * @play_queue: The list of pending start/stop requests
> + * @active_effect_id: index into @effects[] currently under play
> + * @active_mode: mode of the effect currently armed in hardware
> + * @fifo_empty_irq: IRQ number for the FIFO-empty interrupt
> + * @pm_ref_held: true while a pm_runtime_get is held
> + * @irq_enabled: true if fifo_empty_irq is enabled
> + * @fifo_lock: mutex protecting the FIFO streaming data
> + * @fifo_data: pointer of the data buffer for FIFO streaming
> + * @data_len: length of the data buffer for current effect
> + * @data_written: number of samples written to the hardware FIFO
> + * @effects: table of the effects
> + */
> +struct qcom_haptics {
> + struct device *dev;
> + struct regmap *regmap;
> + struct input_dev *input;
> +
> + u32 cfg_base;
> + u32 ptn_base;
> + u32 t_lra_us;
> + u32 vmax_mv;
> + u32 fifo_len;
> + atomic_t gain;
> +
> + struct delayed_work play_work;
> + struct mutex play_lock; /* mutex used to serialize playbacks */
> +
> + spinlock_t play_queue_lock; /* protects play_queue */
> + struct list_head play_queue;
> +
> + int active_effect_id;
> + enum qcom_haptics_mode active_mode;
> +
> + int fifo_empty_irq;
> + bool pm_ref_held;
> + bool irq_enabled;
> +
> + struct mutex fifo_lock; /* protect the FIFO data during play */
> + const s8 *fifo_data;
> + u32 data_len;
> + u32 data_written;
> +
> + struct qcom_haptics_effect effects[HAPTICS_MAX_EFFECTS];
> +};
> +
> +static int cfg_write(struct qcom_haptics *h, u32 off, u32 val)
> +{
> + return regmap_write(h->regmap, h->cfg_base + off, val);
> +}
> +
> +static int cfg_update_bits(struct qcom_haptics *h, u32 off, u32 mask, u32 val)
> +{
> + return regmap_update_bits(h->regmap, h->cfg_base + off, mask, val);
> +}
> +
> +static int ptn_write(struct qcom_haptics *h, u32 off, u32 val)
> +{
> + return regmap_write(h->regmap, h->ptn_base + off, val);
> +}
> +
> +static int ptn_update_bits(struct qcom_haptics *h, u32 off, u32 mask, u32 val)
> +{
> + return regmap_update_bits(h->regmap, h->ptn_base + off, mask, val);
> +}
> +
> +static int ptn_bulk_write(struct qcom_haptics *h, u32 off,
> + const void *buf, size_t count)
> +{
> + return regmap_bulk_write(h->regmap, h->ptn_base + off, buf, count);
> +}
> +
> +static int haptics_clear_faults(struct qcom_haptics *h)
> +{
> + return cfg_write(h, HAP_CFG_FAULT_CLR_REG, FAULT_CLR_ALL);
> +}
> +
> +static int haptics_set_vmax(struct qcom_haptics *h, u32 vmax_mv)
> +{
> + return cfg_write(h, HAP_CFG_VMAX_REG, vmax_mv / VMAX_STEP_MV);
> +}
> +
> +static int haptics_config_lra_period(struct qcom_haptics *h)
> +{
> + u32 tmp = h->t_lra_us / TLRA_STEP_US;
> + int ret;
Please use "error" or "err" for such variables that hold error codes.
> +
> + ret = cfg_write(h, HAP_CFG_TLRA_OL_HIGH_REG, (tmp >> 8) & TLRA_OL_MSB_MASK);
> + if (ret)
> + return ret;
> +
> + return cfg_write(h, HAP_CFG_TLRA_OL_LOW_REG, tmp & 0xFF);
Explicit error handling and "return 0;" in cases where there are
multiple failure points.
> +}
> +
> +static int haptics_enable_module(struct qcom_haptics *h, bool enable)
> +{
> + return cfg_update_bits(h, HAP_CFG_EN_CTL_REG, HAPTICS_EN_BIT,
> + enable ? HAPTICS_EN_BIT : 0);
> +}
> +
> +static int haptics_configure_autores(struct qcom_haptics *h)
> +{
> + int ret;
> +
> + /* AUTORES_CFG: enable, 10-cycle delay, 25% error window */
> + ret = cfg_write(h, HAP_CFG_AUTORES_CFG_REG,
> + AUTORES_EN_BIT |
> + FIELD_PREP(AUTORES_EN_DLY_MASK, AUTORES_EN_DLY_CYCLES) |
> + FIELD_PREP(AUTORES_ERR_WIN_MASK, AUTORES_ERR_WIN_25PCT));
> + if (ret)
> + return ret;
> +
> + /* DRV_DUTY: adaptive drive/brake duty cycles at 62.5% */
> + ret = cfg_write(h, HAP_CFG_DRV_DUTY_CFG_REG,
> + ADT_DRV_DUTY_EN_BIT | ADT_BRK_DUTY_EN_BIT |
> + FIELD_PREP(DRV_DUTY_MASK, AUTORES_DRV_DUTY_62P5) |
> + FIELD_PREP(BRK_DUTY_MASK, AUTORES_BRK_DUTY_62P5));
> + if (ret)
> + return ret;
> +
> + /* Pre-HIZ delay: 10 µs */
> + ret = cfg_write(h, HAP_CFG_RAMP_DN_CFG2_REG, AUTORES_PRE_HIZ_DLY_10US);
> + if (ret)
> + return ret;
> +
> + /* Zero-cross window: debounce 3, no hysteresis, height 2 */
> + return cfg_write(h, HAP_CFG_ZX_WIND_CFG_REG,
> + FIELD_PREP(ZX_DEBOUNCE_MASK, AUTORES_ZX_DEBOUNCE) |
> + FIELD_PREP(ZX_WIN_HEIGHT_MASK, AUTORES_ZX_WIN_HEIGHT));
> +}
> +
> +static int haptics_write_fifo_chunk(struct qcom_haptics *h,
> + const s8 *data, u32 len)
> +{
> + u32 bulk_len = ALIGN_DOWN(len, 4);
> + int i, ret;
> +
> + /*
> + * FIFO data writing supports both 4-byte bulk writes using registers
> + * [HAP_PTN_FIFO_DIN_0_REG ... HAP_PTN_FIFO_DIN_3_REG], and 1-byte writes
> + * using the HAP_PTN_FIFO_DIN_1B_REG register. The 4-byte bulk write is more
> + * efficient, so use 4-byte writes for the initial 4-byte aligned data,
> + * and 1-byte writes for any trailing remainder.
> + */
> + for (i = 0; i < bulk_len; i += 4) {
> + ret = ptn_bulk_write(h, HAP_PTN_FIFO_DIN_0_REG, &data[i], 4);
> + if (ret)
> + return ret;
> + }
> +
> + for (; i < len; i++) {
> + ret = ptn_write(h, HAP_PTN_FIFO_DIN_1B_REG, (u8)data[i]);
> + if (ret)
> + return ret;
> + }
> +
> + return 0;
> +}
> +
> +static int haptics_configure_fifo_mmap(struct qcom_haptics *h)
> +{
> + u32 fifo_len, fifo_units;
> +
> + /* Config all memory space for FIFO usage for now */
> + fifo_len = HAP530_MEM_TOTAL_BYTES;
> + fifo_len = ALIGN_DOWN(fifo_len, 64);
> + fifo_units = fifo_len / 64;
> + h->fifo_len = fifo_len;
> +
> + return ptn_write(h, HAP_PTN_MMAP_FIFO_REG,
> + MMAP_FIFO_EXIST_BIT |
> + FIELD_PREP(MMAP_FIFO_LEN_MASK, fifo_units - 1));
> +}
> +
> +static u32 haptics_gain_scaled_vmax(struct qcom_haptics *h, u32 vmax_mv)
> +{
> + u16 gain = atomic_read(&h->gain);
> + u32 v = mult_frac(vmax_mv, gain, 0xFFFF);
> +
> + return max_t(u32, v, VMAX_STEP_MV);
> +}
> +
> +static void haptics_fifo_irq_enable(struct qcom_haptics *h, bool enable)
> +{
> + if (h->irq_enabled == enable)
> + return;
Should t you know if given code runs with interrupts disabled or
enabled? I believe this tracking and the wrapper should be removed.
> +
> + if (enable)
> + enable_irq(h->fifo_empty_irq);
> + else
> + disable_irq(h->fifo_empty_irq);
> +
> + h->irq_enabled = enable;
> +}
> +
> +static int haptics_enqueue_play_req(struct qcom_haptics *h, int effect_id, bool play)
> +{
> + struct haptics_play_req *req;
> +
> + guard(spinlock_irqsave)(&h->play_queue_lock);
> +
> + /*
> + * Coalesce with an already queued request for
> + * the same effect instead of appending.
> + */
> + list_for_each_entry(req, &h->play_queue, node) {
> + if (req->effect_id == effect_id) {
> + req->play = play;
> + return 0;
> + }
> + }
> +
> + req = kmalloc_obj(*req, GFP_ATOMIC);
> + if (!req)
> + return -ENOMEM;
> +
> + req->effect_id = effect_id;
> + req->play = play;
> +
> + list_add_tail(&req->node, &h->play_queue);
> +
> + return 0;
> +}
> +
> +static struct haptics_play_req *haptics_dequeue_play_req(struct qcom_haptics *h)
> +{
> + struct haptics_play_req *req = NULL;
> +
> + guard(spinlock_irqsave)(&h->play_queue_lock);
> +
> + if (!list_empty(&h->play_queue)) {
> + req = list_first_entry(&h->play_queue, struct haptics_play_req, node);
> + list_del(&req->node);
> + }
> +
> + return req;
> +}
> +
> +static bool haptics_queue_pending(struct qcom_haptics *h)
> +{
> + guard(spinlock_irqsave)(&h->play_queue_lock);
> +
> + return !list_empty(&h->play_queue);
I strongly dislike this kind of functions since their result is
obsolete the moment they return. Sometimes it is OK, but usually it
isn't. In this driver I think it is fine, but I would much rather inline
this into the caller and added comment. We can also schedule work
holding this spinlock I think...
> +}
> +
> +static void haptics_queue_remove_effect(struct qcom_haptics *h, int effect_id)
> +{
> + struct haptics_play_req *req, *tmp;
> +
> + guard(spinlock_irqsave)(&h->play_queue_lock);
> +
> + list_for_each_entry_safe(req, tmp, &h->play_queue, node) {
> + if (req->effect_id == effect_id) {
> + list_del(&req->node);
> + kfree(req);
> + }
> + }
> +}
> +
> +static void haptics_queue_flush(struct qcom_haptics *h)
> +{
> + struct haptics_play_req *req, *tmp;
> +
> + guard(spinlock_irqsave)(&h->play_queue_lock);
> +
> + list_for_each_entry_safe(req, tmp, &h->play_queue, node) {
> + list_del(&req->node);
> + kfree(req);
> + }
> +}
> +
> +static int haptics_start_direct_play(struct qcom_haptics *h, int effect_id)
> +{
> + struct qcom_haptics_effect *eff = &h->effects[effect_id];
> + int ret;
> +
> + ret = haptics_clear_faults(h);
> + if (ret)
> + return ret;
> +
> + /* Enable auto-resonance for DIRECT_PLAY mode */
> + ret = cfg_update_bits(h, HAP_CFG_AUTORES_CFG_REG,
> + AUTORES_EN_BIT, AUTORES_EN_BIT);
> + if (ret)
> + return ret;
> +
> + ret = haptics_set_vmax(h, haptics_gain_scaled_vmax(h, h->vmax_mv));
> + if (ret)
> + return ret;
> +
> + ret = ptn_write(h, HAP_PTN_DIRECT_PLAY_REG, eff->amplitude);
> + if (ret)
> + return ret;
> +
> + return cfg_write(h, HAP_CFG_SPMI_PLAY_REG,
> + PLAY_EN_BIT | FIELD_PREP(PAT_SRC_MASK, PAT_SRC_DIRECT_PLAY));
> +}
> +
> +static int haptics_start_fifo(struct qcom_haptics *h, int effect_id)
> +{
> + struct qcom_haptics_effect *eff = &h->effects[effect_id];
> + u32 vmax = eff->vmax_mv ? eff->vmax_mv : h->vmax_mv;
> + u32 init_len;
> + bool data_done;
> + int ret;
> +
> + if (!eff->fifo_data || !eff->data_len)
> + return -EINVAL;
> +
> + ret = haptics_clear_faults(h);
> + if (ret)
> + return ret;
> +
> + /* Disable auto-resonance for FIFO mode */
> + ret = cfg_update_bits(h, HAP_CFG_AUTORES_CFG_REG, AUTORES_EN_BIT, 0);
> + if (ret)
> + return ret;
> +
> + ret = haptics_set_vmax(h, haptics_gain_scaled_vmax(h, vmax));
> + if (ret)
> + return ret;
> +
> + ret = ptn_update_bits(h, HAP_PTN_FIFO_PLAY_RATE_REG,
> + FIFO_PLAY_RATE_MASK,
> + FIELD_PREP(FIFO_PLAY_RATE_MASK, eff->play_rate));
> + if (ret)
> + return ret;
> +
> + /* Flush FIFO before loading new data */
> + ret = ptn_write(h, HAP_PTN_MEM_OP_ACCESS_REG, MEM_FLUSH_RELOAD_BIT);
> + if (ret)
> + return ret;
> + ret = ptn_write(h, HAP_PTN_MEM_OP_ACCESS_REG, 0);
> + if (ret)
> + return ret;
> +
> + guard(mutex)(&h->fifo_lock);
> +
> + /* Write the initial chunk and initialise streaming state */
> + init_len = min_t(u32, eff->data_len, FIFO_INIT_FILL);
> + ret = haptics_write_fifo_chunk(h, eff->fifo_data, init_len);
> + if (ret)
> + return ret;
> +
> + h->fifo_data = eff->fifo_data;
> + h->data_len = eff->data_len;
> + h->data_written = init_len;
> +
> + /*
> + * Set empty threshold. When threshold > 0 the hardware fires the
> + * FIFO-empty interrupt when occupancy drops below the threshold,
> + * allowing the driver to refill.
> + */
> + data_done = (h->data_written >= h->data_len);
> + if (!data_done) {
> + ret = ptn_write(h, HAP_PTN_FIFO_EMPTY_CFG_REG,
> + FIFO_EMPTY_THRESH / FIFO_THRESH_LSB);
> + if (ret) {
> + dev_err(h->dev, "set FIFO empty threshold failed, ret=%d\n", ret);
> + h->fifo_data = NULL;
> + return ret;
> + }
> +
> + haptics_fifo_irq_enable(h, true);
> + }
> +
> + ret = cfg_write(h, HAP_CFG_SPMI_PLAY_REG,
> + PLAY_EN_BIT | FIELD_PREP(PAT_SRC_MASK, PAT_SRC_FIFO));
> + if (ret) {
> + dev_err(h->dev, "trigger FIFO play failed, ret=%d\n", ret);
> + ptn_write(h, HAP_PTN_FIFO_EMPTY_CFG_REG, 0);
> + /*
> + * HW play never started since this SPMI write itself failed,
> + * and the FIFO empty IRQ has never fired, so disabling the
> + * IRQ synchronously here can't deadlock against 'fifo_lock'.
> + */
> + haptics_fifo_irq_enable(h, false);
> + h->fifo_data = NULL;
> + return ret;
> + }
> +
> + return 0;
> +}
> +
> +/*
> + * haptics_fifo_empty_irq: Threaded IRQ handler for the FIFO-empty interrupt.
> + *
> + * While a FIFO play is in progress the hardware fires this interrupt when
> + * the number of samples in the FIFO drops below the programmed threshold.
> + * The handler refills the FIFO from the effect's data buffer. When all
> + * samples have been written the threshold is set to zero. The HW would
> + * stop the play automatically after all of the samples in FIFO memory are
> + * played out.
> + */
> +static irqreturn_t haptics_fifo_empty_irq(int irq, void *dev_id)
> +{
> + struct qcom_haptics *h = dev_id;
> + u32 sts, to_write;
> + int ret;
> +
> + ret = regmap_read(h->regmap,
> + h->cfg_base + HAP_CFG_INT_RT_STS_REG, &sts);
> + if (ret || !(sts & FIFO_EMPTY_BIT))
> + return IRQ_HANDLED;
> +
> + guard(mutex)(&h->fifo_lock);
> +
> + if (!h->fifo_data)
> + return IRQ_HANDLED;
> +
> + /* Refill: write the next chunk */
> + to_write = min_t(u32, h->data_len - h->data_written,
> + h->fifo_len - FIFO_EMPTY_THRESH);
> + ret = haptics_write_fifo_chunk(h, &h->fifo_data[h->data_written], to_write);
> + if (ret) {
> + dev_err(h->dev, "refill FIFO samples failed, ret=%d\n", ret);
> + /*
> + * If data refilling is failed,stop the HW play and disable the
> + * IRQ to prevent the FIFO empty IRQ being fired continuously.
> + */
Is recovery possible after this?
> + cfg_write(h, HAP_CFG_SPMI_PLAY_REG, 0);
> + disable_irq_nosync(h->fifo_empty_irq);
> + h->irq_enabled = false;
> + return IRQ_HANDLED;
> + }
> +
> + h->data_written += to_write;
> +
> + /* Disable the interrupt after all the data is queued */
> + if (h->data_written >= h->data_len)
> + ptn_write(h, HAP_PTN_FIFO_EMPTY_CFG_REG, 0);
> +
> + return IRQ_HANDLED;
> +}
> +
> +/*
> + * haptics_stop_locked: stop play in HW and put runtime PM
> + * @h: haptics device
> + * @put_noidle: a flag to put noidle
> + *
> + * Stop HW play, clear the FIFO data if the active effect was FIFO-mode, and
> + * put runtime PM into either noidle or autosuspend based on put_noidle flag.
> + *
> + * Must be called with play_lock held.
Maybe add lockdep_assert_held() instead of comments/naming?
> + */
> +static void haptics_stop_locked(struct qcom_haptics *h, bool put_noidle)
> +{
> + cfg_write(h, HAP_CFG_SPMI_PLAY_REG, 0);
> +
> + if (h->active_mode == HAPTICS_FIFO) {
> + ptn_write(h, HAP_PTN_FIFO_EMPTY_CFG_REG, 0);
> + haptics_fifo_irq_enable(h, false);
> + scoped_guard(mutex, &h->fifo_lock) {
> + h->fifo_data = NULL;
> + }
> + }
> +
> + h->active_effect_id = -1;
> + h->active_mode = HAPTICS_MODE_NONE;
> +
> + if (h->pm_ref_held) {
> + if (put_noidle)
> + pm_runtime_put_noidle(h->dev);
> + else
> + pm_runtime_put_autosuspend(h->dev);
> +
> + h->pm_ref_held = false;
> + }
> +}
> +
> +/*
> + * haptics_start_locked: start to play an effect
> + * @h: haptics device
> + * @effect_id: the index of the effect
> + *
> + * If an effect is currently active, stop it 1st. Acquires a PM ref
> + * if not already held, and then trigger the play based on the
> + * play mode. Update active_effect_id/active_mode on success.
> + *
> + * Must be called with play_lock held.
> + */
> +static int haptics_start_locked(struct qcom_haptics *h, int effect_id)
> +{
> + int ret;
> +
> + if (h->active_effect_id != -1)
> + haptics_stop_locked(h, false);
> +
> + if (!h->pm_ref_held) {
> + ret = pm_runtime_resume_and_get(h->dev);
> + if (ret < 0) {
> + dev_err(h->dev, "failed to resume device: %d\n", ret);
> + return ret;
> + }
> +
> + h->pm_ref_held = true;
> + }
> +
> + switch (h->effects[effect_id].mode) {
> + case HAPTICS_DIRECT_PLAY:
> + ret = haptics_start_direct_play(h, effect_id);
> + break;
> + case HAPTICS_FIFO:
> + ret = haptics_start_fifo(h, effect_id);
> + break;
> + default:
> + ret = -EINVAL;
> + }
> +
> + if (ret) {
> + dev_err(h->dev, "failed to start effect %d: %d\n", effect_id, ret);
> + if (h->pm_ref_held) {
> + pm_runtime_put_autosuspend(h->dev);
> + h->pm_ref_held = false;
> + }
> +
> + return ret;
> + }
> +
> + h->active_effect_id = effect_id;
> + h->active_mode = h->effects[effect_id].mode;
> +
> + return 0;
> +}
> +
> +static void haptics_play_work(struct work_struct *work)
> +{
> + struct qcom_haptics *h = container_of(to_delayed_work(work),
> + struct qcom_haptics, play_work);
> + struct haptics_play_req *req = haptics_dequeue_play_req(h);
> + bool rearmed = false;
> + u32 length_us;
> + int ret;
> +
> + guard(mutex)(&h->play_lock);
> +
> + /* No valid request available */
> + if (req == NULL)
if (!req)
> + return;
> +
> + if (req->play) {
> + ret = haptics_start_locked(h, req->effect_id);
> + if (ret) {
> + dev_err(h->dev, "play haptics failed, ret=%d\n", ret);
> + } else {
> + /*
> + * Re-arm the work to stop the play or advance
> + * to next play after the play duration
> + */
> + length_us = h->effects[req->effect_id].length_us;
> + if (length_us) {
> + schedule_delayed_work(&h->play_work,
> + usecs_to_jiffies(length_us));
> + rearmed = true;
> + }
> + }
> + } else {
> + if (req->effect_id != h->active_effect_id)
> + dev_warn(h->dev, "effect %d is not under playing\n", req->effect_id);
> + else
> + haptics_stop_locked(h, false);
> + }
> +
> + kfree(req);
> +
> + /*
> + * Handle the remaining requests if doesn't need to wait
> + */
> + if (!rearmed && haptics_queue_pending(h))
> + schedule_delayed_work(&h->play_work, 0);
> +}
> +
> +static void haptics_clear_effect(struct qcom_haptics *h,
> + struct qcom_haptics_effect *effect)
> +{
> + guard(mutex)(&h->fifo_lock);
> +
> + if (h->fifo_data == effect->fifo_data)
> + h->fifo_data = NULL;
> +
> + kvfree(effect->fifo_data);
> + effect->fifo_data = NULL;
> + effect->data_len = 0;
> +}
> +
> +/*
> + * haptics_fifo_length_us: Calculate the play duration of a FIFO effect.
> + * @h: haptics device pointer
> + * @rate: FIFO data play rate
> + * @data_len: FFO data length
> + *
> + * Each FIFO sample is played out over one play-rate period, so the total
> + * duration is the number of samples times that period:
> + *
> + * length_us = (clk_base * data_len * multiplier) / divider
> + *
> + * For T_LRA-based rates the period derives from the LRA resonance period
> + * (h->t_lra_us): DIV_2/4/8 shorten it, X_2/4/8 lengthen it. For the
> + * kHz-based rates the period is 1000 us / freq_khz.
> + */
> +static u32 haptics_fifo_length_us(struct qcom_haptics *h,
> + enum qcom_haptics_play_rate rate,
> + u32 data_len)
> +{
> + u32 clk_base = 1000, multiplier = 1, divider = 1;
> +
> + switch (rate) {
> + case PLAY_RATE_T_LRA:
> + clk_base = h->t_lra_us;
> + break;
> + case PLAY_RATE_T_LRA_DIV_2:
> + clk_base = h->t_lra_us;
> + divider = 2;
> + break;
> + case PLAY_RATE_T_LRA_DIV_4:
> + clk_base = h->t_lra_us;
> + divider = 4;
> + break;
> + case PLAY_RATE_T_LRA_DIV_8:
> + clk_base = h->t_lra_us;
> + divider = 8;
> + break;
> + case PLAY_RATE_T_LRA_X_2:
> + clk_base = h->t_lra_us;
> + multiplier = 2;
> + break;
> + case PLAY_RATE_T_LRA_X_4:
> + clk_base = h->t_lra_us;
> + multiplier = 4;
> + break;
> + case PLAY_RATE_T_LRA_X_8:
> + clk_base = h->t_lra_us;
> + multiplier = 8;
> + break;
> + case PLAY_RATE_F_8KHZ:
> + divider = 8;
> + break;
> + case PLAY_RATE_F_16KHZ:
> + divider = 16;
> + break;
> + case PLAY_RATE_F_24KHZ:
> + divider = 24;
> + break;
> + case PLAY_RATE_F_32KHZ:
> + divider = 32;
> + break;
> + case PLAY_RATE_F_44P1KHZ:
> + clk_base *= 10;
> + divider = 441;
> + break;
> + case PLAY_RATE_F_48KHZ:
> + divider = 48;
> + break;
> + default:
> + /* Unexpected rate: fall back to the resonance period. */
> + clk_base = h->t_lra_us;
> + break;
> + }
> +
> + return div_u64((u64)clk_base * data_len * multiplier, divider);
> +}
> +
> +static int haptics_upload_effect(struct input_dev *dev,
> + struct ff_effect *effect,
> + struct ff_effect *old)
> +{
> + struct qcom_haptics *h = input_get_drvdata(dev);
> + struct qcom_haptics_effect *priv;
> + int id = effect->id;
> + u32 data_len, level;
> + s8 *fifo;
> +
> + s16 *buf __free(kvfree) = NULL;
> +
> + guard(mutex)(&h->play_lock);
> +
> + if (id < 0 || id >= HAPTICS_MAX_EFFECTS)
> + return -EINVAL;
> +
> + if (id == h->active_effect_id) {
> + dev_err(h->dev, "effect %d is under playing\n", id);
> + return -EBUSY;
> + }
> +
> + priv = &h->effects[id];
> +
> + switch (effect->type) {
> + case FF_CONSTANT:
> + haptics_clear_effect(h, priv);
> + level = effect->u.constant.level <= 0 ? 0 : effect->u.constant.level;
> + priv->amplitude = (u8)mult_frac(level, 255, 0x7FFF);
> + priv->length_us = effect->replay.length * USEC_PER_MSEC;
> + priv->mode = HAPTICS_DIRECT_PLAY;
> + return 0;
> +
> + case FF_PERIODIC:
> + if (effect->u.periodic.waveform != FF_CUSTOM)
> + return -EINVAL;
> + /*
> + * Minimum 3 elements: play-rate code + vmax + at least one sample.
> + * Limit the maximum data length to ~48K so that it can at least
> + * handle ~1s vibration at the fast (48K) play rate.
> + */
> + if (effect->u.periodic.custom_len < 3 ||
> + effect->u.periodic.custom_len > CUSTOM_DATA_MAX_LEN + 2)
> + return -EINVAL;
> +
> + buf = vmemdup_array_user(effect->u.periodic.custom_data,
> + effect->u.periodic.custom_len,
> + sizeof(s16));
> + if (IS_ERR(buf))
> + return PTR_ERR(no_free_ptr(buf));
> +
> + if (buf[CUSTOM_DATA_RATE_IDX] < 0 ||
> + buf[CUSTOM_DATA_RATE_IDX] > PLAY_RATE_MAX ||
> + buf[CUSTOM_DATA_RATE_IDX] == PLAY_RATE_RESERVED)
> + return -EINVAL;
> +
> + data_len = effect->u.periodic.custom_len - CUSTOM_DATA_SAMPLE_START;
> +
> + fifo = kvmalloc(data_len, GFP_KERNEL);
> + if (!fifo)
> + return -ENOMEM;
> +
> + /* Pack: one s8 sample per s16 slot (lower byte) */
> + for (int i = 0; i < data_len; i++)
> + fifo[i] = (s8)buf[CUSTOM_DATA_SAMPLE_START + i];
> +
> + haptics_clear_effect(h, priv);
> +
> + scoped_guard(mutex, &h->fifo_lock) {
> + priv->fifo_data = fifo;
> + priv->data_len = data_len;
> + }
> +
> + priv->play_rate = (u8)buf[CUSTOM_DATA_RATE_IDX];
> + priv->vmax_mv = (u32)clamp_val(buf[CUSTOM_DATA_VMAX_IDX], 0, VMAX_MV_MAX);
> + priv->length_us = haptics_fifo_length_us(h, priv->play_rate, data_len);
> + priv->mode = HAPTICS_FIFO;
> +
> + return 0;
> +
> + default:
> + return -EINVAL;
> + }
> +}
> +
> +static int haptics_playback(struct input_dev *dev, int effect_id, int val)
> +{
> + struct qcom_haptics *h = input_get_drvdata(dev);
> + int ret;
> +
> + ret = haptics_enqueue_play_req(h, effect_id, val > 0);
> + if (ret)
> + return ret;
> +
> + if (val > 0)
> + /*
> + * Queue the play. If a duration re-arm is already pending this
> + * is a no-op, so the new play waits for the current effect to
> + * finish before the worker dequeues it.
> + */
> + schedule_delayed_work(&h->play_work, 0);
> + else
> + /*
> + * Run the worker now, cancelling any pending duration re-arm,
> + * so an explicit stop takes effect immediately.
> + */
> + mod_delayed_work(system_percpu_wq, &h->play_work, 0);
> +
> + return 0;
> +}
> +
> +static int haptics_erase(struct input_dev *dev, int effect_id)
> +{
> + struct qcom_haptics *h = input_get_drvdata(dev);
> + struct qcom_haptics_effect *priv = &h->effects[effect_id];
> +
> + guard(mutex)(&h->play_lock);
> +
> + haptics_queue_remove_effect(h, effect_id);
> +
> + if (h->active_effect_id == effect_id)
> + haptics_stop_locked(h, false);
> +
> + haptics_clear_effect(h, priv);
> + priv->mode = HAPTICS_MODE_NONE;
> +
> + return 0;
> +}
> +
> +static void haptics_set_gain(struct input_dev *dev, u16 gain)
> +{
> + struct qcom_haptics *h = input_get_drvdata(dev);
> +
> + atomic_set(&h->gain, gain);
You need to adjust currently played effects here: new gain should take
effect immediately.
> +}
> +
> +static int qcom_haptics_probe(struct platform_device *pdev)
> +{
> + struct qcom_haptics *h;
> + struct input_dev *input;
> + struct ff_device *ff;
> + u32 regs[2], vmax_uv;
> + int ret, irq;
> +
> + h = devm_kzalloc(&pdev->dev, sizeof(*h), GFP_KERNEL);
> + if (!h)
> + return -ENOMEM;
> +
> + h->dev = &pdev->dev;
> +
> + h->regmap = dev_get_regmap(pdev->dev.parent, NULL);
> + if (!h->regmap)
> + return dev_err_probe(h->dev, -ENODEV, "no regmap from parent\n");
> +
> + ret = device_property_read_u32_array(h->dev, "reg", regs, ARRAY_SIZE(regs));
> + if (ret)
> + return dev_err_probe(h->dev, ret, "failed to read 'reg' property\n");
> +
> + h->cfg_base = regs[0];
> + h->ptn_base = regs[1];
> +
> + ret = device_property_read_u32(h->dev, "qcom,lra-period-us", &h->t_lra_us);
> + if (ret)
> + return dev_err_probe(h->dev, ret, "missing qcom,lra-period-us\n");
> +
> + h->t_lra_us = clamp(h->t_lra_us, (u32)TLRA_STEP_US, (u32)TLRA_US_MAX);
> +
> + ret = device_property_read_u32(h->dev, "qcom,vmax-microvolt", &vmax_uv);
> + if (ret)
> + return dev_err_probe(h->dev, ret, "missing qcom,vmax-microvolt\n");
> +
> + h->vmax_mv = clamp(vmax_uv / 1000, (u32)VMAX_STEP_MV, (u32)VMAX_MV_MAX);
> +
> + ret = haptics_config_lra_period(h);
> + if (ret)
> + return ret;
> +
> + ret = haptics_configure_autores(h);
> + if (ret)
> + return ret;
> +
> + ret = haptics_set_vmax(h, h->vmax_mv);
> + if (ret)
> + return ret;
> +
> + ret = haptics_configure_fifo_mmap(h);
> + if (ret)
> + return ret;
> +
> + mutex_init(&h->play_lock);
> + mutex_init(&h->fifo_lock);
> + spin_lock_init(&h->play_queue_lock);
> + INIT_LIST_HEAD(&h->play_queue);
> + INIT_DELAYED_WORK(&h->play_work, haptics_play_work);
> + atomic_set(&h->gain, 0xFFFF);
> + h->active_effect_id = -1;
> +
> + irq = platform_get_irq_byname(pdev, "fifo-empty");
> + if (irq < 0)
> + return dev_err_probe(h->dev, irq, "failed to get fifo-empty IRQ\n");
> +
> + ret = devm_request_threaded_irq(h->dev, irq, NULL,
> + haptics_fifo_empty_irq,
> + IRQF_ONESHOT | IRQF_NO_AUTOEN,
> + "qcom-haptics-fifo-empty", h);
> + if (ret)
> + return dev_err_probe(h->dev, ret, "failed to request fifo-empty IRQ\n");
> +
> + h->fifo_empty_irq = irq;
> + platform_set_drvdata(pdev, h);
> +
> + pm_runtime_use_autosuspend(h->dev);
> + pm_runtime_set_autosuspend_delay(h->dev, HAPTICS_AUTOSUSPEND_MS);
> + ret = devm_pm_runtime_enable(h->dev);
> + if (ret)
> + return dev_err_probe(h->dev, ret, "enable runtime PM failed\n");
> +
> + input = devm_input_allocate_device(h->dev);
> + if (!input)
> + return -ENOMEM;
> +
> + input->name = "qcom-spmi-haptics";
Set bus type? BUS_HOST?
> + input_set_drvdata(input, h);
> + h->input = input;
> +
> + input_set_capability(input, EV_FF, FF_CONSTANT);
> + input_set_capability(input, EV_FF, FF_PERIODIC);
> + input_set_capability(input, EV_FF, FF_CUSTOM);
> + input_set_capability(input, EV_FF, FF_GAIN);
> +
> + ret = input_ff_create(input, HAPTICS_MAX_EFFECTS);
> + if (ret)
> + return ret;
> +
> + ff = input->ff;
> + ff->upload = haptics_upload_effect;
> + ff->playback = haptics_playback;
> + ff->erase = haptics_erase;
> + ff->set_gain = haptics_set_gain;
> +
> + ret = input_register_device(input);
> + if (ret) {
> + input_ff_destroy(input);
This is harmless but not needed: FF portion will be freed automatically.
> + return dev_err_probe(h->dev, ret, "failed to register input device\n");
> + }
> +
> + return 0;
> +}
> +
> +static void qcom_haptics_remove(struct platform_device *pdev)
> +{
> + struct qcom_haptics *h = platform_get_drvdata(pdev);
> +
> + /*
> + * Unregister the input device explicitly at the beginning
> + * to avoid the input device being used after the resources
> + * are freed.
> + */
> + input_unregister_device(h->input);
> + disable_delayed_work_sync(&h->play_work);
> + scoped_guard(mutex, &h->play_lock) {
> + haptics_queue_flush(h);
> + haptics_stop_locked(h, true);
> + }
> +
> + haptics_enable_module(h, false);
> +
> + scoped_guard(mutex, &h->fifo_lock) {
> + for (int i = 0; i < HAPTICS_MAX_EFFECTS; i++) {
> + kvfree(h->effects[i].fifo_data);
> + h->effects[i].fifo_data = NULL;
> + }
> + }
Most of this is better suited to close() method of input device.
> +}
> +
> +static int qcom_haptics_runtime_suspend(struct device *dev)
> +{
> + struct qcom_haptics *h = dev_get_drvdata(dev);
> +
> + return haptics_enable_module(h, false);
> +}
> +
> +static int qcom_haptics_runtime_resume(struct device *dev)
> +{
> + struct qcom_haptics *h = dev_get_drvdata(dev);
> +
> + return haptics_enable_module(h, true);
> +}
> +
> +static int qcom_haptics_suspend(struct device *dev)
> +{
> + struct qcom_haptics *h = dev_get_drvdata(dev);
> + int ret;
> +
> + disable_delayed_work_sync(&h->play_work);
> + scoped_guard(mutex, &h->play_lock) {
> + haptics_queue_flush(h);
> + haptics_stop_locked(h, true);
> + }
> +
> + ret = pm_runtime_force_suspend(dev);
> + if (ret)
> + enable_delayed_work(&h->play_work);
> +
> + return ret;
> +}
> +
> +static int qcom_haptics_resume(struct device *dev)
> +{
> + struct qcom_haptics *h = dev_get_drvdata(dev);
> +
> + enable_delayed_work(&h->play_work);
> +
> + return pm_runtime_force_resume(dev);
> +}
> +
> +static const struct dev_pm_ops qcom_haptics_pm_ops = {
> + SYSTEM_SLEEP_PM_OPS(qcom_haptics_suspend, qcom_haptics_resume)
> + RUNTIME_PM_OPS(qcom_haptics_runtime_suspend, qcom_haptics_runtime_resume,
> + NULL)
> +};
> +
> +static const struct of_device_id qcom_haptics_of_match[] = {
> + { .compatible = "qcom,spmi-haptics" },
> + {}
{ }
> +};
> +MODULE_DEVICE_TABLE(of, qcom_haptics_of_match);
> +
> +static struct platform_driver qcom_haptics_driver = {
> + .probe = qcom_haptics_probe,
> + .remove = qcom_haptics_remove,
> + .driver = {
> + .name = "qcom-spmi-haptics",
> + .of_match_table = qcom_haptics_of_match,
> + .pm = pm_ptr(&qcom_haptics_pm_ops),
> + },
> +};
> +module_platform_driver(qcom_haptics_driver);
> +
> +MODULE_DESCRIPTION("Qualcomm SPMI PMIC Haptics driver");
> +MODULE_LICENSE("GPL");
>
Thanks.
--
Dmitry
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH v4 0/3] input: misc: Add an initial driver for haptics inside Qcom PMIH010x PMIC
2026-07-17 7:28 [PATCH v4 0/3] input: misc: Add an initial driver for haptics inside Qcom PMIH010x PMIC Fenglin Wu
` (3 preceding siblings ...)
2026-07-17 9:25 ` [PATCH v4 0/3] input: misc: Add an initial driver for haptics inside Qcom PMIH010x PMIC Krzysztof Kozlowski
@ 2026-07-17 18:52 ` Jeff Johnson
4 siblings, 0 replies; 8+ messages in thread
From: Jeff Johnson @ 2026-07-17 18:52 UTC (permalink / raw)
To: Fenglin Wu, linux-arm-msm, Dmitry Torokhov, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Lee Jones, Stephen Boyd,
Bjorn Andersson, Konrad Dybcio
Cc: David Collins, Subbaraman Narayanamurthy, Kamal Wadhwa,
linux-input, devicetree, linux-kernel, Krzysztof Kozlowski
On 7/17/2026 12:28 AM, Fenglin Wu wrote:
> Dependencies:
> - [patch 2/3] depends on [patch 1/3] and they should be applied together
???
it is normal for later patches in a series to depend upon earlier patches, but
that doesn't normally mean they need to be applied together, just that the
later patch needs to be applied after the earlier patch, which is normal.
you'd only need them to be applied together if they have cross dependencies.
But then that would be bad because it would mean you can't bisect the changes.
So this dependencies statement doesn't really make any sense.
/jeff
^ permalink raw reply [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-07-17 18:52 UTC | newest]
Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-17 7:28 [PATCH v4 0/3] input: misc: Add an initial driver for haptics inside Qcom PMIH010x PMIC Fenglin Wu
2026-07-17 7:28 ` [PATCH v4 1/3] dt-bindings: input: Add Qualcomm SPMI PMIC haptics Fenglin Wu
2026-07-17 7:29 ` [PATCH v4 2/3] dt-bindings: mfd: qcom,spmi-pmic: Document haptics device Fenglin Wu
2026-07-17 7:29 ` [PATCH v4 3/3] input: misc: Add Qualcomm SPMI PMIC haptics driver Fenglin Wu
2026-07-17 7:42 ` sashiko-bot
2026-07-17 16:51 ` Dmitry Torokhov
2026-07-17 9:25 ` [PATCH v4 0/3] input: misc: Add an initial driver for haptics inside Qcom PMIH010x PMIC Krzysztof Kozlowski
2026-07-17 18:52 ` Jeff Johnson
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox