From 2790106c339614ba188c69c14148c8df44d89ecf Mon Sep 17 00:00:00 2001 From: Fabio Baltieri Date: Sat, 1 May 2021 23:11:14 +0100 Subject: [PATCH] input: add a gpio qdec input driver Add a GPIO based quadrature decoder driver that reports relative axes movements using the input subsystem. Signed-off-by: Fabio Baltieri --- drivers/input/CMakeLists.txt | 1 + drivers/input/Kconfig | 1 + drivers/input/Kconfig.gpio_qdec | 9 ++ drivers/input/input_gpio_qdec.c | 240 ++++++++++++++++++++++++++++++ dts/bindings/input/gpio-qdec.yaml | 63 ++++++++ 5 files changed, 314 insertions(+) create mode 100644 drivers/input/Kconfig.gpio_qdec create mode 100644 drivers/input/input_gpio_qdec.c create mode 100644 dts/bindings/input/gpio-qdec.yaml diff --git a/drivers/input/CMakeLists.txt b/drivers/input/CMakeLists.txt index c775654d47..d6e0f2e802 100644 --- a/drivers/input/CMakeLists.txt +++ b/drivers/input/CMakeLists.txt @@ -5,5 +5,6 @@ zephyr_library_property(ALLOW_EMPTY TRUE) zephyr_library_sources_ifdef(CONFIG_INPUT_FT5336 input_ft5336.c) zephyr_library_sources_ifdef(CONFIG_INPUT_GPIO_KEYS input_gpio_keys.c) +zephyr_library_sources_ifdef(CONFIG_INPUT_GPIO_QDEC input_gpio_qdec.c) zephyr_library_sources_ifdef(CONFIG_INPUT_NPCX_KBD input_npcx_kbd.c) zephyr_library_sources_ifdef(CONFIG_INPUT_SDL_TOUCH input_sdl_touch.c) diff --git a/drivers/input/Kconfig b/drivers/input/Kconfig index f1fb121b76..ee9508ae4d 100644 --- a/drivers/input/Kconfig +++ b/drivers/input/Kconfig @@ -7,6 +7,7 @@ menu "Input drivers" source "drivers/input/Kconfig.ft5336" source "drivers/input/Kconfig.gpio_keys" +source "drivers/input/Kconfig.gpio_qdec" source "drivers/input/Kconfig.npcx" source "drivers/input/Kconfig.sdl" diff --git a/drivers/input/Kconfig.gpio_qdec b/drivers/input/Kconfig.gpio_qdec new file mode 100644 index 0000000000..2e66fa3699 --- /dev/null +++ b/drivers/input/Kconfig.gpio_qdec @@ -0,0 +1,9 @@ +# Copyright 2023 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +config INPUT_GPIO_QDEC + bool "GPIO based QDEC input driver" + default y + depends on DT_HAS_GPIO_QDEC_ENABLED + help + GPIO Quadrature Decoder input driver. diff --git a/drivers/input/input_gpio_qdec.c b/drivers/input/input_gpio_qdec.c new file mode 100644 index 0000000000..85716fdcc1 --- /dev/null +++ b/drivers/input/input_gpio_qdec.c @@ -0,0 +1,240 @@ +/* + * Copyright 2023 Google LLC + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#define DT_DRV_COMPAT gpio_qdec + +#include +#include + +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(input_gpio_qdec, CONFIG_INPUT_LOG_LEVEL); + +#define GPIO_QDEC_GPIO_NUM 2 + +struct gpio_qdec_config { + struct gpio_dt_spec gpio[GPIO_QDEC_GPIO_NUM]; + uint32_t sample_time_us; + uint32_t idle_timeout_ms; + uint16_t axis; + uint8_t steps_per_period; +}; + +struct gpio_qdec_data { + const struct device *dev; + struct k_timer sample_timer; + uint8_t prev_step; + int32_t acc; + struct k_work event_work; + struct k_work_delayable idle_work; + struct gpio_callback gpio_cb; +}; + +/* Positive transitions */ +#define QDEC_LL_LH 0x01 +#define QDEC_LH_HH 0x13 +#define QDEC_HH_HL 0x32 +#define QDEC_HL_LL 0x20 + +/* Negative transitions */ +#define QDEC_LL_HL 0x02 +#define QDEC_LH_LL 0x10 +#define QDEC_HH_LH 0x31 +#define QDEC_HL_HH 0x23 + +static uint8_t gpio_qdec_get_step(const struct device *dev) +{ + const struct gpio_qdec_config *cfg = dev->config; + uint8_t step = 0x00; + + if (gpio_pin_get_dt(&cfg->gpio[0])) { + step |= 0x01; + } + if (gpio_pin_get_dt(&cfg->gpio[1])) { + step |= 0x02; + } + + return step; +} + +static void gpio_qdec_sample_timer_timeout(struct k_timer *timer) +{ + const struct device *dev = k_timer_user_data_get(timer); + const struct gpio_qdec_config *cfg = dev->config; + struct gpio_qdec_data *data = dev->data; + int8_t delta = 0; + unsigned int key; + uint8_t step; + + step = gpio_qdec_get_step(dev); + + if (data->prev_step == step) { + return; + } + + switch ((data->prev_step << 4U) | step) { + case QDEC_LL_LH: + case QDEC_LH_HH: + case QDEC_HH_HL: + case QDEC_HL_LL: + delta = 1; + break; + case QDEC_LL_HL: + case QDEC_LH_LL: + case QDEC_HH_LH: + case QDEC_HL_HH: + delta = -1; + break; + default: + LOG_WRN("%s: lost steps", dev->name); + } + + data->prev_step = step; + + key = irq_lock(); + data->acc += delta; + irq_unlock(key); + + if (abs(data->acc) >= cfg->steps_per_period) { + k_work_submit(&data->event_work); + } + + k_work_reschedule(&data->idle_work, K_MSEC(cfg->idle_timeout_ms)); +} + +static void gpio_qdec_event_worker(struct k_work *work) +{ + struct gpio_qdec_data *data = CONTAINER_OF( + work, struct gpio_qdec_data, event_work); + const struct device *dev = data->dev; + const struct gpio_qdec_config *cfg = dev->config; + unsigned int key; + int32_t acc; + + key = irq_lock(); + acc = data->acc / cfg->steps_per_period; + data->acc -= acc * cfg->steps_per_period; + irq_unlock(key); + + if (acc != 0) { + input_report_rel(data->dev, cfg->axis, acc, true, K_FOREVER); + } +} + +static void gpio_qdec_irq_setup(const struct device *dev, bool enable) +{ + const struct gpio_qdec_config *cfg = dev->config; + unsigned int flags = enable ? GPIO_INT_EDGE_BOTH : GPIO_INT_DISABLE; + int ret; + + for (int i = 0; i < GPIO_QDEC_GPIO_NUM; i++) { + const struct gpio_dt_spec *gpio = &cfg->gpio[i]; + + ret = gpio_pin_interrupt_configure_dt(gpio, flags); + if (ret != 0) { + LOG_ERR("Pin %d interrupt configuration failed: %d", i, ret); + return; + } + } +} + +static void gpio_qdec_idle_worker(struct k_work *work) +{ + struct gpio_qdec_data *data = CONTAINER_OF( + work, struct gpio_qdec_data, idle_work); + const struct device *dev = data->dev; + + k_timer_stop(&data->sample_timer); + + gpio_qdec_irq_setup(dev, true); + + LOG_DBG("polling stop"); +} + +static void gpio_qdec_cb(const struct device *gpio_dev, struct gpio_callback *cb, + uint32_t pins) +{ + struct gpio_qdec_data *data = CONTAINER_OF( + cb, struct gpio_qdec_data, gpio_cb); + const struct device *dev = data->dev; + const struct gpio_qdec_config *cfg = dev->config; + + gpio_qdec_irq_setup(dev, false); + + k_timer_start(&data->sample_timer, K_NO_WAIT, + K_USEC(cfg->sample_time_us)); + + LOG_DBG("polling start"); +} + +static int gpio_qdec_init(const struct device *dev) +{ + const struct gpio_qdec_config *cfg = dev->config; + struct gpio_qdec_data *data = dev->data; + int ret; + + data->dev = dev; + + k_work_init(&data->event_work, gpio_qdec_event_worker); + k_work_init_delayable(&data->idle_work, gpio_qdec_idle_worker); + + k_timer_init(&data->sample_timer, gpio_qdec_sample_timer_timeout, NULL); + k_timer_user_data_set(&data->sample_timer, (void *)dev); + + gpio_init_callback(&data->gpio_cb, gpio_qdec_cb, + BIT(cfg->gpio[0].pin) | BIT(cfg->gpio[1].pin)); + for (int i = 0; i < GPIO_QDEC_GPIO_NUM; i++) { + const struct gpio_dt_spec *gpio = &cfg->gpio[i]; + + if (!gpio_is_ready_dt(gpio)) { + LOG_ERR("%s is not ready", gpio->port->name); + return -ENODEV; + } + + ret = gpio_pin_configure_dt(gpio, GPIO_INPUT); + if (ret != 0) { + LOG_ERR("Pin %d configuration failed: %d", i, ret); + return ret; + } + + gpio_add_callback_dt(gpio, &data->gpio_cb); + } + + data->prev_step = gpio_qdec_get_step(dev); + + gpio_qdec_irq_setup(dev, true); + + LOG_DBG("Device %s initialized", dev->name); + + return 0; +} + +#define QDEC_GPIO_INIT(n) \ + BUILD_ASSERT(DT_INST_PROP_LEN(n, gpios) == GPIO_QDEC_GPIO_NUM, \ + "input_gpio_qdec: gpios must have exactly two entries"); \ + \ + static const struct gpio_qdec_config gpio_qdec_cfg_##n = { \ + .gpio = {GPIO_DT_SPEC_INST_GET_BY_IDX(n, gpios, 0), \ + GPIO_DT_SPEC_INST_GET_BY_IDX(n, gpios, 1)}, \ + .sample_time_us = DT_INST_PROP(n, sample_time_us), \ + .idle_timeout_ms = DT_INST_PROP(n, idle_timeout_ms), \ + .steps_per_period = DT_INST_PROP(n, steps_per_period), \ + .axis = DT_INST_PROP(n, zephyr_axis), \ + }; \ + \ + static struct gpio_qdec_data gpio_qdec_data_##n; \ + \ + DEVICE_DT_INST_DEFINE(n, gpio_qdec_init, NULL, \ + &gpio_qdec_data_##n, \ + &gpio_qdec_cfg_##n, \ + POST_KERNEL, CONFIG_INPUT_INIT_PRIORITY, \ + NULL); + +DT_INST_FOREACH_STATUS_OKAY(QDEC_GPIO_INIT) diff --git a/dts/bindings/input/gpio-qdec.yaml b/dts/bindings/input/gpio-qdec.yaml new file mode 100644 index 0000000000..ef9da618bb --- /dev/null +++ b/dts/bindings/input/gpio-qdec.yaml @@ -0,0 +1,63 @@ +# Copyright 2023 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +description: | + GPIO based QDEC input device + + Implement an input device generating relative axis event reports for a rotary + encoder connected to two GPIOs. The driver is normally idling until it sees a + transition on any of the encoder signal lines, then switches to polling mode + and samples the two signal lines periodically to track the encoder position, + and goes back to idling after the specified timeout. + + Example configuration: + + #include + + qdec { + compatible = "gpio-qdec"; + gpios = <&gpio0 14 (GPIO_PULL_UP | GPIO_ACTIVE_HIGH)>, + <&gpio0 13 (GPIO_PULL_UP | GPIO_ACTIVE_HIGH)>; + steps-per-period = <4>; + zephyr,axis = ; + sample-time-us = <2000>; + idle-timeout-ms = <200>; + }; + +compatible: "gpio-qdec" + +include: base.yaml + +properties: + gpios: + type: phandle-array + required: true + description: | + GPIO for the A and B encoder signals. + + steps-per-period: + type: int + required: true + description: | + How many steps to count before reporting an input event. + + zephyr,axis: + type: int + required: true + description: | + The input code for the axis to report for the device, typically any of + INPUT_REL_*. + + sample-time-us: + type: int + required: true + description: | + How often to sample the A and B signal lines when tracking the encoder + movement. + + idle-timeout-ms: + type: int + required: true + description: | + Timeout for the last detected transition before stopping the sampling + timer and going back to idle state.