EC11 Rotary Encoder with Sensor API

I have an EC11 rotary encoder wired to my nRF52840 and I am trying to get readings from it using the sensor API. However, I'm having issues with (seemingly) garbage data. When I rotate one "click" I get a variety of readings. Sometimes no readings whatsoever, sometimes multiple readings. I referenced the nrf_desktop wheel implementation for my configuration.

I should note that I am relying on software pullups for my configuration, hence the specific gpio definition for the encoder pins.

It is important to me that I do not poll the encoder in order to save power. Am I missing something obvious?

Here's my overlay file:

/ {
    aliases {
        sw0 = &button0;
        sw1 = &button1;
        sw2 = &button2;
        qdec0 = &qdec0;
        qenca = &encoder_a;
        qencb = &encoder_b;
    };
    
    encoder_a: encoder_a_pin {
        gpios = <&gpio1 13 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
        label = "Rotary interrupt A";
    };
    encoder_b: encoder_b_pin {
        gpios = <&gpio1 15 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
        label = "Rotary interrupt B";
    };
};


&pinctrl {
    qdec_pinctrl: qdec_pinctrl {
        group1 {
            psels = <NRF_PSEL(QDEC_A, 1, 13)>,
                    <NRF_PSEL(QDEC_B, 1, 15)>;
        };
    };
};

&qdec0 {
    status = "okay";
    pinctrl-0 = <&qdec_pinctrl>;
    pinctrl-names = "default";
    steps = <80>;
    led-pre = <0>;
};

My interface code:

const struct device *const encoder_device = DEVICE_DT_GET(DT_NODELABEL(qdec0));

static void sample_encoder()
{
    struct sensor_value val;
    int rc;
    rc = sensor_sample_fetch(encoder_device);
    if (rc != 0)
    {
        LOG_INF("Failed to fetch sample (%d)\n", rc);
        return 0;
    }

    rc = sensor_channel_get(encoder_device, SENSOR_CHAN_ROTATION, &val);
    if (rc != 0)
    {
        LOG_INF("Failed to get data (%d)\n", rc);
        return 0;
    }

    if (val.val1 != position)
    {
        position = val.val1;
        LOG_INF("Position = %d degrees\n", position);
    }
}

void init_encoder(){
    if (!device_is_ready(encoder_device))
    {
        LOG_ERR("Cannot get QDEC device");
        return -ENXIO;
    }

    static const struct sensor_trigger qdec_trig = {
        .type = SENSOR_TRIG_DATA_READY,
        .chan = SENSOR_CHAN_ROTATION,
    };

    err = sensor_trigger_set(encoder_device, (struct sensor_trigger *)&qdec_trig,
                             sample_encoder);
 }

Here is an example log of one "click" clockwise:

[00:11:40.740,112] <inf> main_logging: Position = 4 degrees

[00:11:40.822,021] <inf> main_logging: Position = 13 degrees

And here is the logs from three "clicks" counterclockwise:

[00:12:14.900,543] <inf> main_logging: Position = -9 degrees

[00:12:17.931,579] <inf> main_logging: Position = -18 degrees

[00:12:19.979,553] <inf> main_logging: Position = -4 degrees

