using pulse counter to read optical (quadrature) decoder?

Posts: 52
Joined: Wed Jan 17, 2018 11:55 pm

using pulse counter to read optical (quadrature) decoder?

Postby kbaud1 » Mon Mar 12, 2018 10:16 pm

Does anyone know an example of using the pulse counter peripheral as a quadrature decoder? ... /pcnt.html

Posts: 47
Joined: Sat Sep 23, 2017 12:36 pm

Re: using pulse counter to read optical (quadrature) decoder?

Postby clarkster » Sat Mar 24, 2018 3:40 am

I don’t think you will be able to do this. The PCNT is a very simple device. If I were you I’d investigate other ICs to use in conjunction with an ESP32.

Posts: 3045
Joined: Thu Nov 26, 2015 4:08 am

Re: using pulse counter to read optical (quadrature) decoder?

Postby ESP_Sprite » Sat Mar 24, 2018 10:05 am

Actually, I think it's pretty doable... the reason the pulse counter has two inputs which can be configured to do different things in combination with eachother is a.o. to decode qaudrature encoded signals.

Posts: 47
Joined: Sat Sep 23, 2017 12:36 pm

Re: using pulse counter to read optical (quadrature) decoder?

Postby clarkster » Tue Mar 27, 2018 2:19 am

Well, ESP_Sprite is right (as usual!).

Here is some code that shows how to use the PCNT to count quadrature pulses. The code also uses the TIMER to generate quadrature pulses, simulating both the forward and reverse direction of an encoder. The code also uses the PCNT to properly count the simulated quadrature pulses. The simulator can produce quadrature pulses in the forward or reverse direction.

The code includes 1X and 2X quadrature counting modes. It does not include mode 4X. Mode 4X requires that the counter be capable of incrementing or decrementing the count whenever either the pulse input or the control input to the PCNT change. As far as I can tell, the PCNT only counts pulses on the pulse input. I don't believe the PCNT can count changes to the control input, but maybe ESP_Sprite can show me a way to do this.

Anyway, it was fun learning about the TIMER and PCNT peripherals.

Heres the code:

Code: Select all

#include <stdio.h>
#include "esp_types.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "soc/timer_group_struct.h"
#include "driver/periph_ctrl.h"
#include "driver/timer.h"
#include "driver/gpio.h"
#include <esp_log.h>
#include "driver/pcnt.h"

#define RELOAD_TMR      		1
#define ENC_CHANNEL_A				13		// gpio for channels of encoder simulator
#define ENC_CHANNEL_B				15
#define PCNT_PULSE_GPIO				12		// gpio for PCNT
#define PCNT_CONTROL_GPIO			14
#define DIRECTION					25		// gpio for encoder direction input

#define PCNT_H_LIM_VAL      1000
#define PCNT_L_LIM_VAL     -1000

typedef enum {
    QUAD_ENC_MODE_1 = 1,
} quad_encoder_mode;


xQueueHandle pcnt_evt_queue;   // A queue to handle pulse counter events

