* [PATCH v3 0/3] counter: add GPIO-based quadrature encoder driver
@ 2026-05-01 20:07 Wadim Mueller
2026-05-01 20:07 ` [PATCH v3 1/3] dt-bindings: counter: add gpio-quadrature-encoder binding Wadim Mueller
` (4 more replies)
0 siblings, 5 replies; 16+ messages in thread
From: Wadim Mueller @ 2026-05-01 20:07 UTC (permalink / raw)
To: wbg
Cc: conor+dt, krzk+dt, robh, conor.dooley, linux-iio, devicetree,
linux-kernel
This series adds a new counter subsystem driver that implements
quadrature encoder position tracking using plain GPIO pins with
edge-triggered interrupts.
The driver is intended for low to medium speed rotary encoders where
hardware counter peripherals (eQEP, FTM, etc.) are unavailable or
already in use. It targets the same use-cases as interrupt-cnt.c but
provides full quadrature decoding instead of simple pulse counting.
Features:
- X1, X2, X4 quadrature decoding and pulse-direction mode
- Optional index signal for zero-reset
- Configurable ceiling (position clamping)
- Standard counter subsystem sysfs + chrdev interface
- Enable/disable via sysfs with IRQ gating
Tested on TI AM64x (Cortex-A53) with a motor-driven rotary encoder
at up to 2 kHz quadrature edge rate.
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 hardware, not
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 | 710 ++++++++++++++++++
5 files changed, 793 insertions(+)
create mode 100644 Documentation/devicetree/bindings/counter/gpio-quadrature-encoder.yaml
create mode 100644 drivers/counter/gpio-quadrature-encoder.c
--
2.52.0
^ permalink raw reply [flat|nested] 16+ messages in thread* [PATCH v3 1/3] dt-bindings: counter: add gpio-quadrature-encoder binding 2026-05-01 20:07 [PATCH v3 0/3] counter: add GPIO-based quadrature encoder driver Wadim Mueller @ 2026-05-01 20:07 ` Wadim Mueller 2026-05-01 20:07 ` [PATCH v3 2/3] counter: add GPIO-based quadrature encoder driver Wadim Mueller ` (3 subsequent siblings) 4 siblings, 0 replies; 16+ messages in thread From: Wadim Mueller @ 2026-05-01 20:07 UTC (permalink / raw) To: wbg Cc: conor+dt, krzk+dt, robh, conor.dooley, linux-iio, devicetree, linux-kernel Add devicetree binding documentation for the GPIO-based quadrature encoder counter driver. The driver reads A/B quadrature signals and an optional index pulse via edge-triggered GPIO interrupts, supporting X1, X2, X4 quadrature decoding and pulse-direction mode. This is useful on SoCs that lack a dedicated hardware quadrature decoder or where the encoder is wired to generic GPIO pins. Signed-off-by: Wadim Mueller <wafgo01@gmail.com> Acked-by: Conor Dooley <conor.dooley@microchip.com> --- .../counter/gpio-quadrature-encoder.yaml | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 Documentation/devicetree/bindings/counter/gpio-quadrature-encoder.yaml diff --git a/Documentation/devicetree/bindings/counter/gpio-quadrature-encoder.yaml b/Documentation/devicetree/bindings/counter/gpio-quadrature-encoder.yaml new file mode 100644 index 000000000..741396b29 --- /dev/null +++ b/Documentation/devicetree/bindings/counter/gpio-quadrature-encoder.yaml @@ -0,0 +1,60 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/counter/gpio-quadrature-encoder.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: GPIO-based Quadrature Encoder + +maintainers: + - Wadim Mueller <wadim.mueller@cmblu.de> + +description: | + A generic GPIO-based quadrature encoder counter. Reads A/B quadrature + signals and an optional index pulse via edge-triggered GPIO interrupts. + Supports X1, X2, X4 quadrature decoding and pulse-direction mode. + + This is useful on SoCs that lack a dedicated hardware quadrature + decoder (eQEP, QEI, etc.) or where the encoder is wired to generic + GPIO pins rather than to a dedicated peripheral. + +properties: + compatible: + const: gpio-quadrature-encoder + + encoder-a-gpios: + maxItems: 1 + description: + GPIO connected to the encoder's A (phase A) output. + + encoder-b-gpios: + maxItems: 1 + description: + GPIO connected to the encoder's B (phase B) output. + + encoder-index-gpios: + maxItems: 1 + description: + Optional GPIO connected to the encoder's index (Z) output. + The index signal pulses once per revolution and can be used + as a reference point for absolute position tracking. + +required: + - compatible + - encoder-a-gpios + - encoder-b-gpios + +additionalProperties: false + +examples: + - | + #include <dt-bindings/gpio/gpio.h> + + quadrature-encoder { + compatible = "gpio-quadrature-encoder"; + encoder-a-gpios = <&gpio0 10 GPIO_ACTIVE_LOW>; + encoder-b-gpios = <&gpio0 11 GPIO_ACTIVE_LOW>; + encoder-index-gpios = <&gpio0 12 GPIO_ACTIVE_LOW>; + }; + +... -- 2.52.0 ^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH v3 2/3] counter: add GPIO-based quadrature encoder driver 2026-05-01 20:07 [PATCH v3 0/3] counter: add GPIO-based quadrature encoder driver Wadim Mueller 2026-05-01 20:07 ` [PATCH v3 1/3] dt-bindings: counter: add gpio-quadrature-encoder binding Wadim Mueller @ 2026-05-01 20:07 ` Wadim Mueller 2026-05-04 20:54 ` Krzysztof Kozlowski 2026-05-15 5:48 ` William Breathitt Gray 2026-05-01 20:07 ` [PATCH v3 3/3] MAINTAINERS: add entry for GPIO quadrature encoder counter driver Wadim Mueller ` (2 subsequent siblings) 4 siblings, 2 replies; 16+ messages in thread From: Wadim Mueller @ 2026-05-01 20:07 UTC (permalink / raw) To: wbg Cc: conor+dt, krzk+dt, robh, conor.dooley, linux-iio, devicetree, linux-kernel Add a platform driver that turns ordinary GPIOs into a quadrature encoder counter device. The driver requests edge-triggered interrupts on the A and B (and optional Index) GPIOs and decodes the quadrature signal in software using a classic state-table approach. Supported counting modes: - Quadrature X1 (count on A rising edge only) - Quadrature X2 (count on both A edges) - Quadrature X4 (count on every A and B edge) - Pulse-direction (A = pulse, B = direction) An optional index signal resets the count to zero on its rising edge when enabled through sysfs. A configurable ceiling clamps the count to [0, ceiling]. Signed-off-by: Wadim Mueller <wafgo01@gmail.com> --- drivers/counter/Kconfig | 15 + drivers/counter/Makefile | 1 + drivers/counter/gpio-quadrature-encoder.c | 710 ++++++++++++++++++++++ 3 files changed, 726 insertions(+) create mode 100644 drivers/counter/gpio-quadrature-encoder.c diff --git a/drivers/counter/Kconfig b/drivers/counter/Kconfig index d30d22dfe..72c5c8159 100644 --- a/drivers/counter/Kconfig +++ b/drivers/counter/Kconfig @@ -68,6 +68,21 @@ config INTEL_QEP To compile this driver as a module, choose M here: the module will be called intel-qep. +config GPIO_QUADRATURE_ENCODER + tristate "GPIO-based quadrature encoder counter driver" + depends on GPIOLIB + help + Select this option to enable the GPIO-based quadrature encoder + counter driver. It reads A/B quadrature signals and an optional + index pulse via edge-triggered GPIO interrupts, supporting X1, X2, + X4 quadrature decoding and pulse-direction mode. + + This is useful on SoCs that lack a dedicated hardware quadrature + decoder or where the encoder is wired to generic GPIO pins. + + To compile this driver as a module, choose M here: the + module will be called gpio-quadrature-encoder. + config INTERRUPT_CNT tristate "Interrupt counter driver" depends on GPIOLIB diff --git a/drivers/counter/Makefile b/drivers/counter/Makefile index fa3c1d08f..2bef64d10 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_QUADRATURE_ENCODER) += gpio-quadrature-encoder.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-quadrature-encoder.c b/drivers/counter/gpio-quadrature-encoder.c new file mode 100644 index 000000000..0822f0a8a --- /dev/null +++ b/drivers/counter/gpio-quadrature-encoder.c @@ -0,0 +1,710 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * GPIO-based Quadrature Encoder Counter Driver + * + * Reads quadrature encoder signals (A, B, and optional Index) via GPIOs. + * Supports X1, X2, X4 quadrature decoding and pulse-direction mode. + * + * 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_qenc_function { + GPIO_QENC_FUNC_QUAD_X1 = 0, + GPIO_QENC_FUNC_QUAD_X2, + GPIO_QENC_FUNC_QUAD_X4, + GPIO_QENC_FUNC_PULSE_DIR, +}; + +enum gpio_qenc_signal_id { + GPIO_QENC_SIGNAL_A = 0, + GPIO_QENC_SIGNAL_B, + GPIO_QENC_SIGNAL_INDEX, +}; + +struct gpio_qenc_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; + + s64 count; + u64 ceiling; + bool enabled; + enum counter_count_direction direction; + enum gpio_qenc_function function; + + int prev_a; + int prev_b; + + bool index_enabled; + + struct counter_signal signals[3]; + struct counter_synapse synapses[3]; + struct counter_count cnts; +}; + +/* + * Quadrature state table for X4 decoding. + * Rows = previous state (A<<1 | B), Columns = new state (A<<1 | B). + * Values: 0 = no change, +1 = forward, -1 = backward, 2 = error (skip). + */ +static const int quad_table[4][4] = { + /* 00 01 10 11 <- new */ + /* 00 */ { 0, -1, 1, 2 }, + /* 01 */ { 1, 0, 2, -1 }, + /* 10 */ { -1, 2, 0, 1 }, + /* 11 */ { 2, 1, -1, 0 }, +}; + +static void gpio_qenc_update_count(struct gpio_qenc_priv *priv, int delta) +{ + s64 new_count; + + if (!delta) + return; + + new_count = priv->count + delta; + + if (priv->ceiling) { + if (new_count < 0) + new_count = 0; + else if (new_count > (s64)priv->ceiling) + new_count = priv->ceiling; + } + + priv->count = new_count; + priv->direction = (delta > 0) ? COUNTER_COUNT_DIRECTION_FORWARD + : COUNTER_COUNT_DIRECTION_BACKWARD; +} + +static irqreturn_t gpio_qenc_a_isr(int irq, void *dev_id) +{ + struct counter_device *counter = dev_id; + struct gpio_qenc_priv *priv = counter_priv(counter); + unsigned long flags; + int a, b, prev_state, new_state, delta; + + spin_lock_irqsave(&priv->lock, flags); + + if (!priv->enabled) + goto out; + + a = gpiod_get_value(priv->gpio_a); + b = gpiod_get_value(priv->gpio_b); + + prev_state = (priv->prev_a << 1) | priv->prev_b; + new_state = (a << 1) | b; + + switch (priv->function) { + case GPIO_QENC_FUNC_QUAD_X4: + delta = quad_table[prev_state][new_state]; + if (delta == 2) + delta = 0; + gpio_qenc_update_count(priv, delta); + break; + + case GPIO_QENC_FUNC_QUAD_X2: + delta = quad_table[prev_state][new_state]; + if (delta == 2) + delta = 0; + gpio_qenc_update_count(priv, delta); + break; + + case GPIO_QENC_FUNC_QUAD_X1: + if (!priv->prev_a && a) { + delta = b ? -1 : 1; + gpio_qenc_update_count(priv, delta); + } + break; + + case GPIO_QENC_FUNC_PULSE_DIR: + if (!priv->prev_a && a) { + delta = b ? -1 : 1; + gpio_qenc_update_count(priv, delta); + } + break; + } + + 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; + +out: + spin_unlock_irqrestore(&priv->lock, flags); + return IRQ_HANDLED; +} + +static irqreturn_t gpio_qenc_b_isr(int irq, void *dev_id) +{ + struct counter_device *counter = dev_id; + struct gpio_qenc_priv *priv = counter_priv(counter); + unsigned long flags; + int a, b, prev_state, new_state, delta; + + spin_lock_irqsave(&priv->lock, flags); + + if (!priv->enabled) + goto out; + + a = gpiod_get_value(priv->gpio_a); + b = gpiod_get_value(priv->gpio_b); + + prev_state = (priv->prev_a << 1) | priv->prev_b; + new_state = (a << 1) | b; + + switch (priv->function) { + case GPIO_QENC_FUNC_QUAD_X4: + delta = quad_table[prev_state][new_state]; + if (delta == 2) + delta = 0; + gpio_qenc_update_count(priv, delta); + break; + + case GPIO_QENC_FUNC_QUAD_X2: + /* X2: only A-channel edges update count */ + break; + + case GPIO_QENC_FUNC_QUAD_X1: + case GPIO_QENC_FUNC_PULSE_DIR: + break; + } + + priv->prev_a = a; + priv->prev_b = b; + + spin_unlock_irqrestore(&priv->lock, flags); + return IRQ_HANDLED; + +out: + spin_unlock_irqrestore(&priv->lock, flags); + return IRQ_HANDLED; +} + +static irqreturn_t gpio_qenc_index_isr(int irq, void *dev_id) +{ + struct counter_device *counter = dev_id; + struct gpio_qenc_priv *priv = counter_priv(counter); + unsigned long flags; + + spin_lock_irqsave(&priv->lock, flags); + + if (priv->enabled && priv->index_enabled) + priv->count = 0; + + spin_unlock_irqrestore(&priv->lock, flags); + + counter_push_event(counter, COUNTER_EVENT_INDEX, 0); + + return IRQ_HANDLED; +} + +static int gpio_qenc_count_read(struct counter_device *counter, + struct counter_count *count, u64 *val) +{ + struct gpio_qenc_priv *priv = counter_priv(counter); + unsigned long flags; + + spin_lock_irqsave(&priv->lock, flags); + *val = (u64)priv->count; + spin_unlock_irqrestore(&priv->lock, flags); + + return 0; +} + +static int gpio_qenc_count_write(struct counter_device *counter, + struct counter_count *count, const u64 val) +{ + struct gpio_qenc_priv *priv = counter_priv(counter); + unsigned long flags; + + spin_lock_irqsave(&priv->lock, flags); + + if (priv->ceiling && val > priv->ceiling) { + spin_unlock_irqrestore(&priv->lock, flags); + return -EINVAL; + } + + priv->count = (s64)val; + spin_unlock_irqrestore(&priv->lock, flags); + + return 0; +} + +static const enum counter_function gpio_qenc_functions[] = { + COUNTER_FUNCTION_QUADRATURE_X1_A, + COUNTER_FUNCTION_QUADRATURE_X2_A, + COUNTER_FUNCTION_QUADRATURE_X4, + COUNTER_FUNCTION_PULSE_DIRECTION, +}; + +static int gpio_qenc_function_read(struct counter_device *counter, + struct counter_count *count, + enum counter_function *function) +{ + struct gpio_qenc_priv *priv = counter_priv(counter); + unsigned long flags; + + spin_lock_irqsave(&priv->lock, flags); + + switch (priv->function) { + case GPIO_QENC_FUNC_QUAD_X1: + *function = COUNTER_FUNCTION_QUADRATURE_X1_A; + break; + case GPIO_QENC_FUNC_QUAD_X2: + *function = COUNTER_FUNCTION_QUADRATURE_X2_A; + break; + case GPIO_QENC_FUNC_QUAD_X4: + *function = COUNTER_FUNCTION_QUADRATURE_X4; + break; + case GPIO_QENC_FUNC_PULSE_DIR: + *function = COUNTER_FUNCTION_PULSE_DIRECTION; + break; + } + + spin_unlock_irqrestore(&priv->lock, flags); + return 0; +} + +static int gpio_qenc_function_write(struct counter_device *counter, + struct counter_count *count, + enum counter_function function) +{ + struct gpio_qenc_priv *priv = counter_priv(counter); + unsigned long flags; + + spin_lock_irqsave(&priv->lock, flags); + + switch (function) { + case COUNTER_FUNCTION_QUADRATURE_X1_A: + priv->function = GPIO_QENC_FUNC_QUAD_X1; + break; + case COUNTER_FUNCTION_QUADRATURE_X2_A: + priv->function = GPIO_QENC_FUNC_QUAD_X2; + break; + case COUNTER_FUNCTION_QUADRATURE_X4: + priv->function = GPIO_QENC_FUNC_QUAD_X4; + break; + case COUNTER_FUNCTION_PULSE_DIRECTION: + priv->function = GPIO_QENC_FUNC_PULSE_DIR; + break; + default: + spin_unlock_irqrestore(&priv->lock, flags); + return -EINVAL; + } + + spin_unlock_irqrestore(&priv->lock, flags); + return 0; +} + +static const enum counter_synapse_action gpio_qenc_synapse_actions[] = { + COUNTER_SYNAPSE_ACTION_BOTH_EDGES, + COUNTER_SYNAPSE_ACTION_RISING_EDGE, + COUNTER_SYNAPSE_ACTION_NONE, +}; + +static int gpio_qenc_action_read(struct counter_device *counter, + struct counter_count *count, + struct counter_synapse *synapse, + enum counter_synapse_action *action) +{ + struct gpio_qenc_priv *priv = counter_priv(counter); + enum gpio_qenc_signal_id signal_id = synapse->signal->id; + + switch (priv->function) { + case GPIO_QENC_FUNC_QUAD_X4: + if (signal_id == GPIO_QENC_SIGNAL_A || + signal_id == GPIO_QENC_SIGNAL_B) + *action = COUNTER_SYNAPSE_ACTION_BOTH_EDGES; + else + *action = COUNTER_SYNAPSE_ACTION_RISING_EDGE; + return 0; + + case GPIO_QENC_FUNC_QUAD_X2: + if (signal_id == GPIO_QENC_SIGNAL_A) + *action = COUNTER_SYNAPSE_ACTION_BOTH_EDGES; + else if (signal_id == GPIO_QENC_SIGNAL_B) + *action = COUNTER_SYNAPSE_ACTION_NONE; + else + *action = COUNTER_SYNAPSE_ACTION_RISING_EDGE; + return 0; + + case GPIO_QENC_FUNC_QUAD_X1: + if (signal_id == GPIO_QENC_SIGNAL_A) + *action = COUNTER_SYNAPSE_ACTION_RISING_EDGE; + else if (signal_id == GPIO_QENC_SIGNAL_B) + *action = COUNTER_SYNAPSE_ACTION_NONE; + else + *action = COUNTER_SYNAPSE_ACTION_RISING_EDGE; + return 0; + + case GPIO_QENC_FUNC_PULSE_DIR: + if (signal_id == GPIO_QENC_SIGNAL_A) + *action = COUNTER_SYNAPSE_ACTION_RISING_EDGE; + else + *action = COUNTER_SYNAPSE_ACTION_NONE; + return 0; + } + + return -EINVAL; +} + +static int gpio_qenc_signal_read(struct counter_device *counter, + struct counter_signal *signal, + enum counter_signal_level *level) +{ + struct gpio_qenc_priv *priv = counter_priv(counter); + struct gpio_desc *gpio; + int ret; + + switch (signal->id) { + case GPIO_QENC_SIGNAL_A: + gpio = priv->gpio_a; + break; + case GPIO_QENC_SIGNAL_B: + gpio = priv->gpio_b; + break; + case GPIO_QENC_SIGNAL_INDEX: + gpio = priv->gpio_index; + break; + default: + return -EINVAL; + } + + if (!gpio) + 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_qenc_events_configure(struct counter_device *counter) +{ + return 0; +} + +static int gpio_qenc_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_qenc_ops = { + .count_read = gpio_qenc_count_read, + .count_write = gpio_qenc_count_write, + .function_read = gpio_qenc_function_read, + .function_write = gpio_qenc_function_write, + .action_read = gpio_qenc_action_read, + .signal_read = gpio_qenc_signal_read, + .events_configure = gpio_qenc_events_configure, + .watch_validate = gpio_qenc_watch_validate, +}; + +static int gpio_qenc_ceiling_read(struct counter_device *counter, + struct counter_count *count, u64 *val) +{ + struct gpio_qenc_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_qenc_ceiling_write(struct counter_device *counter, + struct counter_count *count, const u64 val) +{ + struct gpio_qenc_priv *priv = counter_priv(counter); + unsigned long flags; + + spin_lock_irqsave(&priv->lock, flags); + priv->ceiling = val; + spin_unlock_irqrestore(&priv->lock, flags); + + return 0; +} + +static int gpio_qenc_enable_read(struct counter_device *counter, + struct counter_count *count, u8 *enable) +{ + struct gpio_qenc_priv *priv = counter_priv(counter); + + *enable = priv->enabled; + return 0; +} + +static int gpio_qenc_enable_write(struct counter_device *counter, + struct counter_count *count, u8 enable) +{ + struct gpio_qenc_priv *priv = counter_priv(counter); + unsigned long flags; + + spin_lock_irqsave(&priv->lock, flags); + + if (priv->enabled == !!enable) { + spin_unlock_irqrestore(&priv->lock, flags); + return 0; + } + + if (enable) { + priv->enabled = true; + spin_unlock_irqrestore(&priv->lock, flags); + enable_irq(priv->irq_a); + enable_irq(priv->irq_b); + if (priv->irq_index) + enable_irq(priv->irq_index); + } else { + priv->enabled = false; + spin_unlock_irqrestore(&priv->lock, flags); + disable_irq(priv->irq_a); + disable_irq(priv->irq_b); + if (priv->irq_index) + disable_irq(priv->irq_index); + } + + return 0; +} + +static int gpio_qenc_direction_read(struct counter_device *counter, + struct counter_count *count, u32 *direction) +{ + struct gpio_qenc_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_qenc_index_enable_read(struct counter_device *counter, + struct counter_count *count, u8 *val) +{ + struct gpio_qenc_priv *priv = counter_priv(counter); + + *val = priv->index_enabled; + return 0; +} + +static int gpio_qenc_index_enable_write(struct counter_device *counter, + struct counter_count *count, u8 val) +{ + struct gpio_qenc_priv *priv = counter_priv(counter); + unsigned long flags; + + spin_lock_irqsave(&priv->lock, flags); + priv->index_enabled = !!val; + spin_unlock_irqrestore(&priv->lock, flags); + + return 0; +} + +static struct counter_comp gpio_qenc_count_ext[] = { + COUNTER_COMP_CEILING(gpio_qenc_ceiling_read, gpio_qenc_ceiling_write), + COUNTER_COMP_ENABLE(gpio_qenc_enable_read, gpio_qenc_enable_write), + COUNTER_COMP_DIRECTION(gpio_qenc_direction_read), + COUNTER_COMP_COUNT_BOOL("index_enabled", + gpio_qenc_index_enable_read, + gpio_qenc_index_enable_write), +}; + +static int gpio_qenc_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct counter_device *counter; + struct gpio_qenc_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, "encoder-a", GPIOD_IN); + if (IS_ERR(priv->gpio_a)) + return dev_err_probe(dev, PTR_ERR(priv->gpio_a), + "failed to get encoder-a GPIO\n"); + + priv->gpio_b = devm_gpiod_get(dev, "encoder-b", GPIOD_IN); + if (IS_ERR(priv->gpio_b)) + return dev_err_probe(dev, PTR_ERR(priv->gpio_b), + "failed to get encoder-b GPIO\n"); + + priv->gpio_index = devm_gpiod_get_optional(dev, "encoder-index", + GPIOD_IN); + if (IS_ERR(priv->gpio_index)) + return dev_err_probe(dev, PTR_ERR(priv->gpio_index), + "failed to get encoder-index GPIO\n"); + + has_index = !!priv->gpio_index; + + 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 encoder-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 encoder-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 encoder-index\n"); + } + + priv->prev_a = gpiod_get_value(priv->gpio_a); + priv->prev_b = gpiod_get_value(priv->gpio_b); + + priv->function = GPIO_QENC_FUNC_QUAD_X4; + priv->direction = COUNTER_COUNT_DIRECTION_FORWARD; + + num_signals = has_index ? 3 : 2; + + priv->signals[GPIO_QENC_SIGNAL_A].id = GPIO_QENC_SIGNAL_A; + priv->signals[GPIO_QENC_SIGNAL_A].name = "Signal A"; + + priv->signals[GPIO_QENC_SIGNAL_B].id = GPIO_QENC_SIGNAL_B; + priv->signals[GPIO_QENC_SIGNAL_B].name = "Signal B"; + + if (has_index) { + priv->signals[GPIO_QENC_SIGNAL_INDEX].id = + GPIO_QENC_SIGNAL_INDEX; + priv->signals[GPIO_QENC_SIGNAL_INDEX].name = "Index"; + } + + num_synapses = num_signals; + + priv->synapses[0].actions_list = gpio_qenc_synapse_actions; + priv->synapses[0].num_actions = ARRAY_SIZE(gpio_qenc_synapse_actions); + priv->synapses[0].signal = &priv->signals[GPIO_QENC_SIGNAL_A]; + + priv->synapses[1].actions_list = gpio_qenc_synapse_actions; + priv->synapses[1].num_actions = ARRAY_SIZE(gpio_qenc_synapse_actions); + priv->synapses[1].signal = &priv->signals[GPIO_QENC_SIGNAL_B]; + + if (has_index) { + priv->synapses[2].actions_list = gpio_qenc_synapse_actions; + priv->synapses[2].num_actions = + ARRAY_SIZE(gpio_qenc_synapse_actions); + priv->synapses[2].signal = + &priv->signals[GPIO_QENC_SIGNAL_INDEX]; + } + + priv->cnts.id = 0; + priv->cnts.name = "Position"; + priv->cnts.functions_list = gpio_qenc_functions; + priv->cnts.num_functions = ARRAY_SIZE(gpio_qenc_functions); + priv->cnts.synapses = priv->synapses; + priv->cnts.num_synapses = num_synapses; + priv->cnts.ext = gpio_qenc_count_ext; + priv->cnts.num_ext = ARRAY_SIZE(gpio_qenc_count_ext); + + counter->name = dev_name(dev); + counter->parent = dev; + counter->ops = &gpio_qenc_ops; + counter->signals = priv->signals; + counter->num_signals = num_signals; + counter->counts = &priv->cnts; + counter->num_counts = 1; + + irq_set_status_flags(priv->irq_a, IRQ_NOAUTOEN); + ret = devm_request_irq(dev, priv->irq_a, gpio_qenc_a_isr, + IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, + "gpio-qenc-a", counter); + if (ret) + return dev_err_probe(dev, ret, + "failed to request IRQ for encoder-a\n"); + + irq_set_status_flags(priv->irq_b, IRQ_NOAUTOEN); + ret = devm_request_irq(dev, priv->irq_b, gpio_qenc_b_isr, + IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, + "gpio-qenc-b", counter); + if (ret) + return dev_err_probe(dev, ret, + "failed to request IRQ for encoder-b\n"); + + if (has_index) { + irq_set_status_flags(priv->irq_index, IRQ_NOAUTOEN); + ret = devm_request_irq(dev, priv->irq_index, + gpio_qenc_index_isr, + IRQF_TRIGGER_RISING, + "gpio-qenc-index", counter); + if (ret) + return dev_err_probe(dev, ret, + "failed to request IRQ for encoder-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 quadrature encoder registered (signals: A, B%s)\n", + has_index ? ", Index" : ""); + + return 0; +} + +static const struct of_device_id gpio_qenc_of_match[] = { + { .compatible = "gpio-quadrature-encoder" }, + {} +}; +MODULE_DEVICE_TABLE(of, gpio_qenc_of_match); + +static struct platform_driver gpio_qenc_driver = { + .probe = gpio_qenc_probe, + .driver = { + .name = "gpio-quadrature-encoder", + .of_match_table = gpio_qenc_of_match, + }, +}; +module_platform_driver(gpio_qenc_driver); + +MODULE_ALIAS("platform:gpio-quadrature-encoder"); +MODULE_AUTHOR("Wadim Mueller <wafgo01@gmail.com>"); +MODULE_DESCRIPTION("GPIO-based quadrature encoder counter driver"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("COUNTER"); -- 2.52.0 ^ permalink raw reply related [flat|nested] 16+ messages in thread
* Re: [PATCH v3 2/3] counter: add GPIO-based quadrature encoder driver 2026-05-01 20:07 ` [PATCH v3 2/3] counter: add GPIO-based quadrature encoder driver Wadim Mueller @ 2026-05-04 20:54 ` Krzysztof Kozlowski 2026-05-04 21:15 ` Wadim Mueller 2026-05-15 5:48 ` William Breathitt Gray 1 sibling, 1 reply; 16+ messages in thread From: Krzysztof Kozlowski @ 2026-05-04 20:54 UTC (permalink / raw) To: Wadim Mueller, wbg Cc: conor+dt, krzk+dt, robh, conor.dooley, linux-iio, devicetree, linux-kernel On 01/05/2026 22:07, Wadim Mueller wrote: > + > +static int gpio_qenc_count_read(struct counter_device *counter, > + struct counter_count *count, u64 *val) > +{ > + struct gpio_qenc_priv *priv = counter_priv(counter); > + unsigned long flags; > + > + spin_lock_irqsave(&priv->lock, flags); > + *val = (u64)priv->count; > + spin_unlock_irqrestore(&priv->lock, flags); > + > + return 0; > +} > + > +static int gpio_qenc_count_write(struct counter_device *counter, > + struct counter_count *count, const u64 val) Please don't continue this broken 'const scalar' pattern. You probably copied this code, but no such new code should be ever added. It's not necessary - compiler/preprocessor does not care from function signature point of view. It's not helping - it's scalar and no sane code modifies such argument, thus there is no single need to protect it. It's not making code easier to read. Quite opposite: raises eyebrows for no real reason. Same in few other places. Best regards, Krzysztof ^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH v3 2/3] counter: add GPIO-based quadrature encoder driver 2026-05-04 20:54 ` Krzysztof Kozlowski @ 2026-05-04 21:15 ` Wadim Mueller 0 siblings, 0 replies; 16+ messages in thread From: Wadim Mueller @ 2026-05-04 21:15 UTC (permalink / raw) To: Krzysztof Kozlowski Cc: wbg, conor+dt, krzk+dt, robh, conor.dooley, linux-iio, devicetree, linux-kernel On 2026-05-04 22:54, Krzysztof Kozlowski wrote: > On 01/05/2026 22:07, Wadim Mueller wrote: > > + > > +static int gpio_qenc_count_read(struct counter_device *counter, > > + struct counter_count *count, u64 *val) > > +{ > > + struct gpio_qenc_priv *priv = counter_priv(counter); > > + unsigned long flags; > > + > > + spin_lock_irqsave(&priv->lock, flags); > > + *val = (u64)priv->count; > > + spin_unlock_irqrestore(&priv->lock, flags); > > + > > + return 0; > > +} > > + > > +static int gpio_qenc_count_write(struct counter_device *counter, > > + struct counter_count *count, const u64 val) > > Please don't continue this broken 'const scalar' pattern. You probably > copied this code, but no such new code should be ever added. > > It's not necessary - compiler/preprocessor does not care from function > signature point of view. It's not helping - it's scalar and no sane code > modifies such argument, thus there is no single need to protect it. It's > not making code easier to read. Quite opposite: raises eyebrows for no > real reason. > > Same in few other places. > You're right -- I picked it up from the existing counter callbacks without thinking. I'll drop the const from the two scalar callbacks (count_write and ceiling_write) for v4. > Best regards, > Krzysztof ^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH v3 2/3] counter: add GPIO-based quadrature encoder driver 2026-05-01 20:07 ` [PATCH v3 2/3] counter: add GPIO-based quadrature encoder driver Wadim Mueller 2026-05-04 20:54 ` Krzysztof Kozlowski @ 2026-05-15 5:48 ` William Breathitt Gray 2026-05-15 15:28 ` Wadim Mueller 1 sibling, 1 reply; 16+ messages in thread From: William Breathitt Gray @ 2026-05-15 5:48 UTC (permalink / raw) To: Wadim Mueller Cc: William Breathitt Gray, conor+dt, krzk+dt, robh, conor.dooley, linux-iio, devicetree, linux-kernel, linusw, brgl On Fri, May 01, 2026 at 10:07:48PM +0200, Wadim Mueller wrote: > Add a platform driver that turns ordinary GPIOs into a quadrature > encoder counter device. The driver requests edge-triggered interrupts > on the A and B (and optional Index) GPIOs and decodes the quadrature > signal in software using a classic state-table approach. > > Supported counting modes: > - Quadrature X1 (count on A rising edge only) > - Quadrature X2 (count on both A edges) > - Quadrature X4 (count on every A and B edge) > - Pulse-direction (A = pulse, B = direction) Hi Wadim, The Documentation/ABI/testing/sysfs-bus-counter file documents the Count function modes. Because we're interpreting GPIO lines and can define our own encoding states, it would be prudent to support all these Count function modes (i.e. increase, decrease, x1 b, and x2 b modes). > > An optional index signal resets the count to zero on its rising edge > when enabled through sysfs. A configurable ceiling clamps the count > to [0, ceiling]. > > Signed-off-by: Wadim Mueller <wafgo01@gmail.com> I recommend adding support at some point for a compare (COUNTER_COMP_COMPARE) component and floor component (COUNTER_COMP_FLOOR) which are common functionalities in quadrature devices. It'd be nice as well to have events pushed for COUNTER_EVENT_OVERFLOW, COUNTER_EVENT_UNDERFLOW, COUNTER_EVENT_OVERFLOW_UNDERFLOW, COUNTER_EVENT_THRESHOLD, and COUNTER_EVENT_DIRECTION_CHANGE. > +enum gpio_qenc_function { > + GPIO_QENC_FUNC_QUAD_X1 = 0, > + GPIO_QENC_FUNC_QUAD_X2, > + GPIO_QENC_FUNC_QUAD_X4, > + GPIO_QENC_FUNC_PULSE_DIR, > +}; We use device specific enums to map device hardware register values to the Counter subsystem function enum constants. However, because this driver is not tied to any specific device hardware, you don't need to define a new enum. In other words, just use the COUNTER_FUNCTION_* constants directly in your code. > +struct gpio_qenc_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; > + > + s64 count; The count_read()/count_write() callbacks pass a u64 count value, so I suspect this private count value store can be u64 as well. > + u64 ceiling; > + bool enabled; > + enum counter_count_direction direction; > + enum gpio_qenc_function function; > + > + int prev_a; > + int prev_b; > + > + bool index_enabled; > + > + struct counter_signal signals[3]; > + struct counter_synapse synapses[3]; > + struct counter_count cnts; I know you only have a single count, but for consistency with the rest of the code and Counter subsystem, I recommend declaring this as a single element array: struct counter_count cnts[1]; > +}; > + > +/* > + * Quadrature state table for X4 decoding. > + * Rows = previous state (A<<1 | B), Columns = new state (A<<1 | B). > + * Values: 0 = no change, +1 = forward, -1 = backward, 2 = error (skip). > + */ > +static const int quad_table[4][4] = { > + /* 00 01 10 11 <- new */ > + /* 00 */ { 0, -1, 1, 2 }, > + /* 01 */ { 1, 0, 2, -1 }, > + /* 10 */ { -1, 2, 0, 1 }, > + /* 11 */ { 2, 1, -1, 0 }, > +}; > + > +static void gpio_qenc_update_count(struct gpio_qenc_priv *priv, int delta) > +{ > + s64 new_count; > + > + if (!delta) > + return; > + > + new_count = priv->count + delta; > + > + if (priv->ceiling) { > + if (new_count < 0) > + new_count = 0; > + else if (new_count > (s64)priv->ceiling) > + new_count = priv->ceiling; > + } > + > + priv->count = new_count; > + priv->direction = (delta > 0) ? COUNTER_COUNT_DIRECTION_FORWARD > + : COUNTER_COUNT_DIRECTION_BACKWARD; Quadrature encoding represents changes of a single unit value at a time. Because we never increase nor decrease more than a value of 1 at a time, I think the code would read clearer if we determine the direction first, then make the adjustments to the count value thereafter. Maybe something like this (assuming priv->count is u64): if (!delta) return; priv->direction = (delta > 0) ? COUNTER_COUNT_DIRECTION_FORWARD : COUNTER_COUNT_DIRECTION_BACKWARD; if (priv->direction == COUNTER_COUNT_DIRECTION_FORWARD) priv->count = (priv->count == priv->ceiling) ? priv->ceiling : priv->count + 1; else priv->count = (priv->count == 0) ? priv->ceiling : priv->count - 1; > +} > + > +static irqreturn_t gpio_qenc_a_isr(int irq, void *dev_id) > +{ > + struct counter_device *counter = dev_id; > + struct gpio_qenc_priv *priv = counter_priv(counter); > + unsigned long flags; > + int a, b, prev_state, new_state, delta; > + > + spin_lock_irqsave(&priv->lock, flags); > + > + if (!priv->enabled) > + goto out; Do we need to check the enabled state? If the IRQ is disabled, we don't reach this path, or do we? > + > + a = gpiod_get_value(priv->gpio_a); > + b = gpiod_get_value(priv->gpio_b); > + > + prev_state = (priv->prev_a << 1) | priv->prev_b; > + new_state = (a << 1) | b; I might make sense to wrap the state operation into a macro to make the intention of these bitwise operations more intuitive. For example: prev_state = CREATE_QE_STATE(priv->prev_a, priv->prev_b); new_state = CREATE_QE_STATE(a, b); Or something like that makes it more obvious that these two lines are just building the indices for the state array rather than any inherent meaning in the binary representation of the variables. > + > + switch (priv->function) { > + case GPIO_QENC_FUNC_QUAD_X4: > + delta = quad_table[prev_state][new_state]; > + if (delta == 2) > + delta = 0; > + gpio_qenc_update_count(priv, delta); > + break; > + > + case GPIO_QENC_FUNC_QUAD_X2: > + delta = quad_table[prev_state][new_state]; > + if (delta == 2) > + delta = 0; > + gpio_qenc_update_count(priv, delta); > + break; After determining the delta value we call the gpio_qenc_update_count() function immedidately. We can save on a function call and remove the !delta check in gpio_qenc_update_count() if you make the call only when delta is known to update the count; for example: if (!delta && delta != 2) gpio_qenc_update_count(priv, delta); In fact, you may consider just removing the "2" states from the quad_table and leaving it as just "0" because the effect is the same, right? > +static irqreturn_t gpio_qenc_b_isr(int irq, void *dev_id) > +{ > + struct counter_device *counter = dev_id; > + struct gpio_qenc_priv *priv = counter_priv(counter); > + unsigned long flags; > + int a, b, prev_state, new_state, delta; > + > + spin_lock_irqsave(&priv->lock, flags); > + > + if (!priv->enabled) > + goto out; > + > + a = gpiod_get_value(priv->gpio_a); > + b = gpiod_get_value(priv->gpio_b); > + > + prev_state = (priv->prev_a << 1) | priv->prev_b; > + new_state = (a << 1) | b; > + > + switch (priv->function) { > + case GPIO_QENC_FUNC_QUAD_X4: > + delta = quad_table[prev_state][new_state]; > + if (delta == 2) > + delta = 0; > + gpio_qenc_update_count(priv, delta); > + break; > + > + case GPIO_QENC_FUNC_QUAD_X2: > + /* X2: only A-channel edges update count */ > + break; > + > + case GPIO_QENC_FUNC_QUAD_X1: > + case GPIO_QENC_FUNC_PULSE_DIR: > + break; I guess you added these two cases to pacify the compiler warning for missing switch cases. You can provide a default case instead so we don't need to list out every case that is ignored. > +static irqreturn_t gpio_qenc_index_isr(int irq, void *dev_id) > +{ > + struct counter_device *counter = dev_id; > + struct gpio_qenc_priv *priv = counter_priv(counter); > + unsigned long flags; > + > + spin_lock_irqsave(&priv->lock, flags); > + > + if (priv->enabled && priv->index_enabled) > + priv->count = 0; > + > + spin_unlock_irqrestore(&priv->lock, flags); > + > + counter_push_event(counter, COUNTER_EVENT_INDEX, 0); > + > + return IRQ_HANDLED; > +} In many quadrature encoder devices I've seen, Index functions can be chosen which determine what happens to the count when an Index signal occurs. Typically, one of those functions is to reset the count, but that is not necessary the only function nor is it unconditionally enabled. Existing drivers such as 104-quad-8 use COUNTER_COMP_PRESET to define a preset component that holds a value to preset the counter on an Index signal; COUNTER_COMP_PRESET_ENABLE is used to enable/disable such functionality. You may want to implement something similar for gpio-quadrature-encoder, and consider other such Index functionality that may be desirable to provide to users. > +static int gpio_qenc_count_write(struct counter_device *counter, > + struct counter_count *count, const u64 val) > +{ > + struct gpio_qenc_priv *priv = counter_priv(counter); > + unsigned long flags; > + > + spin_lock_irqsave(&priv->lock, flags); > + > + if (priv->ceiling && val > priv->ceiling) { > + spin_unlock_irqrestore(&priv->lock, flags); > + return -EINVAL; > + } So the ceiling component represents the highest value the count can reach. I would argue a ceiling value of 0 does not indicate a disabled ceiling but rather a Count which is limited to the value of 0; n.b. this technically is not a disabled Count because COUNTER_EVENT_OVERFLOW could still be pushed in this scenario, albeit a rather dubious configuration for the Count in real life applications. To "disable" a ceiling, the user would set the ceiling value to the maximum value supported by the device hardware (or U64_MAX in our case if priv->count is u64). In fact, you should initialize priv->ceiling to this maximum value in your gpio_qenc_probe() callback so that we don't limit. > +static int gpio_qenc_function_read(struct counter_device *counter, > + struct counter_count *count, > + enum counter_function *function) > +{ > + struct gpio_qenc_priv *priv = counter_priv(counter); > + unsigned long flags; > + > + spin_lock_irqsave(&priv->lock, flags); > + > + switch (priv->function) { > + case GPIO_QENC_FUNC_QUAD_X1: > + *function = COUNTER_FUNCTION_QUADRATURE_X1_A; > + break; > + case GPIO_QENC_FUNC_QUAD_X2: > + *function = COUNTER_FUNCTION_QUADRATURE_X2_A; > + break; > + case GPIO_QENC_FUNC_QUAD_X4: > + *function = COUNTER_FUNCTION_QUADRATURE_X4; > + break; > + case GPIO_QENC_FUNC_PULSE_DIR: > + *function = COUNTER_FUNCTION_PULSE_DIRECTION; > + break; > + } If you use declare priv->function as enum counter_function, you can replace the entire switch as a simple set operation: *function = priv->function; > + > + spin_unlock_irqrestore(&priv->lock, flags); > + return 0; > +} > + > +static int gpio_qenc_function_write(struct counter_device *counter, > + struct counter_count *count, > + enum counter_function function) > +{ > + struct gpio_qenc_priv *priv = counter_priv(counter); > + unsigned long flags; > + > + spin_lock_irqsave(&priv->lock, flags); > + > + switch (function) { > + case COUNTER_FUNCTION_QUADRATURE_X1_A: > + priv->function = GPIO_QENC_FUNC_QUAD_X1; > + break; > + case COUNTER_FUNCTION_QUADRATURE_X2_A: > + priv->function = GPIO_QENC_FUNC_QUAD_X2; > + break; > + case COUNTER_FUNCTION_QUADRATURE_X4: > + priv->function = GPIO_QENC_FUNC_QUAD_X4; > + break; > + case COUNTER_FUNCTION_PULSE_DIRECTION: > + priv->function = GPIO_QENC_FUNC_PULSE_DIR; > + break; > + default: > + spin_unlock_irqrestore(&priv->lock, flags); > + return -EINVAL; > + } Same suggestion as for gpio_qenc_function_read(): priv->function = function; > + > + spin_unlock_irqrestore(&priv->lock, flags); > + return 0; > +} > + > +static const enum counter_synapse_action gpio_qenc_synapse_actions[] = { > + COUNTER_SYNAPSE_ACTION_BOTH_EDGES, > + COUNTER_SYNAPSE_ACTION_RISING_EDGE, > + COUNTER_SYNAPSE_ACTION_NONE, > +}; If I'm not mistaken, you can fire interrupts on the falling edge of a GPIO signal. It'd be good to support such a configuration as it's necessary for Quadrature X1 A mode. > +static int gpio_qenc_action_read(struct counter_device *counter, > + struct counter_count *count, > + struct counter_synapse *synapse, > + enum counter_synapse_action *action) > +{ > + struct gpio_qenc_priv *priv = counter_priv(counter); > + enum gpio_qenc_signal_id signal_id = synapse->signal->id; You can eliminate a lot of the else statements the switch block by handling a couple default cases. For example, you can define a default action mode of "none" and have the rest of the code only set an action if meets the right criteria. Furthermore, the Index synapse should be treated as independent of the quadrature mode; the Signal is not evaluated in determining the encoding state, and is commonly used on physical quadrature encoder devices as a count reset trigger in non-quadrature modes. So I recommend handling it early here as well: /* Default action mode */ *action = COUNTER_SYNAPSE_ACTION_NONE; /* Handle Index Signal */ if (priv->index_enabled && signal_id == GPIO_QENC_SIGNAL_INDEX) { *action = COUNTER_SYNAPSE_ACTION_RISING_EDGE; return 0; } > + > + switch (priv->function) { > + case GPIO_QENC_FUNC_QUAD_X4: > + if (signal_id == GPIO_QENC_SIGNAL_A || > + signal_id == GPIO_QENC_SIGNAL_B) > + *action = COUNTER_SYNAPSE_ACTION_BOTH_EDGES; > + else > + *action = COUNTER_SYNAPSE_ACTION_RISING_EDGE; > + return 0; With the default cases suggested earlier, the if statement and else go away completely and you can directly set the action to "both edges": case COUNTER_FUNCTION_QUADRATURE_X4: *action = COUNTER_SYNAPSE_ACTION_BOTH_EDGES; return 0; > + > + case GPIO_QENC_FUNC_QUAD_X2: > + if (signal_id == GPIO_QENC_SIGNAL_A) > + *action = COUNTER_SYNAPSE_ACTION_BOTH_EDGES; > + else if (signal_id == GPIO_QENC_SIGNAL_B) > + *action = COUNTER_SYNAPSE_ACTION_NONE; > + else > + *action = COUNTER_SYNAPSE_ACTION_RISING_EDGE; > + return 0; Ditto: case COUNTER_FUNCTION_QUADRATURE_X2_A: if (signal_id == GPIO_QENC_SIGNAL_A) *action = COUNTER_SYNAPSE_ACTION_BOTH_EDGES; return 0; > + > + case GPIO_QENC_FUNC_QUAD_X1: > + if (signal_id == GPIO_QENC_SIGNAL_A) > + *action = COUNTER_SYNAPSE_ACTION_RISING_EDGE; > + else if (signal_id == GPIO_QENC_SIGNAL_B) > + *action = COUNTER_SYNAPSE_ACTION_NONE; > + else > + *action = COUNTER_SYNAPSE_ACTION_RISING_EDGE; > + return 0; In the commit description you mention that this mode counts only rising edges, but you matched this to COUNTER_FUNCTION_QUADRATURE_X1_A which depends on the current direction to determine which edge is active. I'll assume that was your intention (otherwise this becomes a duplicate of pulse-direction mode with Index handled independent of quadrature mode): case COUNTER_FUNCTION_QUADRATURE_X1_A: if (signal_id == GPIO_QENC_SIGNAL_A) { if (priv->direction == COUNTER_COUNT_DIRECTION_FORWARD) *action = COUNTER_SYNAPSE_ACTION_RISING_EDGE; else *action = COUNTER_SYNAPSE_ACTION_FALLING_EDGE; } return 0; > + > + case GPIO_QENC_FUNC_PULSE_DIR: > + if (signal_id == GPIO_QENC_SIGNAL_A) > + *action = COUNTER_SYNAPSE_ACTION_RISING_EDGE; > + else > + *action = COUNTER_SYNAPSE_ACTION_NONE; > + return 0; This cas becomes simple too: case COUNTER_FUNCTION_PULSE_DIRECTION: if (signal_id == GPIO_QENC_SIGNAL_A) *action = COUNTER_SYNAPSE_ACTION_RISING_EDGE; return 0; > + } > + > + return -EINVAL; > +} > + > +static int gpio_qenc_signal_read(struct counter_device *counter, > + struct counter_signal *signal, > + enum counter_signal_level *level) > +{ > + struct gpio_qenc_priv *priv = counter_priv(counter); > + struct gpio_desc *gpio; > + int ret; > + > + switch (signal->id) { > + case GPIO_QENC_SIGNAL_A: > + gpio = priv->gpio_a; > + break; > + case GPIO_QENC_SIGNAL_B: > + gpio = priv->gpio_b; > + break; > + case GPIO_QENC_SIGNAL_INDEX: > + gpio = priv->gpio_index; > + break; > + default: > + return -EINVAL; The default case is unneeded because all possible enum constants are handled by the switch block. > + } > + > + if (!gpio) > + return -EINVAL; Why is it possible to get back a zero value? Is this to handle the case where a GPIO line does not exist? Would it be possible avoid adding Counter Signals for missing GPIO during the gpio_qenc_probe() call so we don't encounter such zombie Counter Signals? > + > + 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_qenc_events_configure(struct counter_device *counter) > +{ > + return 0; > +} The events_configure() callback is option, so you don't need to define it if it doesn't do anything. Its intention is to enable/disable device interrupts based on the Counter watches requested by users; that doesn't apply for this driver because our encoding state is virtual and we push Counter events as needed when we update the virtual state. > +static int gpio_qenc_enable_write(struct counter_device *counter, > + struct counter_count *count, u8 enable) > +{ > + struct gpio_qenc_priv *priv = counter_priv(counter); > + unsigned long flags; > + > + spin_lock_irqsave(&priv->lock, flags); > + > + if (priv->enabled == !!enable) { > + spin_unlock_irqrestore(&priv->lock, flags); > + return 0; > + } > + > + if (enable) { > + priv->enabled = true; > + spin_unlock_irqrestore(&priv->lock, flags); > + enable_irq(priv->irq_a); > + enable_irq(priv->irq_b); > + if (priv->irq_index) > + enable_irq(priv->irq_index); > + } else { > + priv->enabled = false; > + spin_unlock_irqrestore(&priv->lock, flags); > + disable_irq(priv->irq_a); > + disable_irq(priv->irq_b); > + if (priv->irq_index) > + disable_irq(priv->irq_index); > + } > + > + return 0; > +} The value of enable is ensured by the Counter subsystem to be a bool when passed to the enable_write() callback, so there's no need for the double negation; you can compare against enable directly. Also you can simplify the if block by setting priv->enabled unconditionally and adding a return 0 to the if path: spin_lock_irqsave(&priv->lock, flags); if (priv->enabled == enable) { spin_unlock_irqrestore(&priv->lock, flags); return 0; } priv->enabled = enable; spin_unlock_irqrestore(&priv->lock, flags); if (enable) { enable_irq(priv->irq_a); enable_irq(priv->irq_b); if (priv->irq_index) enable_irq(priv->irq_index); return 0; } disable_irq(priv->irq_a); disable_irq(priv->irq_b); if (priv->irq_index) disable_irq(priv->irq_index); return 0; > +static struct counter_comp gpio_qenc_count_ext[] = { > + COUNTER_COMP_CEILING(gpio_qenc_ceiling_read, gpio_qenc_ceiling_write), > + COUNTER_COMP_ENABLE(gpio_qenc_enable_read, gpio_qenc_enable_write), > + COUNTER_COMP_DIRECTION(gpio_qenc_direction_read), > + COUNTER_COMP_COUNT_BOOL("index_enabled", > + gpio_qenc_index_enable_read, > + gpio_qenc_index_enable_write), The Index function in this driver is used to preset (reset) the Count so rather than define your own "index_enabled" component, the idiomatic way to handle this behavior in the Counter subsystem is to set priv->index_enabled via COUNTER_COMP_PRESET_ENABLE(). > + priv->synapses[0].actions_list = gpio_qenc_synapse_actions; > + priv->synapses[0].num_actions = ARRAY_SIZE(gpio_qenc_synapse_actions); > + priv->synapses[0].signal = &priv->signals[GPIO_QENC_SIGNAL_A]; > + > + priv->synapses[1].actions_list = gpio_qenc_synapse_actions; > + priv->synapses[1].num_actions = ARRAY_SIZE(gpio_qenc_synapse_actions); > + priv->synapses[1].signal = &priv->signals[GPIO_QENC_SIGNAL_B]; > + > + if (has_index) { > + priv->synapses[2].actions_list = gpio_qenc_synapse_actions; The Index Signal only has two possible Synapse actions "rising edge" and "none". Because that differs from the Quadrature A and B Signals (which can also have "both edges" for example), you need to define a separate static const enum counter_synapse_action array for just the Index Signal which has just those two actions. > + priv->cnts.id = 0; > + priv->cnts.name = "Position"; Name this something more generic such as "Count" because positioning is only one of the possible applications for a quadrature encoder not its sole use case. > + counter->name = dev_name(dev); > + counter->parent = dev; > + counter->ops = &gpio_qenc_ops; > + counter->signals = priv->signals; > + counter->num_signals = num_signals; > + counter->counts = &priv->cnts; > + counter->num_counts = 1; Use ARRAY_SIZE() for num_counts for the sake of consistency (and in case we ever support more than one Count in future updates). William Breathitt Gray ^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH v3 2/3] counter: add GPIO-based quadrature encoder driver 2026-05-15 5:48 ` William Breathitt Gray @ 2026-05-15 15:28 ` Wadim Mueller 0 siblings, 0 replies; 16+ messages in thread From: Wadim Mueller @ 2026-05-15 15:28 UTC (permalink / raw) To: William Breathitt Gray Cc: conor+dt, krzk+dt, robh, conor.dooley, linux-iio, devicetree, linux-kernel, linusw, brgl Hi William, thanks a lot for the very thorough review -- this was really helpful. I went through all your points and addressed them in v4 which i will send shortly as a follow-up to this thread (in-reply-to the v3 cover). A short rundown so you can already see what to expect: - gpio_qenc_function enum dropped, priv stores enum counter_function directly, function_read/write are now trivial accessors. - priv->count is now u64 and ceiling defaults to U64_MAX in probe, so ceiling == 0 is a real "ceiling of 0" again, not a sentinel. - update_count rewritten direction-first, with proper saturation at 0 / ceiling, +/-1 per transition. - CREATE_QE_STATE(prev_a, prev_b, curr_a, curr_b) macro plus a single 16-entry X4 transition table; the delta == 2 path is gone, the table just has 0 in those slots. - ISR switches use default: instead of listing every ignored case. - The "enabled" flag in the ISRs is dropped. Gating happens via enable_irq/disable_irq anyway and enable_read derives the state from irqd_irq_disabled(irq_get_irq_data(priv->irq_a)) so there is only one source of truth. - enable_write simplified: no !!, assign directly, two branches split by return. - events_configure() removed (was no-op). - Synapses: A and B keep the full action list including FALLING_EDGE; Index gets its own list with only NONE and RISING_EDGE. - action_read restructured: default to NONE, Index handled as early return, per-function switch. X1_A and X1_B now report RISING_EDGE when going forward and FALLING_EDGE when going backward, as the COUNTER_FUNCTION_QUADRATURE_X1_{A,B} semantics say. - All generic Count functions supported now: INCREASE, DECREASE, PULSE_DIRECTION, QUADRATURE_X1_{A,B}, QUADRATURE_X2_{A,B}, QUADRATURE_X4. - Index migrated to the COUNTER_COMP_PRESET + PRESET_ENABLE pair, the custom "index_enabled" COUNTER_COMP_COUNT_BOOL is gone. The Index ISR loads preset into count when preset_enable is set, like intel-qep does. - signal_read: the !gpio guard is gone; the Index synapse is only registered in probe when an index GPIO is actually wired, so no zombie entries. - cnts is now a single-element array (priv->cnts[1]) and counter->num_counts uses ARRAY_SIZE(priv->cnts). To not trigger checkpatch's "deprecated flexible array" warning the array is not the last field of the priv struct anymore. - The Count is renamed from "Position" to "Count". About the COMPARE/FLOOR components and the OVERFLOW/UNDERFLOW/ THRESHOLD/DIRECTION_CHANGE events you suggested: i would prefer to send those as a separate series on top of this one, once it lands, so the diff size for the initial driver stays in a reviewable shape. I will announce that as a follow-up in the v4 cover letter. While at it i also picked up Krzysztofs note from v2 on the const scalar parameters -- v3 only had the Acked-by from Conor on the binding and no code changes, so the const scalars were still in v3. They are gone in v4. If anything from above is not what you had in mind please tell me, i will fix it in v5. Thanks again for the time, Wadim ^ permalink raw reply [flat|nested] 16+ messages in thread
* [PATCH v3 3/3] MAINTAINERS: add entry for GPIO quadrature encoder counter driver 2026-05-01 20:07 [PATCH v3 0/3] counter: add GPIO-based quadrature encoder driver Wadim Mueller 2026-05-01 20:07 ` [PATCH v3 1/3] dt-bindings: counter: add gpio-quadrature-encoder binding Wadim Mueller 2026-05-01 20:07 ` [PATCH v3 2/3] counter: add GPIO-based quadrature encoder driver Wadim Mueller @ 2026-05-01 20:07 ` Wadim Mueller 2026-05-04 9:36 ` [PATCH v3 0/3] counter: add GPIO-based quadrature encoder driver William Breathitt Gray 2026-05-15 15:36 ` [PATCH v4 " Wadim Mueller 4 siblings, 0 replies; 16+ messages in thread From: Wadim Mueller @ 2026-05-01 20:07 UTC (permalink / raw) To: wbg Cc: conor+dt, krzk+dt, robh, conor.dooley, linux-iio, devicetree, linux-kernel Add myself as maintainer for the new gpio-quadrature-encoder counter driver and its devicetree binding. Signed-off-by: Wadim Mueller <wafgo01@gmail.com> --- MAINTAINERS | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/MAINTAINERS b/MAINTAINERS index 06a8c7457..fca62baa7 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -11018,6 +11018,13 @@ F: Documentation/dev-tools/gpio-sloppy-logic-analyzer.rst F: drivers/gpio/gpio-sloppy-logic-analyzer.c F: tools/gpio/gpio-sloppy-logic-analyzer.sh +GPIO QUADRATURE ENCODER COUNTER DRIVER +M: Wadim Mueller <wafgo01@gmail.com> +L: linux-iio@vger.kernel.org +S: Maintained +F: Documentation/devicetree/bindings/counter/gpio-quadrature-encoder.yaml +F: drivers/counter/gpio-quadrature-encoder.c + GPIO SUBSYSTEM M: Linus Walleij <linusw@kernel.org> M: Bartosz Golaszewski <brgl@kernel.org> -- 2.52.0 ^ permalink raw reply related [flat|nested] 16+ messages in thread
* Re: [PATCH v3 0/3] counter: add GPIO-based quadrature encoder driver 2026-05-01 20:07 [PATCH v3 0/3] counter: add GPIO-based quadrature encoder driver Wadim Mueller ` (2 preceding siblings ...) 2026-05-01 20:07 ` [PATCH v3 3/3] MAINTAINERS: add entry for GPIO quadrature encoder counter driver Wadim Mueller @ 2026-05-04 9:36 ` William Breathitt Gray 2026-05-04 19:37 ` Wadim Mueller 2026-05-06 6:50 ` Wadim Mueller 2026-05-15 15:36 ` [PATCH v4 " Wadim Mueller 4 siblings, 2 replies; 16+ messages in thread From: William Breathitt Gray @ 2026-05-04 9:36 UTC (permalink / raw) To: Wadim Mueller Cc: William Breathitt Gray, linusw, brgl, conor+dt, krzk+dt, robh, conor.dooley, linux-iio, devicetree, linux-kernel On Fri, May 01, 2026 at 10:07:46PM +0200, Wadim Mueller wrote: > This series adds a new counter subsystem driver that implements > quadrature encoder position tracking using plain GPIO pins with > edge-triggered interrupts. > > The driver is intended for low to medium speed rotary encoders where > hardware counter peripherals (eQEP, FTM, etc.) are unavailable or > already in use. It targets the same use-cases as interrupt-cnt.c but > provides full quadrature decoding instead of simple pulse counting. > > Features: > - X1, X2, X4 quadrature decoding and pulse-direction mode > - Optional index signal for zero-reset > - Configurable ceiling (position clamping) > - Standard counter subsystem sysfs + chrdev interface > - Enable/disable via sysfs with IRQ gating > > Tested on TI AM64x (Cortex-A53) with a motor-driven rotary encoder > at up to 2 kHz quadrature edge rate. Hello Wadim, This is certainly a neat idea! :-) Several times I have wished for a convenient way to just plug in a quadrature encoder to the GPIO lines of my system and immediately start reading position data. However, I want to be sure this makes sense as a Counter subsystem driver before I proceed with a full review. If I understand correctly from my brief overview, the core approach in the gpio-quadrature-encoder module is to take two GPIO lines (A and B), setup interrupt service routines for them, compare their GPIO values on each interrupt, and respectively update a persistent count based on the quadrature relationship. From that description, I don't immediately see a need for this to occur in kernelspace. Couldn't the same design be accomplished effectively in userspace via the libgpiod API[^1]? I believe that library allows you to watch for GPIO edge events and request GPIO line values. (I'm CCing the GPIO subsystem maintainers in case I'm missing something obvious here.) Although the Counter subsystem does provide an established user interface for counter devices, I'm not sure that alone justifies a kernel driver when the same can be achieved by an equivalent userspace application. If you can argue for why this should exist in the kernel, I'll feel more comfortable with accepting the Counter subsystem as the right home for the gpio-quadrature-encoder module. > Changes in v3: > - Pick up Acked-by: Conor Dooley on the DT binding patch. > - No code changes. As an aside, you don't need to resend the patchset if there are no code changes, I'll make sure to pick up the tags in the mail threads when the patches are accepted. This helps reduce the amount the messages we need to parse on the mailing list. Thanks, William Breathitt Gray [^1] https://libgpiod.readthedocs.io/ ^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH v3 0/3] counter: add GPIO-based quadrature encoder driver 2026-05-04 9:36 ` [PATCH v3 0/3] counter: add GPIO-based quadrature encoder driver William Breathitt Gray @ 2026-05-04 19:37 ` Wadim Mueller 2026-05-06 6:50 ` Wadim Mueller 1 sibling, 0 replies; 16+ messages in thread From: Wadim Mueller @ 2026-05-04 19:37 UTC (permalink / raw) To: William Breathitt Gray Cc: linusw, brgl, conor+dt, krzk+dt, robh, conor.dooley, linux-iio, devicetree, linux-kernel On 2026-05-04 18:36, William Breathitt Gray wrote: > On Fri, May 01, 2026 at 10:07:46PM +0200, Wadim Mueller wrote: > > This series adds a new counter subsystem driver that implements > > quadrature encoder position tracking using plain GPIO pins with > > edge-triggered interrupts. > > > > The driver is intended for low to medium speed rotary encoders where > > hardware counter peripherals (eQEP, FTM, etc.) are unavailable or > > already in use. It targets the same use-cases as interrupt-cnt.c but > > provides full quadrature decoding instead of simple pulse counting. > > > > Features: > > - X1, X2, X4 quadrature decoding and pulse-direction mode > > - Optional index signal for zero-reset > > - Configurable ceiling (position clamping) > > - Standard counter subsystem sysfs + chrdev interface > > - Enable/disable via sysfs with IRQ gating > > > > Tested on TI AM64x (Cortex-A53) with a motor-driven rotary encoder > > at up to 2 kHz quadrature edge rate. > > Hello Wadim, > > This is certainly a neat idea! :-) Several times I have wished for a > convenient way to just plug in a quadrature encoder to the GPIO lines of > my system and immediately start reading position data. However, I want > to be sure this makes sense as a Counter subsystem driver before I > proceed with a full review. > > If I understand correctly from my brief overview, the core approach in > the gpio-quadrature-encoder module is to take two GPIO lines (A and B), > setup interrupt service routines for them, compare their GPIO values on > each interrupt, and respectively update a persistent count based on the > quadrature relationship. > > From that description, I don't immediately see a need for this to occur > in kernelspace. Couldn't the same design be accomplished effectively in > userspace via the libgpiod API[^1]? I believe that library allows you > to watch for GPIO edge events and request GPIO line values. (I'm CCing > the GPIO subsystem maintainers in case I'm missing something obvious > here.) > > Although the Counter subsystem does provide an established user > interface for counter devices, I'm not sure that alone justifies a > kernel driver when the same can be achieved by an equivalent userspace > application. If you can argue for why this should exist in the kernel, > I'll feel more comfortable with accepting the Counter subsystem as the > right home for the gpio-quadrature-encoder module. > Hello William, thanks for the look -- fair question. Let me try. Even with A and B in a single line request (so one event fd, no extra get_value() between edges) the libgpiod path is: edge IRQ -> kernel edge event -> chardev fifo -> scheduler wakeup -> read() -> decode -> update count The wake-to-update delay is bounded by the scheduler, not by the IRQ. At 2 kHz X4 the inter-edge spacing is ~125 us, which is firmly inside scheduler-jitter territory on a stock kernel under load -- and a single missed edge is a permanent position error rather than a transient glitch. In the kernel the same work is essentially spin_lock_irqsave / gpiod_get_value / count update / unlock in interrupt context, which is what gives quadrature decoding the atomicity it needs. To be honest I haven't put real numbers on this yet -- the argument above is from experience, not measurement. I'm going to set up a proper back-to-back test (libgpiod v2 decoder vs the in-kernel driver, same hardware, deterministic edge source via eHRPWM so the generator side doesn't pollute the result) and follow up on this thread with the results once they're in. If userspace turns out to hold up under load on this SoC I'll happily drop the series. The other angle: interrupt-cnt.c is the same shape of driver -- one GPIO IRQ feeding a Counter -- and quadrature off two GPIOs feels more like a sibling of that than something to push out to userspace. Keeping both behind the Counter ABI also means downstream tooling doesn't need a parallel libgpiod path next to the existing Counter one. > > Changes in v3: > > - Pick up Acked-by: Conor Dooley on the DT binding patch. > > - No code changes. > > As an aside, you don't need to resend the patchset if there are no code > changes, I'll make sure to pick up the tags in the mail threads when the > patches are accepted. This helps reduce the amount the messages we need > to parse on the mailing list. Got it on the resend, thanks. I'll just collect tags in-thread next time. Thanks, Wadim > > Thanks, > > William Breathitt Gray > > [^1] https://libgpiod.readthedocs.io/ ^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH v3 0/3] counter: add GPIO-based quadrature encoder driver 2026-05-04 9:36 ` [PATCH v3 0/3] counter: add GPIO-based quadrature encoder driver William Breathitt Gray 2026-05-04 19:37 ` Wadim Mueller @ 2026-05-06 6:50 ` Wadim Mueller 2026-05-14 13:17 ` William Breathitt Gray 1 sibling, 1 reply; 16+ messages in thread From: Wadim Mueller @ 2026-05-06 6:50 UTC (permalink / raw) To: William Breathitt Gray Cc: linusw, brgl, conor+dt, krzk+dt, robh, conor.dooley, linux-iio, devicetree, linux-kernel On 2026-05-04 18:36, William Breathitt Gray wrote: > On Fri, May 01, 2026 at 10:07:46PM +0200, Wadim Mueller wrote: > > This series adds a new counter subsystem driver that implements > > quadrature encoder position tracking using plain GPIO pins with > > edge-triggered interrupts. > > > > The driver is intended for low to medium speed rotary encoders where > > hardware counter peripherals (eQEP, FTM, etc.) are unavailable or > > already in use. It targets the same use-cases as interrupt-cnt.c but > > provides full quadrature decoding instead of simple pulse counting. > > > > Features: > > - X1, X2, X4 quadrature decoding and pulse-direction mode > > - Optional index signal for zero-reset > > - Configurable ceiling (position clamping) > > - Standard counter subsystem sysfs + chrdev interface > > - Enable/disable via sysfs with IRQ gating > > > > Tested on TI AM64x (Cortex-A53) with a motor-driven rotary encoder > > at up to 2 kHz quadrature edge rate. > > Hello Wadim, > > This is certainly a neat idea! :-) Several times I have wished for a > convenient way to just plug in a quadrature encoder to the GPIO lines of > my system and immediately start reading position data. However, I want > to be sure this makes sense as a Counter subsystem driver before I > proceed with a full review. > > If I understand correctly from my brief overview, the core approach in > the gpio-quadrature-encoder module is to take two GPIO lines (A and B), > setup interrupt service routines for them, compare their GPIO values on > each interrupt, and respectively update a persistent count based on the > quadrature relationship. > > From that description, I don't immediately see a need for this to occur > in kernelspace. Couldn't the same design be accomplished effectively in > userspace via the libgpiod API[^1]? I believe that library allows you > to watch for GPIO edge events and request GPIO line values. (I'm CCing > the GPIO subsystem maintainers in case I'm missing something obvious > here.) > > Although the Counter subsystem does provide an established user > interface for counter devices, I'm not sure that alone justifies a > kernel driver when the same can be achieved by an equivalent userspace > application. If you can argue for why this should exist in the kernel, > I'll feel more comfortable with accepting the Counter subsystem as the > right home for the gpio-quadrature-encoder module. > > > Changes in v3: > > - Pick up Acked-by: Conor Dooley on the DT binding patch. > > - No code changes. > > As an aside, you don't need to resend the patchset if there are no code > changes, I'll make sure to pick up the tags in the mail threads when the > patches are accepted. This helps reduce the amount the messages we need > to parse on the mailing list. > > Thanks, > > William Breathitt Gray > > [^1] https://libgpiod.readthedocs.io/ Hi, to give the discussion "why a kernel driver, gpiomon should be enough?" some real numbers, i did a small benchmark on the AM64x board where this driver was developed on. From my side the data below looks like a kernel side counter is the right tool for this job, but in the end this is of course your decision as a maintainter. Please tell me wether i should send a v4 or rather drop the series, so i know how to continue. Setup ----- SoC: TI AM64x, Cortex-A53 dual core, gpio-davinci Kernel: 6.6.32, CONFIG_PREEMPT=y, CONFIG_HZ=250, no isolcpus, no RT Source: EHRPWM driving one GPIO line (square wave) Window: 2.0 s per point, 3 runs per point (mean +- stdev in the repo) A: counter/gpio-quadrature-encoder, pulse-direction, B held low, signal_a_action=rising-edge B: gpiomon -c gpiochipN -e rising -F %o (libgpiod v2.1.2 CLI), lines counted from the output file afterwards Both ways are seeing the same physical edge. Rising edge counts (mean error vs. expected, n=3) ------------------------------------------------- f [Hz] kernel err% gpiomon err% 1000 -0.16 -0.13 10000 -0.42 -0.88 20000 -0.89 -3.18 50000 -2.37 -6.77 75002 -0.29 -10.93 100000 -0.65 -20.53 150000 n/a* -42.46 200000 n/a* -60.74 CPU cost at 75 kHz ------------------ task CPU sys irq+softirq (sum of 2 cores) kernel: 0 % ~50 % gpiomon: ~60 % ~25 % Some interpretation ------------------- - Below ~10 kHz: not really distinguishable from quantization noise (one rising edge of phase ambiguity per measurement window). So no meanigful difference between the two paths here. - 20 - 100 kHz: gpiomon error grows more or less linear with the rate while the kernel counter stays at the noise floor. At 75 kHz gpiomon is dropping roughly one event out of nine and consums 60 % of one core in user/sys time, on top of the IRQ work; the kernel counter looses ~0.3 % at zero task CPU. - *at >= 150 kHz the davinci-gpio bank IRQ saturates at around 200k irq/s. This is a SoC limit, not a software stack limit, and applies to both paths. The bench harness aborts the kernel sweep on >30 % apparent loss to stay away from soft-lockups; the gpiomon numbers above this point are only listed to show that the same hardware ceiling costs the userspace path much more useable counts, because of the poll/read latency on top of the IRQ. Honest caveats -------------- - "gpiomon" here is the libgpiod reference CLI: single threaded, writes one text line per event into a file. A hand written uAPI v2 consumer with a tight read() loop and a binary buffer would most likley be cheaper. The IRQ rate ceiling at ~200k irq/s on this SoC is not affected from this. Patches against the bench are very welcome. - The driver is benchmarked in pulse-direction mode with B held low, so one IRQ per edge. A real two channel quadrature source would double the bank IRQ load accordingly. - CONFIG_HZ=250, no PREEMPT_RT, no isolcpus. PREEMPT_RT and pinning would soften the userspace cliff but does not change the per-edge cost of the kernel path. Reproducer, raw CSVs and plots: https://github.com/wafgo/qenc-bench (README.md has the full sweep and plots; data/aggregate.csv has the table above with full precision.) What i would like to know ------------------------- My reading of the numbers is that on this kind of SoC a kernel side edge counter is the only way to get correct counts at industrial encoder rates without burning a whole core on a userspace listener, and that the proposed driver does exactly fit this role. But like i said, this is your subsystem and your call. So concretely: - if you would like me to send a v4 (with whatever changes from this round you want me to fold in) i am happy to do that; - if you would rather not take the driver at all, please tell me so and i will drop the series. I would just like to know either way, so i can stop sitting on the branch. Thanks for taking the time to look at it. ^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH v3 0/3] counter: add GPIO-based quadrature encoder driver 2026-05-06 6:50 ` Wadim Mueller @ 2026-05-14 13:17 ` William Breathitt Gray 0 siblings, 0 replies; 16+ messages in thread From: William Breathitt Gray @ 2026-05-14 13:17 UTC (permalink / raw) To: Wadim Mueller Cc: William Breathitt Gray, linusw, brgl, conor+dt, krzk+dt, robh, conor.dooley, linux-iio, devicetree, linux-kernel On Wed, May 06, 2026 at 08:50:33AM +0200, Wadim Mueller wrote: > What i would like to know > ------------------------- > My reading of the numbers is that on this kind of SoC a kernel side > edge counter is the only way to get correct counts at industrial > encoder rates without burning a whole core on a userspace listener, > and that the proposed driver does exactly fit this role. But like i > said, this is your subsystem and your call. So concretely: > > - if you would like me to send a v4 (with whatever changes from > this round you want me to fold in) i am happy to do that; > - if you would rather not take the driver at all, please tell me > so and i will drop the series. I would just like to know either > way, so i can stop sitting on the branch. > > Thanks for taking the time to look at it. Thank you for the throughout analysis of the different GPIO counter approaches. Your arguments make sense so I can see the merit of having a GPIO kernel driver to achieve this. I'm going to complete a full review in the next day or so, after which you can submit a v4 addressing whatever comes up, and we can hopefully get this merged in to the Counter subsystem when it's all ready. William Breathitt Gray ^ permalink raw reply [flat|nested] 16+ messages in thread
* [PATCH v4 0/3] counter: add GPIO-based quadrature encoder driver 2026-05-01 20:07 [PATCH v3 0/3] counter: add GPIO-based quadrature encoder driver Wadim Mueller ` (3 preceding siblings ...) 2026-05-04 9:36 ` [PATCH v3 0/3] counter: add GPIO-based quadrature encoder driver William Breathitt Gray @ 2026-05-15 15:36 ` Wadim Mueller 2026-05-15 15:36 ` [PATCH v4 1/3] dt-bindings: counter: add gpio-quadrature-encoder binding Wadim Mueller ` (2 more replies) 4 siblings, 3 replies; 16+ messages in thread From: Wadim Mueller @ 2026-05-15 15:36 UTC (permalink / raw) To: William Breathitt Gray, Krzysztof Kozlowski, Rob Herring, Conor Dooley Cc: linux-iio, devicetree, linux-kernel, Wadim Mueller 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 ^ permalink raw reply [flat|nested] 16+ messages in thread
* [PATCH v4 1/3] dt-bindings: counter: add gpio-quadrature-encoder binding 2026-05-15 15:36 ` [PATCH v4 " Wadim Mueller @ 2026-05-15 15:36 ` Wadim Mueller 2026-05-15 15:36 ` [PATCH v4 2/3] counter: add GPIO-based quadrature encoder driver Wadim Mueller 2026-05-15 15:36 ` [PATCH v4 3/3] MAINTAINERS: add entry for GPIO quadrature encoder counter driver Wadim Mueller 2 siblings, 0 replies; 16+ messages in thread From: Wadim Mueller @ 2026-05-15 15:36 UTC (permalink / raw) To: William Breathitt Gray, Krzysztof Kozlowski, Rob Herring, Conor Dooley Cc: linux-iio, devicetree, linux-kernel, Wadim Mueller, Conor Dooley Add devicetree binding documentation for the GPIO-based quadrature encoder counter driver. The driver reads A/B quadrature signals and an optional index pulse via edge-triggered GPIO interrupts, supporting X1, X2, X4 quadrature decoding and pulse-direction mode. This is useful on SoCs that lack a dedicated hardware quadrature decoder or where the encoder is wired to generic GPIO pins. Signed-off-by: Wadim Mueller <wafgo01@gmail.com> Acked-by: Conor Dooley <conor.dooley@microchip.com> --- .../counter/gpio-quadrature-encoder.yaml | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 Documentation/devicetree/bindings/counter/gpio-quadrature-encoder.yaml diff --git a/Documentation/devicetree/bindings/counter/gpio-quadrature-encoder.yaml b/Documentation/devicetree/bindings/counter/gpio-quadrature-encoder.yaml new file mode 100644 index 000000000..741396b29 --- /dev/null +++ b/Documentation/devicetree/bindings/counter/gpio-quadrature-encoder.yaml @@ -0,0 +1,60 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/counter/gpio-quadrature-encoder.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: GPIO-based Quadrature Encoder + +maintainers: + - Wadim Mueller <wadim.mueller@cmblu.de> + +description: | + A generic GPIO-based quadrature encoder counter. Reads A/B quadrature + signals and an optional index pulse via edge-triggered GPIO interrupts. + Supports X1, X2, X4 quadrature decoding and pulse-direction mode. + + This is useful on SoCs that lack a dedicated hardware quadrature + decoder (eQEP, QEI, etc.) or where the encoder is wired to generic + GPIO pins rather than to a dedicated peripheral. + +properties: + compatible: + const: gpio-quadrature-encoder + + encoder-a-gpios: + maxItems: 1 + description: + GPIO connected to the encoder's A (phase A) output. + + encoder-b-gpios: + maxItems: 1 + description: + GPIO connected to the encoder's B (phase B) output. + + encoder-index-gpios: + maxItems: 1 + description: + Optional GPIO connected to the encoder's index (Z) output. + The index signal pulses once per revolution and can be used + as a reference point for absolute position tracking. + +required: + - compatible + - encoder-a-gpios + - encoder-b-gpios + +additionalProperties: false + +examples: + - | + #include <dt-bindings/gpio/gpio.h> + + quadrature-encoder { + compatible = "gpio-quadrature-encoder"; + encoder-a-gpios = <&gpio0 10 GPIO_ACTIVE_LOW>; + encoder-b-gpios = <&gpio0 11 GPIO_ACTIVE_LOW>; + encoder-index-gpios = <&gpio0 12 GPIO_ACTIVE_LOW>; + }; + +... -- 2.52.0 ^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH v4 2/3] counter: add GPIO-based quadrature encoder driver 2026-05-15 15:36 ` [PATCH v4 " Wadim Mueller 2026-05-15 15:36 ` [PATCH v4 1/3] dt-bindings: counter: add gpio-quadrature-encoder binding Wadim Mueller @ 2026-05-15 15:36 ` Wadim Mueller 2026-05-15 15:36 ` [PATCH v4 3/3] MAINTAINERS: add entry for GPIO quadrature encoder counter driver Wadim Mueller 2 siblings, 0 replies; 16+ messages in thread From: Wadim Mueller @ 2026-05-15 15:36 UTC (permalink / raw) To: William Breathitt Gray, Krzysztof Kozlowski, Rob Herring, Conor Dooley Cc: linux-iio, devicetree, linux-kernel, Wadim Mueller Add a platform driver that turns ordinary GPIOs into a quadrature encoder counter device. The driver requests edge-triggered interrupts on the A and B (and optional Index) GPIOs and decodes the quadrature signal in software using a classic state-table approach. Supported counting modes: - Quadrature X1 (count on A rising edge only) - Quadrature X2 (count on both A edges) - Quadrature X4 (count on every A and B edge) - Pulse-direction (A = pulse, B = direction) An optional index signal resets the count to zero on its rising edge when enabled through sysfs. A configurable ceiling clamps the count to [0, ceiling]. Signed-off-by: Wadim Mueller <wafgo01@gmail.com> --- drivers/counter/Kconfig | 15 + drivers/counter/Makefile | 1 + drivers/counter/gpio-quadrature-encoder.c | 739 ++++++++++++++++++++++ 3 files changed, 755 insertions(+) create mode 100644 drivers/counter/gpio-quadrature-encoder.c diff --git a/drivers/counter/Kconfig b/drivers/counter/Kconfig index d30d22dfe..72c5c8159 100644 --- a/drivers/counter/Kconfig +++ b/drivers/counter/Kconfig @@ -68,6 +68,21 @@ config INTEL_QEP To compile this driver as a module, choose M here: the module will be called intel-qep. +config GPIO_QUADRATURE_ENCODER + tristate "GPIO-based quadrature encoder counter driver" + depends on GPIOLIB + help + Select this option to enable the GPIO-based quadrature encoder + counter driver. It reads A/B quadrature signals and an optional + index pulse via edge-triggered GPIO interrupts, supporting X1, X2, + X4 quadrature decoding and pulse-direction mode. + + This is useful on SoCs that lack a dedicated hardware quadrature + decoder or where the encoder is wired to generic GPIO pins. + + To compile this driver as a module, choose M here: the + module will be called gpio-quadrature-encoder. + config INTERRUPT_CNT tristate "Interrupt counter driver" depends on GPIOLIB diff --git a/drivers/counter/Makefile b/drivers/counter/Makefile index fa3c1d08f..2bef64d10 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_QUADRATURE_ENCODER) += gpio-quadrature-encoder.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-quadrature-encoder.c b/drivers/counter/gpio-quadrature-encoder.c new file mode 100644 index 000000000..53e9ef878 --- /dev/null +++ b/drivers/counter/gpio-quadrature-encoder.c @@ -0,0 +1,739 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * GPIO-based Quadrature Encoder Counter Driver + * + * Reads quadrature encoder signals (A, B, and optional Index) via GPIOs. + * Supports X1, X2, X4 quadrature decoding and pulse-direction mode in + * both increase and decrease orientation, and pure increase/decrease + * pulse counters. + * + * 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_qenc_signal_id { + GPIO_QENC_SIGNAL_A = 0, + GPIO_QENC_SIGNAL_B, + GPIO_QENC_SIGNAL_INDEX, +}; + +struct gpio_qenc_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; + + /* Serialises ISR updates against sysfs read/write paths. */ + spinlock_t lock; + + u64 count; + u64 ceiling; + u64 preset; + bool preset_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]; +}; + +/* + * Encode the four quadrature transitions in a single 4-bit state: + * bit3 = prev_a, bit2 = prev_b, bit1 = curr_a, bit0 = curr_b. + * + * Indexing the table with this value yields the signed delta for an + * X4 decoder. Illegal transitions (both inputs toggled at once) + * remain 0 so the count is unchanged. + */ +#define CREATE_QE_STATE(prev_a, prev_b, curr_a, curr_b) \ + (((prev_a) << 3) | ((prev_b) << 2) | ((curr_a) << 1) | (curr_b)) + +static const s8 gpio_qenc_quad_x4_table[16] = { + [CREATE_QE_STATE(0, 0, 0, 0)] = 0, + [CREATE_QE_STATE(0, 0, 0, 1)] = -1, + [CREATE_QE_STATE(0, 0, 1, 0)] = 1, + [CREATE_QE_STATE(0, 0, 1, 1)] = 0, + [CREATE_QE_STATE(0, 1, 0, 0)] = 1, + [CREATE_QE_STATE(0, 1, 0, 1)] = 0, + [CREATE_QE_STATE(0, 1, 1, 0)] = 0, + [CREATE_QE_STATE(0, 1, 1, 1)] = -1, + [CREATE_QE_STATE(1, 0, 0, 0)] = -1, + [CREATE_QE_STATE(1, 0, 0, 1)] = 0, + [CREATE_QE_STATE(1, 0, 1, 0)] = 0, + [CREATE_QE_STATE(1, 0, 1, 1)] = 1, + [CREATE_QE_STATE(1, 1, 0, 0)] = 0, + [CREATE_QE_STATE(1, 1, 0, 1)] = 1, + [CREATE_QE_STATE(1, 1, 1, 0)] = -1, + [CREATE_QE_STATE(1, 1, 1, 1)] = 0, +}; + +static void gpio_qenc_update_count(struct gpio_qenc_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_qenc_a_delta(struct gpio_qenc_priv *priv, int a, int b, + int prev_a, int prev_b) +{ + int state = CREATE_QE_STATE(prev_a, prev_b, a, b); + + switch (priv->function) { + case COUNTER_FUNCTION_QUADRATURE_X4: + return gpio_qenc_quad_x4_table[state]; + + case COUNTER_FUNCTION_QUADRATURE_X2_A: + /* Both edges of A; sign comes from current A vs B. */ + return (a == b) ? -1 : 1; + + case COUNTER_FUNCTION_QUADRATURE_X1_A: + /* Rising edge of A only. */ + if (!prev_a && a) + return b ? -1 : 1; + return 0; + + case COUNTER_FUNCTION_PULSE_DIRECTION: + /* A is pulse, B is 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_qenc_b_delta(struct gpio_qenc_priv *priv, int a, int b, + int prev_a, int prev_b) +{ + int state = CREATE_QE_STATE(prev_a, prev_b, a, b); + + switch (priv->function) { + case COUNTER_FUNCTION_QUADRATURE_X4: + return gpio_qenc_quad_x4_table[state]; + + case COUNTER_FUNCTION_QUADRATURE_X2_B: + return (a == b) ? 1 : -1; + + case COUNTER_FUNCTION_QUADRATURE_X1_B: + if (!prev_b && b) + return a ? 1 : -1; + return 0; + + default: + return 0; + } +} + +static irqreturn_t gpio_qenc_a_isr(int irq, void *dev_id) +{ + struct counter_device *counter = dev_id; + struct gpio_qenc_priv *priv = counter_priv(counter); + unsigned long flags; + int a, b, delta; + + spin_lock_irqsave(&priv->lock, flags); + + a = gpiod_get_value(priv->gpio_a); + b = gpiod_get_value(priv->gpio_b); + + delta = gpio_qenc_a_delta(priv, a, b, priv->prev_a, priv->prev_b); + gpio_qenc_update_count(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_qenc_b_isr(int irq, void *dev_id) +{ + struct counter_device *counter = dev_id; + struct gpio_qenc_priv *priv = counter_priv(counter); + unsigned long flags; + int a, b, delta; + + spin_lock_irqsave(&priv->lock, flags); + + a = gpiod_get_value(priv->gpio_a); + b = gpiod_get_value(priv->gpio_b); + + delta = gpio_qenc_b_delta(priv, a, b, priv->prev_a, priv->prev_b); + gpio_qenc_update_count(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_qenc_index_isr(int irq, void *dev_id) +{ + struct counter_device *counter = dev_id; + struct gpio_qenc_priv *priv = counter_priv(counter); + unsigned long flags; + + spin_lock_irqsave(&priv->lock, flags); + if (priv->preset_enabled) + priv->count = priv->preset; + spin_unlock_irqrestore(&priv->lock, flags); + + counter_push_event(counter, COUNTER_EVENT_INDEX, 0); + + return IRQ_HANDLED; +} + +static int gpio_qenc_count_read(struct counter_device *counter, + struct counter_count *count, u64 *val) +{ + struct gpio_qenc_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_qenc_count_write(struct counter_device *counter, + struct counter_count *count, u64 val) +{ + struct gpio_qenc_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_qenc_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_qenc_function_read(struct counter_device *counter, + struct counter_count *count, + enum counter_function *function) +{ + struct gpio_qenc_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_qenc_function_write(struct counter_device *counter, + struct counter_count *count, + enum counter_function function) +{ + struct gpio_qenc_priv *priv = counter_priv(counter); + unsigned long flags; + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(gpio_qenc_functions); i++) + if (gpio_qenc_functions[i] == function) + break; + if (i == ARRAY_SIZE(gpio_qenc_functions)) + return -EINVAL; + + spin_lock_irqsave(&priv->lock, flags); + priv->function = function; + spin_unlock_irqrestore(&priv->lock, flags); + + return 0; +} + +static const enum counter_synapse_action gpio_qenc_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_qenc_index_synapse_actions[] = { + COUNTER_SYNAPSE_ACTION_NONE, + COUNTER_SYNAPSE_ACTION_RISING_EDGE, +}; + +static int gpio_qenc_action_read(struct counter_device *counter, + struct counter_count *count, + struct counter_synapse *synapse, + enum counter_synapse_action *action) +{ + struct gpio_qenc_priv *priv = counter_priv(counter); + enum gpio_qenc_signal_id signal_id = synapse->signal->id; + + /* Index synapse always observes rising edges, regardless of mode. */ + if (signal_id == GPIO_QENC_SIGNAL_INDEX) { + *action = COUNTER_SYNAPSE_ACTION_RISING_EDGE; + return 0; + } + + *action = COUNTER_SYNAPSE_ACTION_NONE; + + switch (priv->function) { + case COUNTER_FUNCTION_QUADRATURE_X4: + *action = COUNTER_SYNAPSE_ACTION_BOTH_EDGES; + break; + + case COUNTER_FUNCTION_QUADRATURE_X2_A: + if (signal_id == GPIO_QENC_SIGNAL_A) + *action = COUNTER_SYNAPSE_ACTION_BOTH_EDGES; + break; + + case COUNTER_FUNCTION_QUADRATURE_X2_B: + if (signal_id == GPIO_QENC_SIGNAL_B) + *action = COUNTER_SYNAPSE_ACTION_BOTH_EDGES; + break; + + case COUNTER_FUNCTION_QUADRATURE_X1_A: + if (signal_id == GPIO_QENC_SIGNAL_A) { + if (priv->direction == COUNTER_COUNT_DIRECTION_FORWARD) + *action = COUNTER_SYNAPSE_ACTION_RISING_EDGE; + else + *action = COUNTER_SYNAPSE_ACTION_FALLING_EDGE; + } + break; + + case COUNTER_FUNCTION_QUADRATURE_X1_B: + if (signal_id == GPIO_QENC_SIGNAL_B) { + if (priv->direction == COUNTER_COUNT_DIRECTION_FORWARD) + *action = COUNTER_SYNAPSE_ACTION_RISING_EDGE; + else + *action = COUNTER_SYNAPSE_ACTION_FALLING_EDGE; + } + break; + + case COUNTER_FUNCTION_PULSE_DIRECTION: + case COUNTER_FUNCTION_INCREASE: + case COUNTER_FUNCTION_DECREASE: + if (signal_id == GPIO_QENC_SIGNAL_A) + *action = COUNTER_SYNAPSE_ACTION_RISING_EDGE; + break; + + default: + return -EINVAL; + } + + return 0; +} + +static int gpio_qenc_signal_read(struct counter_device *counter, + struct counter_signal *signal, + enum counter_signal_level *level) +{ + struct gpio_qenc_priv *priv = counter_priv(counter); + struct gpio_desc *gpio; + int ret; + + switch (signal->id) { + case GPIO_QENC_SIGNAL_A: + gpio = priv->gpio_a; + break; + case GPIO_QENC_SIGNAL_B: + gpio = priv->gpio_b; + break; + case GPIO_QENC_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_qenc_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_qenc_ops = { + .count_read = gpio_qenc_count_read, + .count_write = gpio_qenc_count_write, + .function_read = gpio_qenc_function_read, + .function_write = gpio_qenc_function_write, + .action_read = gpio_qenc_action_read, + .signal_read = gpio_qenc_signal_read, + .watch_validate = gpio_qenc_watch_validate, +}; + +static int gpio_qenc_ceiling_read(struct counter_device *counter, + struct counter_count *count, u64 *val) +{ + struct gpio_qenc_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_qenc_ceiling_write(struct counter_device *counter, + struct counter_count *count, u64 val) +{ + struct gpio_qenc_priv *priv = counter_priv(counter); + unsigned long flags; + + spin_lock_irqsave(&priv->lock, flags); + priv->ceiling = val; + if (priv->count > val) + priv->count = val; + spin_unlock_irqrestore(&priv->lock, flags); + + return 0; +} + +static int gpio_qenc_enable_read(struct counter_device *counter, + struct counter_count *count, u8 *enable) +{ + struct gpio_qenc_priv *priv = counter_priv(counter); + unsigned long flags; + bool irq_enabled; + + spin_lock_irqsave(&priv->lock, flags); + irq_enabled = !irqd_irq_disabled(irq_get_irq_data(priv->irq_a)); + spin_unlock_irqrestore(&priv->lock, flags); + + *enable = irq_enabled; + return 0; +} + +static int gpio_qenc_enable_write(struct counter_device *counter, + struct counter_count *count, u8 enable) +{ + struct gpio_qenc_priv *priv = counter_priv(counter); + + if (enable) { + enable_irq(priv->irq_a); + enable_irq(priv->irq_b); + if (priv->irq_index) + enable_irq(priv->irq_index); + return 0; + } + + disable_irq(priv->irq_a); + disable_irq(priv->irq_b); + if (priv->irq_index) + disable_irq(priv->irq_index); + return 0; +} + +static int gpio_qenc_direction_read(struct counter_device *counter, + struct counter_count *count, u32 *direction) +{ + struct gpio_qenc_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_qenc_preset_read(struct counter_device *counter, + struct counter_count *count, u64 *val) +{ + struct gpio_qenc_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_qenc_preset_write(struct counter_device *counter, + struct counter_count *count, u64 val) +{ + struct gpio_qenc_priv *priv = counter_priv(counter); + unsigned long flags; + + if (val > priv->ceiling) + return -EINVAL; + + spin_lock_irqsave(&priv->lock, flags); + priv->preset = val; + spin_unlock_irqrestore(&priv->lock, flags); + + return 0; +} + +static int gpio_qenc_preset_enable_read(struct counter_device *counter, + struct counter_count *count, u8 *val) +{ + struct gpio_qenc_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_qenc_preset_enable_write(struct counter_device *counter, + struct counter_count *count, u8 val) +{ + struct gpio_qenc_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_qenc_count_ext[] = { + COUNTER_COMP_CEILING(gpio_qenc_ceiling_read, gpio_qenc_ceiling_write), + COUNTER_COMP_ENABLE(gpio_qenc_enable_read, gpio_qenc_enable_write), + COUNTER_COMP_DIRECTION(gpio_qenc_direction_read), + COUNTER_COMP_PRESET(gpio_qenc_preset_read, gpio_qenc_preset_write), + COUNTER_COMP_PRESET_ENABLE(gpio_qenc_preset_enable_read, + gpio_qenc_preset_enable_write), +}; + +static int gpio_qenc_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct counter_device *counter; + struct gpio_qenc_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, "encoder-a", GPIOD_IN); + if (IS_ERR(priv->gpio_a)) + return dev_err_probe(dev, PTR_ERR(priv->gpio_a), + "failed to get encoder-a GPIO\n"); + + priv->gpio_b = devm_gpiod_get(dev, "encoder-b", GPIOD_IN); + if (IS_ERR(priv->gpio_b)) + return dev_err_probe(dev, PTR_ERR(priv->gpio_b), + "failed to get encoder-b GPIO\n"); + + priv->gpio_index = devm_gpiod_get_optional(dev, "encoder-index", + GPIOD_IN); + if (IS_ERR(priv->gpio_index)) + return dev_err_probe(dev, PTR_ERR(priv->gpio_index), + "failed to get encoder-index GPIO\n"); + + has_index = !!priv->gpio_index; + + 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 encoder-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 encoder-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 encoder-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; + + num_signals = has_index ? 3 : 2; + num_synapses = num_signals; + + priv->signals[GPIO_QENC_SIGNAL_A].id = GPIO_QENC_SIGNAL_A; + priv->signals[GPIO_QENC_SIGNAL_A].name = "Signal A"; + + priv->signals[GPIO_QENC_SIGNAL_B].id = GPIO_QENC_SIGNAL_B; + priv->signals[GPIO_QENC_SIGNAL_B].name = "Signal B"; + + priv->synapses[0].actions_list = gpio_qenc_synapse_actions; + priv->synapses[0].num_actions = ARRAY_SIZE(gpio_qenc_synapse_actions); + priv->synapses[0].signal = &priv->signals[GPIO_QENC_SIGNAL_A]; + + priv->synapses[1].actions_list = gpio_qenc_synapse_actions; + priv->synapses[1].num_actions = ARRAY_SIZE(gpio_qenc_synapse_actions); + priv->synapses[1].signal = &priv->signals[GPIO_QENC_SIGNAL_B]; + + if (has_index) { + priv->signals[GPIO_QENC_SIGNAL_INDEX].id = + GPIO_QENC_SIGNAL_INDEX; + priv->signals[GPIO_QENC_SIGNAL_INDEX].name = "Index"; + + priv->synapses[2].actions_list = gpio_qenc_index_synapse_actions; + priv->synapses[2].num_actions = + ARRAY_SIZE(gpio_qenc_index_synapse_actions); + priv->synapses[2].signal = + &priv->signals[GPIO_QENC_SIGNAL_INDEX]; + } + + priv->cnts[0].id = 0; + priv->cnts[0].name = "Count"; + priv->cnts[0].functions_list = gpio_qenc_functions; + priv->cnts[0].num_functions = ARRAY_SIZE(gpio_qenc_functions); + priv->cnts[0].synapses = priv->synapses; + priv->cnts[0].num_synapses = num_synapses; + priv->cnts[0].ext = gpio_qenc_count_ext; + priv->cnts[0].num_ext = ARRAY_SIZE(gpio_qenc_count_ext); + + counter->name = dev_name(dev); + counter->parent = dev; + counter->ops = &gpio_qenc_ops; + counter->signals = priv->signals; + counter->num_signals = num_signals; + counter->counts = priv->cnts; + counter->num_counts = ARRAY_SIZE(priv->cnts); + + irq_set_status_flags(priv->irq_a, IRQ_NOAUTOEN); + ret = devm_request_irq(dev, priv->irq_a, gpio_qenc_a_isr, + IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, + "gpio-qenc-a", counter); + if (ret) + return dev_err_probe(dev, ret, + "failed to request IRQ for encoder-a\n"); + + irq_set_status_flags(priv->irq_b, IRQ_NOAUTOEN); + ret = devm_request_irq(dev, priv->irq_b, gpio_qenc_b_isr, + IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, + "gpio-qenc-b", counter); + if (ret) + return dev_err_probe(dev, ret, + "failed to request IRQ for encoder-b\n"); + + if (has_index) { + irq_set_status_flags(priv->irq_index, IRQ_NOAUTOEN); + ret = devm_request_irq(dev, priv->irq_index, + gpio_qenc_index_isr, + IRQF_TRIGGER_RISING, + "gpio-qenc-index", counter); + if (ret) + return dev_err_probe(dev, ret, + "failed to request IRQ for encoder-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 quadrature encoder registered (signals: A, B%s)\n", + has_index ? ", Index" : ""); + + return 0; +} + +static const struct of_device_id gpio_qenc_of_match[] = { + { .compatible = "gpio-quadrature-encoder" }, + {} +}; +MODULE_DEVICE_TABLE(of, gpio_qenc_of_match); + +static struct platform_driver gpio_qenc_driver = { + .probe = gpio_qenc_probe, + .driver = { + .name = "gpio-quadrature-encoder", + .of_match_table = gpio_qenc_of_match, + }, +}; +module_platform_driver(gpio_qenc_driver); + +MODULE_ALIAS("platform:gpio-quadrature-encoder"); +MODULE_AUTHOR("Wadim Mueller <wafgo01@gmail.com>"); +MODULE_DESCRIPTION("GPIO-based quadrature encoder counter driver"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("COUNTER"); -- 2.52.0 ^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH v4 3/3] MAINTAINERS: add entry for GPIO quadrature encoder counter driver 2026-05-15 15:36 ` [PATCH v4 " Wadim Mueller 2026-05-15 15:36 ` [PATCH v4 1/3] dt-bindings: counter: add gpio-quadrature-encoder binding Wadim Mueller 2026-05-15 15:36 ` [PATCH v4 2/3] counter: add GPIO-based quadrature encoder driver Wadim Mueller @ 2026-05-15 15:36 ` Wadim Mueller 2 siblings, 0 replies; 16+ messages in thread From: Wadim Mueller @ 2026-05-15 15:36 UTC (permalink / raw) To: William Breathitt Gray, Krzysztof Kozlowski, Rob Herring, Conor Dooley Cc: linux-iio, devicetree, linux-kernel, Wadim Mueller Add myself as maintainer for the new gpio-quadrature-encoder counter driver and its devicetree binding. Signed-off-by: Wadim Mueller <wafgo01@gmail.com> --- MAINTAINERS | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/MAINTAINERS b/MAINTAINERS index 06a8c7457..fca62baa7 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -11018,6 +11018,13 @@ F: Documentation/dev-tools/gpio-sloppy-logic-analyzer.rst F: drivers/gpio/gpio-sloppy-logic-analyzer.c F: tools/gpio/gpio-sloppy-logic-analyzer.sh +GPIO QUADRATURE ENCODER COUNTER DRIVER +M: Wadim Mueller <wafgo01@gmail.com> +L: linux-iio@vger.kernel.org +S: Maintained +F: Documentation/devicetree/bindings/counter/gpio-quadrature-encoder.yaml +F: drivers/counter/gpio-quadrature-encoder.c + GPIO SUBSYSTEM M: Linus Walleij <linusw@kernel.org> M: Bartosz Golaszewski <brgl@kernel.org> -- 2.52.0 ^ permalink raw reply related [flat|nested] 16+ messages in thread
end of thread, other threads:[~2026-05-15 15:36 UTC | newest] Thread overview: 16+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-05-01 20:07 [PATCH v3 0/3] counter: add GPIO-based quadrature encoder driver Wadim Mueller 2026-05-01 20:07 ` [PATCH v3 1/3] dt-bindings: counter: add gpio-quadrature-encoder binding Wadim Mueller 2026-05-01 20:07 ` [PATCH v3 2/3] counter: add GPIO-based quadrature encoder driver Wadim Mueller 2026-05-04 20:54 ` Krzysztof Kozlowski 2026-05-04 21:15 ` Wadim Mueller 2026-05-15 5:48 ` William Breathitt Gray 2026-05-15 15:28 ` Wadim Mueller 2026-05-01 20:07 ` [PATCH v3 3/3] MAINTAINERS: add entry for GPIO quadrature encoder counter driver Wadim Mueller 2026-05-04 9:36 ` [PATCH v3 0/3] counter: add GPIO-based quadrature encoder driver William Breathitt Gray 2026-05-04 19:37 ` Wadim Mueller 2026-05-06 6:50 ` Wadim Mueller 2026-05-14 13:17 ` William Breathitt Gray 2026-05-15 15:36 ` [PATCH v4 " Wadim Mueller 2026-05-15 15:36 ` [PATCH v4 1/3] dt-bindings: counter: add gpio-quadrature-encoder binding Wadim Mueller 2026-05-15 15:36 ` [PATCH v4 2/3] counter: add GPIO-based quadrature encoder driver Wadim Mueller 2026-05-15 15:36 ` [PATCH v4 3/3] MAINTAINERS: add entry for GPIO quadrature encoder counter driver Wadim Mueller
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox