The Linux Kernel Mailing List
 help / color / mirror / Atom feed
* [PATCH v6 0/3] counter: add GPIO-based counter driver
@ 2026-06-28 21:02 Wadim Mueller
  2026-06-28 21:02 ` [PATCH v6 1/3] dt-bindings: counter: add gpio-counter binding Wadim Mueller
                   ` (2 more replies)
  0 siblings, 3 replies; 6+ messages in thread
From: Wadim Mueller @ 2026-06-28 21:02 UTC (permalink / raw)
  To: wbg
  Cc: krzk+dt, robh, conor+dt, o.rempel, linux-iio, devicetree,
	linux-kernel, Wadim Mueller, kernel

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.

Following William's v5 review [1][2] the driver now exposes two
Counts on the same wires:

  Count 1: Synapses on A and B (+ optional Index 1). Supports
           QUADRATURE_X1_{A,B} / X2_{A,B} / X4, PULSE_DIRECTION,
           INCREASE and DECREASE.
  Count 2: Synapse on B (+ optional Index 2). INCREASE / DECREASE.

== Changes in v6 ==

Driver:
  - Two-Count layout with per-Count state struct
    (gpio_counter_count_priv) so future per-Count features can
    be added without parallel arrays.
  - ISR rewrite: STATE_CHANGED check used in all quadrature modes
    (not only X4); the *_delta helpers are gone.
  - INCREASE / DECREASE now count on both edges of their input
    (A for Count 1, B for Count 2).
  - PULSE_DIRECTION: direction is sampled in the B-ISR, the A-ISR
    only steps the value.
  - direction is initialised in function_write so the first read
    after a mode switch returns a defined value.
  - enable_write: added enable_lock mutex and reordered the
    enable/disable sequence so the spinlock is dropped before
    disable_irq() (which can sleep). irq_b is shared by Count 1
    and Count 2, so it is refcounted; the per-Count irqs (A,
    Index 1, Index 2) are toggled directly.

Binding:
  - index-gpios takes 1..2 entries: Index for Count 1, optional
    Index for Count 2.
  - Description and Counts wording updated for the two-Count
    layout.
  - signal-b stays required.  The single-line case is already
    covered by interrupt-cnt.  Oleksij suggested [3] extending
    interrupt-cnt with a multi-line variant; I kept this as its
    own driver because the Generic Counter ABI (Counts /
    Synapses / Signals, ceiling, preset, action) does not fit
    interrupt-cnt's pulse hot path well.
  - Conor's v5 Acked-by [4] is dropped because of the binding
    changes -- a fresh Ack would be appreciated.

[1] https://lore.kernel.org/all/20260617074929.333876-1-wbg@kernel.org/
[2] https://lore.kernel.org/all/20260623053800.120909-1-wbg@kernel.org/
[3] https://lore.kernel.org/all/ajoXoYc1CYKZyQ5l@pengutronix.de/
[4] https://lore.kernel.org/all/20260525-register-bogus-b198545b69a8@spud/

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                | 983 ++++++++++++++++++
 5 files changed, 1067 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


^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH v6 1/3] dt-bindings: counter: add gpio-counter binding
  2026-06-28 21:02 [PATCH v6 0/3] counter: add GPIO-based counter driver Wadim Mueller
@ 2026-06-28 21:02 ` Wadim Mueller
  2026-06-29 15:35   ` Conor Dooley
  2026-06-28 21:02 ` [PATCH v6 2/3] counter: add GPIO-based counter driver Wadim Mueller
  2026-06-28 21:02 ` [PATCH v6 3/3] MAINTAINERS: add entry for GPIO " Wadim Mueller
  2 siblings, 1 reply; 6+ messages in thread
From: Wadim Mueller @ 2026-06-28 21:02 UTC (permalink / raw)
  To: wbg
  Cc: krzk+dt, robh, conor+dt, o.rempel, linux-iio, devicetree,
	linux-kernel, Wadim Mueller, kernel

Add a binding for a generic GPIO-based counter. Two GPIOs (signal-a,
signal-b) drive two Counts; optional index GPIOs load a preset. The
counter function (quadrature, pulse-direction, increase/decrease) is
selected at runtime via 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..79b7afd7d
--- /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 <wafgo01@gmail.com>
+
+description: |
+  GPIO-based software counter with two Counts:
+    * Count 1: A + B. Quadrature X1/X2/X4, pulse-direction, inc/dec.
+    * Count 2: B. Inc / dec.
+  The function is selected at runtime via the counter sysfs ABI. Each
+  Count has an optional Index (Z) that loads its preset on rising edge.
+
+properties:
+  compatible:
+    const: gpio-counter
+
+  signal-a-gpios:
+    maxItems: 1
+    description: Signal A (phase A; pulse input in pulse-direction/inc/dec).
+
+  signal-b-gpios:
+    maxItems: 1
+    description:
+      Signal B (phase B; direction in pulse-direction; pulse input for
+      Count 2).
+
+  index-gpios:
+    minItems: 1
+    maxItems: 2
+    description:
+      Optional Index (Z) inputs. First entry indexes Count 1, second
+      (optional) indexes Count 2.
+
+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>,
+                      <&gpio0 13 GPIO_ACTIVE_LOW>;
+    };
+
+...
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH v6 2/3] counter: add GPIO-based counter driver
  2026-06-28 21:02 [PATCH v6 0/3] counter: add GPIO-based counter driver Wadim Mueller
  2026-06-28 21:02 ` [PATCH v6 1/3] dt-bindings: counter: add gpio-counter binding Wadim Mueller
@ 2026-06-28 21:02 ` Wadim Mueller
  2026-07-07 10:44   ` William Breathitt Gray
  2026-06-28 21:02 ` [PATCH v6 3/3] MAINTAINERS: add entry for GPIO " Wadim Mueller
  2 siblings, 1 reply; 6+ messages in thread
From: Wadim Mueller @ 2026-06-28 21:02 UTC (permalink / raw)
  To: wbg
  Cc: krzk+dt, robh, conor+dt, o.rempel, linux-iio, devicetree,
	linux-kernel, Wadim Mueller, kernel

Add a platform driver that turns plain GPIOs into a counter device.
Edge interrupts on signal-a, signal-b (and optional index) are
decoded in software. Quadrature steps are validated with a 2-bit
Gray-code parity check; direction comes from the same identity.

