Quadrature Decoder Zephyr

I am developing on a custom board using the NRF52811 processor in Zephyr 3.2.99.  We have a Kailh CEN985012R20-1 Quadrature Encoder mounted on the board with thne A/B encoder pins connected to GPIO's on the processor.  Zephyr's sample code provides a .dts overlay for an ST board as follows:

/ {
        aliases {
                qdec0 = &qdec;
        };
};

&timers3 {
        status = "okay";

        qdec: qdec {
                status = "okay";
                pinctrl-0 = <&tim3_ch1_pa6 &tim3_ch2_pa7>;
                pinctrl-names = "default";
                st,input-polarity-inverted;
                st,input-filter-level = <FDIV32_N8>;
                st,counts-per-revolution = <16>;
        };
};

Then the sample code uses Zephyr sensor driver API's to read the A/B encoder inputs, I'm assuming using the interrupt defined in the qdec dtsi. It is a clean, elegant solution which would suit or needs very well.  I have reviewed the NRFX quadrature driver API's but I would really love to avoid using complex, vendor specific drivers. We do not need absolute positioning or RPM counts, etc.  We only need relative postioning (plus or minus) in "clicks" for every turn of the dial.

There are qdec and timer nodes defined in the nrf52811.dtsi file.  I simply need to know how the ST.dts properties map to the NRF properties.  Other examples I have found on this forum use deprecated properties for the pin outs.  I can overcome that issue by using pinctrl schemas from othe nodes I have defined in my device tree files.  But i am curious about the other properties in that node.

Our customer is quite anxious as we are about to got to production with this board and this is the last peripheral I have to configure.

Thank you in advance,