[00:12:20.061,492] <inf> main_logging: Position = -13 degrees

  • I'm afraid that there is not much help from my side with Zephyr drivers, as we do not have much rotary encoders in our products. 

    Looking at zephyr/dts/bindings/input/gpio-qdec.yaml please make sure that you have configured the rotary encoder with the following

      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 = <INPUT_REL_WHEEL>;
              sample-time-us = <2000>;
              idle-timeout-ms = <200>;
      };

    This is used in zephyr/samples/subsys/display/lvgl/boards/native_posix.overlay

    Does this help with your rotary encoder output? Next step would be to debug using e.g. logic analyzer to ensure the signals are correct

    Looking at the zephyr/samples/subsys/display/lvgl/src/main.c the following snippet should provide some help, and perhaps help with debugging i.e. setting break points

    #ifdef CONFIG_LV_Z_ENCODER_INPUT
    	lv_obj_t *arc;
    	lv_group_t *arc_group;
    
    	arc = lv_arc_create(lv_scr_act());
    	lv_obj_align(arc, LV_ALIGN_CENTER, 0, -15);
    	lv_obj_set_size(arc, 150, 150);
    
    	arc_group = lv_group_create();
    	lv_group_add_obj(arc_group, arc);
    	lv_indev_set_group(lvgl_input_get_indev(lvgl_encoder), arc_group);
    #endif /* CONFIG_LV_Z_ENCODER_INPUT */

    zephyr/modules/lvgl/input/lvgl_encoder_input.c

    And I assume you have been looking at the QDEC sameple (zephyr/samples/sensor/qdec/src/main.c)

  • This is what I needed!

    I got the encoder working correctly using the inputs library.

    Here's my new .overlay file:

    / {
        gnd_pins {
            compatible = "gpio-leds";
            qdec_gnd: qdec_gnd_pin {
                gpios = <&gpio1 14 (GPIO_ACTIVE_HIGH)>;
                label = "Rotary GND";
            };
            qdec_btn_gnd: qdec_btn_gnd_pin {
                gpios = <&gpio0 5 (GPIO_ACTIVE_HIGH)>;
                label = "Rotary btn GND";
            };
        };
        buttons: buttons {
            compatible = "gpio-keys";
            debounce-interval-ms = <50>;
            qdec_btn: btn_pin {
                gpios = <&gpio0 29 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
                label = "Encoder switch interrupt";
                zephyr,code = <INPUT_BTN_0>;
            };
        };
        qdec_btn {
            input = <&buttons>;
            compatible = "zephyr,input-longpress";
            input-codes = <INPUT_BTN_0>;
            short-codes = <INPUT_BTN_1>;
            long-codes = <INPUT_BTN_2>;
            long-delay-ms = <1000>;
        };
        qdec {
            compatible = "gpio-qdec";
            status = "okay";
            gpios = <&gpio1 13 (GPIO_PULL_UP | GPIO_ACTIVE_HIGH)>,
                    <&gpio1 15 (GPIO_PULL_UP | GPIO_ACTIVE_HIGH)>;
            steps-per-period = <4>;
            zephyr,axis = <INPUT_REL_WHEEL>;
            sample-time-us = <2000>;
            idle-timeout-ms = <200>;
        };
    };

    My test main.c file:

    #include <zephyr/kernel.h>
    #include <zephyr/pm/pm.h>
    #include <zephyr/pm/policy.h>
    #include <zephyr/pm/state.h>
    #include <zephyr/pm/device.h>
    #include <zephyr/input/input.h>
    #include <zephyr/drivers/gpio.h>
    
    static const struct gpio_dt_spec qdec_gnd = GPIO_DT_SPEC_GET(DT_NODELABEL(qdec_gnd), gpios);
    static const struct gpio_dt_spec qdec_btn_gnd = GPIO_DT_SPEC_GET(DT_NODELABEL(qdec_btn_gnd), gpios);
    
    static void test_cb(struct input_event *evt)
    {
            if (evt->code == INPUT_REL_WHEEL)
            {
                    printk("z event %d\n", evt->value);
            }
            if (evt->code == INPUT_BTN_1 && evt->value == 1)
            {
                    printk("short press %d\n", evt->value);
            }
            if (evt->code == INPUT_BTN_2 && evt->value == 1)
            {
                    printk("long press %d\n", evt->value);
            }
    }
    
    INPUT_CALLBACK_DEFINE(NULL, test_cb);
    
    int main(void)
    {
            if (!gpio_is_ready_dt(&qdec_gnd) || !gpio_is_ready_dt(&qdec_btn_gnd))
            {
                    printk("Device not ready\n");
            }
    
            gpio_pin_configure_dt(&qdec_gnd, GPIO_OUTPUT_LOW);
            gpio_pin_configure_dt(&qdec_btn_gnd, GPIO_OUTPUT_LOW);
    
            while (1)
            {
                    k_sleep(K_FOREVER);
            }
    }
    

    And prj.conf file:

    # Config encoder
    CONFIG_INPUT=y
    CONFIG_INPUT_GPIO_QDEC=y

    For anyone wondering about the ground pins, here's how I have my encoder connected:

    Full sample project also included.

    xiao-pp.zip

Related