The driver exposes two Counts on the same wires:

  "Count 1" - Synapses on A, B and optional Index 1. Supports
              QUADRATURE_X1_{A,B} / X2_{A,B} / X4, PULSE_DIRECTION,
              INCREASE and DECREASE.
  "Count 2" - Synapse on B and optional Index 2. Supports INCREASE
              and DECREASE only.

Per-Count state (value, ceiling, preset, enable, function, direction)
lives in struct gpio_counter_count_priv so new per-Count features
(e.g. floor) can be added without touching parallel arrays.

Sleepable GPIO providers (I2C/SPI expanders) are rejected at probe
since 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 | 1015 ++++++++++++++++++++++++++++++++
 3 files changed, 1033 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..8d9fa0b61
--- /dev/null
+++ b/drivers/counter/gpio-counter.c
@@ -0,0 +1,1015 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * GPIO-based Counter Driver
+ *
+ * Software A/B (+ optional Z) edge decoding exposed as two Counts:
+ *   Count 1: A + B (+ Index 1). Quadrature, pulse-direction, inc/dec.
+ *   Count 2: B (+ Index 2). Inc / dec.
+ *
+ * Copyright (C) 2026 CMBlu Energy AG
+ * Author: Wadim Mueller <wafgo01@gmail.com>
+ */
+
+#include <linux/cleanup.h>
+#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/mutex.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_INDEX1,
+	GPIO_COUNTER_SIGNAL_INDEX2,
+	GPIO_COUNTER_NUM_SIGNALS_MAX,
+};
+
+enum gpio_counter_count_id {
+	GPIO_COUNTER_COUNT_1 = 0,
+	GPIO_COUNTER_COUNT_2,
+	GPIO_COUNTER_NUM_COUNTS,
+};
+
+#define GPIO_COUNTER_COUNT1_NUM_SYNAPSES	3	/* A, B, Index 1 */
+#define GPIO_COUNTER_COUNT2_NUM_SYNAPSES	2	/* B, Index 2 */
+#define GPIO_COUNTER_MAX_INDEX			2
+
+/* per-count state */
+struct gpio_counter_count_priv {
+	u64				value;
+	u64				ceiling;
+	u64				preset;
+	bool				preset_enabled;
+	bool				enabled;
+	enum counter_function		function;
+	enum counter_count_direction	direction;
+};
+
+struct gpio_counter_priv {
+	struct gpio_desc *gpio_a;
+	struct gpio_desc *gpio_b;
+	struct gpio_desc *gpio_idx[GPIO_COUNTER_MAX_INDEX];
+
+	int irq_a;
+	int irq_b;
+	int irq_idx[GPIO_COUNTER_MAX_INDEX];
+
+	int n_idx;
+
+	/* protects count state, prev_a, prev_b */
+	spinlock_t lock;
+
+	/* serialises enable_write() callers and b_irq_users */
+	struct mutex enable_lock;
+
+	/* irq_b is shared by Count 1 and Count 2 (under enable_lock) */
+	int b_irq_users;
+
+	int prev_a;
+	int prev_b;
+
+	struct gpio_counter_count_priv count_priv[GPIO_COUNTER_NUM_COUNTS];
+
+	struct counter_signal  signals[GPIO_COUNTER_NUM_SIGNALS_MAX];
+	struct counter_synapse synapses_count1[GPIO_COUNTER_COUNT1_NUM_SYNAPSES];
+	struct counter_synapse synapses_count2[GPIO_COUNTER_COUNT2_NUM_SYNAPSES];
+	struct counter_count   counts[GPIO_COUNTER_NUM_COUNTS];
+};
+
+/* Gray-code: valid step <=> exactly one of A/B toggled. */
+#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)
+
+/* priv->lock held. */
+static void gpio_counter_value_step(struct gpio_counter_count_priv *cp)
+{
+	if (cp->direction == COUNTER_COUNT_DIRECTION_FORWARD) {
+		if (cp->value < cp->ceiling)
+			cp->value++;
+	} else {
+		if (cp->value > 0)
+			cp->value--;
+	}
+}
+
+/* priv->lock held. */
+static void gpio_counter_quadrature_x1_step(struct gpio_counter_count_priv *cp,
+					    int level)
+{
+	if (level && cp->direction == COUNTER_COUNT_DIRECTION_FORWARD)
+		gpio_counter_value_step(cp);
+	else if (!level && cp->direction == COUNTER_COUNT_DIRECTION_BACKWARD)
+		gpio_counter_value_step(cp);
+}
+
+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);
+	struct gpio_counter_count_priv *cp =
+		&priv->count_priv[GPIO_COUNTER_COUNT_1];
+	unsigned long flags;
+	bool enabled;
+	int a, b;
+
+	/* !! drops 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);
+
+	enabled = cp->enabled;
+	if (!enabled)
+		goto out;
+
+	switch (cp->function) {
+	case COUNTER_FUNCTION_QUADRATURE_X4:
+	case COUNTER_FUNCTION_QUADRATURE_X2_A:
+	case COUNTER_FUNCTION_QUADRATURE_X1_A:
+		if (!GPIO_COUNTER_STATE_CHANGED(priv->prev_a, priv->prev_b,
+						a, b))
+			break;
+		cp->direction = GPIO_COUNTER_GET_DIRECTION(priv->prev_b, a);
+		if (cp->function == COUNTER_FUNCTION_QUADRATURE_X1_A)
+			gpio_counter_quadrature_x1_step(cp, a);
+		else
+			gpio_counter_value_step(cp);
+		break;
+
+	case COUNTER_FUNCTION_PULSE_DIRECTION:
+		/* direction set in the B-ISR */
+		if (a)
+			gpio_counter_value_step(cp);
+		break;
+
+	case COUNTER_FUNCTION_INCREASE:
+	case COUNTER_FUNCTION_DECREASE:
+		gpio_counter_value_step(cp);
+		break;
+
+	default:
+		break;
+	}
+
+out:
+	priv->prev_a = a;
+	priv->prev_b = b;
+
+	spin_unlock_irqrestore(&priv->lock, flags);
+
+	if (enabled)
+		counter_push_event(counter, COUNTER_EVENT_CHANGE_OF_STATE,
+				   GPIO_COUNTER_COUNT_1);
+
+	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);
+	struct gpio_counter_count_priv *c1 =
+		&priv->count_priv[GPIO_COUNTER_COUNT_1];
+	struct gpio_counter_count_priv *c2 =
+		&priv->count_priv[GPIO_COUNTER_COUNT_2];
+	bool c1_enabled, c2_enabled;
+	unsigned long flags;
+	int a, b;
+
+	a = !!gpiod_get_value(priv->gpio_a);
+	b = !!gpiod_get_value(priv->gpio_b);
+
+	spin_lock_irqsave(&priv->lock, flags);
+
+	c1_enabled = c1->enabled;
+	c2_enabled = c2->enabled;
+
+	/* Count 1: A+B decoding on this B edge */
+	if (c1_enabled) {
+		switch (c1->function) {
+		case COUNTER_FUNCTION_QUADRATURE_X4:
+		case COUNTER_FUNCTION_QUADRATURE_X2_B:
+		case COUNTER_FUNCTION_QUADRATURE_X1_B:
+			if (!GPIO_COUNTER_STATE_CHANGED(priv->prev_a,
+							priv->prev_b, a, b))
+				break;
+			c1->direction =
+				GPIO_COUNTER_GET_DIRECTION(priv->prev_b, a);
+			if (c1->function == COUNTER_FUNCTION_QUADRATURE_X1_B)
+				gpio_counter_quadrature_x1_step(c1, b);
+			else
+				gpio_counter_value_step(c1);
+			break;
+
+		case COUNTER_FUNCTION_PULSE_DIRECTION:
+			/* B is the direction line */
+			c1->direction = b ? COUNTER_COUNT_DIRECTION_BACKWARD
+					  : COUNTER_COUNT_DIRECTION_FORWARD;
+			break;
+
+		default:
+			break;
+		}
+	}
+
+	/* Count 2: B-only */
+	if (c2_enabled)
+		gpio_counter_value_step(c2);
+
+	priv->prev_a = a;
+	priv->prev_b = b;
+
+	spin_unlock_irqrestore(&priv->lock, flags);
+
+	if (c1_enabled)
+		counter_push_event(counter, COUNTER_EVENT_CHANGE_OF_STATE,
+				   GPIO_COUNTER_COUNT_1);
+	if (c2_enabled)
+		counter_push_event(counter, COUNTER_EVENT_CHANGE_OF_STATE,
+				   GPIO_COUNTER_COUNT_2);
+
+	return IRQ_HANDLED;
+}
+
+static void gpio_counter_index_pulse(struct counter_device *counter,
+				     enum gpio_counter_count_id count_id)
+{
+	struct gpio_counter_priv *priv = counter_priv(counter);
+	struct gpio_counter_count_priv *cp = &priv->count_priv[count_id];
+	unsigned long flags;
+
+	spin_lock_irqsave(&priv->lock, flags);
+	if (cp->preset_enabled) {
+		cp->value = cp->preset;
+		if (cp->value > cp->ceiling)
+			cp->value = cp->ceiling;
+	}
+	spin_unlock_irqrestore(&priv->lock, flags);
+
+	counter_push_event(counter, COUNTER_EVENT_INDEX, count_id);
+}
+
+static irqreturn_t gpio_counter_index1_isr(int irq, void *dev_id)
+{
+	gpio_counter_index_pulse(dev_id, GPIO_COUNTER_COUNT_1);
+	return IRQ_HANDLED;
+}
+
+static irqreturn_t gpio_counter_index2_isr(int irq, void *dev_id)
+{
+	gpio_counter_index_pulse(dev_id, GPIO_COUNTER_COUNT_2);
+	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);
+	struct gpio_counter_count_priv *cp = &priv->count_priv[count->id];
+	unsigned long flags;
+
+	spin_lock_irqsave(&priv->lock, flags);
+	*val = cp->value;
+	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);
+	struct gpio_counter_count_priv *cp = &priv->count_priv[count->id];
+	unsigned long flags;
+
+	spin_lock_irqsave(&priv->lock, flags);
+
+	if (val > cp->ceiling) {
+		spin_unlock_irqrestore(&priv->lock, flags);
+		return -EINVAL;
+	}
+	cp->value = val;
+	spin_unlock_irqrestore(&priv->lock, flags);
+
+	return 0;
+}
+
+static const enum counter_function gpio_counter_count1_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 const enum counter_function gpio_counter_count2_functions[] = {
+	COUNTER_FUNCTION_INCREASE,
+	COUNTER_FUNCTION_DECREASE,
+};
+
+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);
+	struct gpio_counter_count_priv *cp = &priv->count_priv[count->id];
+	unsigned long flags;
+
+	spin_lock_irqsave(&priv->lock, flags);
+	*function = cp->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);
+	struct gpio_counter_count_priv *cp = &priv->count_priv[count->id];
+	unsigned long flags;
+
+	spin_lock_irqsave(&priv->lock, flags);
+	cp->function = function;
+
+	switch (function) {
+	case COUNTER_FUNCTION_INCREASE:
+		cp->direction = COUNTER_COUNT_DIRECTION_FORWARD;
+		break;
+	case COUNTER_FUNCTION_DECREASE:
+		cp->direction = COUNTER_COUNT_DIRECTION_BACKWARD;
+		break;
+	case COUNTER_FUNCTION_PULSE_DIRECTION:
+		cp->direction = !!gpiod_get_value(priv->gpio_b)
+					? COUNTER_COUNT_DIRECTION_BACKWARD
+					: COUNTER_COUNT_DIRECTION_FORWARD;
+		break;
+	default:
+		/* quadrature: direction is set in the ISR */
+		break;
+	}
+	spin_unlock_irqrestore(&priv->lock, flags);
+
+	return 0;
+}
+
+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);
+	struct gpio_counter_count_priv *cp = &priv->count_priv[count->id];
+	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_INDEX1 ||
+	    signal_id == GPIO_COUNTER_SIGNAL_INDEX2) {
+		*action = COUNTER_SYNAPSE_ACTION_RISING_EDGE;
+		return 0;
+	}
+
+	spin_lock_irqsave(&priv->lock, flags);
+	function = cp->function;
+	direction = cp->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:
+		if (signal_id == GPIO_COUNTER_SIGNAL_A)
+			*action = COUNTER_SYNAPSE_ACTION_RISING_EDGE;
+		return 0;
+
+	case COUNTER_FUNCTION_INCREASE:
+	case COUNTER_FUNCTION_DECREASE:
+		if ((count->id == GPIO_COUNTER_COUNT_1 &&
+		     signal_id == GPIO_COUNTER_SIGNAL_A) ||
+		    (count->id == GPIO_COUNTER_COUNT_2 &&
+		     signal_id == GPIO_COUNTER_SIGNAL_B))
+			*action = COUNTER_SYNAPSE_ACTION_BOTH_EDGES;
+		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_INDEX1:
+		gpio = priv->gpio_idx[0];
+		break;
+	case GPIO_COUNTER_SIGNAL_INDEX2:
+		gpio = priv->gpio_idx[1];
+		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 >= GPIO_COUNTER_NUM_COUNTS)
+		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);
+	struct gpio_counter_count_priv *cp = &priv->count_priv[count->id];
+	unsigned long flags;
+
+	spin_lock_irqsave(&priv->lock, flags);
+	*val = cp->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);
+	struct gpio_counter_count_priv *cp = &priv->count_priv[count->id];
+	unsigned long flags;
+
+	spin_lock_irqsave(&priv->lock, flags);
+	cp->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);
+	struct gpio_counter_count_priv *cp = &priv->count_priv[count->id];
+	unsigned long flags;
+
+	spin_lock_irqsave(&priv->lock, flags);
+	*enable = cp->enabled;
+	spin_unlock_irqrestore(&priv->lock, flags);
+
+	return 0;
+}
+
+/* enable_lock held */
+static void gpio_counter_enable_b(struct gpio_counter_priv *priv)
+{
+	if (priv->b_irq_users++ == 0)
+		enable_irq(priv->irq_b);
+}
+
+/* enable_lock held */
+static void gpio_counter_disable_b(struct gpio_counter_priv *priv)
+{
+	if (--priv->b_irq_users == 0)
+		disable_irq(priv->irq_b);
+}
+
+static void gpio_counter_enable_irqs(struct gpio_counter_priv *priv,
+				     enum gpio_counter_count_id count_id)
+{
+	switch (count_id) {
+	case GPIO_COUNTER_COUNT_1:
+		enable_irq(priv->irq_a);
+		gpio_counter_enable_b(priv);
+		if (priv->n_idx >= 1)
+			enable_irq(priv->irq_idx[0]);
+		break;
+	case GPIO_COUNTER_COUNT_2:
+		gpio_counter_enable_b(priv);
+		if (priv->n_idx >= 2)
+			enable_irq(priv->irq_idx[1]);
+		break;
+	default:
+		break;
+	}
+}
+
+static void gpio_counter_disable_irqs(struct gpio_counter_priv *priv,
+				      enum gpio_counter_count_id count_id)
+{
+	switch (count_id) {
+	case GPIO_COUNTER_COUNT_1:
+		if (priv->n_idx >= 1)
+			disable_irq(priv->irq_idx[0]);
+		gpio_counter_disable_b(priv);
+		disable_irq(priv->irq_a);
+		break;
+	case GPIO_COUNTER_COUNT_2:
+		if (priv->n_idx >= 2)
+			disable_irq(priv->irq_idx[1]);
+		gpio_counter_disable_b(priv);
+		break;
+	default:
+		break;
+	}
+}
+
+static int gpio_counter_enable_write(struct counter_device *counter,
+				     struct counter_count *count, u8 enable)
+{
+	struct gpio_counter_priv *priv = counter_priv(counter);
+	struct gpio_counter_count_priv *cp = &priv->count_priv[count->id];
+	unsigned long flags;
+	bool current_state;
+
+	guard(mutex)(&priv->enable_lock);
+
+	spin_lock_irqsave(&priv->lock, flags);
+	current_state = cp->enabled;
+	spin_unlock_irqrestore(&priv->lock, flags);
+
+	if (current_state == enable)
+		return 0;
+
+	/* disable_irq() may sleep, so it can not run under the spinlock. */
+	if (enable) {
+		spin_lock_irqsave(&priv->lock, flags);
+		priv->prev_a = !!gpiod_get_value(priv->gpio_a);
+		priv->prev_b = !!gpiod_get_value(priv->gpio_b);
+		cp->enabled = true;
+		spin_unlock_irqrestore(&priv->lock, flags);
+		gpio_counter_enable_irqs(priv, count->id);
+	} else {
+		spin_lock_irqsave(&priv->lock, flags);
+		cp->enabled = false;
+		spin_unlock_irqrestore(&priv->lock, flags);
+		gpio_counter_disable_irqs(priv, count->id);
+	}
+
+	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);
+	struct gpio_counter_count_priv *cp = &priv->count_priv[count->id];
+	unsigned long flags;
+
+	spin_lock_irqsave(&priv->lock, flags);
+	*direction = cp->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);
+	struct gpio_counter_count_priv *cp = &priv->count_priv[count->id];
+	unsigned long flags;
+
+	spin_lock_irqsave(&priv->lock, flags);
+	*val = cp->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);
+	struct gpio_counter_count_priv *cp = &priv->count_priv[count->id];
+	unsigned long flags;
+	int ret = 0;
+
+	spin_lock_irqsave(&priv->lock, flags);
+	if (val > cp->ceiling) {
+		ret = -EINVAL;
+		goto out;
+	}
+	cp->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);
+	struct gpio_counter_count_priv *cp = &priv->count_priv[count->id];
+	unsigned long flags;
+
+	spin_lock_irqsave(&priv->lock, flags);
+	*val = cp->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);
+	struct gpio_counter_count_priv *cp = &priv->count_priv[count->id];
+	unsigned long flags;
+
+	spin_lock_irqsave(&priv->lock, flags);
+	cp->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 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_RISING_EDGE,
+};
+
+static int gpio_counter_get_index_gpios(struct device *dev,
+					struct gpio_counter_priv *priv)
+{
+	struct gpio_descs *descs;
+	int i;
+
+	descs = devm_gpiod_get_array_optional(dev, "index", GPIOD_IN);
+	if (IS_ERR(descs))
+		return dev_err_probe(dev, PTR_ERR(descs),
+				     "failed to get index GPIOs\n");
+	if (!descs) {
+		priv->n_idx = 0;
+		return 0;
+	}
+
+	if (descs->ndescs < 1 || descs->ndescs > GPIO_COUNTER_MAX_INDEX)
+		return dev_err_probe(dev, -EINVAL,
+				     "index-gpios: expected 1..%d entries, got %u\n",
+				     GPIO_COUNTER_MAX_INDEX, descs->ndescs);
+
+	for (i = 0; i < descs->ndescs; i++)
+		priv->gpio_idx[i] = descs->desc[i];
+	priv->n_idx = descs->ndescs;
+
+	return 0;
+}
+
+static int gpio_counter_setup_signals(struct gpio_counter_priv *priv)
+{
+	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";
+
+	if (priv->n_idx >= 1) {
+		priv->signals[GPIO_COUNTER_SIGNAL_INDEX1].id =
+			GPIO_COUNTER_SIGNAL_INDEX1;
+		priv->signals[GPIO_COUNTER_SIGNAL_INDEX1].name = "Index 1";
+	}
+	if (priv->n_idx >= 2) {
+		priv->signals[GPIO_COUNTER_SIGNAL_INDEX2].id =
+			GPIO_COUNTER_SIGNAL_INDEX2;
+		priv->signals[GPIO_COUNTER_SIGNAL_INDEX2].name = "Index 2";
+	}
+
+	return 2 + priv->n_idx;
+}
+
+static int gpio_counter_setup_synapses(struct gpio_counter_priv *priv)
+{
+	int n_c1 = 2, n_c2 = 1;
+
+	priv->synapses_count1[0].actions_list = gpio_counter_synapse_actions;
+	priv->synapses_count1[0].num_actions =
+		ARRAY_SIZE(gpio_counter_synapse_actions);
+	priv->synapses_count1[0].signal = &priv->signals[GPIO_COUNTER_SIGNAL_A];
+
+	priv->synapses_count1[1].actions_list = gpio_counter_synapse_actions;
+	priv->synapses_count1[1].num_actions =
+		ARRAY_SIZE(gpio_counter_synapse_actions);
+	priv->synapses_count1[1].signal = &priv->signals[GPIO_COUNTER_SIGNAL_B];
+
+	if (priv->n_idx >= 1) {
+		priv->synapses_count1[2].actions_list =
+			gpio_counter_index_synapse_actions;
+		priv->synapses_count1[2].num_actions =
+			ARRAY_SIZE(gpio_counter_index_synapse_actions);
+		priv->synapses_count1[2].signal =
+			&priv->signals[GPIO_COUNTER_SIGNAL_INDEX1];
+		n_c1 = 3;
+	}
+
+	priv->synapses_count2[0].actions_list = gpio_counter_synapse_actions;
+	priv->synapses_count2[0].num_actions =
+		ARRAY_SIZE(gpio_counter_synapse_actions);
+	priv->synapses_count2[0].signal = &priv->signals[GPIO_COUNTER_SIGNAL_B];
+
+	if (priv->n_idx >= 2) {
+		priv->synapses_count2[1].actions_list =
+			gpio_counter_index_synapse_actions;
+		priv->synapses_count2[1].num_actions =
+			ARRAY_SIZE(gpio_counter_index_synapse_actions);
+		priv->synapses_count2[1].signal =
+			&priv->signals[GPIO_COUNTER_SIGNAL_INDEX2];
+		n_c2 = 2;
+	}
+
+	priv->counts[GPIO_COUNTER_COUNT_1].id = GPIO_COUNTER_COUNT_1;
+	priv->counts[GPIO_COUNTER_COUNT_1].name = "Count 1";
+	priv->counts[GPIO_COUNTER_COUNT_1].functions_list =
+		gpio_counter_count1_functions;
+	priv->counts[GPIO_COUNTER_COUNT_1].num_functions =
+		ARRAY_SIZE(gpio_counter_count1_functions);
+	priv->counts[GPIO_COUNTER_COUNT_1].synapses = priv->synapses_count1;
+	priv->counts[GPIO_COUNTER_COUNT_1].num_synapses = n_c1;
+	priv->counts[GPIO_COUNTER_COUNT_1].ext = gpio_counter_count_ext;
+	priv->counts[GPIO_COUNTER_COUNT_1].num_ext =
+		ARRAY_SIZE(gpio_counter_count_ext);
+
+	priv->counts[GPIO_COUNTER_COUNT_2].id = GPIO_COUNTER_COUNT_2;
+	priv->counts[GPIO_COUNTER_COUNT_2].name = "Count 2";
+	priv->counts[GPIO_COUNTER_COUNT_2].functions_list =
+		gpio_counter_count2_functions;
+	priv->counts[GPIO_COUNTER_COUNT_2].num_functions =
+		ARRAY_SIZE(gpio_counter_count2_functions);
+	priv->counts[GPIO_COUNTER_COUNT_2].synapses = priv->synapses_count2;
+	priv->counts[GPIO_COUNTER_COUNT_2].num_synapses = n_c2;
+	priv->counts[GPIO_COUNTER_COUNT_2].ext = gpio_counter_count_ext;
+	priv->counts[GPIO_COUNTER_COUNT_2].num_ext =
+		ARRAY_SIZE(gpio_counter_count_ext);
+
+	return 0;
+}
+
+static int gpio_counter_request_irqs(struct device *dev,
+				     struct counter_device *counter)
+{
+	struct gpio_counter_priv *priv = counter_priv(counter);
+	irq_handler_t idx_handlers[GPIO_COUNTER_MAX_INDEX] = {
+		gpio_counter_index1_isr,
+		gpio_counter_index2_isr,
+	};
+	const char *idx_names[GPIO_COUNTER_MAX_INDEX] = {
+		"gpio-counter-index1",
+		"gpio-counter-index2",
+	};
+	int ret, i;
+
+	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");
+
+	for (i = 0; i < priv->n_idx; i++) {
+		ret = devm_request_irq(dev, priv->irq_idx[i], idx_handlers[i],
+				       IRQF_TRIGGER_RISING | IRQF_NO_AUTOEN,
+				       idx_names[i], counter);
+		if (ret)
+			return dev_err_probe(dev, ret,
+					     "failed to request IRQ for index %d\n",
+					     i);
+	}
+
+	return 0;
+}
+
+static int gpio_counter_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct counter_device *counter;
+	struct gpio_counter_priv *priv;
+	int num_signals;
+	int i, ret;
+
+	counter = devm_counter_alloc(dev, sizeof(*priv));
+	if (!counter)
+		return -ENOMEM;
+
+	priv = counter_priv(counter);
+	spin_lock_init(&priv->lock);
+	ret = devm_mutex_init(dev, &priv->enable_lock);
+	if (ret)
+		return ret;
+
+	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");
+
+	ret = gpio_counter_get_index_gpios(dev, priv);
+	if (ret)
+		return ret;
+
+	if (gpiod_cansleep(priv->gpio_a) || gpiod_cansleep(priv->gpio_b))
+		return dev_err_probe(dev, -EINVAL,
+				     "signal GPIO may sleep, not usable in hardirq\n");
+	for (i = 0; i < priv->n_idx; i++) {
+		if (gpiod_cansleep(priv->gpio_idx[i]))
+			return dev_err_probe(dev, -EINVAL,
+					     "index GPIO %d may sleep, not usable in hardirq\n",
+					     i);
+	}
+
+	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");
+
+	for (i = 0; i < priv->n_idx; i++) {
+		priv->irq_idx[i] = gpiod_to_irq(priv->gpio_idx[i]);
+		if (priv->irq_idx[i] < 0)
+			return dev_err_probe(dev, priv->irq_idx[i],
+					     "failed to get IRQ for index %d\n",
+					     i);
+	}
+
+	priv->prev_a = !!gpiod_get_value(priv->gpio_a);
+	priv->prev_b = !!gpiod_get_value(priv->gpio_b);
+
+	priv->count_priv[GPIO_COUNTER_COUNT_1].function =
+		COUNTER_FUNCTION_QUADRATURE_X4;
+	priv->count_priv[GPIO_COUNTER_COUNT_1].direction =
+		COUNTER_COUNT_DIRECTION_FORWARD;
+	priv->count_priv[GPIO_COUNTER_COUNT_1].ceiling = U64_MAX;
+
+	priv->count_priv[GPIO_COUNTER_COUNT_2].function =
+		COUNTER_FUNCTION_INCREASE;
+	priv->count_priv[GPIO_COUNTER_COUNT_2].direction =
+		COUNTER_COUNT_DIRECTION_FORWARD;
+	priv->count_priv[GPIO_COUNTER_COUNT_2].ceiling = U64_MAX;
+
+	num_signals = gpio_counter_setup_signals(priv);
+	gpio_counter_setup_synapses(priv);
+
+	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->counts;
+	counter->num_counts	= ARRAY_SIZE(priv->counts);
+
+	ret = gpio_counter_request_irqs(dev, counter);
+	if (ret)
+		return ret;
+
+	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%s)\n",
+		 priv->n_idx >= 1 ? ", Index 1" : "",
+		 priv->n_idx >= 2 ? ", Index 2" : "");
+
+	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");
+MODULE_LICENSE("GPL");
+MODULE_IMPORT_NS("COUNTER");
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH v6 3/3] MAINTAINERS: add entry for GPIO counter driver
  2026-06-28 21:02 [PATCH v6 0/3] counter: add GPIO-based counter driver Wadim Mueller
  2026-06-28 21:02 ` [PATCH v6 1/3] dt-bindings: counter: add gpio-counter binding Wadim Mueller
  2026-06-28 21:02 ` [PATCH v6 2/3] counter: add GPIO-based counter driver Wadim Mueller
