QDEC Peripheral with high speed encoder

Hello,

I have been testing an encoder using the QDEC peripheral.

The encoder produces 256 pulses per rotation. Testing without any load assigned to the motor, the encoder rotates at 2000-3000 rpm depending on the input.

Using the zephyr sensor api or the nrfx_qdec driver I have not been able to get correct measurements.

I tried lowering the SAMPLEPER to the lowest possible setting (128us) but it didn't work either.

However, using gpio pin interrupts I have been able to get correct measurements.

Is the QDEC peripheral suitable for such a frequency of pulses ?

Is there any other way to do this to avoid using multiple interrupts (like GPIOTE and TIMER as I read somewhere) ?

Parents
  • Hi 

    If my math is correct you will get pulses every 80us (meaning a rising or falling flank every 40us) when the encoder is rotating at 3000 RPM with 256 pulses pr rotation. This is too fast for the QDEC, even at its fastest setting. 

    It is true that you can use the GPIOTE, TIMER and PPI peripherals to measure the length of a pulse without having to rapidly process an interrupt. A simple way to do this is simply to have a free running timer that will trigger a capture and clear every time you get a pulse, and the CC register will then change depending on how long time it takes between captures. 

    The simple example included below (written for the nRF52 series originally) illustrates this concept. To run on the nRF5340 it will need some rewriting because of the difference between the PPI and DPPI peripherals. 

    #define PC_TIMER		NRF_TIMER0
    #define PC_GPIOTE_CH	0
    #define PC_PPI_CH		0
    
    static void pulse_capture_init(uint32_t pin_num)
    {
    	PC_TIMER->BITMODE = TIMER_BITMODE_BITMODE_32Bit << TIMER_BITMODE_BITMODE_Pos;
    	PC_TIMER->PRESCALER = 0;
    	NRF_GPIOTE->CONFIG[PC_GPIOTE_CH] = 	GPIOTE_CONFIG_MODE_Event << GPIOTE_CONFIG_MODE_Pos |
    										GPIOTE_CONFIG_POLARITY_LoToHi << GPIOTE_CONFIG_POLARITY_Pos |
    										pin_num << GPIOTE_CONFIG_PSEL_Pos;
    	NRF_PPI->CH[PC_PPI_CH].EEP = (uint32_t)&NRF_GPIOTE->EVENTS_IN[PC_GPIOTE_CH];
    	NRF_PPI->CH[PC_PPI_CH].TEP = (uint32_t)&PC_TIMER->TASKS_CAPTURE[0];
    	NRF_PPI->FORK[PC_PPI_CH].TEP = (uint32_t)&PC_TIMER->TASKS_CLEAR;
    	NRF_PPI->CHENSET = (1 << PC_PPI_CH);
    
    	PC_TIMER->TASKS_START = 1;
    }
    
    static uint32_t pulse_capture_get()
    {
    	return PC_TIMER->CC[0];
    }
    

    Best regards
    Torbjørn

  • Hello Ovrebekk,

    thank you for your answer and for confirming this.

    Also thank you for sharing this sample.

    I will read more about the TIMER and GPIOTE peripherals since I have never used them before and I will try to implement this.

  • Hi,

    ideally I would need to be able to capture displacement in both directions. 

    I was thinking of checking this by capturing the PINB status whenever the PINA GPIOTE event occurs. 

    On the other hand, direction is something that perhaps I would be able to exclude considering that I will be controlling the motor's rotation and therefore I can try considering the actual direction equal to the command direction I set (in order to avoid the need for calculating the direction through the encoder). 

  • Hi 

    If you can exclude it it would definitely simplify the implementation, but possibly it is enough to occasionally trigger an interrupt on the PINA event and check the status of PINB. You just need some system to detect if the interrupt is delayed, since this would give you an incorrect reading. 

    Best regards
    Torbjørn

  • Hello,

    I can do that considering the direction equal to the motor control direction and use it to calculate the position combining this and the displacement.

    Considering this the interrupt routine will only include a counter increase and not any time consuming process.

    I will try this and if it works I will update here for the record.

    How could I check whether the interrupt is delayed ?

    Thanks again.

  • Hello Ovrebekk,

    would it be a solution to increase the APP core frequency (to 128Mhz) ?

  • Hi 

    There is no direct way to find out if an interrupt is delayed unfortunately. You might be able to do some benchmarking and testing to verify whether or not there are any interrupts in the system that could delay you long enough to miss the value of PINB. 

    Alternatively you could implement a more complex system where you connect both the PINA and PINB inputs to capture on different CC registers of a timer, and use this to analyze if PINB goes low or high following a rising flank on PINA. 

    pekon94 said:
    would it be a solution to increase the APP core frequency (to 128Mhz) ?

    Not a solution necessarily, but increasing the app core frequency should reduce the runtime of the various interrupts, and also reduce the interrupt latency. 

    Best regards
    Torbjørn

