Skip to main content

Encoder and Touch

Two humble peripherals, one design idea: the input layer reports gestures, never pin states, and knows nothing about what they do.

The rotary encoder, from zero

An EC11 encoder contains two switches (A and B) that open and close 90 degrees out of phase as the shaft turns: quadrature. Between detents both signals complete one cycle, and the phase relationship encodes direction: if A leads B, the shaft turns one way; B leads A, the other. Reading it means treating (A,B) as a 2-bit state and walking the Gray-code sequence 00 - 01 - 11 - 10; a transition table maps each (previous, current) pair to +1, -1, or "invalid".

The transition-table approach is also the debouncer: mechanical contacts bounce (microseconds of chatter on each edge), but bounce produces invalid transitions (repeats or skips in the Gray sequence) which the table simply discards. No RC filters, no delay-based debouncing that would eat fast spins; a wrong intermediate state contributes zero net counts. Counts accumulate per detent (four states) so one physical click is exactly one logical step.

input_handler's encoder task samples the pins (internal pull-ups, so the switches just short to ground), walks the table, and publishes:

  • EVENT_UI_ENCODER_TURN with a signed step count,
  • EVENT_UI_BUTTON_PRESS for the push switch, with short/long press discrimination (a long press is a press that survives a threshold; the timer runs in the task, not an ISR).

The push switch pin (GPIO15) pulls double duty as the deep-sleep wake source, which is why the power chapter cares about this pin's wiring.

Touch, from zero

The ESP32-S3 has native capacitive touch: a pad's capacitance rises when a finger approaches, and the touch peripheral measures charge cycles per interval. Firmware calibrates a baseline at boot and thresholds the delta; the pad itself is just copper (a PCB zone, foil behind 1 mm of enclosure plastic, anything conductive on GPIO4/TOUCH4).

The touch task samples, hysteresis-filters (touch asserts below one threshold, releases above another, so a hovering finger cannot chatter), and publishes EVENT_UI_TOUCH on the press edge. One gesture only, tap; the controls philosophy is that the most-used control should be unmistakable.

From gesture to meaning

Neither driver knows what a turn or tap does. Mapping lives in main/event_handlers.c (with mode state owned by the display's browse machinery):

GestureNormal modeBrowse mode
Turnaudio_player_set_volume(current + steps)move selection
Short pressenter browsedescend / play
Long pressaudio_player_stop()back / exit
Tapplay/pause toggleplay/pause toggle

This indirection is the event bus doing its job at the smallest scale: remapping the controls, or driving the whole device from a test script publishing synthetic gestures, touches one table and zero drivers.