Device migrations, drivers and fixes (#571)

Device migrations:

- cyd-4848S040c
- guition-jc1060p470ciwy
- guition-jc2432w328c
- guition-jc3248w535c (known issue with display and touch, Shadowtrance will look into it)
- guition-jc8048w550c
- heltec-wifi-lora-32-v3
- lilygo-tdeck-max (excluding graphics)
- lilygo-tdisplay
- lilygo-tdisplay-s3
- lilygo-tdongle-s3
- lilygo-tlora-pager

Driver migrations:

- Implemented haptic driver interface in kernel
- AXS15231b
- BQ25896
- BQ27220
- CST328
- CST6xx
- DRV2605
- JD9165
- Custom LilyGO driver for T-Lora Pager
- SSD1306
- ST7701
- ST7735
- SY6970
- TCA8418

Fixes/improvements:

- Boot app: support for multiple power devices, improved UI
- lvgl_devices and related code: fixes for mapping
- Support for arrays in dts parser
This commit is contained in:
Ken Van Hoeylandt
2026-07-18 15:01:42 +02:00
committed by GitHub
parent 3b5a401594
commit d896657bf9
290 changed files with 9113 additions and 6719 deletions
+11
View File
@@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.20)
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
tactility_add_module(tca8418-module
SRCS ${SOURCE_FILES}
INCLUDE_DIRS include/
REQUIRES TactilityKernel
)
@@ -0,0 +1,74 @@
description: >
Texas Instruments TCA8418 I2C keypad matrix scanner. Exposed as a generic KEYBOARD_TYPE
device: the driver only scans the matrix and reports raw key events, while the character
layout comes entirely from the keymap-lc/keymap-uc/keymap-sy devicetree properties, so the
same driver works for any board's physical key layout.
include: ["i2c-device.yaml"]
compatible: "ti,tca8418"
bus: i2c
properties:
rows:
type: int
required: true
description: Number of keypad matrix rows wired (max 8)
columns:
type: int
required: true
description: Number of keypad matrix columns wired (max 10)
reverse-columns:
type: boolean
default: false
description: >
Some boards wire columns in reverse of their silkscreen/keymap order; when true, the raw
hardware column is mirrored (columns-1-col) before indexing into the keymap arrays and
before comparing against shift-row/shift-col/sym-row/sym-col.
keymap-lc:
type: array
element-type: uint8_t
required: true
description: >
Base (lowercase) layer keymap, rows*columns bytes in row-major order (already in
silkscreen/keymap column order - see reverse-columns). 0 = no key at this position (e.g.
a blank matrix position, or a position handled as a modifier via shift-row/shift-col/
sym-row/sym-col instead). Non-zero bytes are sent as-is via KeyboardKeyData::key (ASCII
or an LVGL LV_KEY_* code).
keymap-uc:
type: array
element-type: uint8_t
description: >
Uppercase/shift layer keymap, same shape as keymap-lc. Omit if this keyboard has no
separate uppercase layer (shift-row/shift-col should then also be left unset).
keymap-sy:
type: array
element-type: uint8_t
description: >
Symbol layer keymap, same shape as keymap-lc. Omit if this keyboard has no symbol layer
(sym-row/sym-col should then also be left unset).
shift-row:
type: int
default: 255
description: Matrix row (keymap column order) of the shift/uppercase modifier key, 255 if none
shift-col:
type: int
default: 255
description: Matrix column (keymap column order) of the shift/uppercase modifier key, 255 if none
sym-row:
type: int
default: 255
description: Matrix row (keymap column order) of the symbol-layer modifier key, 255 if none
sym-col:
type: int
default: 255
description: Matrix column (keymap column order) of the symbol-layer modifier key, 255 if none
pin-reset:
type: phandles
default: GPIO_PIN_SPEC_NONE
description: Optional reset GPIO pin, pulsed once at start (20 ms assert, 60 ms settle)
reset-active-high:
type: boolean
default: false
description: Whether the reset pin is active high
+3
View File
@@ -0,0 +1,3 @@
dependencies:
- TactilityKernel
bindings: bindings
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/bindings/bindings.h>
#include <drivers/tca8418.h>
DEFINE_DEVICETREE(tca8418, struct Tca8418Config)
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stdbool.h>
#include <tactility/drivers/gpio.h>
struct Tca8418Config {
// Devicetree address hint (0x34 on the LilyGO T-Deck Max).
uint8_t address;
uint8_t rows;
uint8_t columns;
bool reverse_columns;
const uint8_t* keymap_lc;
uint32_t keymap_lc_length;
const uint8_t* keymap_uc;
uint32_t keymap_uc_length;
const uint8_t* keymap_sy;
uint32_t keymap_sy_length;
uint8_t shift_row;
uint8_t shift_col;
uint8_t sym_row;
uint8_t sym_col;
struct GpioPinSpec pin_reset;
bool reset_active_high;
};
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/module.h>
#ifdef __cplusplus
extern "C" {
#endif
extern struct Module tca8418_module;
#ifdef __cplusplus
}
#endif
+32
View File
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/check.h>
#include <tactility/driver.h>
#include <tactility/module.h>
extern "C" {
extern Driver tca8418_driver;
static error_t start() {
/* We crash when construct fails, because if a single driver fails to construct,
* there is no guarantee that the previously constructed drivers can be destroyed */
check(driver_construct_add(&tca8418_driver) == ERROR_NONE);
return ERROR_NONE;
}
static error_t stop() {
/* We crash when destruct fails, because if a single driver fails to destruct,
* there is no guarantee that the previously destroyed drivers can be recovered */
check(driver_remove_destruct(&tca8418_driver) == ERROR_NONE);
return ERROR_NONE;
}
Module tca8418_module = {
.name = "tca8418",
.start = start,
.stop = stop,
.symbols = nullptr,
.internal = nullptr
};
} // extern "C"
+284
View File
@@ -0,0 +1,284 @@
// SPDX-License-Identifier: Apache-2.0
#include <drivers/tca8418.h>
#include <tca8418_module.h>
#include <tactility/check.h>
#include <tactility/delay.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/gpio_controller.h>
#include <tactility/drivers/i2c_controller.h>
#include <tactility/drivers/keyboard.h>
#include <tactility/error.h>
#include <tactility/log.h>
#include <cstdlib>
#define TAG "TCA8418"
#define GET_CONFIG(device) (static_cast<const Tca8418Config*>((device)->config))
static const TickType_t I2C_TIMEOUT = pdMS_TO_TICKS(50);
// TCA8418 register map (https://www.ti.com/lit/ds/symlink/tca8418.pdf).
static constexpr uint8_t REG_CFG = 0x01;
static constexpr uint8_t REG_KEY_EVENT_A = 0x04;
static constexpr uint8_t REG_KP_GPIO1 = 0x1D;
static constexpr uint8_t REG_KP_GPIO2 = 0x1E;
static constexpr uint8_t REG_KP_GPIO3 = 0x1F;
// The KEY_EVENT register always encodes a key's position as row*10+col+1, using a fixed
// 10-column addressing space regardless of how many columns are actually wired/enabled (a
// TCA8418 hardware constant, not related to Tca8418Config::columns).
static constexpr uint8_t KEY_EVENT_ENCODING_WIDTH = 10;
static constexpr uint8_t NO_MODIFIER = 255;
// Worst case per read_key() poll: 16 drained hardware events, each a char key producing a
// press+release pair.
static constexpr uint8_t PENDING_CAPACITY = 32;
struct Tca8418PendingEvent {
uint32_t key;
bool pressed;
};
struct Tca8418Internal {
bool shift_pressed;
bool sym_pressed;
bool cap_toggle;
bool cap_toggle_armed;
Tca8418PendingEvent pending[PENDING_CAPACITY];
uint8_t pending_head;
uint8_t pending_count;
};
static void push_pending(Tca8418Internal* internal, uint32_t key, bool pressed) {
if (internal->pending_count >= PENDING_CAPACITY) {
LOG_W(TAG, "Pending event queue full, dropping event");
return;
}
uint8_t tail = (internal->pending_head + internal->pending_count) % PENDING_CAPACITY;
internal->pending[tail] = { .key = key, .pressed = pressed };
internal->pending_count++;
}
static bool pop_pending(Tca8418Internal* internal, Tca8418PendingEvent* out_event) {
if (internal->pending_count == 0) {
return false;
}
*out_event = internal->pending[internal->pending_head];
internal->pending_head = (internal->pending_head + 1) % PENDING_CAPACITY;
internal->pending_count--;
return true;
}
// region Driver lifecycle
// Ported from Adafruit_TCA8418::matrix() (see Drivers/TCA8418's now-removed deprecated-HAL
// equivalent / https://github.com/adafruit/Adafruit_TCA8418).
static bool init_matrix(Device* i2c, uint8_t address, uint8_t rows, uint8_t columns) {
if (rows > 8 || columns > 10) {
return false;
}
if (rows == 0 || columns == 0) {
return true;
}
uint8_t mask = 0;
for (int r = 0; r < rows; r++) {
mask = static_cast<uint8_t>((mask << 1) | 1);
}
if (i2c_controller_register8_set(i2c, address, REG_KP_GPIO1, mask, I2C_TIMEOUT) != ERROR_NONE) {
return false;
}
mask = 0;
for (int c = 0; c < columns && c < 8; c++) {
mask = static_cast<uint8_t>((mask << 1) | 1);
}
if (i2c_controller_register8_set(i2c, address, REG_KP_GPIO2, mask, I2C_TIMEOUT) != ERROR_NONE) {
return false;
}
if (columns > 8) {
mask = (columns == 9) ? 0x01 : 0x03;
if (i2c_controller_register8_set(i2c, address, REG_KP_GPIO3, mask, I2C_TIMEOUT) != ERROR_NONE) {
return false;
}
}
return true;
}
// Some boards route this chip's reset line through an external GPIO/IO-expander pin that must
// be released before the chip will respond (see Tca8418Config::pin_reset).
static void perform_hardware_reset(const Tca8418Config* config) {
if (config->pin_reset.gpio_controller == nullptr) {
return;
}
auto* rst = gpio_descriptor_acquire(config->pin_reset.gpio_controller, config->pin_reset.pin, GPIO_OWNER_GPIO);
if (rst == nullptr) {
return;
}
gpio_descriptor_set_flags(rst, GPIO_FLAG_DIRECTION_OUTPUT);
gpio_descriptor_set_level(rst, config->reset_active_high);
delay_millis(20);
gpio_descriptor_set_level(rst, !config->reset_active_high);
gpio_descriptor_release(rst);
delay_millis(60);
}
static error_t start(Device* device) {
auto* parent = device_get_parent(device);
check(device_get_type(parent) == &I2C_CONTROLLER_TYPE);
const auto* config = GET_CONFIG(device);
auto* internal = static_cast<Tca8418Internal*>(calloc(1, sizeof(Tca8418Internal)));
if (internal == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
internal->cap_toggle_armed = true;
perform_hardware_reset(config);
if (!init_matrix(parent, config->address, config->rows, config->columns)) {
LOG_E(TAG, "Failed to configure keypad matrix");
free(internal);
return ERROR_RESOURCE;
}
// BIT7 AI=1 auto-increment, BIT6 GPI_E_CFG=0, BIT5 OVR_FLOW_M=0 (overflow data lost), BIT4
// INT_CFG=1, others 0: 0x99. Matches the vendor factory firmware / Adafruit_TCA8418 default.
if (i2c_controller_register8_set(parent, config->address, REG_CFG, 0x99, I2C_TIMEOUT) != ERROR_NONE) {
LOG_E(TAG, "Failed to configure keypad controller");
free(internal);
return ERROR_RESOURCE;
}
device_set_driver_data(device, internal);
return ERROR_NONE;
}
static error_t stop(Device* device) {
auto* internal = static_cast<Tca8418Internal*>(device_get_driver_data(device));
free(internal);
device_set_driver_data(device, nullptr);
return ERROR_NONE;
}
// endregion
// region KeyboardApi
static uint8_t keymap_lookup(const uint8_t* keymap, uint32_t keymap_length, uint8_t row, uint8_t vcol, uint8_t columns) {
uint32_t index = static_cast<uint32_t>(row) * columns + vcol;
if (keymap == nullptr || index >= keymap_length) {
return 0;
}
return keymap[index];
}
static void handle_key_event(Tca8418Internal* internal, const Tca8418Config* config, uint8_t event) {
bool key_down = (event & 0x80) != 0;
uint8_t code = static_cast<uint8_t>((event & 0x7F) - 1);
uint8_t row = static_cast<uint8_t>(code / KEY_EVENT_ENCODING_WIDTH);
uint8_t col = static_cast<uint8_t>(code % KEY_EVENT_ENCODING_WIDTH);
uint8_t vcol = config->reverse_columns ? static_cast<uint8_t>(config->columns - 1 - col) : col;
bool is_shift = row == config->shift_row && vcol == config->shift_col;
bool is_sym = row == config->sym_row && vcol == config->sym_col;
if (!key_down) {
// Release event: only modifier state cares about releases.
if (is_shift) {
internal->shift_pressed = false;
}
if (is_sym) {
internal->sym_pressed = false;
}
} else {
if (is_shift) {
internal->shift_pressed = true;
} else if (is_sym) {
internal->sym_pressed = true;
} else {
const uint8_t* keymap = config->keymap_lc;
uint32_t keymap_length = config->keymap_lc_length;
if (internal->sym_pressed) {
keymap = config->keymap_sy;
keymap_length = config->keymap_sy_length;
} else if (internal->shift_pressed || internal->cap_toggle) {
keymap = config->keymap_uc;
keymap_length = config->keymap_uc_length;
}
uint8_t chr = keymap_lookup(keymap, keymap_length, row, vcol, config->columns);
if (chr != 0) {
// LVGL only registers a key on a RELEASED->PRESSED edge, so every keystroke is
// queued as an immediate press+release pair.
push_pending(internal, chr, true);
push_pending(internal, chr, false);
}
}
if (internal->sym_pressed && internal->shift_pressed && internal->cap_toggle_armed &&
config->sym_row != NO_MODIFIER && config->shift_row != NO_MODIFIER) {
internal->cap_toggle = !internal->cap_toggle;
internal->cap_toggle_armed = false;
}
}
if (!internal->sym_pressed && !internal->shift_pressed) {
internal->cap_toggle_armed = true;
}
}
static error_t tca8418_read_key(Device* device, KeyboardKeyData* data) {
auto* internal = static_cast<Tca8418Internal*>(device_get_driver_data(device));
const auto* config = GET_CONFIG(device);
auto* parent = device_get_parent(device);
// Each register read pops one event from the TCA8418's FIFO. Drain it fully per call
// (bounded, in case of a misbehaving bus) so a fast typing burst isn't throttled to one
// event per LVGL indev poll period.
for (int drained = 0; drained < 16; drained++) {
uint8_t event = 0;
if (i2c_controller_register8_get(parent, config->address, REG_KEY_EVENT_A, &event, I2C_TIMEOUT) != ERROR_NONE) {
break;
}
if (event == 0) {
break;
}
handle_key_event(internal, config, event);
}
Tca8418PendingEvent 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 tca8418_api = {
.read_key = tca8418_read_key,
};
Driver tca8418_driver = {
.name = "tca8418",
.compatible = (const char*[]) { "ti,tca8418", nullptr },
.start_device = start,
.stop_device = stop,
.api = &tca8418_api,
.device_type = &KEYBOARD_TYPE,
.owner = &tca8418_module,
.internal = nullptr
};