Drew

  • That was most helpful.  Sorry for the delayed reply.

    I'm using the sensor API's to read the SENSOR_CHAN_ROTATION which seems to work reasonably well.  Occasionally, I'm experiencing some latency in sensor response but I have not tracked that down yet.

    Thank you for the direction.

  • OK.  I figured it out! There are no physical pull ups on the A & B encoder pins.  I've overcome this by adding the GPIO_PULL_UP flag to my encoder pin configuration. This overcomes the sensor latency and erroneous readings.  It also permits us to build the board without adding the pull ups which is at the very least inconvenient to our final design.

    gpio_pin_configure_dt(&ena, GPIO_INPUT | GPIO_PULL_UP);

    To summarize for anyone following, from my root node in the dtsi include:

                    qdec: qdec0: qdec@40012000 {
                            compatible = "nordic,nrf-qdec";
                            reg = <0x40012000 0x1000>;
                            interrupts = <18 NRF_DEFAULT_IRQ_PRIORITY>;
                            status = "disabled";
                    };
    

    I have configured my pinctrl-dtsi with:

    	qdec_default: qdec_default {
    		group1 {
    			psels = <NRF_PSEL(QDEC_A, 0, 19)>,
    				<NRF_PSEL(QDEC_B, 0, 20)>;
    		};
    	};
    
    	qdec_sleep: qdec_sleep {
    		group1 {
    			psels = <NRF_PSEL(QDEC_A, 0, 19)>,
    				<NRF_PSEL(QDEC_B, 0, 20)>;
    			low-power-enable;
    		};
    	};
    

    In my .dts I have:

    &qdec {
    	status = "okay";   
    	enable-pin = <19>;
    	led-pre = <0>;
    	steps = <12>;
    	pinctrl-0 = <&qdec_default>;
    	pinctrl-1 = <&qdec_sleep>;
    	pinctrl-names = "default", "sleep";
    	label = "QDEC";
    };
    

    Then in my main, I have configured the encoder pins with interrupts and callbacks.  The ISR gives a semaphore to a thread with the sensor code so it does not poll the sensor constantly:

    void thread3_run()
    {
    	printk("Thread 3 started\n");
    	
    	struct sensor_value val;
    	int rc;
    	const struct device *const dev = DEVICE_DT_GET(DT_ALIAS(qdec0));
    
    	if (!device_is_ready(dev)) {
    			printk("Qdec device is not ready\n");
    			return;
    	}
    
    	printk("Quadrature decoder sensor test\n");
    
    	while (true) {
    
    		k_sem_take(&enSem, K_FOREVER);
    		//printk("Enable\n");
    			
    		rc = sensor_sample_fetch(dev);
    		if (rc != 0) {
    			printk("Failed to fetch sample (%d)\n", rc);
    			return;
    		}
    		
    		rc = sensor_channel_get(dev, SENSOR_CHAN_ROTATION, &val);
    		if (rc != 0) {
    			printk("Failed to get data (%d)\n", rc);
    			return;
    		}
    
    		//printk("Position = %d degrees\n", val.val1);
    		if(val.val1 > 0) {
    				printk("+\n");
    		}
    		else if (val.val1 < 0) {
    				printk("-\n");
    		}
    
    		//k_msleep(200);
    	}
    }

    This approach works great.  But there a couple of things I don't yet fully understand...

    1. In the root node of the .dtsi include file, there is an interrupt property defined.  How is that used?

    2. In the local BSP .dts there is an "enable-pin" property.  The .yaml says that this property is not required but should be configured the same as the a-pin property but it is not clear how that is used:

        enable-pin:
          type: int
          required: false
          description: |
            The enable pin to use, to enable a connected QDEC device. The
            pin numbering scheme is the same as the a-pin property's.
    

    Overall, I'm extremely satisfied with how the sensor API's perform. We could just sample the pin's directly but the API avoids writing some clever algorithm to throw out bad sensor readings. For all intents and purposes you may consider this ticket closed.

    Thank you again for the guidance.

    Drew

  • Hi  :

    I don't know how to configure the encoder pins with interrupts and callbacks. Would you mind to share your code ? Thank you.

  •  It's been more than a year since I wrote this code but I believe it works like this. Following what I did above, I configure the device structs as follows:

    // Encoder
    #define ENA_NODE DT_ALIAS(ena) // from dts alias
    #define ENB_NODE DT_ALIAS(enb)
    
    static const struct gpio_dt_spec ena = GPIO_DT_SPEC_GET_OR(ENA_NODE, gpios, {0});
    static const struct gpio_dt_spec enb = GPIO_DT_SPEC_GET_OR(ENB_NODE, gpios, {0});

    Then define the callbacks:

    // Encoder Callbacks
    static struct gpio_callback ena_cb_data;
    static struct gpio_callback enb_cb_data;

    Then configure the pins:

    	// ENA
    	ret = gpio_pin_configure_dt(&ena, GPIO_INPUT | GPIO_PULL_UP); // Check Zephyr documentation for addtional flags
    	if (ret != 0) {
    			printk("Error %d: failed to configure %s pin %d\n",
    					ret, ena.port->name, ena.pin);
    			return;
    	}
    
    	// ENB
    	ret = gpio_pin_configure_dt(&enb, GPIO_INPUT | GPIO_PULL_UP); // Check Zephyr documentation for addtional flags
    	if (ret != 0) {
    			printk("Error %d: failed to configure %s pin %d\n",
    					ret, enb.port->name, enb.pin);
    			return;
    	}
    

    Then interrupts and callbacks:

    	// Configure Enable A Interrupt
    	ret = gpio_pin_interrupt_configure_dt(&ena, GPIO_INT_EDGE_TO_ACTIVE);
    	if (ret != 0) {
    			printk("Error %d: failed to configure interrupt on %s pin %d\n",
    					ret, ena.port->name, ena.pin);
    			return;
    	}
    	// Configure Enable A Callback
    	gpio_init_callback(&ena_cb_data, ena_active, BIT(ena.pin));
    	gpio_add_callback(ena.port, &ena_cb_data);
    	printk("Set up button at %s pin %d\n", ena.port->name, ena.pin);
    
    	// Configure Enable B Interrupt
    	ret = gpio_pin_interrupt_configure_dt(&enb, GPIO_INT_EDGE_TO_ACTIVE);
    	if (ret != 0) {
    			printk("Error %d: failed to configure interrupt on %s pin %d\n",
    					ret, enb.port->name, enb.pin);
    			return;
    	}
    	// Configure Enable B Callback
    	gpio_init_callback(&enb_cb_data, enb_active, BIT(enb.pin));
    	gpio_add_callback(enb.port, &enb_cb_data);
    	printk("Set up button at %s pin %d\n", enb.port->name, enb.pin);

    You could put the code in the main thread but I defined a static thread that takes a binary semaphore:

    void thread3_run()
    {
    	printk("Thread 3 started\n");
    	
    	struct sensor_value val;
    	int rc;
    	const struct device *const dev = DEVICE_DT_GET(DT_ALIAS(qdec0));
    
    	if (!device_is_ready(dev)) {
    			printk("Qdec device is not ready\n");
    			return;
    	}
    
    	printk("Quadrature decoder sensor test\n");
    
    	while (true) {
    
    		k_sem_take(&enSem, K_FOREVER);
    		//printk("Enable\n");
    			
    		rc = sensor_sample_fetch(dev);
    		if (rc != 0) {
    			printk("Failed to fetch sample (%d)\n", rc);
    			return;
    		}
    		
    		rc = sensor_channel_get(dev, SENSOR_CHAN_ROTATION, &val);
    		if (rc != 0) {
    			printk("Failed to get data (%d)\n", rc);
    			return;
    		}
    
    		//printk("Position = %d degrees\n", val.val1);
    		if(val.val1 > 0) {
    				printk("+\n");
    		}
    		else if (val.val1 < 0) {
    				printk("-\n");
    		}
    
    		//k_msleep(200);
    	}
    }

    It uses the sensor driver API's to determine the QDEC rotation direction (add "CONFIG_SENSOR=y" to your prj.conf and include the sensor header file). Make sure your QDEC is properly configured based on you device specs (i.e. number of clicks per rotation). 

    Also, it's important to have if a physical pull up resistor on the encoder pins or the signal will float and result in some latency.  If not, I found that a soft pull up configuration will work (as I defined above in the pin configuration).  

    Hope that helps.

    Drew

Related