1dd2307b3f
Change the gpio_qdec driver to support optical encoders. Add a property to use for defining an arbitrary number of GPIOs for the sensing devices (typically infrared LEDs, but could also be the biasing for the phototransistor), and one for adding a delay between turning those on and reading the pin status. The infrared LEDs typically consume a non negligible amount of power, so there's also a new idle-poll-time-us property that enables two possible modes of operation: - if idle-poll-time-us is zero (default) the LEDs are enabled all the time and the driver enters polling mode using the GPIO interrupt as with mechanical encoders. This is usable for mains powered devices and has the lowest overhead on the CPU. - if idle-poll-time-us is non zero, then the driver polls the encoder all the time, turning on the LEDs just before reading the state and shutting them off immediately after, but when the encoder is idle it switches the polling rate to idle-poll-time-us to save power, and only polls at sample-time-us when some movement is detected. Signed-off-by: Fabio Baltieri <fabiobaltieri@google.com>
352 lines
8.5 KiB
C
352 lines
8.5 KiB
C
/*
|
|
* Copyright 2023 Google LLC
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
#define DT_DRV_COMPAT gpio_qdec
|
|
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
|
|
#include <zephyr/device.h>
|
|
#include <zephyr/drivers/gpio.h>
|
|
#include <zephyr/input/input.h>
|
|
#include <zephyr/kernel.h>
|
|
#include <zephyr/sys/atomic.h>
|
|
#include <zephyr/sys/util.h>
|
|
|
|
#include <zephyr/logging/log.h>
|
|
LOG_MODULE_REGISTER(input_gpio_qdec, CONFIG_INPUT_LOG_LEVEL);
|
|
|
|
#define GPIO_QDEC_GPIO_NUM 2
|
|
|
|
struct gpio_qdec_config {
|
|
struct gpio_dt_spec ab_gpio[GPIO_QDEC_GPIO_NUM];
|
|
const struct gpio_dt_spec *led_gpio;
|
|
uint8_t led_gpio_count;
|
|
uint32_t led_pre_us;
|
|
uint32_t sample_time_us;
|
|
uint32_t idle_poll_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;
|
|
atomic_t polling;
|
|
};
|
|
|
|
/* 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 void gpio_qdec_irq_setup(const struct device *dev, bool enable)
|
|
{
|
|
const struct gpio_qdec_config *cfg = dev->config;
|
|
gpio_flags_t 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->ab_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 bool gpio_qdec_idle_polling_mode(const struct device *dev)
|
|
{
|
|
const struct gpio_qdec_config *cfg = dev->config;
|
|
|
|
if (cfg->idle_poll_time_us > 0) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static void gpio_qdec_poll_mode(const struct device *dev)
|
|
{
|
|
const struct gpio_qdec_config *cfg = dev->config;
|
|
struct gpio_qdec_data *data = dev->data;
|
|
|
|
if (!gpio_qdec_idle_polling_mode(dev)) {
|
|
gpio_qdec_irq_setup(dev, false);
|
|
}
|
|
|
|
k_timer_start(&data->sample_timer, K_NO_WAIT,
|
|
K_USEC(cfg->sample_time_us));
|
|
|
|
atomic_set(&data->polling, 1);
|
|
|
|
LOG_DBG("polling start");
|
|
}
|
|
|
|
static void gpio_qdec_idle_mode(const struct device *dev)
|
|
{
|
|
const struct gpio_qdec_config *cfg = dev->config;
|
|
struct gpio_qdec_data *data = dev->data;
|
|
|
|
if (gpio_qdec_idle_polling_mode(dev)) {
|
|
k_timer_start(&data->sample_timer, K_NO_WAIT,
|
|
K_USEC(cfg->idle_poll_time_us));
|
|
} else {
|
|
k_timer_stop(&data->sample_timer);
|
|
gpio_qdec_irq_setup(dev, true);
|
|
}
|
|
|
|
atomic_set(&data->polling, 0);
|
|
|
|
LOG_DBG("polling stop");
|
|
}
|
|
|
|
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_qdec_idle_polling_mode(dev)) {
|
|
for (int i = 0; i < cfg->led_gpio_count; i++) {
|
|
gpio_pin_set_dt(&cfg->led_gpio[i], 1);
|
|
}
|
|
|
|
k_busy_wait(cfg->led_pre_us);
|
|
}
|
|
|
|
if (gpio_pin_get_dt(&cfg->ab_gpio[0])) {
|
|
step |= 0x01;
|
|
}
|
|
if (gpio_pin_get_dt(&cfg->ab_gpio[1])) {
|
|
step |= 0x02;
|
|
}
|
|
|
|
if (gpio_qdec_idle_polling_mode(dev)) {
|
|
for (int i = 0; i < cfg->led_gpio_count; i++) {
|
|
gpio_pin_set_dt(&cfg->led_gpio[i], 0);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (gpio_qdec_idle_polling_mode(dev) &&
|
|
atomic_get(&data->polling) == 0) {
|
|
gpio_qdec_poll_mode(dev);
|
|
}
|
|
|
|
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_idle_worker(struct k_work *work)
|
|
{
|
|
struct k_work_delayable *dwork = k_work_delayable_from_work(work);
|
|
struct gpio_qdec_data *data = CONTAINER_OF(
|
|
dwork, struct gpio_qdec_data, idle_work);
|
|
const struct device *dev = data->dev;
|
|
|
|
gpio_qdec_idle_mode(dev);
|
|
}
|
|
|
|
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;
|
|
|
|
gpio_qdec_poll_mode(dev);
|
|
}
|
|
|
|
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->ab_gpio[0].pin) | BIT(cfg->ab_gpio[1].pin));
|
|
for (int i = 0; i < GPIO_QDEC_GPIO_NUM; i++) {
|
|
const struct gpio_dt_spec *gpio = &cfg->ab_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;
|
|
}
|
|
|
|
if (gpio_qdec_idle_polling_mode(dev)) {
|
|
continue;
|
|
}
|
|
|
|
ret = gpio_add_callback_dt(gpio, &data->gpio_cb);
|
|
if (ret < 0) {
|
|
LOG_ERR("Could not set gpio callback");
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < cfg->led_gpio_count; i++) {
|
|
const struct gpio_dt_spec *gpio = &cfg->led_gpio[i];
|
|
gpio_flags_t mode;
|
|
|
|
if (!gpio_is_ready_dt(gpio)) {
|
|
LOG_ERR("%s is not ready", gpio->port->name);
|
|
return -ENODEV;
|
|
}
|
|
|
|
mode = gpio_qdec_idle_polling_mode(dev) ?
|
|
GPIO_OUTPUT_INACTIVE : GPIO_OUTPUT_ACTIVE;
|
|
|
|
ret = gpio_pin_configure_dt(gpio, mode);
|
|
if (ret != 0) {
|
|
LOG_ERR("Pin %d configuration failed: %d", i, ret);
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
data->prev_step = gpio_qdec_get_step(dev);
|
|
|
|
gpio_qdec_idle_mode(dev);
|
|
|
|
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"); \
|
|
\
|
|
BUILD_ASSERT(!(DT_INST_NODE_HAS_PROP(n, led_gpios) && \
|
|
DT_INST_NODE_HAS_PROP(n, idle_poll_time_us)) || \
|
|
DT_INST_NODE_HAS_PROP(n, led_pre_us), \
|
|
"led-pre-us must be specified when setting led-gpios and " \
|
|
"idle-poll-time-us"); \
|
|
\
|
|
IF_ENABLED(DT_INST_NODE_HAS_PROP(n, led_gpios), ( \
|
|
static const struct gpio_dt_spec gpio_qdec_led_gpio_##n[] = { \
|
|
DT_INST_FOREACH_PROP_ELEM_SEP(n, led_gpios, \
|
|
GPIO_DT_SPEC_GET_BY_IDX, (,)) \
|
|
}; \
|
|
)) \
|
|
\
|
|
static const struct gpio_qdec_config gpio_qdec_cfg_##n = { \
|
|
.ab_gpio = { \
|
|
GPIO_DT_SPEC_INST_GET_BY_IDX(n, gpios, 0), \
|
|
GPIO_DT_SPEC_INST_GET_BY_IDX(n, gpios, 1), \
|
|
}, \
|
|
IF_ENABLED(DT_INST_NODE_HAS_PROP(n, led_gpios), ( \
|
|
.led_gpio = gpio_qdec_led_gpio_##n, \
|
|
.led_gpio_count = ARRAY_SIZE(gpio_qdec_led_gpio_##n), \
|
|
.led_pre_us = DT_INST_PROP_OR(n, led_pre_us, 0), \
|
|
)) \
|
|
.sample_time_us = DT_INST_PROP(n, sample_time_us), \
|
|
.idle_poll_time_us = DT_INST_PROP_OR(n, idle_poll_time_us, 0), \
|
|
.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)
|