Files
tactility/Drivers/gpio-encoder-module/source/gpio_encoder.cpp
T
Ken Van Hoeylandt db48dfe812 Fixes and improvements (#620)
- Standardized keyboard input using Unicode-based key codes across supported devices and the simulator. Keyboards don't emit `LV_KEY_*` anymore.
- Refactored lilygo encoder driver into a reusable GPIO rotary encoder driver (see `Drivers/gpio-encoder-module/`). Added more features to the config file.
- Improved LVGL keyboard device management, including duplicate prevention and reliable reconnects.
- LVGL file mutex now registers with lvgl start/stop
- Improved LVGL startup/shutdown stability and memory allocation reliability.
- Increased simulator LVGL memory capacity and improved USB device-class handling.
2026-08-23 17:21:16 +02:00

338 lines
12 KiB
C++

// SPDX-License-Identifier: Apache-2.0
#include <drivers/gpio_encoder.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/gpio.h>
#include <tactility/drivers/gpio_controller.h>
#include <tactility/drivers/gpio_descriptor.h>
#include <tactility/drivers/keyboard.h>
#include <tactility/log.h>
#include <driver/pulse_cnt.h>
#include <new>
#define TAG "gpio_encoder"
#define GET_CONFIG(device) (static_cast<const GpioEncoderConfig*>((device)->config))
#define GET_INTERNAL(device) (static_cast<GpioEncoderInternal*>(device_get_driver_data(device)))
struct GpioEncoderPendingEvent {
uint32_t key;
bool pressed;
};
struct GpioEncoderInternal {
pcnt_unit_handle_t pcnt_unit = nullptr;
GpioDescriptor* pin_a = nullptr;
GpioDescriptor* pin_b = nullptr;
GpioDescriptor* pin_enter = nullptr;
int32_t pulse_remainder = 0;
bool button_pressed = false;
int32_t pulses_per_detent = 0;
GpioEncoderPendingEvent* pending = nullptr;
uint32_t pending_capacity = 0;
uint32_t pending_head = 0;
uint32_t pending_count = 0;
};
static bool push_pending(GpioEncoderInternal* internal, uint32_t key, bool pressed) {
if (internal->pending_count >= internal->pending_capacity) {
LOG_W(TAG, "Pending event queue full, dropping event");
return false;
}
uint32_t tail = (internal->pending_head + internal->pending_count) % internal->pending_capacity;
internal->pending[tail] = { .key = key, .pressed = pressed };
internal->pending_count++;
return true;
}
static bool pop_pending(GpioEncoderInternal* internal, GpioEncoderPendingEvent* out_event) {
if (internal->pending_count == 0) {
return false;
}
*out_event = internal->pending[internal->pending_head];
internal->pending_head = (internal->pending_head + 1) % internal->pending_capacity;
internal->pending_count--;
return true;
}
extern "C" {
// region Driver lifecycle
// Accumulating count makes over-/underflow automatically compensated; requires watch points at
// the low and high limits (see pcnt_unit_add_watch_point() below). Ported from the deprecated
// HAL's TpagerEncoder::initEncoder().
static constexpr int PCNT_LOW_LIMIT = -127;
static constexpr int PCNT_HIGH_LIMIT = 126;
static error_t init_pcnt_unit(int pin_a, int pin_b, pcnt_unit_handle_t* out_unit) {
pcnt_unit_config_t unit_config = {
.low_limit = PCNT_LOW_LIMIT,
.high_limit = PCNT_HIGH_LIMIT,
.intr_priority = 0,
.flags = { .accum_count = 1 },
};
pcnt_unit_handle_t unit = nullptr;
if (pcnt_new_unit(&unit_config, &unit) != ESP_OK) {
LOG_E(TAG, "Pulse counter initialization failed");
return ERROR_RESOURCE;
}
pcnt_glitch_filter_config_t filter_config = { .max_glitch_ns = 1000 };
if (pcnt_unit_set_glitch_filter(unit, &filter_config) != ESP_OK) {
LOG_E(TAG, "Pulse counter glitch filter config failed");
pcnt_del_unit(unit);
return ERROR_RESOURCE;
}
pcnt_chan_config_t chan_a_config = {
.edge_gpio_num = pin_b,
.level_gpio_num = pin_a,
.flags = {},
};
pcnt_chan_config_t chan_b_config = {
.edge_gpio_num = pin_a,
.level_gpio_num = pin_b,
.flags = {},
};
pcnt_channel_handle_t chan_a = nullptr;
pcnt_channel_handle_t chan_b = nullptr;
if (pcnt_new_channel(unit, &chan_a_config, &chan_a) != ESP_OK ||
pcnt_new_channel(unit, &chan_b_config, &chan_b) != ESP_OK) {
LOG_E(TAG, "Pulse counter channel config failed");
pcnt_del_unit(unit);
return ERROR_RESOURCE;
}
// Standard quadrature decode: each channel counts on its edge, direction decided by the
// other channel's level.
if (pcnt_channel_set_edge_action(chan_a, PCNT_CHANNEL_EDGE_ACTION_DECREASE, PCNT_CHANNEL_EDGE_ACTION_INCREASE) != ESP_OK ||
pcnt_channel_set_edge_action(chan_b, PCNT_CHANNEL_EDGE_ACTION_INCREASE, PCNT_CHANNEL_EDGE_ACTION_DECREASE) != ESP_OK) {
LOG_E(TAG, "Pulse counter edge action config failed");
pcnt_del_unit(unit);
return ERROR_RESOURCE;
}
if (pcnt_channel_set_level_action(chan_a, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE) != ESP_OK ||
pcnt_channel_set_level_action(chan_b, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE) != ESP_OK) {
LOG_E(TAG, "Pulse counter level action config failed");
pcnt_del_unit(unit);
return ERROR_RESOURCE;
}
if (pcnt_unit_add_watch_point(unit, PCNT_LOW_LIMIT) != ESP_OK ||
pcnt_unit_add_watch_point(unit, PCNT_HIGH_LIMIT) != ESP_OK) {
LOG_E(TAG, "Pulse counter watch point config failed");
pcnt_del_unit(unit);
return ERROR_RESOURCE;
}
if (pcnt_unit_enable(unit) != ESP_OK ||
pcnt_unit_clear_count(unit) != ESP_OK ||
pcnt_unit_start(unit) != ESP_OK) {
LOG_E(TAG, "Pulse counter could not be started");
pcnt_del_unit(unit);
return ERROR_RESOURCE;
}
*out_unit = unit;
return ERROR_NONE;
}
static error_t start(Device* device) {
const auto* config = GET_CONFIG(device);
// Backstop for values the devicetree compiler doesn't currently validate: 0 divides by
// zero in poll_wheel(), and a capacity below 2 can never hold one press/release pair.
if (config->pulses_per_detent == 0) {
LOG_E(TAG, "pulses_per_detent must be > 0");
return ERROR_INVALID_ARGUMENT;
}
if (config->pending_capacity < 2) {
LOG_E(TAG, "pending_capacity must be >= 2");
return ERROR_INVALID_ARGUMENT;
}
auto* internal = new (std::nothrow) GpioEncoderInternal();
if (internal == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
internal->pulses_per_detent = static_cast<int32_t>(config->pulses_per_detent);
internal->pending_capacity = config->pending_capacity;
internal->pending = new (std::nothrow) GpioEncoderPendingEvent[internal->pending_capacity];
if (internal->pending == nullptr) {
delete internal;
return ERROR_OUT_OF_MEMORY;
}
internal->pin_a = gpio_descriptor_acquire(config->pin_a.gpio_controller, config->pin_a.pin, config->pin_a.flags | GPIO_FLAG_DIRECTION_INPUT, GPIO_OWNER_GPIO);
if (internal->pin_a == nullptr) {
LOG_E(TAG, "Failed to acquire pin_a");
delete[] internal->pending;
delete internal;
return ERROR_RESOURCE;
}
internal->pin_b = gpio_descriptor_acquire(config->pin_b.gpio_controller, config->pin_b.pin, config->pin_b.flags | GPIO_FLAG_DIRECTION_INPUT, GPIO_OWNER_GPIO);
if (internal->pin_b == nullptr) {
LOG_E(TAG, "Failed to acquire pin_b");
gpio_descriptor_release(internal->pin_a);
delete[] internal->pending;
delete internal;
return ERROR_RESOURCE;
}
int native_pin_a = 0;
int native_pin_b = 0;
if (gpio_descriptor_get_native_pin_number(internal->pin_a, &native_pin_a) != ERROR_NONE ||
gpio_descriptor_get_native_pin_number(internal->pin_b, &native_pin_b) != ERROR_NONE) {
LOG_E(TAG, "Failed to resolve native pin numbers");
gpio_descriptor_release(internal->pin_b);
gpio_descriptor_release(internal->pin_a);
delete[] internal->pending;
delete internal;
return ERROR_RESOURCE;
}
error_t error = init_pcnt_unit(native_pin_a, native_pin_b, &internal->pcnt_unit);
if (error != ERROR_NONE) {
gpio_descriptor_release(internal->pin_b);
gpio_descriptor_release(internal->pin_a);
delete[] internal->pending;
delete internal;
return error;
}
if (config->pin_enter.gpio_controller != nullptr) {
internal->pin_enter = gpio_descriptor_acquire(config->pin_enter.gpio_controller, config->pin_enter.pin, GPIO_FLAG_DIRECTION_INPUT | GPIO_FLAG_ACTIVE_LOW, GPIO_OWNER_GPIO);
if (internal->pin_enter == nullptr) {
pcnt_unit_stop(internal->pcnt_unit);
pcnt_del_unit(internal->pcnt_unit);
gpio_descriptor_release(internal->pin_b);
gpio_descriptor_release(internal->pin_a);
delete[] internal->pending;
delete internal;
return ERROR_RESOURCE;
}
}
device_set_driver_data(device, internal);
return ERROR_NONE;
}
static error_t stop(Device* device) {
auto* internal = GET_INTERNAL(device);
if (internal->pin_enter != nullptr) {
gpio_descriptor_release(internal->pin_enter);
}
if (pcnt_unit_stop(internal->pcnt_unit) != ESP_OK) {
LOG_W(TAG, "Failed to stop encoder");
}
if (pcnt_del_unit(internal->pcnt_unit) != ESP_OK) {
LOG_W(TAG, "Failed to delete encoder");
}
gpio_descriptor_release(internal->pin_b);
gpio_descriptor_release(internal->pin_a);
device_set_driver_data(device, nullptr);
delete[] internal->pending;
delete internal;
return ERROR_NONE;
}
// endregion
// region KeyboardApi
// Wheel rotation is a discrete notch, not a held key, so each detent is reported as an
// immediate press+release pair rather than a persistent pressed state.
static void poll_wheel(GpioEncoderInternal* internal) {
int pulses = 0;
pcnt_unit_get_count(internal->pcnt_unit, &pulses);
pcnt_unit_clear_count(internal->pcnt_unit);
int32_t total = internal->pulse_remainder + pulses;
int32_t detents = total / internal->pulses_per_detent;
internal->pulse_remainder = total % internal->pulses_per_detent;
uint32_t key = detents >= 0 ? CODEPOINT_ARROW_DOWN : CODEPOINT_ARROW_UP;
int32_t count = detents >= 0 ? detents : -detents;
for (int32_t i = 0; i < count; i++) {
// A press without its matching release would leave the consumer thinking the key
// is stuck down, so only enqueue the pair when both fit.
if (internal->pending_count + 2 > internal->pending_capacity) {
LOG_W(TAG, "Pending event queue full, dropping remaining wheel events");
break;
}
push_pending(internal, key, true);
push_pending(internal, key, false);
}
}
static void poll_button(GpioEncoderInternal* internal) {
if (internal->pin_enter == nullptr) {
return;
}
bool pressed = false;
if (gpio_descriptor_get_level(internal->pin_enter, &pressed) != ERROR_NONE) {
return;
}
// Only commit the new state once its event is actually queued - a full FIFO here
// leaves button_pressed unchanged so the same transition is retried next poll instead
// of being lost.
if (pressed != internal->button_pressed) {
if (push_pending(internal, CODEPOINT_ENTER, pressed)) {
internal->button_pressed = pressed;
}
}
}
static error_t gpio_encoder_read_key(Device* device, KeyboardKeyData* data) {
auto* internal = GET_INTERNAL(device);
poll_wheel(internal);
poll_button(internal);
GpioEncoderPendingEvent event;
if (pop_pending(internal, &event)) {
data->key = event.key;
data->pressed = event.pressed;
data->continue_reading = internal->pending_count > 0;
} else {
data->key = 0;
data->pressed = false;
data->continue_reading = false;
}
return ERROR_NONE;
}
// endregion
static constexpr KeyboardApi GPIO_ENCODER_API = {
.read_key = gpio_encoder_read_key,
};
extern Module gpio_encoder_module;
Driver gpio_encoder_driver = {
.name = "gpio_encoder",
.compatible = (const char*[]) { "tactility,gpio-encoder", nullptr },
.start_device = start,
.stop_device = stop,
.api = &GPIO_ENCODER_API,
.device_type = &KEYBOARD_TYPE,
.owner = &gpio_encoder_module,
.internal = nullptr
};
}