[PATCH v4 0/3] counter: add GPIO-based quadrature encoder driver

Wadim Mueller posted 3 patches 1 week, 2 days ago
.../counter/gpio-quadrature-encoder.yaml      |  60 ++
MAINTAINERS                                   |   7 +
drivers/counter/Kconfig                       |  15 +
drivers/counter/Makefile                      |   1 +
drivers/counter/gpio-quadrature-encoder.c     | 739 ++++++++++++++++++
5 files changed, 822 insertions(+)
create mode 100644 Documentation/devicetree/bindings/counter/gpio-quadrature-encoder.yaml
create mode 100644 drivers/counter/gpio-quadrature-encoder.c
[PATCH v4 0/3] counter: add GPIO-based quadrature encoder driver
Posted by Wadim Mueller 1 week, 2 days ago
This series adds a new counter subsystem driver which implements
quadrature encoder position tracking using plain GPIO pins with
edge-triggered interrupts.

The driver targets low- to medium-speed rotary encoders on SoCs where
hardware quadrature counter peripherals (eQEP, FTM, ...) are either
not available or already in use. It complements interrupt-cnt.c by
providing full A/B/Index quadrature decoding instead of just pulse
counting.

The strategic question from v3 -- "why kernel and not libgpiod in
userspace?" -- was already discussed in the v3 cover thread. A
reproducible benchmark on AM64x (kernel counter vs gpiomon/libgpiod
2.1.2, 3-run sweeps from 1 kHz up to 200 kHz) was posted there and
is also mirrored at https://github.com/wafgo/qenc-bench. William
accepted the case for a kernel implementation based on this, so i
will not repeat the numbers here, only the link.

Changes in v4 address the detailed review feedback from William on
PATCH 3/3 of v3:

Counter ABI / generic semantics:
  - Drop the private gpio_qenc_function enum, store and exchange the
    function as enum counter_function directly. function_read/write
    become trivial accessors.
  - Support the full set of generic Count functions:
    INCREASE, DECREASE, PULSE_DIRECTION,
    QUADRATURE_X1_{A,B}, QUADRATURE_X2_{A,B}, QUADRATURE_X4.
  - Add COUNTER_SYNAPSE_ACTION_FALLING_EDGE to the synapse action
    list and report it from action_read for the function modes
    where it does apply.
  - In QUADRATURE_X1_{A,B} the reported synapse action is now
    direction-dependent (RISING_EDGE when going forward, FALLING_EDGE
    when going backward) to match the Counter ABI semantics.
  - Restructure action_read: default to NONE, handle the Index
    synapse as a single early-return case, drop the per-function
    if/else cascade.
  - Use a dedicated action list for the Index synapse with only NONE
    and RISING_EDGE, since the other synapse actions do not apply
    there.
  - Migrate the Index feature from a custom "index_enabled"
    COUNTER_COMP_COUNT_BOOL to the generic COUNTER_COMP_PRESET +
    COUNTER_COMP_PRESET_ENABLE pair. The Index ISR now loads the
    preset value into count when preset_enable is set, like
    intel-qep and other drivers do.

Driver internals:
  - Change priv->count from s64 to u64. ceiling now defaults to
    U64_MAX instead of using ceiling == 0 as a sentinel for "no
    ceiling", which i think is also more clean.
  - Rewrite gpio_qenc_update_count direction-first, +/-1, with
    proper saturation at 0 and at ceiling.
  - Introduce CREATE_QE_STATE(prev_a, prev_b, curr_a, curr_b) and a
    single 16-entry X4 transition table indexed by the macro. The
    delta == 2 "invalid transition" path is gone, the table just
    has 0 in that slot.
  - Use default: in the ISR switches instead of listing every
    ignored case.
  - Drop the now-redundant enabled flag in the ISRs. Gating happens
    via enable_irq/disable_irq anyway and enable_read derives the
    state from irq_get_irq_data() so there is only one source of
    truth.
  - Simplify enable_write: no !!, assign directly, split the two
    branches by return.
  - Drop the no-op events_configure() hook from counter_ops, this
    one was just left over.
  - Do not register a synapse for the Index signal when no Index
    GPIO is wired (no zombie entries) and drop the corresponding
    !gpio guard in signal_read which was only there to catch the
    zombie case.
  - Declare priv->cnts as a single-element array and use
    ARRAY_SIZE(priv->cnts) for counter->num_counts, for consistency
    with the rest of the subsystem.
  - Rename the Count from "Position" to the more generic "Count",
    since positioning is only one of the use cases for a quadrature
    encoder.

Documentation / style:
  - Add a short comment to the priv spinlock.

Krzysztofs "drop const on scalar parameters" note from PATCH 2/3
of v2 is also taken care of in this rewrite -- v3 was Acked-by
Conor only and did not carry code changes yet, so the const scalar
parameters survived there. In v4 there are no const-qualified scalar
parameters left in the driver.

The follow-up scope which was discussed with William -- COMPARE/FLOOR
components plus OVERFLOW/UNDERFLOW/THRESHOLD/DIRECTION_CHANGE events
-- is on purpose not included in v4. I will send those as a separate
series on top of this one once it lands, so the diff size stays
reviewable.

Changes in v4:
  - Major review-driven rewrite of the driver (see above).

Changes in v3:
  - Pick up Acked-by: Conor Dooley on the DT binding patch.
  - No code changes.

Changes in v2:
  - DT binding: rephrase description to describe the hardware, not
    the driver/sysfs behaviour (Conor Dooley)
  - DT binding: drop redundant example without index GPIO (Conor Dooley)

