Rotary Encoder
Rotary Encoder
Feature Overview
Rotary encoders detect rotation direction and angle, supporting forward/reverse counting and button functions. Suitable for volume adjustment, menu navigation, position control, and other scenarios.
Driver Implementation Status: ✅ Full driver implemented (v2.1)
Core Features
- ✅ GPIO Interrupt Mode - Compatible with all ESP32 series chips (ESP32/ESP32-S3/ESP32-C3/ESP32-C6)
- ✅ Quadrature Decoding - A/B phases 90 degrees apart for precise direction detection
- ✅ Real-time Counter Reading - Get counter status via readPin() API
- ✅ Counter Reset - Reset counter to zero via writePin() API with STATE_LOW
- ✅ Interrupt Handling Optimization - Uses IRAM_ATTR attribute to ensure execution in IRAM for faster response
- ✅ Thread-Safe Design - Uses volatile int32_t for interrupt context safety
- ✅ Counter Storage - std::map<String, volatile int32_t> manages multiple encoder instances
💡 Future Optimization: ESP32/ESP32-S3 can be upgraded to PCNT hardware counters (higher precision, no CPU intervention required)
Working Principle
Quadrature Decoding Mechanism
The encoder uses A/B two-phase quadrature signals for direction judgment:
Clockwise Rotation (CW):
Phase A: ─┐ ┌─┐ ┌─┐ ┌─
└───┘ └───┘ └───
Phase B: ───┐ ┌─┐ ┌─┐ ┌
└───┘ └───┘ └───┘
Direction: When Phase A triggers interrupt, Phase B is HIGH → count+1
Counter-Clockwise Rotation (CCW):
Phase A: ─┐ ┌─┐ ┌─┐ ┌─
└───┘ └───┘ └───
Phase B: ┌─┐ ┌─┐ ┌─┐
───┘ └───┘ └───┘ └
Direction: When Phase A triggers interrupt, Phase B is LOW → count-1Interrupt Handling Flow
void IRAM_ATTR PeripheralManager::handleEncoderInterrupt(void* arg) {
// 1. Parse peripheral configuration
PeripheralConfig* config = static_cast<PeripheralConfig*>(arg);
if (!config || config->type != PeripheralType::ENCODER) return;
// 2. Read A/B phase states
uint8_t pinA = config->pins[0];
uint8_t pinB = config->pins[1];
int stateA = digitalRead(pinA);
int stateB = digitalRead(pinB);
// 3. Determine rotation direction
// Phase B HIGH = clockwise, Phase B LOW = counter-clockwise
int32_t direction = stateB ? 1 : -1;
// 4. Update counter (interrupt-safe)
String peripheralId = config->id;
auto& pm = PeripheralManager::getInstance();
auto it = pm.encoderCounters.find(peripheralId);
if (it != pm.encoderCounters.end()) {
it->second += direction;
} else {
pm.encoderCounters[peripheralId] = direction;
}
}Initialization Flow
// 1. Validate pin count (at least 2: Phase A and Phase B)
if (config.pinCount < 2) return false;
// 2. Configure GPIO as pull-up input mode
pinMode(pinA, INPUT_PULLUP);
pinMode(pinB, INPUT_PULLUP);
// 3. Initialize counter
if (config.params.encoder.useInterrupt) {
encoderCounters[config.id] = 0;
// 4. Attach interrupt to Phase A (trigger on both rising and falling edges)
attachInterruptArg(pinA, handleEncoderInterrupt,
const_cast<PeripheralConfig*>(&config), CHANGE);
}Supported Peripheral Types
| Type | type value | Description |
|---|---|---|
| ENCODER | 43 | Rotary encoder (dual-channel A/B phases) |
Hardware Wiring
| Encoder Pin | Function | Description |
|---|---|---|
| CLK (A) | GPIO | Phase A signal |
| DT (B) | GPIO | Phase B signal |
| SW | GPIO (optional) | Button signal (can be used as separate input) |
| VCC | 3.3V | Power |
| GND | GND | Ground |
Usage Methods
Method 1: Web Interface Configuration (Recommended)
Rotary encoders require configuring both A/B signals. Complete configuration flow:
Step 1: Log in to Device Management Page
- Enter ESP32 IP address in browser
- After logging in, navigate to Peripheral Configuration
Step 2: Add Rotary Encoder Peripheral
Click the Add Peripheral button
Fill in the configuration:
Field Example Description Peripheral ID encoder1Unique identifier Name Rotary EncoderDisplay name Peripheral Type Rotary Encoder (type: 43) Dual-channel A/B phases CLK Pin (A) 32Phase A signal DT Pin (B) 33Phase B signal Pulses Per Revolution 20Typically 20 Use Interrupt trueRecommended Click Save
Step 3: Verify Configuration
- Find the newly added peripheral in the peripheral list
- Click the Test button
- Rotate the encoder to observe counter value changes
💡 Tip: If the encoder has a button pin (SW pin), it needs to be separately configured as GPIO digital input (type: 13)
Method 2: JSON Configuration File
Add the configuration to the peripherals array in data/config/peripherals.json:
{
"id": "encoder1",
"name": "Rotary Encoder",
"type": 43,
"enabled": false,
"pins": [32, 33],
"params": {
"resolution": 20,
"useInterrupt": true
}
}Parameter Description
| Parameter | Description |
|---|---|
| resolution | Pulses per revolution (typically 20) |
| useInterrupt | Whether to use interrupt mode (recommended true) |
pins[0] = CLK (Phase A) pin, pins[1] = DT (Phase B) pin
API Reference
Read Encoder Status
// Function signature
GPIOState PeripheralManager::readPin(const String& peripheralId);
// Usage example
GPIOState state = pm.readPin("encoder_01");
// Return value description:
// - STATE_HIGH: Counter is non-zero (rotation activity detected)
// - STATE_LOW: Counter is zero (no rotation or has been reset)
// - STATE_UNDEFINED: Peripheral does not exist or is not initializedUnderlying Implementation:
// Encoder: returns count value (converted to GPIOState, HIGH means non-zero count)
if (config->type == PeripheralType::ENCODER) {
auto it = encoderCounters.find(peripheralId);
if (it == encoderCounters.end()) {
return GPIOState::STATE_UNDEFINED;
}
return it->second != 0 ? GPIOState::STATE_HIGH : GPIOState::STATE_LOW;
}Reset Encoder Counter
// Function signature
bool PeripheralManager::writePin(const String& peripheralId, GPIOState state);
// Usage example
pm.writePin("encoder_01", GPIOState::STATE_LOW); // Reset counter to zero
// Return value:
// - true: Reset successful
// - false: Peripheral does not existUnderlying Implementation:
// Encoder: supports resetting counter (writing LOW means reset)
if (config->type == PeripheralType::ENCODER) {
if (state == GPIOState::STATE_LOW) {
encoderCounters[peripheralId] = 0;
LOG_INFOF("Peripheral Manager: Encoder '%s' counter reset to 0",
peripheralId.c_str());
}
return true;
}Data Reporting Format
Encoder count values are reported via MQTT:
[{"id": "encoder1", "value": "42"}]Rotary encoder data represents cumulative rotation angle values.
Peripheral Execution Linkage
Web Interface Configuration Steps
Encoder as Platform Trigger Source
- Switch to the Peripheral Execution Engine tab
- Click the Add Rule button
- Configure platform trigger conditions:
- Trigger Type: Platform Trigger
- Trigger Source: encoder1
- Comparison Operation: Greater Than
- Comparison Value: 100
- Add actions (e.g., control lights, buzzer, etc.)
- Click Save
Encoder Button as Event Trigger Source
- Configure encoder SW pin as GPIO digital input (type: 13)
- Configure event trigger conditions:
- Trigger Type: Event Trigger
- Event Source: encoder1_btn
- Event Type: button_click
- Add actions
- Click Save
💡 Tip: Interrupt mode is recommended over polling mode for faster rotation response
JSON Configuration Examples
As Platform Trigger Source Resource
Encoder count value changes can trigger linkage:
{
"triggerType": 0,
"triggerPeriphId": "encoder1",
"operatorType": 2,
"compareValue": "100"
}Encoder Button Event
Configure the encoder SW pin as GPIO digital input (pull-up) to use button events:
{
"id": "encoder1_btn",
"name": "Encoder Button",
"type": 13,
"enabled": false,
"pins": [25],
"params": {
"initialState": 0,
"pwmChannel": 0,
"pwmFrequency": 1000,
"pwmResolution": 8,
"defaultDuty": 0
}
}Test Coverage
Unit Tests (7)
| Test Case | Verification Content |
|---|---|
| test_encoder_type_enum_value | Enum value validation (type=43) |
| test_encoder_config_validation_valid | Valid configuration validation |
| test_encoder_config_validation_missing_pins | Insufficient pins detection |
| test_encoder_config_validation_zero_resolution | Resolution=0 detection |
| test_encoder_data_transparency | Data transparency verification |
| test_encoder_read_counter | Counter read functionality |
| test_encoder_reset_counter | Counter reset functionality |
E2E Tests (2)
| Test Case | Verification Content |
|---|---|
| test_e2e_encoder_peripheral_workflow | Complete encoder workflow |
| test_e2e_encoder_mqtt_integration | Encoder MQTT integration |
Integration Tests
- ✅ MQTT linkage trigger
- ✅ Complete counter read/write workflow
- ✅ Interrupt response speed test
- ✅ Multi-encoder instance concurrency test
Precautions
- Interrupt Mode: Interrupt mode is recommended; polling mode may lose counts at high rotation frequencies
- Pull-up Resistors: Encoders typically require pull-up resistors to ensure signal stability
- Pin Selection: Ensure GPIO pins that support interrupts are used
- Counter Overflow: Long-running applications should monitor counter overflow and handle it at the application layer when necessary
- Multiple Encoders: The system supports multiple encoder instances, each counting independently
- Thread Safety: Interrupt handler uses IRAM_ATTR to ensure response speed
Chip Compatibility
| Chip Model | GPIO Interrupt | PCNT Hardware Counter | Description |
|---|---|---|---|
| ESP32 | ✅ Supported | ✅ Supported | Can use PCNT optimization |
| ESP32-S3 | ✅ Supported | ✅ Supported | Can use PCNT optimization |
| ESP32-C3 | ✅ Supported | ❌ Not Supported | GPIO interrupt only |
| ESP32-C6 | ✅ Supported | ❌ Not Supported | GPIO interrupt only |
📌 Note: Current implementation uses GPIO interrupt mode, compatible with all ESP32 series. PCNT hardware counter support can be added for ESP32/ESP32-S3 in the future.