/* A sample structure to pass events from the PCNT
 * interrupt handler to the main program.
typedef struct {
    int unit;  // the PCNT unit that originated an interrupt
    uint32_t status; // information on the event type that caused the interrupt
} pcnt_evt_t;

volatile int cnt = 0;
void IRAM_ATTR quad_end_sim_isr(void *para) {
	int timer_idx = (int) para;

	uint32_t intr_status = TIMERG0.int_st_timers.val;
	if((intr_status & BIT(timer_idx)) && timer_idx == TIMER_0) {
		TIMERG0.hw_timer[timer_idx].update = 1;
		TIMERG0.int_clr_timers.t0 = 1;
		TIMERG0.hw_timer[timer_idx].config.alarm_en = 1;

		uint32_t encA_level = 0;
		uint32_t encB_level = 0;

		switch (cnt) {
		case 0:
		case 1:
			encA_level = 1;
		case 2:
			encA_level = 1;
			encB_level = 1;
		case 3:
			encB_level = 1;

		switch(gpio_get_level(DIRECTION)) {
		case 0:
			gpio_set_level(ENC_CHANNEL_A, encA_level);
			gpio_set_level(ENC_CHANNEL_B, encB_level);
		case 1:
			gpio_set_level(ENC_CHANNEL_A, encB_level);
			gpio_set_level(ENC_CHANNEL_B, encA_level);

		if (cnt >= 4) { cnt = 0; }

static void IRAM_ATTR quad_enc_isr(void *arg)
    uint32_t intr_status = PCNT.int_st.val;
    int i;
    pcnt_evt_t evt;
    portBASE_TYPE HPTaskAwoken = pdFALSE;

    for (i = 0; i < PCNT_UNIT_MAX; i++) {
        if (intr_status & (BIT(i))) {
            evt.unit = i;
            /* Save the PCNT event type that caused an interrupt
               to pass it to the main program */
            evt.status = PCNT.status_unit[i].val;
            PCNT.int_clr.val = BIT(i);
            xQueueSendFromISR(pcnt_evt_queue, &evt, &HPTaskAwoken);
            if (HPTaskAwoken == pdTRUE) {

static void quad_enc_gpio_init() {
	gpio_set_pull_mode(DIRECTION, GPIO_PULLDOWN_ONLY);

static void quad_enc_sim_timer_init(int timer_idx, bool auto_reload, uint32_t frequency, timer_count_dir_t direction ) {
    timer_config_t config;
    config.alarm_en = TIMER_ALARM_EN;
    config.auto_reload = auto_reload;
    config.counter_dir = direction;
    config.divider = 2;
    config.intr_type = TIMER_INTR_LEVEL;
    config.counter_en = TIMER_PAUSE;

    timer_init(TIMER_GROUP_0, timer_idx, &config);
    timer_pause(TIMER_GROUP_0, timer_idx);
    timer_set_counter_value(TIMER_GROUP_0, timer_idx, 0x00000000ULL);

    timer_set_alarm_value(TIMER_GROUP_0, timer_idx, 40000000 / 4 / frequency);
    timer_enable_intr(TIMER_GROUP_0, timer_idx);
    timer_isr_register(TIMER_GROUP_0, timer_idx, quad_end_sim_isr, (void *) timer_idx, ESP_INTR_FLAG_IRAM, NULL);

    timer_start(TIMER_GROUP_0, timer_idx);

static void quadrature_encoder_counter_init(quad_encoder_mode enc_mode) {
	 pcnt_config_t pcnt_config = {
		        .pulse_gpio_num = PCNT_PULSE_GPIO,
		        .ctrl_gpio_num = PCNT_CONTROL_GPIO,
		        .channel = PCNT_CHANNEL_0,
		        .unit = PCNT_UNIT_0,
		        .pos_mode = PCNT_COUNT_INC,   			// Count up on the positive edge
		        .neg_mode = PCNT_COUNT_DIS,   			// Keep the counter value on the negative edge
		        .lctrl_mode = PCNT_MODE_KEEP, 			// Reverse counting direction if low
		        .hctrl_mode = PCNT_MODE_REVERSE,    	// Keep the primary counter mode if high
		        .counter_h_lim = PCNT_H_LIM_VAL,
		        .counter_l_lim = PCNT_L_LIM_VAL,

	switch (enc_mode) {
	case QUAD_ENC_MODE_1:
	case QUAD_ENC_MODE_2:
		 pcnt_config.neg_mode = PCNT_COUNT_DEC;
	case QUAD_ENC_MODE_4:
		// Doesn't appear to be possible to handle 4X mode with the PCNT. THis mode requires the count to increment when the CONTROL input changes.

    pcnt_set_filter_value(PCNT_UNIT_0, 100);

    pcnt_event_enable(PCNT_UNIT_0, PCNT_EVT_ZERO);
    pcnt_event_enable(PCNT_UNIT_0, PCNT_EVT_H_LIM);
    pcnt_event_enable(PCNT_UNIT_0, PCNT_EVT_L_LIM);

    /* Initialize PCNT's counter */

    /* Register ISR handler and enable interrupts for PCNT unit */
    pcnt_isr_register(quad_enc_isr, NULL, 0, NULL);

    /* Everything is set up, now go to counting */

void app_main()
	uint32_t frequency = 10;
    quad_enc_sim_timer_init(TIMER_0, RELOAD_TMR, frequency, TIMER_COUNT_UP);

    /* Initialize PCNT event queue and PCNT functions */
       pcnt_evt_queue = xQueueCreate(10, sizeof(pcnt_evt_t));

       int16_t count = 0;
       pcnt_evt_t evt;
       portBASE_TYPE res;
       while (1) {
           /* Wait for the event information passed from PCNT's interrupt handler.
            * Once received, decode the event type and print it on the serial monitor.
           res = xQueueReceive(pcnt_evt_queue, &evt, 1000 / portTICK_PERIOD_MS);
           if (res == pdTRUE) {
               pcnt_get_counter_value(PCNT_UNIT_0, &count);
               printf("Event PCNT unit[%d]; cnt: %d\n", evt.unit, count);
               if (evt.status & PCNT_STATUS_THRES1_M) {
                   printf("THRES1 EVT\n");
               if (evt.status & PCNT_STATUS_THRES0_M) {
                   printf("THRES0 EVT\n");
               if (evt.status & PCNT_STATUS_L_LIM_M) {
                   printf("L_LIM EVT\n");
               if (evt.status & PCNT_STATUS_H_LIM_M) {
                   printf("H_LIM EVT\n");
               if (evt.status & PCNT_STATUS_ZERO_M) {
                   printf("ZERO EVT\n");
           } else {
               pcnt_get_counter_value(PCNT_UNIT_0, &count);
               printf("Current counter value :%d\n", count);
If anyone sees any way to improve the code, please let me know.

Posts: 1
Joined: Tue Apr 05, 2016 3:33 am

Re: using pulse counter to read optical (quadrature) decoder?

Postby toxicpsion » Sun May 06, 2018 9:03 pm

you can read a Quadrature decoder using only PCNT without interrupts in 1X mode using::

Code: Select all

pcnt_config_t r_enc_config = {
	.pulse_gpio_num = GPIO_NUM_32,   //Rotary Encoder Chan A (GPIO32)
	.ctrl_gpio_num = GPIO_NUM_33,	 //Rotary Encoder Chan B (GPIO33)
	.unit = PCNT_UNIT_0,
	.channel = PCNT_CHANNEL_0,
	.pos_mode = PCNT_COUNT_INC, //Count Only On Rising-Edges
	.neg_mode = PCNT_COUNT_DIS,	// Discard Falling-Edge
	.lctrl_mode = PCNT_MODE_KEEP,    // Rising A on HIGH B = CW Step
	.hctrl_mode = PCNT_MODE_REVERSE, // Rising A on LOW B = CCW Step
	.counter_h_lim = INT16_MAX,
	.counter_l_lim = INT16_MIN
int r_enc_count;

pcnt_set_filter_value(PCNT_UNIT_0, 250);  // Filter Runt Pulses

gpio_pullup_en(GPIO_NUM_25); // Rotary Encoder Button


pcnt_counter_pause(PCNT_UNIT_0); // Initial PCNT init

while (1) {
	pcnt_get_counter_value(PCNT_UNIT_0, &r_enc_count);
	ESP_LOGI("counter", "%d - button:%d", r_enc_count,  gpio_get_level(GPIO_NUM_25));

	vTaskDelay(200 / portTICK_PERIOD_MS); // Delay 1000msec & yield.
which in my case is a raw encoder with no pullups (Common to VCC, A to GPIO32, B to GPIO33, button to Ground on GPIO25).... has a few glitches, but tweaks to pcnt_set_filter_value will take care of most of them i assume.

Posts: 1
Joined: Sun Dec 15, 2019 3:04 am

Re: using pulse counter to read optical (quadrature) decoder?

Postby mrkert » Sun Dec 15, 2019 3:12 am

Stumbled upon this thread looking for 4x encoder solution.

clarkster is really close: to implement 4x mode, you have to use BOTH 0 and 1 channels of a single counter, not just PCNT_CHANNEL_0, as both feed the same counter value. For channel 0, define encoder input A to be CTRL, and B to be SIG. For channel 1, define B to be CTRL and A to be SIG.

Posts: 1
Joined: Tue Dec 31, 2019 2:22 pm

Re: using pulse counter to read optical (quadrature) decoder?

Postby jodaco67 » Tue Dec 31, 2019 2:25 pm

mrkert is exactly right. In addition to flipping control and signal pins for channel 1 you have to flip the high and low control modes too. Once that is done you get full quadrature counting.

Who is online

Users browsing this forum: No registered users and 18 guests