Reply
  • Hi 

    There is no direct way to find out if an interrupt is delayed unfortunately. You might be able to do some benchmarking and testing to verify whether or not there are any interrupts in the system that could delay you long enough to miss the value of PINB. 

    Alternatively you could implement a more complex system where you connect both the PINA and PINB inputs to capture on different CC registers of a timer, and use this to analyze if PINB goes low or high following a rising flank on PINA. 

    pekon94 said:
    would it be a solution to increase the APP core frequency (to 128Mhz) ?

    Not a solution necessarily, but increasing the app core frequency should reduce the runtime of the various interrupts, and also reduce the interrupt latency. 

    Best regards
    Torbjørn

Children
  • Hello,

    I finally got a piece of code working (tested with another encoder) and I intend to use it today with the encoder mentioned above.

    However I have the following question:

    As you can see, in order to initialize the PIN in GPIOTE IN_EVENT mode I had to declare an event handler function. I have left it empty, but isn't this also causing a CPU interrupt ?

    static void event_handler(nrfx_gpiote_pin_t pin, nrf_gpiote_polarity_t action)
    {
    }
    
    static void cust_qdec_init(void)
    {
    	IRQ_CONNECT(DT_IRQN(DT_NODELABEL(gpiote)),
    		    DT_IRQ(DT_NODELABEL(gpiote), priority),
    		    nrfx_isr, nrfx_gpiote_irq_handler, 0);
    
    	printk("Helloooooooooo\n");
    	timer_cfg.bit_width = NRF_TIMER_BIT_WIDTH_32 ;
    	timer_cfg.mode = NRF_TIMER_MODE_COUNTER ;
    	pina_config.pull = NRF_GPIO_PIN_NOPULL;
    
    	printk("1111111111111111\n");
    	err_code = nrfx_timer_init(&qdec_time_counter, &timer_cfg, NULL);
    	if (err_code != NRFX_SUCCESS) {
    		printk("Error in timer initialization !!!! \n");
    		return;
    	}
    	nrfx_timer_enable(&qdec_time_counter);
    	printk("222222222222222\n");
    	if (!nrfx_gpiote_is_init()) {
    		err_code = nrfx_gpiote_init(0);
    		if (err_code != NRFX_SUCCESS) {
    			printk("Error in NRFX GPIOTE initialization !!!! \n");
    			return;
    		}
    	}
    	err_code = nrfx_gpiote_in_init(PINA, &pina_config, event_handler);
      if (err_code != NRFX_SUCCESS) {
        printk("Error in GPIOTE IN_EVENT configuration Initialization");
        return;
      }
    
    	err_code = nrfx_dppi_channel_alloc(&cust_qdec_ppi_chann);
      if (err_code != NRFX_SUCCESS) {
        printk ("Error in allocating DPPI channel \n");
        return;
      }
    	nrfx_gppi_channel_endpoints_setup(cust_qdec_ppi_chann,
                                         nrfx_gpiote_in_event_addr_get(PINA),
                                         nrfx_timer_task_address_get(&qdec_time_counter, NRF_TIMER_TASK_COUNT));
      if (err_code != NRFX_SUCCESS) {
        printk("Eroor in configuring GPPI endpoints \n");
        return;
      }
    	err_code = nrfx_dppi_channel_enable(cust_qdec_ppi_chann);
      if (err_code != NRFX_SUCCESS) {
        printk("Error enabling the DPPI Channel for qdec \n");
        return;
      }
    	nrfx_gpiote_in_event_enable(PINA,true);
    }

    Is there another way to initialize the PIN in GPIOTE IN_EVENT mode ? I tried leaving the event_handler as NULL but this caused the CPU to crash and reboot upon any event.

    I was also wondering if the GPIOTE and TIMER initialization and enable sequence is the correct one.

    Am I correct to assume that I should initialize them before connecting them with dppi but enable them after connecting them with dppi ?

  • Hi

    pekon94 said:
    Is there another way to initialize the PIN in GPIOTE IN_EVENT mode ? I tried leaving the event_handler as NULL but this caused the CPU to crash and reboot upon any event.

    The nrfx_gpiote_in_init(..) function has been deprecated, and you should use the nrfx_gpiote_input_configure(..) function instead. 

    With this function it should be possible to set the p_handler_config argument to NULL, in order to disable the interrupt. 

    pekon94 said:

    I was also wondering if the GPIOTE and TIMER initialization and enable sequence is the correct one.

    Am I correct to assume that I should initialize them before connecting them with dppi but enable them after connecting them with dppi ?

    The natural order is to configure the peripherals first, then make the various DPPI connections, and starting peripherals that need to be started at the very end. 

    Best regards
    Torbjørn

  • Hello,

    thank you for this.

    I used the nrfx_gpiote_input_configure() function and the nrfx_gpiote_trigger_enable() function. In the second one I had the int_enable parameter set to false.

    I noticed though that I had to allocate 2x GPIOTE channels (could not let the nrfx_gpiote_trigger_config_t p_in_channel parameter set to NULL) and add CONFIG_NRFX_GPIOTE_NUM_OF_EVT_HANDLERS=2 in the prj.conf file to make it work.

  • Hi 

    If you need separate events for a rising and falling flank it makes sense that you would need to use 2 channels, the GPIOTE hardware does not support generating different events on a single channel. 

    Is this what you were trying to do?

    Best regards
    Torbjørn

  • Hello,

    no I am expanding this for 2 encoders, so I am using a IN_EVENT configured for 2 different PINs upon a LOTOHI event. 

Related