Wadim Mueller (3):
  dt-bindings: counter: add gpio-quadrature-encoder binding
  counter: add GPIO-based quadrature encoder driver
  MAINTAINERS: add entry for GPIO quadrature encoder counter driver

 .../counter/gpio-quadrature-encoder.yaml      |  60 ++
 MAINTAINERS                                   |   7 +
 drivers/counter/Kconfig                       |  15 +
 drivers/counter/Makefile                      |   1 +
 drivers/counter/gpio-quadrature-encoder.c     | 739 ++++++++++++++++++
 5 files changed, 822 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/counter/gpio-quadrature-encoder.yaml
 create mode 100644 drivers/counter/gpio-quadrature-encoder.c


base-commit: 3cd8b194bf3428dfa53120fee47e827a7c495815
-- 
2.52.0
[PATCH v5 0/3] counter: add GPIO-based counter driver
Posted by Wadim Mueller 11 hours ago
This series adds a counter subsystem driver that does quadrature
encoder position tracking with plain GPIO pins and edge interrupts.
Compared to interrupt-cnt.c (pulse-only) it provides full A/B/Index
decoding and exposes the counter sysfs ABI.  Target hardware is
low- to medium-speed rotary encoders on SoCs without a free eQEP /
FTM / etc.  Benchmark rig: github.com/wafgo/qenc-bench.

Changes in v5
-------------

Following William's v4 review the driver and binding are renamed
from "gpio-quadrature-encoder" to "gpio-counter" -- the name now
reflects what the hardware is, not one of its functions.  This
renames the source file, Kconfig symbol, compatible string, binding
file and DT properties (encoder-{a,b}-gpios -> signal-{a,b}-gpios,
encoder-index-gpios -> index-gpios).  Out-of-tree users on v1-v4
will need to update their DTs.

Conor's v4 Acked-by on the binding was dropped because of these
rename changes -- a fresh Ack would be appreciated.

Driver fixes from William's review:
  - X4 decoder rewritten using the 2-bit Gray-code parity trick
    (STATE_CHANGED = pa^pb^ca^cb, DIRECTION via pb^ca) -- no more
    16-entry lookup table.
  - X1_A / X1_B now count on rising-when-forward / falling-when-
    backward in the per-edge ISRs, with the X1 direction caveat
    documented in the source.
  - action_read holds priv->lock while reading function/direction
    and returns from each case directly.
  - ceiling_write no longer touches priv->count (matches intel-qep,
    ti-eqep, stm32-timer-cnt); the >= guard in the update path
    prevents further growth.
  - Dropped the redundant functions_list check in function_write
    and the !!val rewrite in preset_enable_write.

