Skip to content

factory_encoder

factory_encoder is the rotary encoder driver used exclusively within the factory application. It provides a minimal, interrupt-free implementation of quadrature decoding via the ESP-IDF PCNT (Pulse Counter) peripheral.

The driver exposes exactly two functions — encoder_init() and encoder_read() — and is polled directly from the factory app’s main loop. There are no ISRs, no FreeRTOS queues, and no callbacks; the design is deliberately kept as simple as possible for the factory context, where a ~100 ms polling interval is entirely sufficient.

The same implementation is shared verbatim across every supported board. A compile-time guard (GPIO_IS_VALID_GPIO) allows the same binary to run on boards that have no physical encoder: encoder_init() returns ESP_OK immediately and encoder_read() always returns 0.


factory_encoder lives inside the Factory Application & Bootloader layer. It is a hardware input driver peer to factory_buttons and factory_touch. All three are consumed by factory_input_dispatch in main.c to produce unified navigation events for the menu system.

clicks

pressed

x,y

factory_app

(main.c)

factory_input_dispatch

(dispatch_button / touch_hint_hit_test)

factory_menu_system

(menu_move / menu_select)

factory_encoder

encoder_init / encoder_read

factory_buttons

button_is_pressed / button_wait_press

factory_touch

touch_init / touch_read

At the hardware level, the factory encoder driver is a simplified counterpart to the production bsp_physical_inputs phy_encoder driver. The production driver adds callback support (encoder_pcnt_on_reach) and cumulative step tracking (phy_encoder_get_total_steps); the factory driver omits these features to keep the factory binary small and straightforward.


The factory_encoder module is instantiated for the following boards (one encoder.c per board directory). All files carry an identical implementation; only the pin constants in hw_config.h differ.

BoardSource file
esp32_3248s035cboards/esp32_3248s035c/Factory/main/encoder.c
esp32_3248s035rboards/esp32_3248s035r/Factory/main/encoder.c
esp32s3_4827s043cboards/esp32s3_4827s043c/Factory/main/encoder.c
esp32s3_8048_touch_lcd_7boards/esp32s3_8048_touch_lcd_7/Factory/main/encoder.c
esp32s3_8048s043cboards/esp32s3_8048s043c/Factory/main/encoder.c
esp32s3_8048s050cboards/esp32s3_8048s050c/Factory/main/encoder.c

Board-specific pin assignments are injected via hw_config.h (ENCODER_A_PIN, ENCODER_B_PIN). Boards without a physical encoder define those constants as GPIO_NUM_NC.


static pcnt_unit_handle_t s_pcnt_unit = NULL; // PCNT unit handle
static bool s_initialized = false; // guard for encoder_read()

All state is file-static and module-local. There is no shared data structure and no synchronisation overhead — appropriate for a single-threaded factory application.

The driver configures one PCNT unit with two channels to perform standard quadrature decoding:

PCNT Unit (limits ±1000)

Encoder Hardware

edge signal

level signal

edge signal

level signal

±1

±1

Signal A

(ENCODER_A_PIN)

Signal B

(ENCODER_B_PIN)

Channel A

edge=A_PIN, level=B_PIN

Channel B

edge=B_PIN, level=A_PIN

Counter register

Channel A edge actions:

  • Positive edge on A → DECREASE (CW motion)
  • Negative edge on A → INCREASE

Channel A level actions:

  • B high → KEEP current action
  • B low → INVERSE current action

Channel B edge actions:

  • Positive edge on B → INCREASE
  • Negative edge on B → DECREASE

Channel B level actions:

  • A high → KEEP current action
  • A low → INVERSE current action

This produces the standard quadrature decode table: four counter transitions per full mechanical cycle, one per edge on either signal. Combined with PULSES_PER_DETENT = 4, the resulting integer division yields exactly one click per mechanical detent for a standard 4-pulse-per-cycle encoder.

A 1000 ns hardware glitch filter is applied on the PCNT unit:

pcnt_glitch_filter_config_t filt = { .max_glitch_ns = 1000 };
pcnt_unit_set_glitch_filter(s_pcnt_unit, &filt);

This value is intentionally identical to the setting used by the production phy_encoder driver to ensure consistent debounce behaviour across the firmware lifecycle.


esp_err_t encoder_init(void);

Responsibility: Configure the PCNT unit, apply the glitch filter, configure quadrature decode actions on both channels, enable internal pull-ups, and start the counter.

Flow:

No

Yes

encoder_init called

GPIO_IS_VALID_GPIO

A_PIN AND B_PIN?

Log: no encoder on this board

return ESP_OK

s_initialized stays false

pcnt_new_unit

(limits ±1000)

pcnt_unit_set_glitch_filter

(1000 ns)

pcnt_new_channel A

edge=A_PIN, level=B_PIN

pcnt_new_channel B

edge=B_PIN, level=A_PIN

Set edge and level actions

(quadrature decode table)

gpio_set_pull_mode

PULLUP_ONLY on A and B

pcnt_unit_enable

pcnt_unit_clear_count

pcnt_unit_start

s_initialized = true

return ESP_OK

Notes:

  • All ESP-IDF calls use ESP_ERROR_CHECK — any hardware failure aborts with a panic. This is appropriate for a factory test environment where a misconfigured board must be immediately visible.
  • Internal pull-ups are enabled because none of the supported boards provide external pull-up resistors on encoder signal lines.
  • The s_initialized flag prevents encoder_read() from calling pcnt_unit_get_count() on a NULL handle on boards without an encoder.