@ 2026-06-28 21:02 ` Wadim Mueller
  2 siblings, 0 replies; 6+ messages in thread
From: Wadim Mueller @ 2026-06-28 21:02 UTC (permalink / raw)
  To: wbg
  Cc: krzk+dt, robh, conor+dt, o.rempel, linux-iio, devicetree,
	linux-kernel, Wadim Mueller, kernel

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


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* Re: [PATCH v6 1/3] dt-bindings: counter: add gpio-counter binding
  2026-06-28 21:02 ` [PATCH v6 1/3] dt-bindings: counter: add gpio-counter binding Wadim Mueller
@ 2026-06-29 15:35   ` Conor Dooley
  0 siblings, 0 replies; 6+ messages in thread
From: Conor Dooley @ 2026-06-29 15:35 UTC (permalink / raw)
  To: Wadim Mueller
  Cc: wbg, krzk+dt, robh, conor+dt, o.rempel, linux-iio, devicetree,
	linux-kernel, kernel

[-- Attachment #1: Type: text/plain, Size: 75 bytes --]

Acked-by: Conor Dooley <conor.dooley@microchip.com>
pw-bot: not-applicable

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [PATCH v6 2/3] counter: add GPIO-based counter driver
  2026-06-28 21:02 ` [PATCH v6 2/3] counter: add GPIO-based counter driver Wadim Mueller