Sashiko AI [1] flagged seven issues on v4, all addressed:
  1. Normalise GPIO reads (a = !!a; b = !!b;) so negative error
     codes from gpiod_get_value() cannot index the state tables.
  2. priv->enabled tracked under priv->lock -- enable_write is
     now idempotent.
  3. preset/ceiling TOCTOU closed by moving the check under
     priv->lock; index ISR clamps after preset load.
  4. probe rejects sleepable GPIOs via gpiod_cansleep().
  5. action_read reports RISING/FALLING based on current direction,
     matching what the ISR counts on.
  6. action_read holds priv->lock (same fix as William's review).
  7. IRQF_NO_AUTOEN replaces irq_set_status_flags(IRQ_NOAUTOEN).

MAINTAINERS: section renamed to "GPIO COUNTER DRIVER" and resorted
alphabetically between "GPIO AGGREGATOR" and "GPIO IR Transmitter".

Thanks to William for the patient review and to the Sashiko bot
for the extra finds.

[1] https://sashiko.dev/#/patchset/20260515153616.157605-1-wafgo01@gmail.com?part=2

Wadim Mueller (3):
  dt-bindings: counter: add gpio-counter binding
  counter: add GPIO-based counter driver
  MAINTAINERS: add entry for GPIO counter driver

 .../bindings/counter/gpio-counter.yaml        |  59 ++
 MAINTAINERS                                   |   7 +
 drivers/counter/Kconfig                       |  17 +
 drivers/counter/Makefile                      |   1 +
 drivers/counter/gpio-counter.c                | 744 ++++++++++++++++++
 5 files changed, 828 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/counter/gpio-counter.yaml
 create mode 100644 drivers/counter/gpio-counter.c


base-commit: 3cd8b194bf3428dfa53120fee47e827a7c495815
-- 
2.52.0
[PATCH v5 1/3] dt-bindings: counter: add gpio-counter binding
Posted by Wadim Mueller 11 hours ago
Add a binding for a generic GPIO-based counter.  Two GPIOs (signal-a,
signal-b) drive the counter; an optional index GPIO loads a preset.
The counter function (quadrature, pulse-direction, increase/decrease)
is choosen at runtime through the counter sysfs interface.

Signed-off-by: Wadim Mueller <wafgo01@gmail.com>
---
 .../bindings/counter/gpio-counter.yaml        | 59 +++++++++++++++++++
 1 file changed, 59 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/counter/gpio-counter.yaml

diff --git a/Documentation/devicetree/bindings/counter/gpio-counter.yaml b/Documentation/devicetree/bindings/counter/gpio-counter.yaml
new file mode 100644
index 000000000..4bd972b61
--- /dev/null
+++ b/Documentation/devicetree/bindings/counter/gpio-counter.yaml
@@ -0,0 +1,59 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/counter/gpio-counter.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: GPIO-based Counter
+
+maintainers:
+  - Wadim Mueller <wadim.mueller@cmblu.de>
+
+description:
+  GPIO-based software counter.  Decodes up to two primary signals (A
+  and B) and an optional index pulse via edge-triggered GPIO interrupts
+  into a count.  Supports quadrature X1/X2/X4, pulse-direction, and
+  pure increase/decrease modes; the mode is selected at runtime via
+  the counter sysfs ABI.
+
+properties:
+  compatible:
+    const: gpio-counter
+
+  signal-a-gpios:
+    maxItems: 1
+    description:
+      Signal A input (encoder phase A in quadrature modes; pulse
+      input in pulse-direction and increase/decrease modes).
+
+  signal-b-gpios:
+    maxItems: 1
+    description:
+      Signal B input (encoder phase B in quadrature modes; direction
+      input in pulse-direction mode; unused in increase/decrease).
+
+  index-gpios:
+    maxItems: 1
+    description:
+      Optional index (Z) input.  When pulsed, loads the configured
+      preset into the count.
+
+required:
+  - compatible
+  - signal-a-gpios
+  - signal-b-gpios
+
+additionalProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/gpio/gpio.h>
+
+    counter {
+        compatible = "gpio-counter";
+        signal-a-gpios = <&gpio0 10 GPIO_ACTIVE_LOW>;
+        signal-b-gpios = <&gpio0 11 GPIO_ACTIVE_LOW>;
+        index-gpios = <&gpio0 12 GPIO_ACTIVE_LOW>;
+    };
+
+...
-- 
2.52.0
[PATCH v5 2/3] counter: add GPIO-based counter driver
Posted by Wadim Mueller 11 hours ago
Add a platform driver that turns plain GPIOs into a counter device.
Edge interrupts on the signal-a, signal-b (and optional index) lines
are decoded in software using a 2-bit Gray-code parity trick for the
quadrature X4 mode and direct edge checks for the other modes.

Supports COUNTER_FUNCTION_QUADRATURE_X1_{A,B} / X2_{A,B} / X4,
PULSE_DIRECTION, INCREASE and DECREASE.  Sleepable GPIO providers
(I2C/SPI expanders) are rejected at probe time becouse the ISRs run
in hardirq context.

Signed-off-by: Wadim Mueller <wafgo01@gmail.com>
---
 drivers/counter/Kconfig        |  17 +
 drivers/counter/Makefile       |   1 +
 drivers/counter/gpio-counter.c | 744 +++++++++++++++++++++++++++++++++
 3 files changed, 762 insertions(+)
 create mode 100644 drivers/counter/gpio-counter.c

diff --git a/drivers/counter/Kconfig b/drivers/counter/Kconfig
index d30d22dfe..c20044032 100644
--- a/drivers/counter/Kconfig
+++ b/drivers/counter/Kconfig
@@ -68,6 +68,23 @@ config INTEL_QEP
 	  To compile this driver as a module, choose M here: the module
 	  will be called intel-qep.
 
+config GPIO_COUNTER
+	tristate "GPIO-based counter driver"
+	depends on GPIOLIB
+	help
+	  Select this option to enable the GPIO-based counter driver.  It
+	  reads A/B and an optional index signal via edge-triggered GPIO
+	  interrupts and decodes them according to the selected mode:
+	  Quadrature X1/X2/X4 (rotary or linear encoders), pulse-direction,
+	  and pure increase / decrease pulse counters.
+
+	  This is useful on SoCs that lack a dedicated hardware quadrature
+	  decoder or pulse counter, or where the signals are wired to
+	  generic GPIO pins rather than to a dedicated peripheral.
+
+	  To compile this driver as a module, choose M here: the
+	  module will be called gpio-counter.
+
 config INTERRUPT_CNT
 	tristate "Interrupt counter driver"
 	depends on GPIOLIB
diff --git a/drivers/counter/Makefile b/drivers/counter/Makefile
index fa3c1d08f..3959d69fb 100644
--- a/drivers/counter/Makefile
+++ b/drivers/counter/Makefile
@@ -14,6 +14,7 @@ obj-$(CONFIG_STM32_TIMER_CNT)	+= stm32-timer-cnt.o
 obj-$(CONFIG_STM32_LPTIMER_CNT)	+= stm32-lptimer-cnt.o
 obj-$(CONFIG_TI_EQEP)		+= ti-eqep.o
 obj-$(CONFIG_FTM_QUADDEC)	+= ftm-quaddec.o
+obj-$(CONFIG_GPIO_COUNTER)		+= gpio-counter.o
 obj-$(CONFIG_MICROCHIP_TCB_CAPTURE)	+= microchip-tcb-capture.o
 obj-$(CONFIG_INTEL_QEP)		+= intel-qep.o
 obj-$(CONFIG_TI_ECAP_CAPTURE)	+= ti-ecap-capture.o
diff --git a/drivers/counter/gpio-counter.c b/drivers/counter/gpio-counter.c
new file mode 100644
index 000000000..f50cec33a
--- /dev/null
+++ b/drivers/counter/gpio-counter.c
@@ -0,0 +1,744 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * GPIO-based Counter Driver
+ *
+ * Decodes A/B (and optional Index) signals from GPIO lines in software.
+ * Supports quadrature X1/X2/X4, pulse-direction, and pure
+ * increase/decrease modes.
+ *
+ * Copyright (C) 2026 CMBlu Energy AG
+ * Author: Wadim Mueller <wafgo01@gmail.com>
+ */
+
+#include <linux/counter.h>
+#include <linux/gpio/consumer.h>
+#include <linux/interrupt.h>
+#include <linux/irq.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/spinlock.h>
+#include <linux/types.h>
+
+enum gpio_counter_signal_id {
+	GPIO_COUNTER_SIGNAL_A = 0,
+	GPIO_COUNTER_SIGNAL_B,
+	GPIO_COUNTER_SIGNAL_INDEX,
+};
+
+struct gpio_counter_priv {
+	struct gpio_desc *gpio_a;
+	struct gpio_desc *gpio_b;
+	struct gpio_desc *gpio_index;
+
+	int irq_a;
+	int irq_b;
+	int irq_index;
+
+	spinlock_t lock; /* protects count, ceiling, preset, function, direction, enabled */
+
+	u64 count;
+	u64 ceiling;
+	u64 preset;
+	bool preset_enabled;
+	bool enabled;
+	enum counter_count_direction direction;
+	enum counter_function function;
+
+	int prev_a;
+	int prev_b;
+
+	struct counter_count cnts[1];
+	struct counter_signal signals[3];
+	struct counter_synapse synapses[3];
+};
+
+/* X4 decode via 2-bit Gray-code parity. */
+#define GPIO_COUNTER_STATE_CHANGED(pa, pb, ca, cb) ((pa) ^ (pb) ^ (ca) ^ (cb))
+#define GPIO_COUNTER_GET_DIRECTION(pb, ca)                 \
+	(((pb) ^ (ca)) ? COUNTER_COUNT_DIRECTION_FORWARD : \
+			 COUNTER_COUNT_DIRECTION_BACKWARD)
+
+static void gpio_counter_update(struct gpio_counter_priv *priv, int delta)
+{
+	if (delta > 0) {
+		priv->direction = COUNTER_COUNT_DIRECTION_FORWARD;
+		if (priv->count >= priv->ceiling)
+			return;
+		priv->count++;
+	} else if (delta < 0) {
+		priv->direction = COUNTER_COUNT_DIRECTION_BACKWARD;
+		if (priv->count == 0)
+			return;
+		priv->count--;
+	}
+}
+
+static int gpio_counter_a_delta(struct gpio_counter_priv *priv, int a, int b,
+				int prev_a, int prev_b)
+{
+	enum counter_count_direction dir;
+
+	switch (priv->function) {
+	case COUNTER_FUNCTION_QUADRATURE_X4:
+		if (!GPIO_COUNTER_STATE_CHANGED(prev_a, prev_b, a, b))
+			return 0;
+		dir = GPIO_COUNTER_GET_DIRECTION(prev_b, a);
+		return (dir == COUNTER_COUNT_DIRECTION_FORWARD) ? 1 : -1;
+
+	case COUNTER_FUNCTION_QUADRATURE_X2_A:
+		return (a == b) ? -1 : 1;
+
+	case COUNTER_FUNCTION_QUADRATURE_X1_A:
+		if (a && priv->direction == COUNTER_COUNT_DIRECTION_FORWARD)
+			return 1;
+		if (!a && priv->direction == COUNTER_COUNT_DIRECTION_BACKWARD)
+			return -1;
+		return 0;
+
+	case COUNTER_FUNCTION_PULSE_DIRECTION:
+		if (!prev_a && a)
+			return b ? -1 : 1;
+		return 0;
+
+	case COUNTER_FUNCTION_INCREASE:
+		if (!prev_a && a)
+			return 1;
+		return 0;
+
+	case COUNTER_FUNCTION_DECREASE:
+		if (!prev_a && a)
+			return -1;
+		return 0;
+
+	default:
+		return 0;
+	}
+}
+
+static int gpio_counter_b_delta(struct gpio_counter_priv *priv, int a, int b,
+				int prev_a, int prev_b)
+{
+	enum counter_count_direction dir;
+
+	switch (priv->function) {
+	case COUNTER_FUNCTION_QUADRATURE_X4:
+		if (!GPIO_COUNTER_STATE_CHANGED(prev_a, prev_b, a, b))
+			return 0;
+		dir = GPIO_COUNTER_GET_DIRECTION(prev_b, a);
+		return (dir == COUNTER_COUNT_DIRECTION_FORWARD) ? 1 : -1;
+
+	case COUNTER_FUNCTION_QUADRATURE_X2_B:
+		return (a == b) ? 1 : -1;
+
+	case COUNTER_FUNCTION_QUADRATURE_X1_B:
+		if (b && priv->direction == COUNTER_COUNT_DIRECTION_FORWARD)
+			return 1;
+		if (!b && priv->direction == COUNTER_COUNT_DIRECTION_BACKWARD)
+			return -1;
+		return 0;
+
+	default:
+		return 0;
+	}
+}
+
+static irqreturn_t gpio_counter_a_isr(int irq, void *dev_id)
+{
+	struct counter_device *counter = dev_id;
+	struct gpio_counter_priv *priv = counter_priv(counter);
+	unsigned long flags;
+	int a, b, delta;
+
+	/* !! normalises away negative gpiod_get_value() errors. */
+	a = !!gpiod_get_value(priv->gpio_a);
+	b = !!gpiod_get_value(priv->gpio_b);
+
+	spin_lock_irqsave(&priv->lock, flags);
+
+	delta = gpio_counter_a_delta(priv, a, b, priv->prev_a, priv->prev_b);
+	gpio_counter_update(priv, delta);
+
+	priv->prev_a = a;
+	priv->prev_b = b;
+
+	spin_unlock_irqrestore(&priv->lock, flags);
+
+	counter_push_event(counter, COUNTER_EVENT_CHANGE_OF_STATE, 0);
+
+	return IRQ_HANDLED;
+}
+
+static irqreturn_t gpio_counter_b_isr(int irq, void *dev_id)
+{
+	struct counter_device *counter = dev_id;
+	struct gpio_counter_priv *priv = counter_priv(counter);
+	unsigned long flags;
+	int a, b, delta;
+
+	a = !!gpiod_get_value(priv->gpio_a);
+	b = !!gpiod_get_value(priv->gpio_b);
+
+	spin_lock_irqsave(&priv->lock, flags);
+
+	delta = gpio_counter_b_delta(priv, a, b, priv->prev_a, priv->prev_b);
+	gpio_counter_update(priv, delta);
+
+	priv->prev_a = a;
+	priv->prev_b = b;
+
+	spin_unlock_irqrestore(&priv->lock, flags);
+
+	counter_push_event(counter, COUNTER_EVENT_CHANGE_OF_STATE, 0);
+
+	return IRQ_HANDLED;
+}
+
+static irqreturn_t gpio_counter_index_isr(int irq, void *dev_id)
+{
+	struct counter_device *counter = dev_id;
+	struct gpio_counter_priv *priv = counter_priv(counter);
+	unsigned long flags;
+
+	spin_lock_irqsave(&priv->lock, flags);
+
+	if (priv->preset_enabled) {
+		priv->count = priv->preset;
+		if (priv->count > priv->ceiling)
+			priv->count = priv->ceiling;
+	}
+
+	spin_unlock_irqrestore(&priv->lock, flags);
+
+	counter_push_event(counter, COUNTER_EVENT_INDEX, 0);
+
+	return IRQ_HANDLED;
+}
+
+static int gpio_counter_count_read(struct counter_device *counter,
+				   struct counter_count *count, u64 *val)
+{
+	struct gpio_counter_priv *priv = counter_priv(counter);
+	unsigned long flags;
+
+	spin_lock_irqsave(&priv->lock, flags);
+	*val = priv->count;
+	spin_unlock_irqrestore(&priv->lock, flags);
+
+	return 0;
+}
+
+static int gpio_counter_count_write(struct counter_device *counter,
+				    struct counter_count *count, u64 val)
+{
+	struct gpio_counter_priv *priv = counter_priv(counter);
+	unsigned long flags;
+
+	spin_lock_irqsave(&priv->lock, flags);
+
+	if (val > priv->ceiling) {
+		spin_unlock_irqrestore(&priv->lock, flags);
+		return -EINVAL;
+	}
+
+	priv->count = val;
+	spin_unlock_irqrestore(&priv->lock, flags);
+
+	return 0;
+}
+
+static const enum counter_function gpio_counter_functions[] = {
+	COUNTER_FUNCTION_INCREASE,	  COUNTER_FUNCTION_DECREASE,
+	COUNTER_FUNCTION_PULSE_DIRECTION, COUNTER_FUNCTION_QUADRATURE_X1_A,
+	COUNTER_FUNCTION_QUADRATURE_X1_B, COUNTER_FUNCTION_QUADRATURE_X2_A,
+	COUNTER_FUNCTION_QUADRATURE_X2_B, COUNTER_FUNCTION_QUADRATURE_X4,
+};
+
+static int gpio_counter_function_read(struct counter_device *counter,
+				      struct counter_count *count,
+				      enum counter_function *function)
+{
+	struct gpio_counter_priv *priv = counter_priv(counter);
+	unsigned long flags;
+
+	spin_lock_irqsave(&priv->lock, flags);
+	*function = priv->function;
+	spin_unlock_irqrestore(&priv->lock, flags);
+
+	return 0;
+}
+
+static int gpio_counter_function_write(struct counter_device *counter,
+				       struct counter_count *count,
+				       enum counter_function function)
+{
+	struct gpio_counter_priv *priv = counter_priv(counter);
+	unsigned long flags;
+
+	spin_lock_irqsave(&priv->lock, flags);
+	priv->function = function;
+	spin_unlock_irqrestore(&priv->lock, flags);
+
+	return 0;
+}
+
+static const enum counter_synapse_action gpio_counter_synapse_actions[] = {
+	COUNTER_SYNAPSE_ACTION_NONE,
+	COUNTER_SYNAPSE_ACTION_RISING_EDGE,
+	COUNTER_SYNAPSE_ACTION_FALLING_EDGE,
+	COUNTER_SYNAPSE_ACTION_BOTH_EDGES,
+};
+
+static const enum counter_synapse_action gpio_counter_index_synapse_actions[] = {
+	COUNTER_SYNAPSE_ACTION_NONE,
+	COUNTER_SYNAPSE_ACTION_RISING_EDGE,
+};
+
+static int gpio_counter_action_read(struct counter_device *counter,
+				    struct counter_count *count,
+				    struct counter_synapse *synapse,
+				    enum counter_synapse_action *action)
+{
+	struct gpio_counter_priv *priv = counter_priv(counter);
+	enum gpio_counter_signal_id signal_id = synapse->signal->id;
+	enum counter_function function;
+	enum counter_count_direction direction;
+	unsigned long flags;
+
+	if (signal_id == GPIO_COUNTER_SIGNAL_INDEX) {
+		*action = COUNTER_SYNAPSE_ACTION_RISING_EDGE;
+		return 0;
+	}
+
+	spin_lock_irqsave(&priv->lock, flags);
+	function = priv->function;
+	direction = priv->direction;
+	spin_unlock_irqrestore(&priv->lock, flags);
+
+	*action = COUNTER_SYNAPSE_ACTION_NONE;
+
+	switch (function) {
+	case COUNTER_FUNCTION_QUADRATURE_X4:
+		*action = COUNTER_SYNAPSE_ACTION_BOTH_EDGES;
+		return 0;
+
+	case COUNTER_FUNCTION_QUADRATURE_X2_A:
+		if (signal_id == GPIO_COUNTER_SIGNAL_A)
+			*action = COUNTER_SYNAPSE_ACTION_BOTH_EDGES;
+		return 0;
+
+	case COUNTER_FUNCTION_QUADRATURE_X2_B:
+		if (signal_id == GPIO_COUNTER_SIGNAL_B)
+			*action = COUNTER_SYNAPSE_ACTION_BOTH_EDGES;
+		return 0;
+
+	case COUNTER_FUNCTION_QUADRATURE_X1_A:
+		if (signal_id == GPIO_COUNTER_SIGNAL_A) {
+			if (direction == COUNTER_COUNT_DIRECTION_FORWARD)
+				*action = COUNTER_SYNAPSE_ACTION_RISING_EDGE;
+			else
+				*action = COUNTER_SYNAPSE_ACTION_FALLING_EDGE;
+		}
+		return 0;
+
+	case COUNTER_FUNCTION_QUADRATURE_X1_B:
+		if (signal_id == GPIO_COUNTER_SIGNAL_B) {
+			if (direction == COUNTER_COUNT_DIRECTION_FORWARD)
+				*action = COUNTER_SYNAPSE_ACTION_RISING_EDGE;
+			else
+				*action = COUNTER_SYNAPSE_ACTION_FALLING_EDGE;
+		}
+		return 0;
+
+	case COUNTER_FUNCTION_PULSE_DIRECTION:
+	case COUNTER_FUNCTION_INCREASE:
+	case COUNTER_FUNCTION_DECREASE:
+		if (signal_id == GPIO_COUNTER_SIGNAL_A)
+			*action = COUNTER_SYNAPSE_ACTION_RISING_EDGE;
+		return 0;
+
+	default:
+		return -EINVAL;
+	}
+}
+
+static int gpio_counter_signal_read(struct counter_device *counter,
+				    struct counter_signal *signal,
+				    enum counter_signal_level *level)
+{
+	struct gpio_counter_priv *priv = counter_priv(counter);
+	struct gpio_desc *gpio;
+	int ret;
+
+	switch (signal->id) {
+	case GPIO_COUNTER_SIGNAL_A:
+		gpio = priv->gpio_a;
+		break;
+	case GPIO_COUNTER_SIGNAL_B:
+		gpio = priv->gpio_b;
+		break;
+	case GPIO_COUNTER_SIGNAL_INDEX:
+		gpio = priv->gpio_index;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	ret = gpiod_get_value(gpio);
+	if (ret < 0)
+		return ret;
+
+	*level = ret ? COUNTER_SIGNAL_LEVEL_HIGH : COUNTER_SIGNAL_LEVEL_LOW;
+	return 0;
+}
+
+static int gpio_counter_watch_validate(struct counter_device *counter,
+				       const struct counter_watch *watch)
+{
+	if (watch->channel != 0)
+		return -EINVAL;
+
+	switch (watch->event) {
+	case COUNTER_EVENT_CHANGE_OF_STATE:
+	case COUNTER_EVENT_INDEX:
+		return 0;
+	default:
+		return -EINVAL;
+	}
+}
+
+static const struct counter_ops gpio_counter_ops = {
+	.count_read = gpio_counter_count_read,
+	.count_write = gpio_counter_count_write,
+	.function_read = gpio_counter_function_read,
+	.function_write = gpio_counter_function_write,
+	.action_read = gpio_counter_action_read,
+	.signal_read = gpio_counter_signal_read,
+	.watch_validate = gpio_counter_watch_validate,
+};
+
+static int gpio_counter_ceiling_read(struct counter_device *counter,
+				     struct counter_count *count, u64 *val)
+{
+	struct gpio_counter_priv *priv = counter_priv(counter);
+	unsigned long flags;
+
+	spin_lock_irqsave(&priv->lock, flags);
+	*val = priv->ceiling;
+	spin_unlock_irqrestore(&priv->lock, flags);
+
+	return 0;
+}
+
+static int gpio_counter_ceiling_write(struct counter_device *counter,
+				      struct counter_count *count, u64 val)
+{
+	struct gpio_counter_priv *priv = counter_priv(counter);
+	unsigned long flags;
+
+	/* Leave count untouched on shrink; matches intel-qep / ti-eqep / stm32-timer-cnt. */
+	spin_lock_irqsave(&priv->lock, flags);
+	priv->ceiling = val;
+	spin_unlock_irqrestore(&priv->lock, flags);
+
+	return 0;
+}
+
+static int gpio_counter_enable_read(struct counter_device *counter,
+				    struct counter_count *count, u8 *enable)
+{
+	struct gpio_counter_priv *priv = counter_priv(counter);
+	unsigned long flags;
+
+	spin_lock_irqsave(&priv->lock, flags);
+	*enable = priv->enabled;
+	spin_unlock_irqrestore(&priv->lock, flags);
+
+	return 0;
+}
+
+static int gpio_counter_enable_write(struct counter_device *counter,
+				     struct counter_count *count, u8 enable)
+{
+	struct gpio_counter_priv *priv = counter_priv(counter);
+	unsigned long flags;
+	bool want = enable;
+	bool changed;
+
+	spin_lock_irqsave(&priv->lock, flags);
+	changed = priv->enabled != want;
+	if (changed)
+		priv->enabled = want;
+	spin_unlock_irqrestore(&priv->lock, flags);
+
+	if (!changed)
+		return 0;
+
+	if (want) {
+		enable_irq(priv->irq_a);
+		enable_irq(priv->irq_b);
+		if (priv->irq_index)
+			enable_irq(priv->irq_index);
+	} else {
+		disable_irq(priv->irq_a);
+		disable_irq(priv->irq_b);
+		if (priv->irq_index)
+			disable_irq(priv->irq_index);
+	}
+
+	return 0;
+}
+
+static int gpio_counter_direction_read(struct counter_device *counter,
+				       struct counter_count *count,
+				       u32 *direction)
+{
+	struct gpio_counter_priv *priv = counter_priv(counter);
+	unsigned long flags;
+
+	spin_lock_irqsave(&priv->lock, flags);
+	*direction = priv->direction;
+	spin_unlock_irqrestore(&priv->lock, flags);
+
+	return 0;
+}
+
+static int gpio_counter_preset_read(struct counter_device *counter,
+				    struct counter_count *count, u64 *val)
+{
+	struct gpio_counter_priv *priv = counter_priv(counter);
+	unsigned long flags;
+
+	spin_lock_irqsave(&priv->lock, flags);
+	*val = priv->preset;
+	spin_unlock_irqrestore(&priv->lock, flags);
+
+	return 0;
+}
+
+static int gpio_counter_preset_write(struct counter_device *counter,
+				     struct counter_count *count, u64 val)
+{
+	struct gpio_counter_priv *priv = counter_priv(counter);
+	unsigned long flags;
+	int ret = 0;
+
+	spin_lock_irqsave(&priv->lock, flags);
+	if (val > priv->ceiling) {
+		ret = -EINVAL;
+		goto out;
+	}
+	priv->preset = val;
+out:
+	spin_unlock_irqrestore(&priv->lock, flags);
+
+	return ret;
+}
+
+static int gpio_counter_preset_enable_read(struct counter_device *counter,
+					   struct counter_count *count, u8 *val)
+{
+	struct gpio_counter_priv *priv = counter_priv(counter);
+	unsigned long flags;
+
+	spin_lock_irqsave(&priv->lock, flags);
+	*val = priv->preset_enabled;
+	spin_unlock_irqrestore(&priv->lock, flags);
+
+	return 0;
+}
+
+static int gpio_counter_preset_enable_write(struct counter_device *counter,
+					    struct counter_count *count, u8 val)
+{
+	struct gpio_counter_priv *priv = counter_priv(counter);
+	unsigned long flags;
+
+	spin_lock_irqsave(&priv->lock, flags);
+	priv->preset_enabled = val;
+	spin_unlock_irqrestore(&priv->lock, flags);
+
+	return 0;
+}
+
+static struct counter_comp gpio_counter_count_ext[] = {
+	COUNTER_COMP_CEILING(gpio_counter_ceiling_read,
+			     gpio_counter_ceiling_write),
+	COUNTER_COMP_ENABLE(gpio_counter_enable_read,
+			    gpio_counter_enable_write),
+	COUNTER_COMP_DIRECTION(gpio_counter_direction_read),
+	COUNTER_COMP_PRESET(gpio_counter_preset_read,
+			    gpio_counter_preset_write),
+	COUNTER_COMP_PRESET_ENABLE(gpio_counter_preset_enable_read,
+				   gpio_counter_preset_enable_write),
+};
+
+static int gpio_counter_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct counter_device *counter;
+	struct gpio_counter_priv *priv;
+	bool has_index;
+	int num_signals;
+	int num_synapses;
+	int ret;
+
+	counter = devm_counter_alloc(dev, sizeof(*priv));
+	if (!counter)
+		return -ENOMEM;
+
+	priv = counter_priv(counter);
+	spin_lock_init(&priv->lock);
+
+	priv->gpio_a = devm_gpiod_get(dev, "signal-a", GPIOD_IN);
+	if (IS_ERR(priv->gpio_a))
+		return dev_err_probe(dev, PTR_ERR(priv->gpio_a),
+				     "failed to get signal-a GPIO\n");
+
+	priv->gpio_b = devm_gpiod_get(dev, "signal-b", GPIOD_IN);
+	if (IS_ERR(priv->gpio_b))
+		return dev_err_probe(dev, PTR_ERR(priv->gpio_b),
+				     "failed to get signal-b GPIO\n");
+
+	priv->gpio_index = devm_gpiod_get_optional(dev, "index", GPIOD_IN);
+	if (IS_ERR(priv->gpio_index))
+		return dev_err_probe(dev, PTR_ERR(priv->gpio_index),
+				     "failed to get index GPIO\n");
+
+	has_index = !!priv->gpio_index;
+
+	if (gpiod_cansleep(priv->gpio_a) || gpiod_cansleep(priv->gpio_b) ||
+	    (has_index && gpiod_cansleep(priv->gpio_index)))
+		return dev_err_probe(dev, -EINVAL,
+				     "GPIO controller may sleep; not supported in IRQ context\n");
+
+	priv->irq_a = gpiod_to_irq(priv->gpio_a);
+	if (priv->irq_a < 0)
+		return dev_err_probe(dev, priv->irq_a,
+				     "failed to get IRQ for signal-a\n");
+
+	priv->irq_b = gpiod_to_irq(priv->gpio_b);
+	if (priv->irq_b < 0)
+		return dev_err_probe(dev, priv->irq_b,
+				     "failed to get IRQ for signal-b\n");
+
+	if (has_index) {
+		priv->irq_index = gpiod_to_irq(priv->gpio_index);
+		if (priv->irq_index < 0)
+			return dev_err_probe(dev, priv->irq_index,
+					     "failed to get IRQ for index\n");
+	}
+
+	priv->prev_a = !!gpiod_get_value(priv->gpio_a);
+	priv->prev_b = !!gpiod_get_value(priv->gpio_b);
+
+	priv->function = COUNTER_FUNCTION_QUADRATURE_X4;
+	priv->direction = COUNTER_COUNT_DIRECTION_FORWARD;
+	priv->ceiling = U64_MAX;
+	priv->enabled = false;
+
+	num_signals = has_index ? 3 : 2;
+	num_synapses = num_signals;
+
+	priv->signals[GPIO_COUNTER_SIGNAL_A].id = GPIO_COUNTER_SIGNAL_A;
+	priv->signals[GPIO_COUNTER_SIGNAL_A].name = "Signal A";
+
+	priv->signals[GPIO_COUNTER_SIGNAL_B].id = GPIO_COUNTER_SIGNAL_B;
+	priv->signals[GPIO_COUNTER_SIGNAL_B].name = "Signal B";
+
+	priv->synapses[0].actions_list = gpio_counter_synapse_actions;
+	priv->synapses[0].num_actions =
+		ARRAY_SIZE(gpio_counter_synapse_actions);
+	priv->synapses[0].signal = &priv->signals[GPIO_COUNTER_SIGNAL_A];
+
+	priv->synapses[1].actions_list = gpio_counter_synapse_actions;
+	priv->synapses[1].num_actions =
+		ARRAY_SIZE(gpio_counter_synapse_actions);
+	priv->synapses[1].signal = &priv->signals[GPIO_COUNTER_SIGNAL_B];
+
+	if (has_index) {
+		priv->signals[GPIO_COUNTER_SIGNAL_INDEX].id =
+			GPIO_COUNTER_SIGNAL_INDEX;
+		priv->signals[GPIO_COUNTER_SIGNAL_INDEX].name = "Index";
+
+		priv->synapses[2].actions_list =
+			gpio_counter_index_synapse_actions;
+		priv->synapses[2].num_actions =
+			ARRAY_SIZE(gpio_counter_index_synapse_actions);
+		priv->synapses[2].signal =
+			&priv->signals[GPIO_COUNTER_SIGNAL_INDEX];
+	}
+
+	priv->cnts[0].id = 0;
+	priv->cnts[0].name = "Count";
+	priv->cnts[0].functions_list = gpio_counter_functions;
+	priv->cnts[0].num_functions = ARRAY_SIZE(gpio_counter_functions);
+	priv->cnts[0].synapses = priv->synapses;
+	priv->cnts[0].num_synapses = num_synapses;
+	priv->cnts[0].ext = gpio_counter_count_ext;
+	priv->cnts[0].num_ext = ARRAY_SIZE(gpio_counter_count_ext);
+
+	counter->name = dev_name(dev);
+	counter->parent = dev;
+	counter->ops = &gpio_counter_ops;
+	counter->signals = priv->signals;
+	counter->num_signals = num_signals;
+	counter->counts = priv->cnts;
+	counter->num_counts = ARRAY_SIZE(priv->cnts);
+
+	ret = devm_request_irq(dev, priv->irq_a, gpio_counter_a_isr,
+			       IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING |
+				       IRQF_NO_AUTOEN,
+			       "gpio-counter-a", counter);
+	if (ret)
+		return dev_err_probe(dev, ret,
+				     "failed to request IRQ for signal-a\n");
+
+	ret = devm_request_irq(dev, priv->irq_b, gpio_counter_b_isr,
+			       IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING |
+				       IRQF_NO_AUTOEN,
+			       "gpio-counter-b", counter);
+	if (ret)
+		return dev_err_probe(dev, ret,
+				     "failed to request IRQ for signal-b\n");
+
+	if (has_index) {
+		ret = devm_request_irq(dev, priv->irq_index,
+				       gpio_counter_index_isr,
+				       IRQF_TRIGGER_RISING | IRQF_NO_AUTOEN,
+				       "gpio-counter-index", counter);
+		if (ret)
+			return dev_err_probe(dev, ret,
+					     "failed to request IRQ for index\n");
+	}
+
+	ret = devm_counter_add(dev, counter);
+	if (ret < 0)
+		return dev_err_probe(dev, ret, "failed to add counter\n");
+
+	dev_info(dev, "GPIO counter registered (signals: A, B%s)\n",
+		 has_index ? ", Index" : "");
+
+	return 0;
+}
+
+static const struct of_device_id gpio_counter_of_match[] = {
+	{ .compatible = "gpio-counter" },
+	{}
+};
+MODULE_DEVICE_TABLE(of, gpio_counter_of_match);
+
+static struct platform_driver gpio_counter_driver = {
+	.probe = gpio_counter_probe,
+	.driver = {
+		.name = "gpio-counter",
+		.of_match_table = gpio_counter_of_match,
+	},
+};
+module_platform_driver(gpio_counter_driver);
+
+MODULE_ALIAS("platform:gpio-counter");
+MODULE_AUTHOR("Wadim Mueller <wafgo01@gmail.com>");
+MODULE_DESCRIPTION("GPIO-based counter driver (quadrature, pulse-direction, increase/decrease)");
+MODULE_LICENSE("GPL");
+MODULE_IMPORT_NS("COUNTER");
-- 
2.52.0
[PATCH v5 3/3] MAINTAINERS: add entry for GPIO counter driver
Posted by Wadim Mueller 11 hours ago
Cover the gpio-counter driver and its device-tree binding.

Signed-off-by: Wadim Mueller <wafgo01@gmail.com>
---
 MAINTAINERS | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index 06a8c7457..14f1a4e9f 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -10984,6 +10984,13 @@ S:	Supported
 F:	Documentation/admin-guide/gpio/gpio-aggregator.rst
 F:	drivers/gpio/gpio-aggregator.c
 
+GPIO COUNTER DRIVER
+M:	Wadim Mueller <wafgo01@gmail.com>
+L:	linux-iio@vger.kernel.org
+S:	Maintained
+F:	Documentation/devicetree/bindings/counter/gpio-counter.yaml
+F:	drivers/counter/gpio-counter.c
+
 GPIO IR Transmitter
 M:	Sean Young <sean@mess.org>
 L:	linux-media@vger.kernel.org
-- 
2.52.0