Return value: esp_err_t — always ESP_OK in normal operation (hardware errors panic via ESP_ERROR_CHECK).


int encoder_read(void);

Responsibility: Return the number of detent clicks accumulated since the last call, then reset the counter.

Flow:

No

Yes

No

Yes

encoder_read called

s_initialized?

return 0

pcnt_unit_get_count → count

clicks = count / PULSES_PER_DETENT

clicks != 0?

return 0

pcnt_unit_clear_count

(remainder pulses discarded)

ESP_LOGV: pulses=X clicks=Y

return clicks

Return value: Integer number of detent clicks since the last non-zero read.

  • Positive → clockwise rotation
  • Negative → counter-clockwise rotation
  • 0 → no movement detected, or encoder not present on this board

Remainder handling: After dividing the raw pulse count by PULSES_PER_DETENT, any remainder pulses are discarded when the counter is cleared. This prevents accumulation drift over time. At 100 ms polling, the probability of a missed remainder producing a visible navigation error is negligible — a partial mechanical detent never completes between polls at normal hand-rotation speeds.


Factory Main Loopfactory_encoderESP-IDF PCNT DriverEncoder HardwareFactory Main Loopfactory_encoderESP-IDF PCNT DriverEncoder HardwareA/B signal edges (hardware, continuous)increment/decrement counter registerencoder_read()pcnt_unit_get_count()raw count (e.g. 8)clicks = 8 / 4 = 2pcnt_unit_clear_count()2 (two CW detents)dispatch_button / menu_move(+2)

encoder_init() is called once during app_main() startup, alongside buttons_init(), buzzer_init(), and touch_init(). From that point, the factory app’s main loop polls encoder_read() at each iteration (~100 ms) and passes the result to dispatch_button() in factory_input_dispatch.

menu_movedispatch_buttonencoder_readmain loopbuttons_initencoder_initapp_mainmenu_movedispatch_buttonencoder_readmain loopbuttons_initencoder_initapp_mainloop[Every 100ms]encoder_init()buttons_init()enter polling loopencoder_read()clicksdispatch_button(clicks, ...)menu_move(clicks)

The encoder provides the primary navigation input on boards that include a physical rotary control. On boards without one (e.g. touch-only panels), encoder_read() silently returns 0 and all navigation is handled through touch and buttons exclusively.


ConstantValueDefined inPurpose
PULSES_PER_DETENT4encoder.cRaw PCNT pulses per mechanical detent. Increase if rotation feels too sensitive; decrease if too sluggish.
high_limit1000encoder_init()Upper PCNT counter bound — prevents hardware wrap-around between polls.
low_limit-1000encoder_init()Lower PCNT counter bound.
max_glitch_ns1000encoder_init()Hardware debounce window in nanoseconds. Matches the production firmware setting.
ENCODER_A_PINboard-specifichw_config.hGPIO number for encoder signal A. Set to GPIO_NUM_NC if no encoder on the board.
ENCODER_B_PINboard-specifichw_config.hGPIO number for encoder signal B. Set to GPIO_NUM_NC if no encoder on the board.

Relationship to the Production Encoder Driver

Section titled “Relationship to the Production Encoder Driver”

The factory encoder driver and the production BSP encoder driver (phy_encoder) are parallel but independent implementations backed by the same ESP-IDF PCNT peripheral:

Featurefactory_encoderbsp_physical_inputs / phy_encoder
PeripheralPCNTPCNT
Quadrature decode (2-channel)✅✅
Glitch filter (1000 ns)✅✅
Board-absent guard (GPIO_NUM_NC)✅✅
ISR / reach callback❌✅ encoder_pcnt_on_reach
Cumulative step tracking❌✅ phy_encoder_get_total_steps
Config struct (phy_encoder_config_t)❌ (pin constants only)✅
Deinitialization❌✅ phy_encoder_deinit

The factory driver deliberately omits features it does not need. The factory application is short-lived (runs once for hardware validation and flashing), so deinitialization and interrupt-driven accumulation add complexity without any benefit.


This module follows the factory logging convention provided by factory_logging (factory_log.h):

MacroLevelTrigger
FACTORY_LOGDDebugencoder_init(): board-absent skip message, and successful init summary with pin numbers and pulse rate.
ESP_LOGVVerboseencoder_read(): per-read trace showing raw pulse count and computed click value.

Verbose output (ESP_LOGV) is suppressed in normal builds. To observe raw pulse data when debugging encoder sensitivity on a new board, build with CONFIG_LOG_DEFAULT_LEVEL_VERBOSE or temporarily promote the log call to ESP_LOGD.


ModuleRelationship
factory_appParent application; owns app_main() and the polling main loop that calls both API functions.
factory_buttonsSibling input driver; physical push-buttons fed into the same dispatch_button() path.
factory_touchSibling input driver; capacitive/resistive touch panel also merged in dispatch_button().
factory_loggingProvides the FACTORY_LOGD macro and the SD-stack silence helper used across all factory drivers.
bsp_physical_inputsProduction-firmware counterpart (phy_encoder); full-featured encoder driver with callbacks and deinitialization.