@ 2026-07-07 10:44   ` William Breathitt Gray
  0 siblings, 0 replies; 6+ messages in thread
From: William Breathitt Gray @ 2026-07-07 10:44 UTC (permalink / raw)
  To: Wadim Mueller
  Cc: William Breathitt Gray, krzk+dt, robh, conor+dt, o.rempel,
	linux-iio, devicetree, linux-kernel, kernel

On Sun, Jun 28, 2026 at 11:02:40PM +0200, Wadim Mueller wrote:
> Add a platform driver that turns plain GPIOs into a counter device.
> Edge interrupts on signal-a, signal-b (and optional index) are
> decoded in software. Quadrature steps are validated with a 2-bit
> Gray-code parity check; direction comes from the same identity.
> 
> The driver exposes two Counts on the same wires:
> 
>   "Count 1" - Synapses on A, B and optional Index 1. Supports
>               QUADRATURE_X1_{A,B} / X2_{A,B} / X4, PULSE_DIRECTION,
>               INCREASE and DECREASE.
>   "Count 2" - Synapse on B and optional Index 2. Supports INCREASE
>               and DECREASE only.
> 
> Per-Count state (value, ceiling, preset, enable, function, direction)
> lives in struct gpio_counter_count_priv so new per-Count features
> (e.g. floor) can be added without touching parallel arrays.
> 
> Sleepable GPIO providers (I2C/SPI expanders) are rejected at probe
> since the ISRs run in hardirq context.
> 
> Signed-off-by: Wadim Mueller <wafgo01@gmail.com>

Hi Wadim,

I thought some more about the "enabled" race condition we discussed
previously [^1]. I realized it's not a problem afterall because "enable"
refers to the Count and not inherently the interrupt. In other words, an
interrupt firing during a "disabled" state is okay as long as the Count
is not affected. Because gpio_counter_a_isr and gpio_counter_b_isr only
update the Counts when they are enabled, we should be fine.

It looks like we're almost there with this driver, so hopefully v7 will
be our final revision. Some comments follow below.

> +enum gpio_counter_signal_id {
> +	GPIO_COUNTER_SIGNAL_A = 0,
> +	GPIO_COUNTER_SIGNAL_B,
> +	GPIO_COUNTER_SIGNAL_INDEX1,
> +	GPIO_COUNTER_SIGNAL_INDEX2,
> +	GPIO_COUNTER_NUM_SIGNALS_MAX,
> +};
> +
> +enum gpio_counter_count_id {
> +	GPIO_COUNTER_COUNT_1 = 0,
> +	GPIO_COUNTER_COUNT_2,
> +	GPIO_COUNTER_NUM_COUNTS,
> +};

I'd rename this to GPIO_COUNTER_NUM_COUNTS_MAX for consistency with
GPIO_COUNTER_NUM_SIGNALS_MAX. Albeit, this is nitpick so I don't mind
keeping the name if you prefer it that way.

> +
> +#define GPIO_COUNTER_COUNT1_NUM_SYNAPSES	3	/* A, B, Index 1 */
> +#define GPIO_COUNTER_COUNT2_NUM_SYNAPSES	2	/* B, Index 2 */
> +#define GPIO_COUNTER_MAX_INDEX			2

The theoretical index maximum would be the number of Counts, so you can
set it to GPIO_COUNTER_NUM_COUNTS so it always matches in case we add
new Counts in the future.

> +struct gpio_counter_priv {
> +	struct gpio_desc *gpio_a;
> +	struct gpio_desc *gpio_b;
> +	struct gpio_desc *gpio_idx[GPIO_COUNTER_MAX_INDEX];
> +
> +	int irq_a;
> +	int irq_b;
> +	int irq_idx[GPIO_COUNTER_MAX_INDEX];
> +
> +	int n_idx;
> +
> +	/* protects count state, prev_a, prev_b */
> +	spinlock_t lock;
> +
> +	/* serialises enable_write() callers and b_irq_users */
> +	struct mutex enable_lock;
> +
> +	/* irq_b is shared by Count 1 and Count 2 (under enable_lock) */
> +	int b_irq_users;
> +
> +	int prev_a;
> +	int prev_b;
> +
> +	struct gpio_counter_count_priv count_priv[GPIO_COUNTER_NUM_COUNTS];
> +
> +	struct counter_signal  signals[GPIO_COUNTER_NUM_SIGNALS_MAX];
> +	struct counter_synapse synapses_count1[GPIO_COUNTER_COUNT1_NUM_SYNAPSES];
> +	struct counter_synapse synapses_count2[GPIO_COUNTER_COUNT2_NUM_SYNAPSES];

A future goal is to support pulse-direction mode for Count 2 which will
require two GPIO lines similar to Count 1. So instead of synapses_count1
and synapses_count2, I suggest a GPIO_COUNTER_COUNT_NUM_SYNAPSES set to 3
and defining a single multi-dimensional array called
synapses[GPIO_COUNTER_NUM_COUNTS][GPIO_COUNTER_COUNT_NUM_SYNAPSES].

> +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);
> +	struct gpio_counter_count_priv *cp =
> +		&priv->count_priv[GPIO_COUNTER_COUNT_1];
> +	unsigned long flags;
> +	bool enabled;
> +	int a, b;
> +
> +	/* !! drops 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);
> +
> +	enabled = cp->enabled;
> +	if (!enabled)
> +		goto out;

Shouldn't the enable state check occur before we get the current values
of A and B? If we are in a disabled state, we exit early and thus have
no need for the A and B values.

> +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);
> +	struct gpio_counter_count_priv *c1 =
> +		&priv->count_priv[GPIO_COUNTER_COUNT_1];
> +	struct gpio_counter_count_priv *c2 =
> +		&priv->count_priv[GPIO_COUNTER_COUNT_2];
> +	bool c1_enabled, c2_enabled;
> +	unsigned long flags;
> +	int a, b;
> +
> +	a = !!gpiod_get_value(priv->gpio_a);
> +	b = !!gpiod_get_value(priv->gpio_b);

Same comment as in gpio_counter_a_isr().

> +static void gpio_counter_index_pulse(struct counter_device *counter,
> +				     enum gpio_counter_count_id count_id)
> +{
> +	struct gpio_counter_priv *priv = counter_priv(counter);
> +	struct gpio_counter_count_priv *cp = &priv->count_priv[count_id];
> +	unsigned long flags;
> +
> +	spin_lock_irqsave(&priv->lock, flags);
> +	if (cp->preset_enabled) {
> +		cp->value = cp->preset;
> +		if (cp->value > cp->ceiling)
> +			cp->value = cp->ceiling;

This is just an assignment so a ternary may communicate the intent more
succinctly:

	if (cp->preset_enabled)
		cp->value = (cp->preset > cp->ceiling) ? cp->ceiling : cp->preset;

> +static int gpio_counter_count_read(struct counter_device *counter,
> +				   struct counter_count *count, u64 *val)
> +{
> +	struct gpio_counter_priv *priv = counter_priv(counter);
> +	struct gpio_counter_count_priv *cp = &priv->count_priv[count->id];
> +	unsigned long flags;
> +
> +	spin_lock_irqsave(&priv->lock, flags);
> +	*val = cp->value;
> +	spin_unlock_irqrestore(&priv->lock, flags);

This is a single read operation so the lock is unnecessary here.

> +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);
> +	struct gpio_counter_count_priv *cp = &priv->count_priv[count->id];
> +	unsigned long flags;
> +
> +	spin_lock_irqsave(&priv->lock, flags);
> +	*function = cp->function;
> +	spin_unlock_irqrestore(&priv->lock, flags);

Same comment as in the count_read() callback.

> +static int gpio_counter_ceiling_read(struct counter_device *counter,
> +				     struct counter_count *count, u64 *val)
> +{
> +	struct gpio_counter_priv *priv = counter_priv(counter);
> +	struct gpio_counter_count_priv *cp = &priv->count_priv[count->id];
> +	unsigned long flags;
> +
> +	spin_lock_irqsave(&priv->lock, flags);
> +	*val = cp->ceiling;
> +	spin_unlock_irqrestore(&priv->lock, flags);

Same comment as in the count_read() callback.

> +static int gpio_counter_enable_read(struct counter_device *counter,
> +				    struct counter_count *count, u8 *enable)
> +{
> +	struct gpio_counter_priv *priv = counter_priv(counter);
> +	struct gpio_counter_count_priv *cp = &priv->count_priv[count->id];
> +	unsigned long flags;
> +
> +	spin_lock_irqsave(&priv->lock, flags);
> +	*enable = cp->enabled;
> +	spin_unlock_irqrestore(&priv->lock, flags);

Same comment as in the count_read() callback.

> +/* enable_lock held */
> +static void gpio_counter_enable_b(struct gpio_counter_priv *priv)
> +{
> +	if (priv->b_irq_users++ == 0)
> +		enable_irq(priv->irq_b);
> +}
> +
> +/* enable_lock held */
> +static void gpio_counter_disable_b(struct gpio_counter_priv *priv)
> +{
> +	if (--priv->b_irq_users == 0)
> +		disable_irq(priv->irq_b);
> +}

Conceptually, updating b_irq_users and checking for zero are two
separate tasks, so dedicate each task to its own line.

> +static int gpio_counter_enable_write(struct counter_device *counter,
> +				     struct counter_count *count, u8 enable)
> +{
> +	struct gpio_counter_priv *priv = counter_priv(counter);
> +	struct gpio_counter_count_priv *cp = &priv->count_priv[count->id];
> +	unsigned long flags;
> +	bool current_state;
> +
> +	guard(mutex)(&priv->enable_lock);
> +
> +	spin_lock_irqsave(&priv->lock, flags);
> +	current_state = cp->enabled;
> +	spin_unlock_irqrestore(&priv->lock, flags);

Same comment as in the count_read() callback.

> +
> +	if (current_state == enable)

The current_state variable is only used here so it can go away entirely
if you replace it with cp->enabled directly.

> +		return 0;
> +
> +	/* disable_irq() may sleep, so it can not run under the spinlock. */
> +	if (enable) {
> +		spin_lock_irqsave(&priv->lock, flags);
> +		priv->prev_a = !!gpiod_get_value(priv->gpio_a);
> +		priv->prev_b = !!gpiod_get_value(priv->gpio_b);
> +		cp->enabled = true;
> +		spin_unlock_irqrestore(&priv->lock, flags);
> +		gpio_counter_enable_irqs(priv, count->id);

To ensures everything is ready to go before we start updating the
Counts, should the IRQs be enabled first before setting cp->enabled to
true?

> +	} else {
> +		spin_lock_irqsave(&priv->lock, flags);
> +		cp->enabled = false;
> +		spin_unlock_irqrestore(&priv->lock, flags);
> +		gpio_counter_disable_irqs(priv, count->id);
> +	}
> +
> +	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);
> +	struct gpio_counter_count_priv *cp = &priv->count_priv[count->id];
> +	unsigned long flags;
> +
> +	spin_lock_irqsave(&priv->lock, flags);
> +	*direction = cp->direction;
> +	spin_unlock_irqrestore(&priv->lock, flags);

Same comment as in the count_read() callback.

> +static int gpio_counter_preset_read(struct counter_device *counter,
> +				    struct counter_count *count, u64 *val)
> +{
> +	struct gpio_counter_priv *priv = counter_priv(counter);
> +	struct gpio_counter_count_priv *cp = &priv->count_priv[count->id];
> +	unsigned long flags;
> +
> +	spin_lock_irqsave(&priv->lock, flags);
> +	*val = cp->preset;
> +	spin_unlock_irqrestore(&priv->lock, flags);

Same comment as in the count_read() callback.

> +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);
> +	struct gpio_counter_count_priv *cp = &priv->count_priv[count->id];
> +	unsigned long flags;
> +
> +	spin_lock_irqsave(&priv->lock, flags);
> +	*val = cp->preset_enabled;
> +	spin_unlock_irqrestore(&priv->lock, flags);

Same comment as in the count_read() callback.

> +static int gpio_counter_get_index_gpios(struct device *dev,
> +					struct gpio_counter_priv *priv)
> +{
> +	struct gpio_descs *descs;
> +	int i;
> +
> +	descs = devm_gpiod_get_array_optional(dev, "index", GPIOD_IN);
> +	if (IS_ERR(descs))
> +		return dev_err_probe(dev, PTR_ERR(descs),
> +				     "failed to get index GPIOs\n");
> +	if (!descs) {
> +		priv->n_idx = 0;
> +		return 0;
> +	}
> +
> +	if (descs->ndescs < 1 || descs->ndescs > GPIO_COUNTER_MAX_INDEX)
> +		return dev_err_probe(dev, -EINVAL,
> +				     "index-gpios: expected 1..%d entries, got %u\n",
> +				     GPIO_COUNTER_MAX_INDEX, descs->ndescs);
> +
> +	for (i = 0; i < descs->ndescs; i++)
> +		priv->gpio_idx[i] = descs->desc[i];
> +	priv->n_idx = descs->ndescs;
> +
> +	return 0;
> +}

Initially, I thought it would be fine to let the index-gpios DT entries
map sequentially to the Counts, but now I noticed two problems with this
approach.

First, the code around n_idx is much harder to grok. For example, in
gpio_counter_enable_irqs() the n_idx value is used as an indirect way of
determining whether an index IRQ should be enabled for a Count. The more
natural approach would be a dedicated index variable on each Count, that
way each Count knows its own index IRQ if it has one.

The second problem is that we now require Count 1 to have an index line
even if we only care about Count 2. The would be a problem if the user
has only three GPIO lines available.

As such, we need a different approach in the DT entries that allows
users to directly specify which index lines are for which Counts. That
way, you can store this information in gpio_counter_count_priv and know
easily whether a particular Count has an index IRQ and what it is.

William Breathitt Gray

[^1]: https://lore.kernel.org/all/20260623053800.120909-1-wbg@kernel.org/

^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2026-07-07 10:45 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-28 21:02 [PATCH v6 0/3] counter: add GPIO-based counter driver Wadim Mueller
2026-06-28 21:02 ` [PATCH v6 1/3] dt-bindings: counter: add gpio-counter binding Wadim Mueller
2026-06-29 15:35   ` Conor Dooley
2026-06-28 21:02 ` [PATCH v6 2/3] counter: add GPIO-based counter driver Wadim Mueller
2026-07-07 10:44   ` William Breathitt Gray
2026-06-28 21:02 ` [PATCH v6 3/3] MAINTAINERS: add entry for GPIO " Wadim Mueller

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox