encoder: HW timer-based driver for incremental rotary encoders
The driver polls the pins of an incremental encoder with a periodic
esp_timer, so the pins do not have to be interrupt capable and any number
of encoders work at the same time. Every encoder has its own timer and its own
lock, so the encoders do not disturb each other.
The polling interval is not a part of the configuration: it is fixed at build
time by CONFIG_RE_INTERVAL_US. The same value is the resolution of the
button timings, which are counted in polls, so a value that can be changed per
encoder would make a long press depend on the caller. One full detent of the
encoder takes four polls, and the button dead time, the long press threshold
and the acceleration use the same interval.
The events are reported from the timer task. The callback has to return
quickly, because every other timer of the system waits for it, and it must not
call back into the driver: rotary_encoder_delete(),
rotary_encoder_enable_acceleration() and
rotary_encoder_disable_acceleration() refuse to run inside the callback
with ESP_ERR_INVALID_STATE. Pass the events to another task through a
queue or a task notification instead.
With ESP-IDF 6.1 and newer, rotary_encoder_delete() waits for a callback
that is already running before it frees the handle. Older versions have no way
to wait: the timer stops dispatching events and the lock orders the callbacks
that have started, but a callback that the timer task has dispatched and that
has not taken the lock yet can still run after the handle is freed, so on those
versions delete the encoder only when the events are no longer expected.
Defines
- RE_DEFAULT_BTN_PRESSED_LEVEL
- RE_DEFAULT_ENABLE_INTERNAL_PULLUP
- ROTARY_ENCODER_DEFAULT_CONFIG()
Typedefs
- typedef struct rotary_encoder *rotary_encoder_handle_t
- typedef void (*rotary_encoder_event_cb_t)(const rotary_encoder_event_t *event, void *ctx)
Callback type for encoder events.
The callback runs in the task of the esp_timer, with the lock of the encoder held, so it has to return quickly: a slow callback delays every other timer in the system, not only this encoder.
It must not call rotary_encoder_delete(), rotary_encoder_enable_acceleration() or rotary_encoder_disable_acceleration() for the same encoder. Deleting frees the handle the callback is running on, and the other two try to take the lock that the callback already holds. Pass the event to another task instead, through a queue or a task notification.
Enums
- enum rotary_encoder_btn_state_t
Button state.
Values:
- enumerator RE_BTN_RELEASED
Button currently released.
- enumerator RE_BTN_PRESSED
Button currently pressed.
- enumerator RE_BTN_LONG_PRESSED
Button currently long pressed.
- enum rotary_encoder_event_type_t
Event type.
Values:
- enumerator RE_ET_CHANGED
Encoder turned.
- enumerator RE_ET_BTN_RELEASED
Button released.
- enumerator RE_ET_BTN_PRESSED
Button pressed.
- enumerator RE_ET_BTN_LONG_PRESSED
Button long pressed (press time (us) > RE_BTN_LONG_PRESS_TIME_US)
- enumerator RE_ET_BTN_CLICKED
Button was clicked.
Functions
- esp_err_t rotary_encoder_create(const rotary_encoder_config_t *config, rotary_encoder_handle_t *handle)
Create a new rotary encoder.
- Parameters:
config – Encoder configuration
handle – [out] Encoder handle, populated on success and set to NULL on failure
- Returns:
ESP_OKon successESP_ERR_INVALID_ARGif the configuration is invalid: no callback, an invalid or repeated pin, a button level other than 0 or 1, or an acceleration cap that is zero or larger than the thresholdESP_ERR_NO_MEMif there is no memory for the encoder or its lock the error code of the GPIO or timer that could not be set up
- esp_err_t rotary_encoder_delete(rotary_encoder_handle_t handle)
Delete a rotary encoder.
The timer is stopped and the handle is freed, and the GPIO pins are released back to the floating input state.
With ESP-IDF 6.1 and newer the function waits for an event callback that is already running, so the handle is never freed under the callback. Older versions have no such way to wait: the timer stops dispatching callbacks and the lock orders the callbacks that have started, but a callback that the timer task has dispatched and that has not taken the lock yet can still run after the handle is freed. On those versions delete the encoder only when the events are no longer expected, for example after the buttons have been idle for a polling interval.
- Parameters:
handle – Encoder handle
- Returns:
ESP_OKon successESP_ERR_INVALID_ARGif the handle is NULLESP_ERR_INVALID_STATEif called from the event callback of the same encoder
- esp_err_t rotary_encoder_enable_acceleration(rotary_encoder_handle_t handle, uint16_t coeff)
Enable acceleration on the rotary encoder.
- Parameters:
handle – Encoder handle
coeff – Acceleration coefficient. Higher value means faster acceleration, 0 and 1 disable it
- Returns:
ESP_OKon successESP_ERR_INVALID_ARGif the handle is NULLESP_ERR_INVALID_STATEif called from the event callback of the same encoder
- esp_err_t rotary_encoder_disable_acceleration(rotary_encoder_handle_t handle)
Disable acceleration on the rotary encoder.
- Parameters:
handle – Encoder handle
- Returns:
ESP_OKon successESP_ERR_INVALID_ARGif the handle is NULLESP_ERR_INVALID_STATEif called from the event callback of the same encoder
- struct rotary_encoder_event_t
#include <encoder.h>Event.
Public Members
- rotary_encoder_event_type_t type
Event type.
- rotary_encoder_handle_t sender
Encoder handle.
- int32_t diff
Difference between new and old positions (only if type == RE_ET_CHANGED)
- struct rotary_encoder_config_t
#include <encoder.h>Per-encoder configuration.
The polling interval is not a part of the configuration: it is fixed at build time by
CONFIG_RE_INTERVAL_US, because the driver counts the time of a button press in polling intervals and a per-encoder value would make that accounting depend on a field the caller can change at will.Public Members
- gpio_num_t pin_a
Encoder pin A.
- gpio_num_t pin_b
Encoder pin B.
- gpio_num_t pin_btn
Button pin, or GPIO_NUM_NC if unused.
- uint8_t btn_pressed_level
GPIO level when button is pressed (0 or 1).
When set to 1, the button pin may need a pull-down resistor, either external or set via
gpio_set_pull_mode()before creating the encoder, asenable_internal_pulluponly enables pull-ups.
- bool enable_internal_pullup
Enable internal pull-up resistors on all encoder and.
button GPIO pins. Pull-ups are always used, which is correct for encoders wired with common to ground. When
btn_pressed_levelis 1, the button pin may need a pull-down instead — use an external resistor or callgpio_set_pull_mode()before creating the encoder. When false, pull resistors are left unchanged.
- uint32_t btn_dead_time_us
Button dead time in microseconds.
- uint32_t btn_long_press_time_us
Long press threshold in microseconds.
- uint32_t acceleration_threshold_ms
Acceleration threshold in milliseconds (acceleration starts below this interval)
- uint32_t acceleration_cap_ms
Acceleration cap in milliseconds (minimum interval, limits max acceleration)
- rotary_encoder_event_cb_t callback
Event callback (required)
- void *callback_ctx
User context passed to callback.