Efficient nrfx-based Quadrature Decoder for High-Speed Encoders on NRF5340

Hello,

I'm developing a project based on the NRF5340 and have encountered some challenges. As I'm relatively new to C, I'm trying to implement a quadrature (incremental) encoder decoder that accurately captures all four states of the signal. For example, with an encoder offering 1024 increments on two outputs, I should be able to obtain 4096 distinct positions.

I initially planned to use the Zephyr QDEC driver; however, as noted in this thread (https://devzone.nordicsemi.com/f/nordic-q-a/91398/qdec-peripheral-with-high-speed-encoder/384446), it seems too slow for my application. In addition to decoding the signal, I also need to reliably determine the direction of rotation.

Could anyone point me to a sample or provide guidance on using the nrfx-specific functions for this purpose—ideally in a way that minimizes CPU load?

Parents
  • Hello,

    The QDEC on the nRF52 and nRF53 series is intended for mouse wheels, originally, and is typically too slow for tracking a high speed motor. You didn't mention what kind of speed you would need, but we can see from the product specification of the QDEC on the nRF5340:

    https://docs.nordicsemi.com/bundle/ps_nrf5340/page/chapters/qdec/qdec.html

    That the maximum sample rate is 500kHz. It says that it samples at 1MHz (1048576Hz, given that each sample period is 128µs), but you need two samples to be the same for it not to be caught by the debounce filter. 

    So at what speed/frequency do you need to sample?

    Best regards,

    Edvin

  • Hello Edvin,

    Thank you for your response.

    The motor runs at 3000 RPM with an encoder resolution of 10,000 increments per revolution, which results in a signal frequency of 500 kHz. According to the Nyquist criterion, a sampling frequency of at least 1 MHz would be required to accurately capture the signal. Since the QDEC on the nRF5340 has a maximum sample rate of 500 kHz, it seems like it may not be sufficient for this application.

    Would you recommend using direct GPIO sampling with PPI/DPPI and a TIMER instead? If so, is there any available sample code demonstrating this approach?

    Best regards,

    Philipp

  • Philipp.trem said:
    Would you recommend using direct GPIO sampling with PPI/DPPI and a TIMER instead? If so, is there any available sample code demonstrating this approach?

    We don't have anything. It depends on what you need. If you need a sample every now and then, it may work, but if you need all the updates, which means that you need 3000*10 000/60 = 500 000Hz. 

    There is no way to guarantee that you will be able to access the CPU this fast.

    Even though you set it up using PPI, you can sample some pulses, and store this in some capture compare registers in the timer, but it would be difficult to read them out fast enough, reliably. 

    How about reducing the increments per revolution? Is that an option? If not, it sounds like you perhaps need an external QDEC.

    Best regards,

    Edvin

  •  I want to implement a controller that runs at 5 kHz, so I would only need to read my value every 0.2 ms (200 µs).  Do you think it would be feasible to reliably read the values using GPIO sampling with PPI/DPPI and a timer in this case?

    Best regards,
    Philipp

Reply Children
  • Not sure. You would need to test to see if it is accurate enough for your use case. 

    I have a sample that you can use to get up and running. It is basically a sample using DPPI (using the GPPI API) that measures the length of a pulse. Perhaps you can modify this for your test.

    7752.timed_signals_ncs240.zip

    (written in NCS 2.4.0, but I believe it still works)

    Best regards,

    Edvin

  • Thanks for sharing the sample! I tried it using GPIOTE, and it seems to be working for my use case.

    For anyone who might need it in the future, here’s my sample code:

    #include <zephyr/device.h>
    #include <zephyr/kernel.h>
    #include <nrfx_gpiote.h>
    #include <nrfx_dppi.h>
    #include <nrfx_timer.h>
    #include <nrfx_log.h>
    #include <zephyr/sys/printk.h>
    #include <zephyr/irq.h>
    #include <stdatomic.h>
    #include <stdio.h>
    
    #define INPUT_PIN_A	(32 + 4)
    #define INPUT_PIN_B	(32 + 5)
    
    #define GPIOTE_INST	0 //DT_ALIAS(gpiote0) //NRF_DT_GPIOTE_INST(DT_ALIAS(gpiote0), gpios)
    #define GPIOTE_NODE	DT_NODELABEL(gpiote0)
    
    static bool A = false;
    static bool B = false;
    
    const nrfx_gpiote_t inst = NRFX_GPIOTE_INSTANCE(GPIOTE_INST);
    
    static _Atomic int32_t counter = ATOMIC_INIT(0);
    
    void gpiote_event_handler(nrfx_gpiote_pin_t pin, nrfx_gpiote_trigger_t trigger, void *p_context)
    {
        if (trigger == NRFX_GPIOTE_TRIGGER_LOTOHI)
        {
            //printk("Pin %d transitioned from LOW to HIGH!\n", pin);
        }
        else if (trigger == NRFX_GPIOTE_TRIGGER_HITOLO)
        {
            //printk("Pin %d transitioned from HIGH to LOW!\n", pin);
        } else {
            //printk("Pin %d transitioned\n", pin);
            if (pin != INPUT_PIN_A && pin != INPUT_PIN_B) return;
    
            bool dir = false;
            if (pin == INPUT_PIN_A)
            {
                if (A == false && B == false)
                {
                    A = true;
                    dir = true;
                }
                else if (A == true && B == false)
                {
                    A = false;
                    dir = false;
                }
                else if (A == false && B == true)
                {
                    A = true;
                    dir = false;
                }
                else if (A == true && B == true)
                {
                    A = false;
                    dir = true;
                }
            }
            else if (pin == INPUT_PIN_B)
            {
                if (A == false && B == false)
                {
                    B = true;
                    dir = false;
                }
                else if (A == true && B == false)
                {
                    B = true;
                    dir = true;
                }
                else if (A == false && B == true)
                {
                    B = false;
                    dir = true;
                }
                else if (A == true && B == true)
                {
                    B = false;
                    dir = false;
                }
            }
    
            // Update counter
            
            if (dir) {
                if (atomic_get(&counter) >= 20000) atomic_set(&counter, 1);
                else atomic_inc(&counter);
            } else {
                if (atomic_get(&counter) <= -20000) atomic_set(&counter, -1);
                else atomic_dec(&counter);
            }
        }
    }
    void gpiote_init(void)
    {
        IRQ_CONNECT(DT_IRQN(GPIOTE_NODE), DT_IRQ(GPIOTE_NODE, priority), nrfx_isr,
                    NRFX_CONCAT(nrfx_gpiote_, GPIOTE_INST, _irq_handler), 0);
    
        if (!nrfx_gpiote_init_check(&inst))
        {
            printk("Initializing GPIOTE Instance\n");
            nrfx_err_t err = nrfx_gpiote_init(&inst, 0);
            if (err != NRFX_SUCCESS) {
                printk("Failed to init, error: 0x%08X\n", err);
                return;
            }
            nrfx_gpiote_latency_set(&inst, NRF_GPIOTE_LATENCY_LOWLATENCY);
        }
    
        nrfx_gpiote_input_pin_config_t pin_config_A_LH = {
            .p_pull_config = &(nrf_gpio_pin_pull_t){NRF_GPIO_PIN_NOPULL},
            .p_trigger_config = &(nrfx_gpiote_trigger_config_t){NRFX_GPIOTE_TRIGGER_TOGGLE, NULL},
            .p_handler_config = &(nrfx_gpiote_handler_config_t){gpiote_event_handler, NULL}
        };
    
        nrfx_gpiote_input_pin_config_t pin_config_B_LH = {
            .p_pull_config = &(nrf_gpio_pin_pull_t){NRF_GPIO_PIN_NOPULL},
            .p_trigger_config = &(nrfx_gpiote_trigger_config_t){NRFX_GPIOTE_TRIGGER_TOGGLE, NULL},
            .p_handler_config = &(nrfx_gpiote_handler_config_t){gpiote_event_handler, NULL}
        };
    
        printk("Initializing GPIOTE_PIN_A\n");
        if (nrfx_gpiote_input_configure(&inst, INPUT_PIN_A, &pin_config_A_LH) != NRFX_SUCCESS)
        {
            printk("Failed to configure INPUT_PIN_A!\n");
        }
    
        printk("Initializing GPIOTE_PIN_B\n");
        if (nrfx_gpiote_input_configure(&inst, INPUT_PIN_B, &pin_config_B_LH) != NRFX_SUCCESS)
        {
            printk("Failed to configure INPUT_PIN_B!\n");
        }
    
        nrfx_gpiote_trigger_enable(&inst, INPUT_PIN_A, true);
        nrfx_gpiote_trigger_enable(&inst, INPUT_PIN_B, true);
    
        printk("GPIOTE Initialization Done\n");
    
        A = nrf_gpio_pin_read(INPUT_PIN_A);
        B = nrf_gpio_pin_read(INPUT_PIN_B);
        printk("Initial state is (%d,%d) \n",A,B);
    }
    
    int main(void)
    {
        printk("Initializing GPIOTE...\n");
        gpiote_init();
        printk("Waiting for Event\n");
        while (1)
        {
            k_sleep(K_MSEC(1000));
            printk("Counter is at %d \n",counter);
        }
    }

    Best regards,

    Philipp

Related