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
+2 -3
View File
@@ -1,7 +1,6 @@
file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
file(GLOB_RECURSE SOURCE_FILES source/*.c*)
idf_component_register(
SRCS ${SOURCE_FILES}
INCLUDE_DIRS "Source"
REQUIRES Tactility EspLcdCompat esp_lcd_axs15231b PwmBacklight driver vfs fatfs
REQUIRES TactilityKernel driver
)
@@ -1,493 +0,0 @@
#include "Axs15231bDisplay.h"
#include <Tactility/hal/touch/TouchDevice.h>
#include <Axs15231b/Axs15231bTouch.h>
#include <EspLcdDisplayDriver.h>
#include <esp_lcd_axs15231b.h>
#include <esp_lcd_panel_commands.h>
#include <esp_lcd_panel_ops.h>
#include <esp_heap_caps.h>
#include <tactility/log.h>
constexpr auto* TAG = "AXS15231B";
static const axs15231b_lcd_init_cmd_t lcd_init_cmds[] = {
{0xBB, (uint8_t[]){0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5A, 0xA5}, 8, 0},
{0xA0, (uint8_t[]){0xC0, 0x10, 0x00, 0x02, 0x00, 0x00, 0x04, 0x3F, 0x20, 0x05, 0x3F, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00}, 17, 0},
{0xA2, (uint8_t[]){0x30, 0x3C, 0x24, 0x14, 0xD0, 0x20, 0xFF, 0xE0, 0x40, 0x19, 0x80, 0x80, 0x80, 0x20, 0xf9, 0x10, 0x02, 0xff, 0xff, 0xF0, 0x90, 0x01, 0x32, 0xA0, 0x91, 0xE0, 0x20, 0x7F, 0xFF, 0x00, 0x5A}, 31, 0},
{0xD0, (uint8_t[]){0xE0, 0x40, 0x51, 0x24, 0x08, 0x05, 0x10, 0x01, 0x20, 0x15, 0x42, 0xC2, 0x22, 0x22, 0xAA, 0x03, 0x10, 0x12, 0x60, 0x14, 0x1E, 0x51, 0x15, 0x00, 0x8A, 0x20, 0x00, 0x03, 0x3A, 0x12}, 30, 0},
{0xA3, (uint8_t[]){0xA0, 0x06, 0xAa, 0x00, 0x08, 0x02, 0x0A, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x55, 0x55}, 22, 0},
{0xC1, (uint8_t[]){0x31, 0x04, 0x02, 0x02, 0x71, 0x05, 0x24, 0x55, 0x02, 0x00, 0x41, 0x00, 0x53, 0xFF, 0xFF, 0xFF, 0x4F, 0x52, 0x00, 0x4F, 0x52, 0x00, 0x45, 0x3B, 0x0B, 0x02, 0x0d, 0x00, 0xFF, 0x40}, 30, 0},
{0xC3, (uint8_t[]){0x00, 0x00, 0x00, 0x50, 0x03, 0x00, 0x00, 0x00, 0x01, 0x80, 0x01}, 11, 0},
{0xC4, (uint8_t[]){0x00, 0x24, 0x33, 0x80, 0x00, 0xea, 0x64, 0x32, 0xC8, 0x64, 0xC8, 0x32, 0x90, 0x90, 0x11, 0x06, 0xDC, 0xFA, 0x00, 0x00, 0x80, 0xFE, 0x10, 0x10, 0x00, 0x0A, 0x0A, 0x44, 0x50}, 29, 0},
{0xC5, (uint8_t[]){0x18, 0x00, 0x00, 0x03, 0xFE, 0x3A, 0x4A, 0x20, 0x30, 0x10, 0x88, 0xDE, 0x0D, 0x08, 0x0F, 0x0F, 0x01, 0x3A, 0x4A, 0x20, 0x10, 0x10, 0x00}, 23, 0},
{0xC6, (uint8_t[]){0x05, 0x0A, 0x05, 0x0A, 0x00, 0xE0, 0x2E, 0x0B, 0x12, 0x22, 0x12, 0x22, 0x01, 0x03, 0x00, 0x3F, 0x6A, 0x18, 0xC8, 0x22}, 20, 0},
{0xC7, (uint8_t[]){0x50, 0x32, 0x28, 0x00, 0xa2, 0x80, 0x8f, 0x00, 0x80, 0xff, 0x07, 0x11, 0x9c, 0x67, 0xff, 0x24, 0x0c, 0x0d, 0x0e, 0x0f}, 20, 0},
{0xC9, (uint8_t[]){0x33, 0x44, 0x44, 0x01}, 4, 0},
{0xCF, (uint8_t[]){0x2C, 0x1E, 0x88, 0x58, 0x13, 0x18, 0x56, 0x18, 0x1E, 0x68, 0x88, 0x00, 0x65, 0x09, 0x22, 0xC4, 0x0C, 0x77, 0x22, 0x44, 0xAA, 0x55, 0x08, 0x08, 0x12, 0xA0, 0x08}, 27, 0},
{0xD5, (uint8_t[]){0x40, 0x8E, 0x8D, 0x01, 0x35, 0x04, 0x92, 0x74, 0x04, 0x92, 0x74, 0x04, 0x08, 0x6A, 0x04, 0x46, 0x03, 0x03, 0x03, 0x03, 0x82, 0x01, 0x03, 0x00, 0xE0, 0x51, 0xA1, 0x00, 0x00, 0x00}, 30, 0},
{0xD6, (uint8_t[]){0x10, 0x32, 0x54, 0x76, 0x98, 0xBA, 0xDC, 0xFE, 0x93, 0x00, 0x01, 0x83, 0x07, 0x07, 0x00, 0x07, 0x07, 0x00, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x00, 0x84, 0x00, 0x20, 0x01, 0x00}, 30, 0},
{0xD7, (uint8_t[]){0x03, 0x01, 0x0b, 0x09, 0x0f, 0x0d, 0x1E, 0x1F, 0x18, 0x1d, 0x1f, 0x19, 0x40, 0x8E, 0x04, 0x00, 0x20, 0xA0, 0x1F}, 19, 0},
{0xD8, (uint8_t[]){0x02, 0x00, 0x0a, 0x08, 0x0e, 0x0c, 0x1E, 0x1F, 0x18, 0x1d, 0x1f, 0x19}, 12, 0},
{0xD9, (uint8_t[]){0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F}, 12, 0},
{0xDD, (uint8_t[]){0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F}, 12, 0},
{0xDF, (uint8_t[]){0x44, 0x73, 0x4B, 0x69, 0x00, 0x0A, 0x02, 0x90}, 8, 0},
{0xE0, (uint8_t[]){0x3B, 0x28, 0x10, 0x16, 0x0c, 0x06, 0x11, 0x28, 0x5c, 0x21, 0x0D, 0x35, 0x13, 0x2C, 0x33, 0x28, 0x0D}, 17, 0},
{0xE1, (uint8_t[]){0x37, 0x28, 0x10, 0x16, 0x0b, 0x06, 0x11, 0x28, 0x5C, 0x21, 0x0D, 0x35, 0x14, 0x2C, 0x33, 0x28, 0x0F}, 17, 0},
{0xE2, (uint8_t[]){0x3B, 0x07, 0x12, 0x18, 0x0E, 0x0D, 0x17, 0x35, 0x44, 0x32, 0x0C, 0x14, 0x14, 0x36, 0x3A, 0x2F, 0x0D}, 17, 0},
{0xE3, (uint8_t[]){0x37, 0x07, 0x12, 0x18, 0x0E, 0x0D, 0x17, 0x35, 0x44, 0x32, 0x0C, 0x14, 0x14, 0x36, 0x32, 0x2F, 0x0F}, 17, 0},
{0xE4, (uint8_t[]){0x3B, 0x07, 0x12, 0x18, 0x0E, 0x0D, 0x17, 0x39, 0x44, 0x2E, 0x0C, 0x14, 0x14, 0x36, 0x3A, 0x2F, 0x0D}, 17, 0},
{0xE5, (uint8_t[]){0x37, 0x07, 0x12, 0x18, 0x0E, 0x0D, 0x17, 0x39, 0x44, 0x2E, 0x0C, 0x14, 0x14, 0x36, 0x3A, 0x2F, 0x0F}, 17, 0},
{0xA4, (uint8_t[]){0x85, 0x85, 0x95, 0x82, 0xAF, 0xAA, 0xAA, 0x80, 0x10, 0x30, 0x40, 0x40, 0x20, 0xFF, 0x60, 0x30}, 16, 0},
{0xA4, (uint8_t[]){0x85, 0x85, 0x95, 0x85}, 4, 0},
{0xBB, (uint8_t[]){0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 8, 0},
{0x13, (uint8_t[]){0x00}, 0, 0},
{0x35, (uint8_t[]){0x00}, 1, 0}, // Enable Tearing Effect output (V-blank sync)
{0x11, (uint8_t[]){0x00}, 0, 120},
{0x2C, (uint8_t[]){0x00, 0x00, 0x00, 0x00}, 4, 0}
};
void Axs15231bDisplay::lvgl_port_flush_callback(lv_display_t *drv, const lv_area_t *area, uint8_t *color_map) {
Axs15231bDisplay* self = (Axs15231bDisplay*)lv_display_get_user_data(drv);
assert(self != nullptr);
assert(drv != nullptr);
esp_lcd_panel_handle_t panel_handle = (esp_lcd_panel_handle_t)lv_display_get_driver_data(drv);
assert(panel_handle != nullptr);
// Swap RGB565 byte order for SPI transmission (big-endian wire format)
lv_draw_sw_rgb565_swap(color_map, lv_area_get_size(area));
lv_display_rotation_t rotation = lv_display_get_rotation(drv);
int logical_width = lv_display_get_horizontal_resolution(drv);
int logical_height = lv_display_get_vertical_resolution(drv);
uint16_t* draw_buf = (uint16_t*)color_map;
if (rotation != LV_DISPLAY_ROTATION_0) {
// Lazy-allocate tempBuf on first rotated frame
static bool allocationErrorLogged = false;
if (self->tempBuf == nullptr) {
self->tempBuf = (uint16_t *)heap_caps_malloc(
self->configuration->horizontalResolution * self->configuration->verticalResolution * sizeof(uint16_t),
MALLOC_CAP_SPIRAM);
if (self->tempBuf == nullptr) {
if (!allocationErrorLogged) {
LOG_E(TAG, "Failed to allocate rotation buffer, drawing unrotated");
allocationErrorLogged = true;
}
if (esp_lcd_panel_draw_bitmap(panel_handle, 0, 0, self->configuration->horizontalResolution, self->configuration->verticalResolution, draw_buf) != ESP_OK) {
lv_display_flush_ready(drv);
}
return;
}
}
// Rotate to tempBuf using tile-based approach for cache efficiency.
// SPIRAM random access is very slow due to cache thrashing. By processing
// in small tiles (TILE x TILE pixels), the working set stays within the
// ESP32-S3's 32KB PSRAM cache, dramatically reducing cache misses.
constexpr int TILE = 32;
uint16_t* src = (uint16_t*)color_map;
uint16_t* dst = self->tempBuf;
const int hw = self->configuration->horizontalResolution;
switch (rotation) {
case LV_DISPLAY_ROTATION_90:
for (int ty = 0; ty < logical_height; ty += TILE) {
for (int tx = 0; tx < logical_width; tx += TILE) {
int y_end = (ty + TILE < logical_height) ? ty + TILE : logical_height;
int x_end = (tx + TILE < logical_width) ? tx + TILE : logical_width;
for (int y = ty; y < y_end; y++) {
for (int x = tx; x < x_end; x++) {
dst[(logical_width - 1 - x) * hw + y] = src[y * logical_width + x];
}
}
}
}
break;
case LV_DISPLAY_ROTATION_180: {
// 180° is a simple reverse - sequential reads and writes
const int total = logical_width * logical_height;
for (int i = 0; i < total; i++) {
dst[total - 1 - i] = src[i];
}
break;
}
case LV_DISPLAY_ROTATION_270:
for (int ty = 0; ty < logical_height; ty += TILE) {
for (int tx = 0; tx < logical_width; tx += TILE) {
int y_end = (ty + TILE < logical_height) ? ty + TILE : logical_height;
int x_end = (tx + TILE < logical_width) ? tx + TILE : logical_width;
for (int y = ty; y < y_end; y++) {
for (int x = tx; x < x_end; x++) {
dst[x * hw + (logical_height - 1 - y)] = src[y * logical_width + x];
}
}
}
}
break;
default:
break;
}
draw_buf = self->tempBuf;
}
// Wait for TE (tearing effect) signal before starting SPI transfer.
// This synchronizes frame output with the display's V-blank to reduce tearing.
if (self->teSyncSemaphore != nullptr) {
xSemaphoreTake(self->teSyncSemaphore, 0); // Drain any pending signal
xSemaphoreTake(self->teSyncSemaphore, pdMS_TO_TICKS(20)); // Wait for next V-blank
}
esp_err_t ret = esp_lcd_panel_draw_bitmap(panel_handle, 0, 0, self->configuration->horizontalResolution, self->configuration->verticalResolution, draw_buf);
if (ret != ESP_OK) {
// If SPI transfer failed, on_color_trans_done won't fire.
// Manually signal flush ready to prevent LVGL from hanging.
LOG_E(TAG, "draw_bitmap failed: %s", esp_err_to_name(ret));
lv_display_flush_ready(drv);
}
}
bool Axs15231bDisplay::onColorTransDone(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx) {
lv_display_t *disp = (lv_display_t *)user_ctx;
if (disp != nullptr) {
lv_display_flush_ready(disp);
}
return false;
}
void IRAM_ATTR Axs15231bDisplay::teIsrHandler(void* arg) {
SemaphoreHandle_t sem = (SemaphoreHandle_t)arg;
BaseType_t needYield = pdFALSE;
xSemaphoreGiveFromISR(sem, &needYield);
if (needYield == pdTRUE) {
portYIELD_FROM_ISR();
}
}
bool Axs15231bDisplay::setupTeSync() {
if (configuration->tePin == GPIO_NUM_NC) {
LOG_I(TAG, "TE pin not configured, skipping TE sync");
return true;
}
teSyncSemaphore = xSemaphoreCreateBinary();
if (teSyncSemaphore == nullptr) {
LOG_E(TAG, "Failed to create TE sync semaphore");
return false;
}
gpio_config_t io_conf = {};
io_conf.intr_type = GPIO_INTR_POSEDGE;
io_conf.mode = GPIO_MODE_INPUT;
io_conf.pull_up_en = GPIO_PULLUP_DISABLE;
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
io_conf.pin_bit_mask = (1ULL << configuration->tePin);
if (gpio_config(&io_conf) != ESP_OK) {
LOG_E(TAG, "Failed to configure TE GPIO");
vSemaphoreDelete(teSyncSemaphore);
teSyncSemaphore = nullptr;
return false;
}
esp_err_t err = gpio_install_isr_service(ESP_INTR_FLAG_IRAM);
if (err == ESP_OK) {
isrServiceInstalledByUs = true;
} else if (err != ESP_ERR_INVALID_STATE) {
LOG_E(TAG, "Failed to install GPIO ISR service");
vSemaphoreDelete(teSyncSemaphore);
teSyncSemaphore = nullptr;
return false;
}
if (gpio_isr_handler_add(configuration->tePin, teIsrHandler, (void*)teSyncSemaphore) != ESP_OK) {
LOG_E(TAG, "Failed to add TE ISR handler");
gpio_intr_disable(configuration->tePin);
if (isrServiceInstalledByUs) {
gpio_uninstall_isr_service();
isrServiceInstalledByUs = false;
}
vSemaphoreDelete(teSyncSemaphore);
teSyncSemaphore = nullptr;
return false;
}
teIsrInstalled = true;
LOG_I(TAG, "TE sync enabled on GPIO %d", (int)configuration->tePin);
return true;
}
void Axs15231bDisplay::teardownTeSync() {
if (teIsrInstalled && configuration->tePin != GPIO_NUM_NC) {
gpio_isr_handler_remove(configuration->tePin);
gpio_intr_disable(configuration->tePin);
teIsrInstalled = false;
}
if (isrServiceInstalledByUs) {
gpio_uninstall_isr_service();
isrServiceInstalledByUs = false;
}
if (teSyncSemaphore != nullptr) {
vSemaphoreDelete(teSyncSemaphore);
teSyncSemaphore = nullptr;
}
}
bool Axs15231bDisplay::createIoHandle() {
const esp_lcd_panel_io_spi_config_t panel_io_config = {
.cs_gpio_num = configuration->csPin,
.dc_gpio_num = configuration->dcPin,
.spi_mode = 3,
.pclk_hz = configuration->pixelClockFrequency,
.trans_queue_depth = configuration->transactionQueueDepth,
.on_color_trans_done = nullptr,
.user_ctx = nullptr,
.lcd_cmd_bits = 32,
.lcd_param_bits = 8,
.cs_ena_pretrans = 0,
.cs_ena_posttrans = 0,
.flags = {
.dc_high_on_cmd = 0,
.dc_low_on_data = 0,
.dc_low_on_param = 0,
.octal_mode = 0,
.quad_mode = 1,
.sio_mode = 0,
.lsb_first = 0,
.cs_high_active = 0
}
};
return esp_lcd_new_panel_io_spi(configuration->spiHostDevice, &panel_io_config, &ioHandle) == ESP_OK;
}
bool Axs15231bDisplay::createPanelHandle() {
const axs15231b_vendor_config_t vendor_config = {
.init_cmds = lcd_init_cmds,
.init_cmds_size = sizeof(lcd_init_cmds) / sizeof(lcd_init_cmds[0]),
.flags = {
.use_qspi_interface = 1,
},
};
const esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = configuration->resetPin,
.rgb_ele_order = configuration->rgbElementOrder,
.data_endian = LCD_RGB_DATA_ENDIAN_LITTLE,
.bits_per_pixel = 16,
.flags = {
.reset_active_high = false
},
.vendor_config = (void *)&vendor_config
};
if (esp_lcd_new_panel_axs15231b(ioHandle, &panel_config, &panelHandle) != ESP_OK) {
LOG_E(TAG, "Failed to create axs15231b");
return false;
}
if (esp_lcd_panel_reset(panelHandle) != ESP_OK) {
LOG_E(TAG, "Failed to reset panel");
esp_lcd_panel_del(panelHandle);
panelHandle = nullptr;
return false;
}
if (esp_lcd_panel_init(panelHandle) != ESP_OK) {
LOG_E(TAG, "Failed to init panel");
esp_lcd_panel_del(panelHandle);
panelHandle = nullptr;
return false;
}
//SWAPXY Doesn't work with the JC3248W535... Left in for future compatibility.
if (esp_lcd_panel_swap_xy(panelHandle, configuration->swapXY) != ESP_OK) {
LOG_E(TAG, "Failed to swap XY");
esp_lcd_panel_del(panelHandle);
panelHandle = nullptr;
return false;
}
if (esp_lcd_panel_mirror(panelHandle, configuration->mirrorX, configuration->mirrorY) != ESP_OK) {
LOG_E(TAG, "Failed to mirror panel");
esp_lcd_panel_del(panelHandle);
panelHandle = nullptr;
return false;
}
if (esp_lcd_panel_invert_color(panelHandle, configuration->invertColor) != ESP_OK) {
LOG_E(TAG, "Failed to invert color");
esp_lcd_panel_del(panelHandle);
panelHandle = nullptr;
return false;
}
if (esp_lcd_panel_disp_on_off(panelHandle, false) != ESP_OK) {
LOG_E(TAG, "Failed to turn off panel");
esp_lcd_panel_del(panelHandle);
panelHandle = nullptr;
return false;
}
return true;
}
Axs15231bDisplay::Axs15231bDisplay(std::unique_ptr<Configuration> inConfiguration) :
configuration(std::move(inConfiguration))
{
assert(configuration != nullptr);
}
bool Axs15231bDisplay::start() {
if (!createIoHandle()) {
LOG_E(TAG, "Failed to create IO handle");
return false;
}
if (!createPanelHandle()) {
LOG_E(TAG, "Failed to create panel handle");
esp_lcd_panel_io_del(ioHandle);
ioHandle = nullptr;
return false;
}
if (!setupTeSync()) {
LOG_W(TAG, "TE sync setup failed, continuing without TE synchronization");
}
return true;
}
bool Axs15231bDisplay::stop() {
if (lvglDisplay != nullptr) {
stopLvgl();
}
teardownTeSync();
// Invalidate cached DisplayDriver - it holds a raw panelHandle that is about to be deleted
displayDriver.reset();
if (panelHandle != nullptr && esp_lcd_panel_del(panelHandle) != ESP_OK) {
LOG_E(TAG, "Failed to delete panel");
}
panelHandle = nullptr;
if (ioHandle != nullptr && esp_lcd_panel_io_del(ioHandle) != ESP_OK) {
LOG_E(TAG, "Failed to delete IO");
}
ioHandle = nullptr;
return true;
}
bool Axs15231bDisplay::startLvgl() {
if (lvglDisplay != nullptr) {
LOG_E(TAG, "LVGL already started");
return false;
}
lvglDisplay = lv_display_create(configuration->horizontalResolution, configuration->verticalResolution);
lv_display_set_user_data(lvglDisplay, this);
lv_display_set_color_format(lvglDisplay, LV_COLOR_FORMAT_RGB565);
uint32_t buffer_pixels = configuration->bufferSize;
uint32_t bufferSize = buffer_pixels * sizeof(uint16_t);
buffer1 = (uint16_t*)heap_caps_malloc(bufferSize, MALLOC_CAP_SPIRAM);
buffer2 = (uint16_t*)heap_caps_malloc(bufferSize, MALLOC_CAP_SPIRAM);
if (buffer1 == nullptr || buffer2 == nullptr) {
LOG_E(TAG, "Failed to allocate buffers");
heap_caps_free(buffer1);
heap_caps_free(buffer2);
buffer1 = nullptr;
buffer2 = nullptr;
lv_display_delete(lvglDisplay);
lvglDisplay = nullptr;
return false;
}
lv_display_set_buffers(lvglDisplay, buffer1, buffer2, bufferSize, LV_DISPLAY_RENDER_MODE_FULL);
lv_display_set_flush_cb(lvglDisplay, lvgl_port_flush_callback);
lv_display_set_driver_data(lvglDisplay, panelHandle);
const esp_lcd_panel_io_callbacks_t cbs = {
.on_color_trans_done = onColorTransDone,
};
if (esp_lcd_panel_io_register_event_callbacks(ioHandle, &cbs, lvglDisplay) != ESP_OK) {
LOG_E(TAG, "Failed to register panel IO callbacks");
heap_caps_free(buffer1);
heap_caps_free(buffer2);
buffer1 = nullptr;
buffer2 = nullptr;
lv_display_delete(lvglDisplay);
lvglDisplay = nullptr;
return false;
}
auto touch_device = getTouchDevice();
if (touch_device != nullptr && touch_device->supportsLvgl()) {
touch_device->startLvgl(lvglDisplay);
}
return true;
}
bool Axs15231bDisplay::stopLvgl() {
if (lvglDisplay == nullptr) {
return false;
}
auto touch_device = getTouchDevice();
if (touch_device != nullptr) {
touch_device->stopLvgl();
}
esp_lcd_panel_disp_on_off(panelHandle, false);
// Unregister IO callbacks before deleting the display to prevent use-after-free.
// The on_color_trans_done callback captures lvglDisplay as user_ctx; if a DMA
// transfer completes after lv_display_delete(), it would call
// lv_display_flush_ready() on a freed pointer.
const esp_lcd_panel_io_callbacks_t nullCbs = {
.on_color_trans_done = nullptr,
};
esp_lcd_panel_io_register_event_callbacks(ioHandle, &nullCbs, nullptr);
lv_display_delete(lvglDisplay);
lvglDisplay = nullptr;
heap_caps_free(buffer1);
heap_caps_free(buffer2);
buffer1 = nullptr;
buffer2 = nullptr;
heap_caps_free(tempBuf);
tempBuf = nullptr;
return true;
}
std::shared_ptr<tt::hal::display::DisplayDriver> Axs15231bDisplay::getDisplayDriver() {
if (lvglDisplay != nullptr) {
LOG_E(TAG, "Cannot get DisplayDriver while LVGL is active - call stopLvgl() first");
return nullptr;
}
if (panelHandle == nullptr) {
LOG_E(TAG, "Cannot get DisplayDriver - display is not started");
return nullptr;
}
if (displayDriver == nullptr) {
displayDriver = std::make_shared<EspLcdDisplayDriver>(
panelHandle,
configuration->horizontalResolution,
configuration->verticalResolution,
tt::hal::display::ColorFormat::RGB565Swapped
);
}
return displayDriver;
}
@@ -1,139 +0,0 @@
#pragma once
#include <Tactility/hal/display/DisplayDevice.h>
#include <Tactility/hal/display/DisplayDriver.h>
#include <driver/gpio.h>
#include <driver/spi_common.h>
#include <esp_lcd_panel_io.h>
#include <esp_lcd_types.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <functional>
#include <lvgl.h>
class Axs15231bDisplay final : public tt::hal::display::DisplayDevice {
public:
class Configuration {
public:
Configuration(
spi_host_device_t spiHostDevice,
gpio_num_t csPin,
gpio_num_t dcPin,
gpio_num_t resetPin,
gpio_num_t tePin,
unsigned int horizontalResolution,
unsigned int verticalResolution,
std::shared_ptr<tt::hal::touch::TouchDevice> touch,
bool swapXY = false,
bool mirrorX = false,
bool mirrorY = false,
bool invertColor = false,
uint32_t bufferSize = 0, // Size in pixel count. 0 means default, which is 1/10 of the screen size,
lcd_rgb_element_order_t rgbElementOrder = LCD_RGB_ELEMENT_ORDER_RGB
) : spiHostDevice(spiHostDevice),
csPin(csPin),
dcPin(dcPin),
resetPin(resetPin),
tePin(tePin),
horizontalResolution(horizontalResolution),
verticalResolution(verticalResolution),
swapXY(swapXY),
mirrorX(mirrorX),
mirrorY(mirrorY),
invertColor(invertColor),
bufferSize(bufferSize),
rgbElementOrder(rgbElementOrder),
touch(std::move(touch))
{
if (this->bufferSize == 0) {
this->bufferSize = horizontalResolution * verticalResolution / 10;
}
}
spi_host_device_t spiHostDevice;
gpio_num_t csPin;
gpio_num_t dcPin;
gpio_num_t resetPin;
gpio_num_t tePin;
unsigned int pixelClockFrequency = 40'000'000; // Hertz
size_t transactionQueueDepth = 10;
unsigned int horizontalResolution;
unsigned int verticalResolution;
bool swapXY;
bool mirrorX;
bool mirrorY;
bool invertColor;
uint32_t bufferSize; // Size in pixel count. 0 means default, which is 1/10 of the screen size
lcd_rgb_element_order_t rgbElementOrder;
std::shared_ptr<tt::hal::touch::TouchDevice> touch;
std::function<void(uint8_t)> backlightDutyFunction = nullptr;
};
private:
esp_lcd_panel_io_handle_t ioHandle = nullptr;
esp_lcd_panel_handle_t panelHandle = nullptr;
lv_display_t* lvglDisplay = nullptr;
uint16_t* buffer1 = nullptr;
uint16_t* buffer2 = nullptr;
uint16_t* tempBuf = nullptr;
SemaphoreHandle_t teSyncSemaphore = nullptr;
bool teIsrInstalled = false;
bool isrServiceInstalledByUs = false;
std::unique_ptr<Configuration> configuration;
std::shared_ptr<tt::hal::display::DisplayDriver> displayDriver;
bool createIoHandle();
bool createPanelHandle();
bool setupTeSync();
void teardownTeSync();
static void IRAM_ATTR teIsrHandler(void* arg);
public:
explicit Axs15231bDisplay(std::unique_ptr<Configuration> inConfiguration);
std::string getName() const override { return "AXS15231B Display"; }
std::string getDescription() const override { return "AXS15231B display"; }
bool start() override;
bool stop() override;
bool supportsLvgl() const override { return true; }
bool startLvgl() override;
bool stopLvgl() override;
lv_display_t* getLvglDisplay() const override { return lvglDisplay; }
std::shared_ptr<tt::hal::touch::TouchDevice> getTouchDevice() override { return configuration->touch; }
void setBacklightDuty(uint8_t backlightDuty) override {
if (configuration->backlightDutyFunction != nullptr) {
configuration->backlightDutyFunction(backlightDuty);
}
}
bool supportsBacklightDuty() const override { return configuration->backlightDutyFunction != nullptr; }
bool supportsDisplayDriver() const override { return true; }
std::shared_ptr<tt::hal::display::DisplayDriver> getDisplayDriver() override;
static void lvgl_port_flush_callback(lv_display_t *drv, const lv_area_t *area, uint8_t *color_map);
static bool onColorTransDone(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx);
};
@@ -1,38 +0,0 @@
#include "Axs15231bTouch.h"
#include <esp_lcd_axs15231b.h>
#include <esp_err.h>
bool Axs15231bTouch::createIoHandle(esp_lcd_panel_io_handle_t& outHandle) {
if (configuration == nullptr) {
return false;
}
esp_lcd_panel_io_i2c_config_t io_config = ESP_LCD_TOUCH_IO_I2C_AXS15231B_CONFIG();
return esp_lcd_new_panel_io_i2c(configuration->port, &io_config, &outHandle) == ESP_OK;
}
bool Axs15231bTouch::createTouchHandle(esp_lcd_panel_io_handle_t ioHandle, const esp_lcd_touch_config_t& configuration, esp_lcd_touch_handle_t& panelHandle) {
return esp_lcd_touch_new_i2c_axs15231b(ioHandle, &configuration, &panelHandle) == ESP_OK;
}
esp_lcd_touch_config_t Axs15231bTouch::createEspLcdTouchConfig() {
return {
.x_max = configuration->xMax,
.y_max = configuration->yMax,
.rst_gpio_num = configuration->pinReset,
.int_gpio_num = configuration->pinInterrupt,
.levels = {
.reset = configuration->pinResetLevel,
.interrupt = configuration->pinInterruptLevel,
},
.flags = {
.swap_xy = configuration->swapXy,
.mirror_x = configuration->mirrorX,
.mirror_y = configuration->mirrorY,
},
.process_coordinates = nullptr,
.interrupt_callback = nullptr,
.user_data = nullptr,
.driver_data = nullptr
};
}
@@ -1,72 +0,0 @@
#pragma once
#include <Tactility/hal/touch/TouchDevice.h>
#include <Tactility/TactilityCore.h>
#include <driver/i2c.h>
#include <memory>
#include <string>
#include <EspLcdTouch.h>
class Axs15231bTouch final : public EspLcdTouch {
public:
class Configuration {
public:
Configuration(
i2c_port_t port,
uint16_t xMax,
uint16_t yMax,
bool swapXy = false,
bool mirrorX = false,
bool mirrorY = false,
gpio_num_t pinReset = GPIO_NUM_NC,
gpio_num_t pinInterrupt = GPIO_NUM_NC,
unsigned int pinResetLevel = 0,
unsigned int pinInterruptLevel = 0
) : port(port),
xMax(xMax),
yMax(yMax),
swapXy(swapXy),
mirrorX(mirrorX),
mirrorY(mirrorY),
pinReset(pinReset),
pinInterrupt(pinInterrupt),
pinResetLevel(pinResetLevel),
pinInterruptLevel(pinInterruptLevel)
{}
i2c_port_t port;
uint16_t xMax;
uint16_t yMax;
bool swapXy;
bool mirrorX;
bool mirrorY;
gpio_num_t pinReset;
gpio_num_t pinInterrupt;
unsigned int pinResetLevel;
unsigned int pinInterruptLevel;
};
private:
std::unique_ptr<Configuration> configuration;
bool createIoHandle(esp_lcd_panel_io_handle_t& outHandle) override;
bool createTouchHandle(esp_lcd_panel_io_handle_t ioHandle, const esp_lcd_touch_config_t& configuration, esp_lcd_touch_handle_t& panelHandle) override;
esp_lcd_touch_config_t createEspLcdTouchConfig() override;
public:
explicit Axs15231bTouch(std::unique_ptr<Configuration> inConfiguration) : configuration(std::move(inConfiguration)) {
assert(configuration != nullptr);
}
std::string getName() const override { return "AXS15231B Touch"; }
std::string getDescription() const override { return "AXS15231B I2C touch driver"; }
};
@@ -1,21 +0,0 @@
#include "devices/Display.h"
#include <Tactility/hal/Configuration.h>
#include <PwmBacklight.h>
using namespace tt::hal;
static DeviceVector createDevices() {
return {
createDisplay(),
};
}
static bool initBoot() {
return driver::pwmbacklight::init(GPIO_NUM_1);
}
extern const Configuration hardwareConfiguration = {
.initBoot = initBoot,
.createDevices = createDevices
};
@@ -1,44 +0,0 @@
#include "Display.h"
#include <PwmBacklight.h>
#include <Axs15231b/Axs15231bDisplay.h>
#include <Axs15231b/Axs15231bTouch.h>
static constexpr size_t JC3248W535C_LCD_DRAW_BUFFER_SIZE = 320 * 480;
static std::shared_ptr<tt::hal::touch::TouchDevice> createTouch() {
auto configuration = std::make_unique<Axs15231bTouch::Configuration>(
I2C_NUM_0,
320, // width
480, // height
false, // mirror_x
false, // mirror_y
false // swap_xy
);
return std::make_shared<Axs15231bTouch>(std::move(configuration));
}
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay() {
auto touch = createTouch();
auto configuration = std::make_unique<Axs15231bDisplay::Configuration>(
SPI2_HOST,
GPIO_NUM_45, //CS
GPIO_NUM_NC, //DC
GPIO_NUM_NC, //RST
GPIO_NUM_38, //TE
320, // width
480, // height
touch,
false, // swap_xy
false, // mirror_x
false, // mirror_y
false, // invert color
JC3248W535C_LCD_DRAW_BUFFER_SIZE
);
configuration->backlightDutyFunction = driver::pwmbacklight::setBacklightDuty;
return std::make_shared<Axs15231bDisplay>(std::move(configuration));
}
@@ -1,6 +0,0 @@
#pragma once
#include <memory>
#include <Tactility/hal/display/DisplayDevice.h>
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay();
@@ -12,6 +12,8 @@ hardware.tinyUsb=true
hardware.esptoolFlashFreq=120M
hardware.bluetooth=true
dependencies.useDeprecatedHal=false
storage.userDataLocation=SD
display.size=3.5"
@@ -2,4 +2,5 @@ dependencies:
- Platforms/platform-esp32
- Drivers/dummy-i2s-amp-module
- Drivers/audio-stream-module
- Drivers/axs15231b-module
dts: guition,jc3248w535c.dts
@@ -9,8 +9,11 @@
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/display_placeholder.h>
#include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/pwm_backlight.h>
#include <bindings/dummy_i2s_amp.h>
#include <bindings/axs15231b_display.h>
#include <bindings/axs15231b_touch.h>
/ {
compatible = "root";
@@ -31,14 +34,6 @@
gpio-count = <49>;
};
i2c_internal: i2c0 {
compatible = "espressif,esp32-i2c";
port = <I2C_NUM_0>;
clock-frequency = <400000>;
pin-sda = <&gpio0 4 GPIO_FLAG_NONE>;
pin-scl = <&gpio0 8 GPIO_FLAG_NONE>;
};
i2c_external: i2c1 {
compatible = "espressif,esp32-i2c";
port = <I2C_NUM_1>;
@@ -60,6 +55,25 @@
i2s = <&i2s0>;
};
display_backlight_pwm {
compatible = "espressif,esp32-pwm-ledc";
pin = <&gpio0 1 GPIO_FLAG_NONE>;
period-ns = <25000>;
ledc-timer = <0>;
ledc-channel = <0>;
// Matches the deprecated HAL's old PwmBacklight driver, which hardcoded 8-bit duty
// resolution (the binding's own default is 10-bit).
duty-resolution = <8>;
};
display_backlight {
compatible = "pwm-backlight";
// Off by default so display power-on won't show the screen from before the last power loss.
// The display backlight is turned on during the boot process.
status = "disabled";
pwm = <&display_backlight_pwm>;
};
spi0 {
compatible = "espressif,esp32-spi";
host = <SPI2_HOST>;
@@ -70,8 +84,77 @@
pin-wp = <&gpio0 40 GPIO_FLAG_NONE>;
pin-hd = <&gpio0 39 GPIO_FLAG_NONE>;
display {
compatible = "display-placeholder";
display@0 {
compatible = "axs,axs15231b";
horizontal-resolution = <320>;
vertical-resolution = <480>;
pin-te = <&gpio0 38 GPIO_FLAG_NONE>;
backlight = <&display_backlight>;
// Confirmed on real hardware: a sub-region draw desyncs this panel's row
// auto-increment counter (no QSPI row-address command exists to resync it),
// producing a sheared/doubled image - see requires-full-frame's binding doc.
requires-full-frame;
// Vendor bring-up sequence, carried over unchanged from the deprecated HAL's old
// Axs15231bDisplay.cpp. Framing is [cmd, data-length, delay-ms, data-length bytes of
// data...] - see axs15231b-module's init-sequence binding property for the encoding.
init-sequence = [
0xBB 8 0 0x00 0x00 0x00 0x00 0x00 0x00 0x5A 0xA5
0xA0 17 0 0xC0 0x10 0x00 0x02 0x00 0x00 0x04 0x3F 0x20 0x05 0x3F 0x3F 0x00 0x00 0x00 0x00 0x00
0xA2 31 0 0x30 0x3C 0x24 0x14 0xD0 0x20 0xFF 0xE0 0x40 0x19 0x80 0x80 0x80 0x20 0xf9 0x10 0x02 0xff 0xff 0xF0 0x90 0x01 0x32 0xA0 0x91 0xE0 0x20 0x7F 0xFF 0x00 0x5A
0xD0 30 0 0xE0 0x40 0x51 0x24 0x08 0x05 0x10 0x01 0x20 0x15 0x42 0xC2 0x22 0x22 0xAA 0x03 0x10 0x12 0x60 0x14 0x1E 0x51 0x15 0x00 0x8A 0x20 0x00 0x03 0x3A 0x12
0xA3 22 0 0xA0 0x06 0xAa 0x00 0x08 0x02 0x0A 0x04 0x04 0x04 0x04 0x04 0x04 0x04 0x04 0x04 0x04 0x04 0x04 0x00 0x55 0x55
0xC1 30 0 0x31 0x04 0x02 0x02 0x71 0x05 0x24 0x55 0x02 0x00 0x41 0x00 0x53 0xFF 0xFF 0xFF 0x4F 0x52 0x00 0x4F 0x52 0x00 0x45 0x3B 0x0B 0x02 0x0d 0x00 0xFF 0x40
0xC3 11 0 0x00 0x00 0x00 0x50 0x03 0x00 0x00 0x00 0x01 0x80 0x01
0xC4 29 0 0x00 0x24 0x33 0x80 0x00 0xea 0x64 0x32 0xC8 0x64 0xC8 0x32 0x90 0x90 0x11 0x06 0xDC 0xFA 0x00 0x00 0x80 0xFE 0x10 0x10 0x00 0x0A 0x0A 0x44 0x50
0xC5 23 0 0x18 0x00 0x00 0x03 0xFE 0x3A 0x4A 0x20 0x30 0x10 0x88 0xDE 0x0D 0x08 0x0F 0x0F 0x01 0x3A 0x4A 0x20 0x10 0x10 0x00
0xC6 20 0 0x05 0x0A 0x05 0x0A 0x00 0xE0 0x2E 0x0B 0x12 0x22 0x12 0x22 0x01 0x03 0x00 0x3F 0x6A 0x18 0xC8 0x22
0xC7 20 0 0x50 0x32 0x28 0x00 0xa2 0x80 0x8f 0x00 0x80 0xff 0x07 0x11 0x9c 0x67 0xff 0x24 0x0c 0x0d 0x0e 0x0f
0xC9 4 0 0x33 0x44 0x44 0x01
0xCF 27 0 0x2C 0x1E 0x88 0x58 0x13 0x18 0x56 0x18 0x1E 0x68 0x88 0x00 0x65 0x09 0x22 0xC4 0x0C 0x77 0x22 0x44 0xAA 0x55 0x08 0x08 0x12 0xA0 0x08
0xD5 30 0 0x40 0x8E 0x8D 0x01 0x35 0x04 0x92 0x74 0x04 0x92 0x74 0x04 0x08 0x6A 0x04 0x46 0x03 0x03 0x03 0x03 0x82 0x01 0x03 0x00 0xE0 0x51 0xA1 0x00 0x00 0x00
0xD6 30 0 0x10 0x32 0x54 0x76 0x98 0xBA 0xDC 0xFE 0x93 0x00 0x01 0x83 0x07 0x07 0x00 0x07 0x07 0x00 0x03 0x03 0x03 0x03 0x03 0x03 0x00 0x84 0x00 0x20 0x01 0x00
0xD7 19 0 0x03 0x01 0x0b 0x09 0x0f 0x0d 0x1E 0x1F 0x18 0x1d 0x1f 0x19 0x40 0x8E 0x04 0x00 0x20 0xA0 0x1F
0xD8 12 0 0x02 0x00 0x0a 0x08 0x0e 0x0c 0x1E 0x1F 0x18 0x1d 0x1f 0x19
0xD9 12 0 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F
0xDD 12 0 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F
0xDF 8 0 0x44 0x73 0x4B 0x69 0x00 0x0A 0x02 0x90
0xE0 17 0 0x3B 0x28 0x10 0x16 0x0c 0x06 0x11 0x28 0x5c 0x21 0x0D 0x35 0x13 0x2C 0x33 0x28 0x0D
0xE1 17 0 0x37 0x28 0x10 0x16 0x0b 0x06 0x11 0x28 0x5C 0x21 0x0D 0x35 0x14 0x2C 0x33 0x28 0x0F
0xE2 17 0 0x3B 0x07 0x12 0x18 0x0E 0x0D 0x17 0x35 0x44 0x32 0x0C 0x14 0x14 0x36 0x3A 0x2F 0x0D
0xE3 17 0 0x37 0x07 0x12 0x18 0x0E 0x0D 0x17 0x35 0x44 0x32 0x0C 0x14 0x14 0x36 0x32 0x2F 0x0F
0xE4 17 0 0x3B 0x07 0x12 0x18 0x0E 0x0D 0x17 0x39 0x44 0x2E 0x0C 0x14 0x14 0x36 0x3A 0x2F 0x0D
0xE5 17 0 0x37 0x07 0x12 0x18 0x0E 0x0D 0x17 0x39 0x44 0x2E 0x0C 0x14 0x14 0x36 0x3A 0x2F 0x0F
0xA4 16 0 0x85 0x85 0x95 0x82 0xAF 0xAA 0xAA 0x80 0x10 0x30 0x40 0x40 0x20 0xFF 0x60 0x30
0xA4 4 0 0x85 0x85 0x95 0x85
0xBB 8 0 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x13 0 0
0x35 1 0 0x00
0x11 0 120
0x2C 4 0 0x00 0x00 0x00 0x00
];
};
};
// Declared (and thus started) after display@0 above, not before: the display and touch
// sides of this chip share the same silicon, and the deprecated HAL's old Hal.cpp always
// started the display first, then the touch device second. Starting touch first (as an
// earlier version of this devicetree did) let touch's I2C handshake succeed initially, but
// the display's own reset/init sequence running afterward then reset the whole chip and
// left the touch interface unresponsive - confirmed on real hardware (I2C writes started
// failing consistently right after display bring-up completed).
i2c_internal: i2c0 {
compatible = "espressif,esp32-i2c";
port = <I2C_NUM_0>;
clock-frequency = <400000>;
pin-sda = <&gpio0 4 GPIO_FLAG_NONE>;
pin-scl = <&gpio0 8 GPIO_FLAG_NONE>;
touch {
compatible = "axs,axs15231b-touch";
reg = <0x3B>;
x-max = <320>;
y-max = <480>;
};
};
@@ -3,12 +3,10 @@
extern "C" {
static error_t start() {
// Empty for now
return ERROR_NONE;
}
static error_t stop() {
// Empty for now
return ERROR_NONE;
}