Chapter 166: Encoder Interface with PCNT
Chapter Objectives
By the end of this chapter, you will be able to:
- Understand the working principles of incremental rotary encoders, particularly quadrature encoders.
- Explain how Phase A, Phase B, and optional Index (Z) signals are used to determine position and direction.
- Describe different quadrature decoding modes (1x, 2x, 4x) and their implications.
- Configure the ESP32 PCNT module specifically for robust quadrature encoder interfacing.
- Implement code to read position and direction from a rotary encoder.
- Handle common issues like signal noise and debouncing for mechanical encoders.
- Integrate encoder readings with other functionalities, such as resetting counts or calculating speed.
- Recognize any variant-specific considerations for encoder interfacing using PCNT.
Introduction
In the previous chapter, we explored the versatile Pulse Counter (PCNT) module. One of its most common and powerful applications is interfacing with rotary encoders. Rotary encoders are electromechanical devices that convert the angular position or motion of a shaft or axle into an analog or digital signal. They are fundamental components in user interfaces (knobs for volume control, menu navigation), robotics (motor position and speed feedback), industrial automation (positioning systems), and many other applications where precise rotational measurement is required.
This chapter will delve specifically into using the PCNT module to interface with incremental quadrature encoders. We will cover the theory behind how these encoders work, the different ways their signals can be decoded for higher resolution, and how to configure the ESP32’s PCNT peripheral to efficiently track rotation and direction. By mastering this, you’ll be able to add sophisticated rotational input and feedback mechanisms to your ESP32 projects.
Theory
What is a Rotary Encoder?
A rotary encoder is a sensor that detects the angular position and/or motion of a rotating shaft. There are two main types:
- Absolute Encoders: Provide a unique digital code for each distinct angle of the shaft. Even if power is lost, they know their absolute position when power is restored. They are more complex and generally more expensive.
- Incremental Encoders: Generate pulses as the shaft rotates. By counting these pulses, relative changes in position can be determined. If power is lost, the position reference is also lost and needs to be re-established (e.g., by moving to a known home position or using an index pulse).
This chapter focuses on incremental encoders, specifically quadrature encoders, as they are widely used with microcontrollers like the ESP32.
Incremental Quadrature Encoders
A quadrature encoder typically has two output channels, Phase A and Phase B, that produce digital pulse trains. These signals are 90 degrees out of phase with each other. Some encoders also provide a third channel, Index (Z), which outputs a single pulse per revolution at a specific home position.
Phase A and Phase B Signals:
The key to a quadrature encoder is the phase relationship between signals A and B:
- When the shaft rotates in one direction (e.g., clockwise), one signal will lead the other (e.g., A leads B: A rises before B rises).
- When the shaft rotates in the opposite direction (e.g., counter-clockwise), the phase leadership reverses (e.g., B leads A: B rises before A rises).
By monitoring both signals, we can determine:
- Amount of Rotation: By counting the pulses on either A or B.
- Direction of Rotation: By observing which signal transitions first.
Index (Z) Signal (Optional):
The Z signal provides a reference point once per revolution. It can be used for:
- Homing: Establishing a known zero position after power-up.
- Revolution Counting: Verifying or correcting the accumulated pulse count over multiple revolutions.The PCNT module itself doesn’t have a dedicated input to automatically reset the count on the Z pulse. Handling the Z pulse typically requires using a separate GPIO interrupt. When the Z pulse interrupt occurs, the software can then explicitly clear the PCNT counter (pcnt_unit_clear_count()).
Quadrature Decoding Modes
The two 90-degree out-of-phase signals (A and B) create four distinct states for each cycle of the encoder.
Let’s represent the states as (Phase B, Phase A):
- State 1: (Low, Low)
- State 2: (Low, High)
- State 3: (High, High)
- State 4: (High, Low)
A full cycle of the encoder (e.g., one slot passing the sensor in an optical encoder) causes transitions through these four states.
%%{init: {'theme': 'default', 'flowchart': {'curve': 'basis', 'nodeSpacing': 60, 'rankSpacing': 50}, 'fontFamily': 'Open Sans'}}%% graph TD %% State Nodes S00(["State 00<br/>(B=0, A=0)"]) S01(["State 01<br/>(B=0, A=1)"]) S11(["State 11<br/>(B=1, A=1)"]) S10(["State 10<br/>(B=1, A=0)"]) %% CW Rotation S00 -- "+1 ⟳" --> S01 S01 -- "+1 ⟳" --> S11 S11 -- "+1 ⟳" --> S10 S10 -- "+1 ⟳" --> S00 %% CCW Rotation S00 -- "-1 ⟲" --> S10 S10 -- "-1 ⟲" --> S11 S11 -- "-1 ⟲" --> S01 S01 -- "-1 ⟲" --> S00 %% Link Styles linkStyle 0,1,2,3 stroke:#059669,stroke-width:2.5px; linkStyle 4,5,6,7 stroke:#D97706,stroke-width:2.5px; %% Node Styling classDef stateNode fill:#DBEAFE,stroke:#2563EB,stroke-width:2px,color:#1E40AF,font-size:14px,font-weight:bold; class S00,S01,S11,S10 stateNode; %% Title classDef titleNode fill:none,stroke:none,color:#000000,font-size:18px,font-weight:bold;
We can choose to count pulses at different points in this cycle, leading to different resolutions:
- 1x Decoding (Single-Edge Counting):
- Count only one edge (e.g., rising edge) of one signal (e.g., Phase A).
- The state of the other signal (Phase B) at that moment determines the direction.
- Example: If counting rising edges of A:
- If B is low when A rises: Increment (CW).
- If B is high when A rises: Decrement (CCW).
- This gives N pulses per revolution, where N is the encoder’s native pulses per revolution (PPR).
- 2x Decoding (Dual-Edge Counting on One Channel):
- Count both rising and falling edges of one signal (e.g., Phase A).
- The state of Phase B relative to Phase A determines direction.
- Example:
- A rises, B low: +1
- A falls, B high: +1 (still same direction)
- A rises, B high: -1
- A falls, B low: -1 (still same direction)
- This gives 2N pulses per revolution.
- 4x Decoding (All Edges Counting):
- Count every edge transition on both Phase A and Phase B signals. Each state change (00->01, 01->11, 11->10, 10->00) is counted.
- This provides the highest resolution, giving 4N pulses per revolution.
- Implementing 4x decoding with a single PCNT unit channel (one edge input, one level input) can be tricky. Often, it’s implemented by using two PCNT channels/units (one for A edges, one for B edges, and software logic) or by more sophisticated hardware. However, the ESP32 PCNT can be configured to approximate 2x or achieve a form of 4x by carefully setting edge and level actions, or by using two channels where each channel monitors edges of one phase and the other phase acts as the level control. The most straightforward use of a single PCNT channel is for 1x or 2x decoding.
The ESP32’s PCNT module is well-suited for 1x and 2x decoding using a single channel, where one encoder signal (e.g., Phase A) is connected to the PCNT’s pulse input pin (edge_gpio_num
), and the other signal (e.g., Phase B) is connected to the control input pin (level_gpio_num
).
Configuring PCNT for Quadrature Encoders
As discussed in Chapter 165, the PCNT channel behavior is set by:
pcnt_channel_set_edge_action(channel, pos_action, neg_action)
: Defines action on positive/negative edges ofedge_gpio_num
.pcnt_channel_set_level_action(channel, high_action, low_action)
: Defines how the state oflevel_gpio_num
modifies the edge actions.
Decoding Mode | Principle | Resolution (per PPR) | PCNT Implementation Notes |
---|---|---|---|
1x Decoding | Counts one edge (e.g., rising) of Phase A only. Phase B determines direction. | N pulses | Simple. Use EDGE_ACTION_INCREASE on positive edge and HOLD on negative. Level actions on B provide direction. |
2x Decoding | Counts both rising and falling edges of Phase A. Phase B determines direction. | 2N pulses | Good balance. Use INCREASE on positive and DECREASE on negative edges. Level actions on B provide correct direction. (Most common single-channel method) |
4x Decoding | Counts every state change (rising and falling edges of both Phase A and Phase B). | 4N pulses | Highest resolution. Hard to achieve with one PCNT channel. Typically requires two PCNT channels (one per phase) or complex software logic on top of a 2x setup. |
%%{init: {'fontFamily': 'Open Sans'}}%% flowchart TD A(Start: Configure Encoder) --> B["pcnt_new_unit()<br><i>Set high/low limits, enable accum_count</i>"]; B --> C["pcnt_new_channel()<br><i>Assign Phase A to edge_gpio,<br>Phase B to level_gpio</i>"]; C --> D{"Select Decoding Mode"}; D -- "1x Mode" --> E["pcnt_channel_set_edge_action()<br><i>Pos: INCREASE, Neg: HOLD</i>"]; D -- "2x Mode" --> F["pcnt_channel_set_edge_action()<br><i>Pos: INCREASE, Neg: DECREASE</i>"]; E --> G; F --> G; G["pcnt_channel_set_level_action()<br><i>High: INVERSE, Low: KEEP</i>"]; G --> H["pcnt_unit_set_glitch_filter()<br><i>Crucial for debouncing!</i>"]; H --> I["pcnt_unit_enable()"]; I --> J["pcnt_unit_clear_count()"]; J --> K["pcnt_unit_start()"]; K --> L(End: Ready to Read); %% Styling classDef startNode fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; classDef processNode fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; classDef decisionNode fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E; classDef criticalNode fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B; classDef endNode fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46; class A,L startNode; class B,C,E,F,G,I,J,K processNode; class D decisionNode; class H criticalNode; class L endNode;
For 1x Decoding (Counting on Positive Edges of Phase A):
- Connect Phase A to
edge_gpio_num
. - Connect Phase B to
level_gpio_num
. - Desired logic:
- If A rises and B is LOW: Increment count (e.g., CW direction).
- If A rises and B is HIGH: Decrement count (e.g., CCW direction).
- Ignore falling edges of A.
- PCNT Configuration:
pcnt_channel_set_edge_action(channel, PCNT_CHANNEL_EDGE_ACTION_INCREASE, PCNT_CHANNEL_EDGE_ACTION_HOLD);
pos_action = PCNT_CHANNEL_EDGE_ACTION_INCREASE
: On A’s positive edge, default action is to increase.neg_action = PCNT_CHANNEL_EDGE_ACTION_HOLD
: On A’s negative edge, do nothing.
pcnt_channel_set_level_action(channel, PCNT_CHANNEL_LEVEL_ACTION_INVERSE, PCNT_CHANNEL_LEVEL_ACTION_KEEP);
high_action = PCNT_CHANNEL_LEVEL_ACTION_INVERSE
: If B (level input) is HIGH, invert the edge action. So,INCREASE
becomesDECREASE
.low_action = PCNT_CHANNEL_LEVEL_ACTION_KEEP
: If B (level input) is LOW, keep the original edge action. So,INCREASE
remainsINCREASE
.
This configuration achieves:
- A positive edge, B low:
INCREASE
(original) because level action isKEEP
. - A positive edge, B high:
DECREASE
(inverted fromINCREASE
) because level action isINVERSE
. - A negative edge (any B level):
HOLD
.
For 2x Decoding (Counting on Both Edges of Phase A):
- Connect Phase A to
edge_gpio_num
. - Connect Phase B to
level_gpio_num
. - Desired logic (example for one direction, CW):
- A rises, B is LOW: Increment.
- A falls, B is HIGH: Increment.
- (And the reverse for CCW)
- PCNT Configuration:
pcnt_channel_set_edge_action(channel, PCNT_CHANNEL_EDGE_ACTION_INCREASE, PCNT_CHANNEL_EDGE_ACTION_DECREASE);
pos_action = PCNT_CHANNEL_EDGE_ACTION_INCREASE
: On A’s positive edge, default is increase.neg_action = PCNT_CHANNEL_EDGE_ACTION_DECREASE
: On A’s negative edge, default is decrease.
pcnt_channel_set_level_action(channel, PCNT_CHANNEL_LEVEL_ACTION_INVERSE, PCNT_CHANNEL_LEVEL_ACTION_KEEP);
high_action = PCNT_CHANNEL_LEVEL_ACTION_INVERSE
: If B is HIGH, invert the edge action.low_action = PCNT_CHANNEL_LEVEL_ACTION_KEEP
: If B is LOW, keep the original edge action.
This configuration achieves:
- A positive edge, B low:
INCREASE
(originalINCREASE
, B_lowKEEP
). - A positive edge, B high:
DECREASE
(originalINCREASE
, B_highINVERSE
). - A negative edge, B low:
DECREASE
(originalDECREASE
, B_lowKEEP
). - A negative edge, B high:
INCREASE
(originalDECREASE
, B_highINVERSE
).
This set of actions correctly decodes typical quadrature signals for 2x resolution.
Debouncing and Filtering
Mechanical rotary encoders, due to their physical contacts, can produce noisy signals or “bouncing” during state transitions. Optical encoders are generally cleaner but can still be affected by environmental noise.
The PCNT module’s built-in glitch filter is essential here:
pcnt_unit_set_glitch_filter(pcnt_unit, &filter_config);
filter_config.max_glitch_ns
: Pulses shorter than this duration (in nanoseconds) on the input pins will be ignored. This value needs to be tuned based on the encoder’s characteristics and expected rotation speed. Too short, and noise passes; too long, and valid fast pulses might be missed. Typical values for mechanical encoders might be a few microseconds (e.g., 1000 to 10000 ns).
Practical Examples
Let’s implement rotary encoder interfacing. Ensure your ESP-IDF project is set up as in Chapter 165, with driver
, esp_log
, and esp_timer
components.
Project Setup
- Create/Open an ESP-IDF project.
main/CMakeLists.txt
:idf_component_register(SRCS "main.c" INCLUDE_DIRS "." REQUIRES driver esp_log esp_timer freertos) # freertos for tasks/semaphores if needed
Example 1: Quadrature Encoder (2x Decoding) and Position Display
This example demonstrates 2x decoding of a rotary encoder and prints the accumulated count.
Hardware Setup:
- Encoder Phase A -> GPIO4 (
ENCODER_PHASE_A_GPIO
) - Encoder Phase B -> GPIO5 (
ENCODER_PHASE_B_GPIO
) - Encoder Common (C/GND) -> ESP32 GND
- Pull-up resistors (e.g., 4.7kΩ or 10kΩ) from Phase A to 3.3V and Phase B to 3.3V (if not built into the encoder module).
main/main.c
:
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/pulse_cnt.h"
#include "driver/gpio.h"
#include "esp_log.h"
static const char *TAG = "ENCODER_EXAMPLE";
#define ENCODER_PHASE_A_GPIO 4
#define ENCODER_PHASE_B_GPIO 5
#define PCNT_HIGH_LIMIT 10000 // Arbitrary high limit for example
#define PCNT_LOW_LIMIT -10000// Arbitrary low limit for example
#define ENCODER_GLITCH_FILTER_NS 1000 // Filter out glitches shorter than 1us (1000 ns)
static pcnt_unit_handle_t pcnt_unit = NULL;
// Optional: Event callback for limits (runs in ISR context)
static bool IRAM_ATTR pcnt_event_callback(pcnt_unit_handle_t unit, const pcnt_watch_event_data_t *edata, void *user_ctx)
{
ESP_DRAM_LOGI(TAG, "PCNT Watch Event: %d", edata->watch_point_event_type);
// In a real app, you might signal a task or update a shared variable carefully
return false; // No task woken by this ISR
}
void initialize_encoder(void)
{
ESP_LOGI(TAG, "Initializing PCNT for Quadrature Encoder (2x decoding)");
// 1. Configure PCNT Unit
pcnt_unit_config_t unit_config = {
.high_limit = PCNT_HIGH_LIMIT,
.low_limit = PCNT_LOW_LIMIT,
.flags.accum_count = true, // Allow counter to accumulate beyond limits (wrap around)
};
ESP_ERROR_CHECK(pcnt_new_unit(&unit_config, &pcnt_unit));
// 2. Configure PCNT Channel
pcnt_channel_config_t channel_config = {
.edge_gpio_num = ENCODER_PHASE_A_GPIO,
.level_gpio_num = ENCODER_PHASE_B_GPIO,
.flags.invert_edge_input = false, // Set true if Phase A logic is inverted
.flags.invert_level_input = false, // Set true if Phase B logic is inverted
};
pcnt_channel_handle_t pcnt_channel = NULL;
ESP_ERROR_CHECK(pcnt_new_channel(pcnt_unit, &channel_config, &pcnt_channel));
// 3. Set Edge and Level Actions for 2x Quadrature Decoding
// Action on Phase A's positive edge
// Action on Phase A's negative edge
ESP_ERROR_CHECK(pcnt_channel_set_edge_action(pcnt_channel,
PCNT_CHANNEL_EDGE_ACTION_INCREASE, // Default action on positive edge
PCNT_CHANNEL_EDGE_ACTION_DECREASE)); // Default action on negative edge
// How Phase B's level modifies the above actions
ESP_ERROR_CHECK(pcnt_channel_set_level_action(pcnt_channel,
PCNT_CHANNEL_LEVEL_ACTION_INVERSE, // When B is high, invert the A edge action
PCNT_CHANNEL_LEVEL_ACTION_KEEP)); // When B is low, keep the A edge action
/*
* This results in:
* A_pos_edge, B_low: INCREASE (original INC, B_low keeps)
* A_pos_edge, B_high: DECREASE (original INC, B_high inverts)
* A_neg_edge, B_low: DECREASE (original DEC, B_low keeps)
* A_neg_edge, B_high: INCREASE (original DEC, B_high inverts)
* This is standard 2x quadrature decoding.
*/
// 4. Add Glitch Filter
pcnt_glitch_filter_config_t filter_config = {
.max_glitch_ns = ENCODER_GLITCH_FILTER_NS,
};
ESP_ERROR_CHECK(pcnt_unit_set_glitch_filter(pcnt_unit, &filter_config));
ESP_LOGI(TAG, "Encoder glitch filter enabled: %d ns", ENCODER_GLITCH_FILTER_NS);
// Optional: Register event callbacks for limits
// pcnt_event_callbacks_t cbs = {
// .on_reach_high_limit = pcnt_event_callback,
// .on_reach_low_limit = pcnt_event_callback,
// };
// ESP_ERROR_CHECK(pcnt_unit_register_event_callbacks(pcnt_unit, &cbs, NULL));
// 5. Enable, Clear, and Start PCNT Unit
ESP_ERROR_CHECK(pcnt_unit_enable(pcnt_unit));
ESP_ERROR_CHECK(pcnt_unit_clear_count(pcnt_unit));
ESP_ERROR_CHECK(pcnt_unit_start(pcnt_unit));
ESP_LOGI(TAG, "PCNT unit started for quadrature encoder");
}
void app_main(void)
{
initialize_encoder();
int current_encoder_count = 0;
int last_displayed_count = -1; // Initialize to a value that will trigger first print
while (1) {
ESP_ERROR_CHECK(pcnt_unit_get_count(pcnt_unit, ¤t_encoder_count));
if (current_encoder_count != last_displayed_count) {
ESP_LOGI(TAG, "Encoder Count: %d", current_encoder_count);
last_displayed_count = current_encoder_count;
}
vTaskDelay(pdMS_TO_TICKS(100)); // Poll and print every 100ms if changed
}
}
Build, Flash, and Run:
- Connect your encoder as described.
- Build the project (
idf.py build
). - Flash to your ESP32 (
idf.py -p /dev/ttyUSBX flash
). - Monitor the output (
idf.py monitor
). - Rotate the encoder shaft. You should see the “Encoder Count” log messages changing, incrementing for one direction and decrementing for the other. The count should change by 2 for each “click” or detent of a typical mechanical encoder if it has one pulse per detent per channel.
Example 2: Encoder with Button Press Reset and Index (Z) Pulse Handling
Many rotary encoders include a push-button switch. Let’s use this to reset the encoder count. We’ll also simulate handling an Index (Z) pulse using another GPIO interrupt.
Hardware Setup (Addition to Example 1):
- Encoder Switch Pin -> GPIO18 (
ENCODER_SWITCH_GPIO
) (with external pull-up to 3.3V, switch connects to GND). - Encoder Index (Z) Pin (if available) -> GPIO19 (
ENCODER_INDEX_Z_GPIO
) (with external pull-up, Z pulse typically goes low).
main/main.c
:
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h" // For ISR to task communication
#include "driver/pulse_cnt.h"
#include "driver/gpio.h"
#include "esp_log.h"
static const char *TAG = "ENCODER_ADV_EXAMPLE";
// PCNT Configuration (same as Example 1)
#define ENCODER_PHASE_A_GPIO 4
#define ENCODER_PHASE_B_GPIO 5
#define PCNT_HIGH_LIMIT 32000 // Wider range
#define PCNT_LOW_LIMIT -32000
#define ENCODER_GLITCH_FILTER_NS 1000
// GPIO for Switch and Index
#define ENCODER_SWITCH_GPIO GPIO_NUM_18
#define ENCODER_INDEX_Z_GPIO GPIO_NUM_19 // Optional
static pcnt_unit_handle_t pcnt_unit = NULL;
static QueueHandle_t gpio_evt_queue = NULL;
// ISR handler for Switch and Index GPIOs
static void IRAM_ATTR gpio_isr_handler(void *arg)
{
uint32_t gpio_num = (uint32_t)arg;
xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);
}
// Task to handle GPIO events (button press, index pulse)
static void gpio_event_task(void *arg)
{
uint32_t io_num;
for (;;) {
if (xQueueReceive(gpio_evt_queue, &io_num, portMAX_DELAY)) {
if (io_num == ENCODER_SWITCH_GPIO) {
// Debounce might be needed here in a real app if not handled by hardware/filter
ESP_LOGI(TAG, "Encoder switch pressed! Resetting PCNT count.");
if (pcnt_unit) {
ESP_ERROR_CHECK(pcnt_unit_clear_count(pcnt_unit));
}
} else if (io_num == ENCODER_INDEX_Z_GPIO) {
ESP_LOGI(TAG, "Encoder Index (Z) pulse detected! PCNT count could be referenced or reset.");
// Example: Reset on Z pulse. In a real system, you might record this event
// or use it to calibrate a multi-turn counter.
if (pcnt_unit) {
ESP_ERROR_CHECK(pcnt_unit_clear_count(pcnt_unit));
ESP_LOGI(TAG,"PCNT count reset by Z pulse.");
}
}
}
}
}
void initialize_encoder_with_switch_and_index(void)
{
ESP_LOGI(TAG, "Initializing PCNT and GPIOs for Encoder with Switch/Index");
// --- PCNT Initialization (same as Example 1) ---
pcnt_unit_config_t unit_config = {
.high_limit = PCNT_HIGH_LIMIT,
.low_limit = PCNT_LOW_LIMIT,
.flags.accum_count = true,
};
ESP_ERROR_CHECK(pcnt_new_unit(&unit_config, &pcnt_unit));
pcnt_channel_config_t channel_config = {
.edge_gpio_num = ENCODER_PHASE_A_GPIO,
.level_gpio_num = ENCODER_PHASE_B_GPIO,
};
pcnt_channel_handle_t pcnt_channel = NULL;
ESP_ERROR_CHECK(pcnt_new_channel(pcnt_unit, &channel_config, &pcnt_channel));
ESP_ERROR_CHECK(pcnt_channel_set_edge_action(pcnt_channel,
PCNT_CHANNEL_EDGE_ACTION_INCREASE,
PCNT_CHANNEL_EDGE_ACTION_DECREASE));
ESP_ERROR_CHECK(pcnt_channel_set_level_action(pcnt_channel,
PCNT_CHANNEL_LEVEL_ACTION_INVERSE,
PCNT_CHANNEL_LEVEL_ACTION_KEEP));
pcnt_glitch_filter_config_t filter_config = { .max_glitch_ns = ENCODER_GLITCH_FILTER_NS };
ESP_ERROR_CHECK(pcnt_unit_set_glitch_filter(pcnt_unit, &filter_config));
ESP_ERROR_CHECK(pcnt_unit_enable(pcnt_unit));
ESP_ERROR_CHECK(pcnt_unit_clear_count(pcnt_unit));
ESP_ERROR_CHECK(pcnt_unit_start(pcnt_unit));
ESP_LOGI(TAG, "PCNT unit for encoder started.");
// --- GPIO Initialization for Switch and Index ---
gpio_config_t io_conf = {};
// Interrupt on negative edge (falling edge for pull-up active low switch/Z-pulse)
io_conf.intr_type = GPIO_INTR_NEGEDGE;
// Bit mask of the pins
io_conf.pin_bit_mask = (1ULL << ENCODER_SWITCH_GPIO) | (1ULL << ENCODER_INDEX_Z_GPIO);
// Set as input mode
io_conf.mode = GPIO_MODE_INPUT;
// Enable pull-up mode
io_conf.pull_up_en = GPIO_PULLUP_ENABLE;
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
gpio_config(&io_conf);
// Create a queue to handle gpio events from ISR
gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t));
// Start task to handle GPIO events
xTaskCreate(gpio_event_task, "gpio_event_task", 2048, NULL, 10, NULL);
// Install GPIO ISR service
ESP_ERROR_CHECK(gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT));
// Hook ISR handler for specific GPIO pins
ESP_ERROR_CHECK(gpio_isr_handler_add(ENCODER_SWITCH_GPIO, gpio_isr_handler, (void *)ENCODER_SWITCH_GPIO));
ESP_ERROR_CHECK(gpio_isr_handler_add(ENCODER_INDEX_Z_GPIO, gpio_isr_handler, (void *)ENCODER_INDEX_Z_GPIO));
ESP_LOGI(TAG, "GPIO ISR for switch and index configured.");
}
void app_main(void)
{
initialize_encoder_with_switch_and_index();
int current_encoder_count = 0;
int last_displayed_count = 0;
while (1) {
if (pcnt_unit) { // Check if pcnt_unit is initialized
ESP_ERROR_CHECK(pcnt_unit_get_count(pcnt_unit, ¤t_encoder_count));
if (current_encoder_count != last_displayed_count) {
ESP_LOGI(TAG, "Encoder Count: %d", current_encoder_count);
last_displayed_count = current_encoder_count;
}
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
Build, Flash, and Run:
- Connect the encoder, switch, and optionally the Z-pulse line.
- Build, flash, and monitor.
- Rotate the encoder. The count should change.
- Press the encoder switch. The count should reset to 0.
- If Z-pulse is connected, rotating past the index point should also trigger a log and reset the count.
Tip: Mechanical switches (like encoder buttons) also suffer from bouncing. The example above doesn’t include software debouncing for the GPIO interrupt for simplicity, but in a production application, you would add a delay or a more sophisticated debouncing algorithm within
gpio_event_task
before acting on the switch press. The PCNT glitch filter only applies to PCNT inputs, not general GPIOs.
Variant Notes
The PCNT module’s core functionality for encoder interfacing is largely consistent across ESP32 variants that feature it (ESP32, ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C6, ESP32-H2).
- Number of PCNT Units:
- ESP32: 8 units. Allows for up to 8 single-channel encoders (or 4 encoders if using two channels per encoder for advanced decoding, though less common with the standard API).
- ESP32-S2, S3, C3, C6, H2: 4 units. Allows for up to 4 single-channel encoders.This is the primary limiting factor if you need to interface multiple encoders.
- API and Configuration: The ESP-IDF
driver/pulse_cnt.h
API used in the examples (pcnt_new_unit
,pcnt_new_channel
, etc.) is designed to be portable across these variants. - Glitch Filter: The capabilities and configuration of the glitch filter are generally the same. The filter’s time base is derived from the APB clock, so its exact resolution in nanoseconds might have slight variations depending on the APB clock frequency of the specific chip, but
max_glitch_ns
abstracts this. - GPIO Matrix: All these variants have a flexible GPIO matrix, meaning most GPIOs can be routed to PCNT inputs. Always consult the specific variant’s datasheet for any pin limitations or recommendations for peripheral use.
For most common encoder applications (one or two encoders), any of these variants will perform well. The choice might depend on other factors like the number of other peripherals needed, processing power, or specific connectivity features.
Common Mistakes & Troubleshooting Tips
Mistake / Issue | Symptom(s) | Troubleshooting / Solution |
---|---|---|
Count goes the wrong way Phase A/B swapped or logic inverted. |
Encoder increments when it should decrement, and vice-versa. | Easiest Fix: Swap the GPIO pin numbers for Phase A and Phase B in your code. OR Code Fix: In pcnt_channel_config_t, try setting .flags.invert_level_input = true. |
Count jumps by 2 (or 4) instead of 1 Misunderstanding PPR vs. decoding mode. |
For one “click” (detent) of the encoder, the count changes by more than expected. | This is often correct behavior. A 2x or 4x decoding will give more counts than the encoder’s PPR. If a single detent produces one full cycle on A/B, 2x decoding will yield 2 counts. If you need 1 count per detent, either use 1x decoding or divide the result in software. |
Missing Pull-Up Resistors Encoder has open-drain/collector outputs. |
Count is erratic or doesn’t work at all. May work if you touch the wires (due to body capacitance). | Check encoder datasheet. If it requires pull-ups, add them. Use external 4.7kΩ to 10kΩ resistors to 3.3V for best results. Internal ESP32 pull-ups might work but can be too weak. |
Inadequate Glitch Filter Setting Filter value is too low or too high. |
– Too low: Multiple counts per detent (noise gets through). – Too high: Missed counts at high rotation speeds. |
Always enable the filter. Start with 1000ns (1us) for max_glitch_ns and increase if you still see noise. For fast optical encoders, you may need a smaller value. |
GPIO Interrupt for Z/Switch not working ISR service not installed or pin misconfigured. |
Pressing the button or passing the index pulse does nothing. | 1. Ensure gpio_install_isr_service() is called once in your app. 2. Check that the GPIO is configured as an input with the correct pull-up/pull-down setting. 3. Verify the interrupt trigger type (GPIO_INTR_NEGEDGE for active-low signals with pull-ups). |
Exercises
- 1x Decoding Mode:
- Modify Example 1 to implement 1x quadrature decoding (counting only on positive edges of Phase A, with direction determined by Phase B).
- Verify that the count increments/decrements by 1 for each pulse cycle of the encoder (which might correspond to one or more detents depending on the encoder type).
- Encoder-Controlled LED Brightness:
- Interface a rotary encoder using PCNT.
- Interface an LED using the LEDC (PWM) peripheral (refer to Chapter 145).
- Read the encoder count and map it to the LED’s duty cycle to control its brightness. For example, a count from 0 to 255 could map directly to the PWM duty. Ensure the count stays within reasonable bounds for PWM.
- Calculating RPM (Revolutions Per Minute):
- Using the encoder setup from Example 1 (2x decoding).
- In a periodic task (e.g., using
esp_timer
every second):- Read the current PCNT count.
- Calculate the change in count since the last reading (
delta_count
). - Clear the PCNT count (or subtract the previous reading from the current to get delta if not clearing).
- Given the encoder’s Pulses Per Revolution (PPR) and the decoding mode (2x), calculate the number of revolutions:
revolutions = delta_count / (PPR * 2)
. - Calculate RPM:
RPM = (revolutions / time_interval_seconds) * 60
. - Log the RPM.
- You’ll need to know your encoder’s native PPR.
- Limited Range Counter with Saturation:
- Configure the PCNT unit with
flags.accum_count = false
(default). - Set
high_limit
to 100 andlow_limit
to 0. - Observe that the counter stops counting (saturates) when it reaches 0 or 100, regardless of further encoder rotation in that direction.
- Optionally, use
pcnt_unit_register_event_callbacks
to detecton_reach_high_limit
andon_reach_low_limit
events and log them.
- Configure the PCNT unit with
Summary
- Incremental quadrature encoders use two out-of-phase signals (Phase A, Phase B) to provide rotational position and direction information, and an optional Index (Z) pulse per revolution.
- The PCNT module is ideal for decoding these signals by configuring one signal as the edge input and the other as the level control input.
- Decoding modes (1x, 2x, 4x) offer different resolutions by counting different combinations of edges. 1x and 2x modes are straightforward with a single PCNT channel.
- Proper configuration of
pcnt_channel_set_edge_action()
andpcnt_channel_set_level_action()
is crucial for correct direction sensing and decoding. - The glitch filter (
pcnt_unit_set_glitch_filter()
) is vital for handling noise from mechanical encoders. - The Index (Z) pulse is typically handled with a separate GPIO interrupt to reset or reference the PCNT count.
- ESP32 variants offer multiple PCNT units (4 or 8), allowing several encoders to be interfaced.
- Common issues involve wiring, pull-up resistors, filter settings, and understanding decoding logic.
Further Reading
- ESP-IDF Pulse Counter Documentation:
- Pulse Counter (PCNT) API Guide (Check for your specific variant if needed, e.g.,
esp32s3
). - The
esp-idf/examples/peripherals/pcnt/rotary_encoder
example in your ESP-IDF installation is a key reference.
- Pulse Counter (PCNT) API Guide (Check for your specific variant if needed, e.g.,
- Rotary Encoder Basics:
- Many electronics hobbyist sites and manufacturers (e.g., Bourns, Alps Alpine, SparkFun, Adafruit) provide excellent tutorials and datasheets on rotary encoders. Search for “quadrature encoder working principle.”
- ESP32 Series Technical Reference Manuals:
- For the most in-depth hardware details of the PCNT module, consult the TRM for your specific ESP32 variant available on the Espressif Documentation Page.