Fix Tab5 Display Issues (#579)

And add PPA rotation to lvgl_display
This commit is contained in:
Shadowtrance
2026-07-23 07:58:34 +10:00
committed by GitHub
parent 8b92aa8e5a
commit 08eac48e64
9 changed files with 440 additions and 19 deletions
@@ -113,10 +113,10 @@ void tab5_create_devices_v1(Device* i2c0) {
.vsync_pulse_width = 4, .vsync_pulse_width = 4,
.vsync_back_porch = 20, .vsync_back_porch = 20,
.vsync_front_porch = 20, .vsync_front_porch = 20,
.num_fbs = 1, .num_fbs = 2,
.use_dma2d = false, .use_dma2d = true,
.disable_lp = false, .disable_lp = false,
.allow_tearing = true, // matches old lvgl_port_display_dsi_cfg_t.avoid_tearing = 0 .allow_tearing = true, // matches old lvgl_port_display_dsi_cfg_t.avoid_tearing = 0 (disabled = don't wait)
.init_sequence = display_init_bytes.data(), .init_sequence = display_init_bytes.data(),
.init_sequence_length = (uint32_t)display_init_bytes.size(), .init_sequence_length = (uint32_t)display_init_bytes.size(),
.backlight = backlight, .backlight = backlight,
@@ -92,10 +92,10 @@ void tab5_create_devices_v2(Device* i2c0) {
.vsync_pulse_width = 2, .vsync_pulse_width = 2,
.vsync_back_porch = 8, .vsync_back_porch = 8,
.vsync_front_porch = 220, .vsync_front_porch = 220,
.num_fbs = 1, .num_fbs = 2,
.use_dma2d = false, .use_dma2d = true,
.disable_lp = false, .disable_lp = false,
.allow_tearing = true, .allow_tearing = true, // matches old lvgl_port_display_dsi_cfg_t.avoid_tearing = 0 (disabled = don't wait)
.init_sequence = display_init_bytes.data(), .init_sequence = display_init_bytes.data(),
.init_sequence_length = (uint32_t)display_init_bytes.size(), .init_sequence_length = (uint32_t)display_init_bytes.size(),
.backlight = backlight, .backlight = backlight,
+1 -2
View File
@@ -46,8 +46,7 @@ sdkconfig.CONFIG_ESP_HOSTED_ENABLE_PEER_DATA_TRANSFER=y
sdkconfig.CONFIG_ESP_HOSTED_MAX_CUSTOM_MSG_HANDLERS=8 sdkconfig.CONFIG_ESP_HOSTED_MAX_CUSTOM_MSG_HANDLERS=8
# Performance: larger L2 cache reduces PSRAM stalls for draw/DPI buffers # Performance: larger L2 cache reduces PSRAM stalls for draw/DPI buffers
sdkconfig.CONFIG_CACHE_L2_CACHE_256KB=y sdkconfig.CONFIG_CACHE_L2_CACHE_256KB=y
# Performance: use P4's PPA (pixel processing accelerator for rotation) # Performance: cache-line-aligned draw buffers, required for PPA/DMA2D coherency (see lvgl-module's lvgl_display_alloc_buffer())
sdkconfig.CONFIG_LVGL_PORT_ENABLE_PPA=y
sdkconfig.CONFIG_LV_DRAW_BUF_ALIGN=64 sdkconfig.CONFIG_LV_DRAW_BUF_ALIGN=64
sdkconfig.CONFIG_LV_DEF_REFR_PERIOD=15 sdkconfig.CONFIG_LV_DEF_REFR_PERIOD=15
# SC202CS (SC2356) MIPI CSI camera sensor + ISP pipeline (AE/AWB/demosaicing) # SC202CS (SC2356) MIPI CSI camera sensor + ISP pipeline (AE/AWB/demosaicing)
@@ -44,6 +44,11 @@ struct Ili9881cInternal {
// draw_bitmap() when color_data is one of frame_buffers and avoid_tearing is set - see the // draw_bitmap() when color_data is one of frame_buffers and avoid_tearing is set - see the
// comment there for why. // comment there for why.
SemaphoreHandle_t frame_complete_semaphore; SemaphoreHandle_t frame_complete_semaphore;
// Signaled by on_color_trans_done once the panel driver's own copy/DMA2D transfer of a
// draw_bitmap() call's pixels into the target frame buffer completes. Unlike
// frame_complete_semaphore this is always waited on (regardless of allow_tearing or whether
// color_data is a real frame buffer) - see the comment on draw_bitmap() for why.
SemaphoreHandle_t color_trans_done_semaphore;
// Heap-allocated only when the devicetree supplies a custom init_sequence (see // Heap-allocated only when the devicetree supplies a custom init_sequence (see
// parse_init_sequence()) - nullptr otherwise, since the vendor's built-in default sequence // parse_init_sequence()) - nullptr otherwise, since the vendor's built-in default sequence
// needs no parsing. Its .data pointers alias directly into the devicetree's static const // needs no parsing. Its .data pointers alias directly into the devicetree's static const
@@ -69,6 +74,30 @@ static bool on_refresh_done(esp_lcd_panel_handle_t, esp_lcd_dpi_panel_event_data
return high_task_woken == pdTRUE; return high_task_woken == pdTRUE;
} }
// esp_lcd_dpi_panel_draw_bitmap() copies (memcpy, or DMA2D when use_dma2d is set) the caller's
// pixels into the target frame buffer and returns immediately - the copy itself finishes
// asynchronously. Calling draw_bitmap() again before that copy completes hits the panel driver's
// own re-entrancy guard and logs "previous draw operation is not finished" (lcd.dsi). This is
// unconditional, unlike frame_complete_semaphore's tearing-avoidance wait: it protects the copy
// itself, not scan-out, so it must be waited on regardless of allow_tearing or which buffer
// color_data is.
//
// Unlike on_refresh_done (always ISR, from the VSYNC interrupt), esp_lcd_panel_dpi.c invokes this
// callback from two different contexts depending on the copy path: synchronously from the calling
// task for the CPU-memcpy and fb-direct (zero-copy) paths, but from an ISR (async_fbcpy_done_cb,
// the DMA2D completion interrupt) when use_dma2d is set. Giving a semaphore with the ISR-only API
// from task context is undefined behavior on FreeRTOS, so the context must be checked at runtime.
static bool on_color_trans_done(esp_lcd_panel_handle_t, esp_lcd_dpi_panel_event_data_t*, void* user_ctx) {
auto* internal = static_cast<Ili9881cInternal*>(user_ctx);
if (xPortInIsrContext()) {
BaseType_t high_task_woken = pdFALSE;
xSemaphoreGiveFromISR(internal->color_trans_done_semaphore, &high_task_woken);
return high_task_woken == pdTRUE;
}
xSemaphoreGive(internal->color_trans_done_semaphore);
return false;
}
static int pin_or_unused(const GpioPinSpec& pin) { static int pin_or_unused(const GpioPinSpec& pin) {
return pin.gpio_controller == nullptr ? -1 : static_cast<int>(pin.pin); return pin.gpio_controller == nullptr ? -1 : static_cast<int>(pin.pin);
} }
@@ -310,10 +339,27 @@ static error_t start(Device* device) {
return ERROR_OUT_OF_MEMORY; return ERROR_OUT_OF_MEMORY;
} }
internal->color_trans_done_semaphore = xSemaphoreCreateBinary();
if (internal->color_trans_done_semaphore == nullptr) {
vSemaphoreDelete(internal->frame_complete_semaphore);
esp_lcd_panel_del(internal->panel_handle);
esp_lcd_panel_io_del(internal->io_handle);
esp_lcd_del_dsi_bus(internal->dsi_bus_handle);
esp_ldo_release_channel(internal->ldo_handle);
free(internal->parsed_init_cmds);
free(internal);
return ERROR_OUT_OF_MEMORY;
}
// The panel starts out idle (no draw_bitmap() call in flight), so the first draw_bitmap()
// must not block waiting for a completion event that will never come.
xSemaphoreGive(internal->color_trans_done_semaphore);
esp_lcd_dpi_panel_event_callbacks_t callbacks = {}; esp_lcd_dpi_panel_event_callbacks_t callbacks = {};
callbacks.on_refresh_done = on_refresh_done; callbacks.on_refresh_done = on_refresh_done;
callbacks.on_color_trans_done = on_color_trans_done;
if (esp_lcd_dpi_panel_register_event_callbacks(internal->panel_handle, &callbacks, internal) != ESP_OK) { if (esp_lcd_dpi_panel_register_event_callbacks(internal->panel_handle, &callbacks, internal) != ESP_OK) {
LOG_E(TAG, "Failed to register panel event callbacks"); LOG_E(TAG, "Failed to register panel event callbacks");
vSemaphoreDelete(internal->color_trans_done_semaphore);
vSemaphoreDelete(internal->frame_complete_semaphore); vSemaphoreDelete(internal->frame_complete_semaphore);
esp_lcd_panel_del(internal->panel_handle); esp_lcd_panel_del(internal->panel_handle);
esp_lcd_panel_io_del(internal->io_handle); esp_lcd_panel_io_del(internal->io_handle);
@@ -364,6 +410,7 @@ static error_t stop(Device* device) {
} }
vSemaphoreDelete(internal->frame_complete_semaphore); vSemaphoreDelete(internal->frame_complete_semaphore);
vSemaphoreDelete(internal->color_trans_done_semaphore);
free(internal->parsed_init_cmds); free(internal->parsed_init_cmds);
free(internal); free(internal);
device_set_driver_data(device, nullptr); device_set_driver_data(device, nullptr);
@@ -403,15 +450,32 @@ static bool ili9881c_color_data_is_frame_buffer(const Ili9881cInternal* internal
static error_t ili9881c_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) { static error_t ili9881c_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) {
auto* internal = static_cast<Ili9881cInternal*>(device_get_driver_data(device)); auto* internal = static_cast<Ili9881cInternal*>(device_get_driver_data(device));
// Wait for the previous draw_bitmap()'s copy/DMA2D transfer to finish before issuing a new
// one - see on_color_trans_done's comment for why this is unconditional (unlike the
// tearing-avoidance wait below). The semaphore starts pre-given (see start()), so the first
// call doesn't block here.
xSemaphoreTake(internal->color_trans_done_semaphore, portMAX_DELAY);
bool wait_for_scanout = !GET_CONFIG(device)->allow_tearing && ili9881c_color_data_is_frame_buffer(internal, color_data); bool wait_for_scanout = !GET_CONFIG(device)->allow_tearing && ili9881c_color_data_is_frame_buffer(internal, color_data);
if (wait_for_scanout) { if (wait_for_scanout) {
xSemaphoreTake(internal->frame_complete_semaphore, 0); // clear any already-pending signal xSemaphoreTake(internal->frame_complete_semaphore, 0); // clear any already-pending signal
} }
if (esp_lcd_panel_draw_bitmap(internal->panel_handle, x_start, y_start, x_end, y_end, color_data) != ESP_OK) { if (esp_lcd_panel_draw_bitmap(internal->panel_handle, x_start, y_start, x_end, y_end, color_data) != ESP_OK) {
xSemaphoreGive(internal->color_trans_done_semaphore);
return ERROR_RESOURCE; return ERROR_RESOURCE;
} }
// Also wait for *this* call's own copy to finish before returning: color_data may be a
// caller-owned buffer (e.g. LVGL's draw buffer in owned-buffer/PARTIAL mode), and the
// CPU-memcpy/DMA2D copy paths both read directly from it asynchronously - draw_bitmap()
// returning early would let the caller start overwriting color_data (DisplayApi's contract
// treats draw_bitmap as synchronous - see lvgl_display_flush_cb()'s lv_display_flush_ready()
// call right after) while that read is still in flight. on_color_trans_done() re-gives the
// semaphore here, restoring the pre-given/idle state for the next call.
xSemaphoreTake(internal->color_trans_done_semaphore, portMAX_DELAY);
xSemaphoreGive(internal->color_trans_done_semaphore);
if (wait_for_scanout) { if (wait_for_scanout) {
xSemaphoreTake(internal->frame_complete_semaphore, portMAX_DELAY); xSemaphoreTake(internal->frame_complete_semaphore, portMAX_DELAY);
} }
+64
View File
@@ -44,6 +44,11 @@ struct St7123Internal {
// draw_bitmap() when color_data is one of frame_buffers and avoid_tearing is set - see the // draw_bitmap() when color_data is one of frame_buffers and avoid_tearing is set - see the
// comment there for why. // comment there for why.
SemaphoreHandle_t frame_complete_semaphore; SemaphoreHandle_t frame_complete_semaphore;
// Signaled by on_color_trans_done once the panel driver's own copy/DMA2D transfer of a
// draw_bitmap() call's pixels into the target frame buffer completes. Unlike
// frame_complete_semaphore this is always waited on (regardless of allow_tearing or whether
// color_data is a real frame buffer) - see the comment on draw_bitmap() for why.
SemaphoreHandle_t color_trans_done_semaphore;
// Heap-allocated only when the devicetree supplies a custom init_sequence (see // Heap-allocated only when the devicetree supplies a custom init_sequence (see
// parse_init_sequence()) - nullptr otherwise, since the vendor's built-in default sequence // parse_init_sequence()) - nullptr otherwise, since the vendor's built-in default sequence
// needs no parsing. Its .data pointers alias directly into the devicetree's static const // needs no parsing. Its .data pointers alias directly into the devicetree's static const
@@ -69,6 +74,30 @@ static bool on_refresh_done(esp_lcd_panel_handle_t, esp_lcd_dpi_panel_event_data
return high_task_woken == pdTRUE; return high_task_woken == pdTRUE;
} }
// esp_lcd_dpi_panel_draw_bitmap() copies (memcpy, or DMA2D when use_dma2d is set) the caller's
// pixels into the target frame buffer and returns immediately - the copy itself finishes
// asynchronously. Calling draw_bitmap() again before that copy completes hits the panel driver's
// own re-entrancy guard and logs "previous draw operation is not finished" (lcd.dsi). This is
// unconditional, unlike frame_complete_semaphore's tearing-avoidance wait: it protects the copy
// itself, not scan-out, so it must be waited on regardless of allow_tearing or which buffer
// color_data is.
//
// Unlike on_refresh_done (always ISR, from the VSYNC interrupt), esp_lcd_panel_dpi.c invokes this
// callback from two different contexts depending on the copy path: synchronously from the calling
// task for the CPU-memcpy and fb-direct (zero-copy) paths, but from an ISR (async_fbcpy_done_cb,
// the DMA2D completion interrupt) when use_dma2d is set. Giving a semaphore with the ISR-only API
// from task context is undefined behavior on FreeRTOS, so the context must be checked at runtime.
static bool on_color_trans_done(esp_lcd_panel_handle_t, esp_lcd_dpi_panel_event_data_t*, void* user_ctx) {
auto* internal = static_cast<St7123Internal*>(user_ctx);
if (xPortInIsrContext()) {
BaseType_t high_task_woken = pdFALSE;
xSemaphoreGiveFromISR(internal->color_trans_done_semaphore, &high_task_woken);
return high_task_woken == pdTRUE;
}
xSemaphoreGive(internal->color_trans_done_semaphore);
return false;
}
static int pin_or_unused(const GpioPinSpec& pin) { static int pin_or_unused(const GpioPinSpec& pin) {
return pin.gpio_controller == nullptr ? -1 : static_cast<int>(pin.pin); return pin.gpio_controller == nullptr ? -1 : static_cast<int>(pin.pin);
} }
@@ -309,10 +338,27 @@ static error_t start(Device* device) {
return ERROR_OUT_OF_MEMORY; return ERROR_OUT_OF_MEMORY;
} }
internal->color_trans_done_semaphore = xSemaphoreCreateBinary();
if (internal->color_trans_done_semaphore == nullptr) {
vSemaphoreDelete(internal->frame_complete_semaphore);
esp_lcd_panel_del(internal->panel_handle);
esp_lcd_panel_io_del(internal->io_handle);
esp_lcd_del_dsi_bus(internal->dsi_bus_handle);
esp_ldo_release_channel(internal->ldo_handle);
free(internal->parsed_init_cmds);
free(internal);
return ERROR_OUT_OF_MEMORY;
}
// The panel starts out idle (no draw_bitmap() call in flight), so the first draw_bitmap()
// must not block waiting for a completion event that will never come.
xSemaphoreGive(internal->color_trans_done_semaphore);
esp_lcd_dpi_panel_event_callbacks_t callbacks = {}; esp_lcd_dpi_panel_event_callbacks_t callbacks = {};
callbacks.on_refresh_done = on_refresh_done; callbacks.on_refresh_done = on_refresh_done;
callbacks.on_color_trans_done = on_color_trans_done;
if (esp_lcd_dpi_panel_register_event_callbacks(internal->panel_handle, &callbacks, internal) != ESP_OK) { if (esp_lcd_dpi_panel_register_event_callbacks(internal->panel_handle, &callbacks, internal) != ESP_OK) {
LOG_E(TAG, "Failed to register panel event callbacks"); LOG_E(TAG, "Failed to register panel event callbacks");
vSemaphoreDelete(internal->color_trans_done_semaphore);
vSemaphoreDelete(internal->frame_complete_semaphore); vSemaphoreDelete(internal->frame_complete_semaphore);
esp_lcd_panel_del(internal->panel_handle); esp_lcd_panel_del(internal->panel_handle);
esp_lcd_panel_io_del(internal->io_handle); esp_lcd_panel_io_del(internal->io_handle);
@@ -363,6 +409,7 @@ static error_t stop(Device* device) {
} }
vSemaphoreDelete(internal->frame_complete_semaphore); vSemaphoreDelete(internal->frame_complete_semaphore);
vSemaphoreDelete(internal->color_trans_done_semaphore);
free(internal->parsed_init_cmds); free(internal->parsed_init_cmds);
free(internal); free(internal);
device_set_driver_data(device, nullptr); device_set_driver_data(device, nullptr);
@@ -402,15 +449,32 @@ static bool st7123_color_data_is_frame_buffer(const St7123Internal* internal, co
static error_t st7123_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) { static error_t st7123_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) {
auto* internal = static_cast<St7123Internal*>(device_get_driver_data(device)); auto* internal = static_cast<St7123Internal*>(device_get_driver_data(device));
// Wait for the previous draw_bitmap()'s copy/DMA2D transfer to finish before issuing a new
// one - see on_color_trans_done's comment for why this is unconditional (unlike the
// tearing-avoidance wait below). The semaphore starts pre-given (see start()), so the first
// call doesn't block here.
xSemaphoreTake(internal->color_trans_done_semaphore, portMAX_DELAY);
bool wait_for_scanout = !GET_CONFIG(device)->allow_tearing && st7123_color_data_is_frame_buffer(internal, color_data); bool wait_for_scanout = !GET_CONFIG(device)->allow_tearing && st7123_color_data_is_frame_buffer(internal, color_data);
if (wait_for_scanout) { if (wait_for_scanout) {
xSemaphoreTake(internal->frame_complete_semaphore, 0); // clear any already-pending signal xSemaphoreTake(internal->frame_complete_semaphore, 0); // clear any already-pending signal
} }
if (esp_lcd_panel_draw_bitmap(internal->panel_handle, x_start, y_start, x_end, y_end, color_data) != ESP_OK) { if (esp_lcd_panel_draw_bitmap(internal->panel_handle, x_start, y_start, x_end, y_end, color_data) != ESP_OK) {
xSemaphoreGive(internal->color_trans_done_semaphore);
return ERROR_RESOURCE; return ERROR_RESOURCE;
} }
// Also wait for *this* call's own copy to finish before returning: color_data may be a
// caller-owned buffer (e.g. LVGL's draw buffer in owned-buffer/PARTIAL mode), and the
// CPU-memcpy/DMA2D copy paths both read directly from it asynchronously - draw_bitmap()
// returning early would let the caller start overwriting color_data (DisplayApi's contract
// treats draw_bitmap as synchronous - see lvgl_display_flush_cb()'s lv_display_flush_ready()
// call right after) while that read is still in flight. on_color_trans_done() re-gives the
// semaphore here, restoring the pre-given/idle state for the next call.
xSemaphoreTake(internal->color_trans_done_semaphore, portMAX_DELAY);
xSemaphoreGive(internal->color_trans_done_semaphore);
if (wait_for_scanout) { if (wait_for_scanout) {
xSemaphoreTake(internal->frame_complete_semaphore, portMAX_DELAY); xSemaphoreTake(internal->frame_complete_semaphore, portMAX_DELAY);
} }
+4
View File
@@ -79,6 +79,10 @@ list(APPEND REQUIRES_LIST
if (DEFINED ENV{ESP_IDF_VERSION}) if (DEFINED ENV{ESP_IDF_VERSION})
list(APPEND REQUIRES_LIST esp_lvgl_port) list(APPEND REQUIRES_LIST esp_lvgl_port)
# required for lvgl_ppa
if (IDF_TARGET STREQUAL "esp32p4")
list(APPEND REQUIRES_LIST esp_driver_ppa)
endif ()
else () else ()
list(APPEND REQUIRES_LIST freertos_kernel) list(APPEND REQUIRES_LIST freertos_kernel)
endif () endif ()
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <lvgl.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
// True on targets with a PPA (Pixel Processing Accelerator) unit that lvgl_ppa can use to
// hardware-rotate rotated flush tiles instead of lv_draw_sw_rotate(). False (always) on the
// simulator and on ESP32 targets without a PPA (see SOC_PPA_SUPPORTED).
bool lvgl_ppa_is_supported(void);
// True when color_format has a PPA color mode this module supports (RGB565/RGB888 - see
// lvgl_ppa_color_mode()). LV_COLOR_FORMAT_I1 and others fall back to lv_draw_sw_rotate().
bool lvgl_ppa_supports_color_format(lv_color_format_t color_format);
// Lazily creates (on first call) a PPA client and an output buffer at least out_buffer_size_bytes
// large, big enough for the largest tile this display will ever rotate. Returns NULL on failure -
// callers should fall back to lv_draw_sw_rotate() when that happens. Not thread-safe; callers must
// already hold the LVGL lock (true for all flush_cb callers).
void* lvgl_ppa_get_or_create(size_t out_buffer_size_bytes);
void lvgl_ppa_delete(void* ppa_handle);
// Rotates the tightly-packed w x h block at in_buff by rotation (LV_DISPLAY_ROTATION_90/180/270 -
// numerically identical to ppa_srm_rotation_angle_t's CCW convention, see the .c file) into the
// PPA's own output buffer and returns it. in_buff must have no row padding (PPA reads pic_w/pic_h
// in pixels, not a byte stride) - true for every lvgl_display_flush_cb() tile, which LVGL always
// renders compact. Returns NULL on failure - caller should fall back to lv_draw_sw_rotate().
void* lvgl_ppa_rotate(
void* ppa_handle,
const uint8_t* in_buff,
int32_t w,
int32_t h,
lv_display_rotation_t rotation,
lv_color_format_t color_format,
bool swap_bytes
);
#ifdef __cplusplus
}
#endif
+72 -11
View File
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
#include <tactility/lvgl_display.h> #include <tactility/lvgl_display.h>
#include <tactility/lvgl_ppa.h>
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/driver.h> #include <tactility/driver.h>
#include <tactility/drivers/display.h> #include <tactility/drivers/display.h>
@@ -37,6 +39,13 @@ struct LvglDisplayCtx {
// display_mirror(); rotate_buf holds the rotated pixels and is sized like buf1. // display_mirror(); rotate_buf holds the rotated pixels and is sized like buf1.
bool sw_rotate; bool sw_rotate;
void* rotate_buf; void* rotate_buf;
// Lazily created on the first sw_rotate flush that needs it (see lvgl_display_rotate_tile()).
// Stays NULL - and every rotate falls back to rotate_buf/lv_draw_sw_rotate() - when the target
// has no PPA (lvgl_ppa_is_supported()), the color format has no PPA color mode
// (lvgl_ppa_supports_color_format()), or creating the PPA client/buffer failed once already.
void* ppa_handle;
bool ppa_unavailable;
bool ppa_eligible;
// Size of buf1/buf2 (each) - used by lvgl_display_fb_base() to range-check which real buffer // Size of buf1/buf2 (each) - used by lvgl_display_fb_base() to range-check which real buffer
// (for the fb-direct case) a given color_map pointer falls into. // (for the fb-direct case) a given color_map pointer falls into.
size_t buf_size_bytes; size_t buf_size_bytes;
@@ -166,7 +175,37 @@ static void* lvgl_display_fb_base(struct LvglDisplayCtx* ctx, const uint8_t* col
return ctx->buf1; return ctx->buf1;
} }
static void lvgl_display_flush_cb(lv_display_t* disp, const lv_area_t* area, uint8_t* color_map) { // Tries to rotate the tightly-packed w x h block at in_buff via PPA, returning the PPA output
// buffer on success or NULL if this tile/format/target can't use it - in which case the caller
// must fall back to lv_draw_sw_rotate() into ctx->rotate_buf. Lazily creates the PPA client on the
// first eligible call, sized to ctx->buf_size_bytes (the largest tile or full-frame buffer this
// display will ever flush - see lvgl_display_add()); once creation fails once, ppa_unavailable
// latches so later tiles don't retry it. Never asks PPA to byte-swap: lvgl_display_flush_cb()
// applies ctx->byte_swap itself, once, at a single point regardless of which rotation path ran -
// simpler than tracking whether it was already done by PPA (PARTIAL mode) vs. already done
// per-tile before FULL mode's whole-frame rotate (see the two call sites).
static void* lvgl_display_try_ppa_rotate(struct LvglDisplayCtx* ctx, const uint8_t* in_buff, int32_t w, int32_t h,
lv_display_rotation_t rotation, lv_color_format_t color_format) {
if (!ctx->ppa_eligible || ctx->ppa_unavailable) {
return NULL;
}
// PPA reads pic_w/pic_h in pixels with no separate stride - only safe when the tile has no
// row padding beyond w * bytes-per-pixel (see lvgl_ppa.h).
uint32_t bpp = lv_color_format_get_size(color_format);
if (lv_draw_buf_width_to_stride(w, color_format) != (uint32_t)w * bpp) {
return NULL;
}
if (ctx->ppa_handle == NULL) {
ctx->ppa_handle = lvgl_ppa_get_or_create(ctx->buf_size_bytes);
if (ctx->ppa_handle == NULL) {
ctx->ppa_unavailable = true;
return NULL;
}
}
return lvgl_ppa_rotate(ctx->ppa_handle, in_buff, w, h, rotation, color_format, false);
}
static void lvgl_display_flush_cb(lv_display_t* disp, const lv_area_t* area, uint8_t* color_map) {
struct LvglDisplayCtx* ctx = (struct LvglDisplayCtx*)lv_display_get_driver_data(disp); struct LvglDisplayCtx* ctx = (struct LvglDisplayCtx*)lv_display_get_driver_data(disp);
bool is_i1 = lv_display_get_color_format(disp) == LV_COLOR_FORMAT_I1; bool is_i1 = lv_display_get_color_format(disp) == LV_COLOR_FORMAT_I1;
@@ -190,14 +229,20 @@ static void* lvgl_display_fb_base(struct LvglDisplayCtx* ctx, const uint8_t* col
lv_color_format_t color_format = lv_display_get_color_format(disp); lv_color_format_t color_format = lv_display_get_color_format(disp);
int32_t w = x2 - x1 + 1; int32_t w = x2 - x1 + 1;
int32_t h = y2 - y1 + 1; int32_t h = y2 - y1 + 1;
uint32_t w_stride = lv_draw_buf_width_to_stride(w, color_format);
uint32_t h_stride = lv_draw_buf_width_to_stride(h, color_format); void* ppa_out = lvgl_display_try_ppa_rotate(ctx, color_map, w, h, rotation, color_format);
if (rotation == LV_DISPLAY_ROTATION_180) { if (ppa_out != NULL) {
lv_draw_sw_rotate(color_map, ctx->rotate_buf, w, h, w_stride, w_stride, rotation, color_format); color_map = (uint8_t*)ppa_out;
} else { } else {
lv_draw_sw_rotate(color_map, ctx->rotate_buf, w, h, w_stride, h_stride, rotation, color_format); uint32_t w_stride = lv_draw_buf_width_to_stride(w, color_format);
uint32_t h_stride = lv_draw_buf_width_to_stride(h, color_format);
if (rotation == LV_DISPLAY_ROTATION_180) {
lv_draw_sw_rotate(color_map, ctx->rotate_buf, w, h, w_stride, w_stride, rotation, color_format);
} else {
lv_draw_sw_rotate(color_map, ctx->rotate_buf, w, h, w_stride, h_stride, rotation, color_format);
}
color_map = (uint8_t*)ctx->rotate_buf;
} }
color_map = (uint8_t*)ctx->rotate_buf;
lv_area_t rotated_area = { x1, y1, x2, y2 }; lv_area_t rotated_area = { x1, y1, x2, y2 };
lv_display_rotate_area(disp, &rotated_area); lv_display_rotate_area(disp, &rotated_area);
x1 = rotated_area.x1; x1 = rotated_area.x1;
@@ -246,10 +291,16 @@ static void* lvgl_display_fb_base(struct LvglDisplayCtx* ctx, const uint8_t* col
bool swapped_wh = rotation == LV_DISPLAY_ROTATION_90 || rotation == LV_DISPLAY_ROTATION_270; bool swapped_wh = rotation == LV_DISPLAY_ROTATION_90 || rotation == LV_DISPLAY_ROTATION_270;
int32_t logical_w = swapped_wh ? (int32_t)vres : (int32_t)hres; int32_t logical_w = swapped_wh ? (int32_t)vres : (int32_t)hres;
int32_t logical_h = swapped_wh ? (int32_t)hres : (int32_t)vres; int32_t logical_h = swapped_wh ? (int32_t)hres : (int32_t)vres;
uint32_t src_stride = lv_draw_buf_width_to_stride(logical_w, color_format);
uint32_t dest_stride = lv_draw_buf_width_to_stride(hres, color_format); void* ppa_out = lvgl_display_try_ppa_rotate(ctx, fb_base, logical_w, logical_h, rotation, color_format);
lv_draw_sw_rotate(fb_base, ctx->rotate_buf, logical_w, logical_h, src_stride, dest_stride, rotation, color_format); if (ppa_out != NULL) {
fb_base = (uint8_t*)ctx->rotate_buf; fb_base = (uint8_t*)ppa_out;
} else {
uint32_t src_stride = lv_draw_buf_width_to_stride(logical_w, color_format);
uint32_t dest_stride = lv_draw_buf_width_to_stride(hres, color_format);
lv_draw_sw_rotate(fb_base, ctx->rotate_buf, logical_w, logical_h, src_stride, dest_stride, rotation, color_format);
fb_base = (uint8_t*)ctx->rotate_buf;
}
} }
display_draw_bitmap(ctx->device, 0, 0, hres, vres, fb_base); display_draw_bitmap(ctx->device, 0, 0, hres, vres, fb_base);
@@ -291,6 +342,13 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig*
ctx->device = device; ctx->device = device;
ctx->byte_swap = config->swap_bytes; ctx->byte_swap = config->swap_bytes;
ctx->sw_rotate = config->sw_rotate; ctx->sw_rotate = config->sw_rotate;
// Only relevant when sw_rotate is set - lvgl_display_try_ppa_rotate() also checks
// ctx->ppa_eligible directly, so this is safe to compute unconditionally.
ctx->ppa_eligible = lvgl_ppa_is_supported() && lvgl_ppa_supports_color_format(lv_color_format);
if (config->sw_rotate && !ctx->ppa_eligible) {
LOG_I(TAG, "PPA not available for this display (supported=%d, color_format=%d) - using lv_draw_sw_rotate()",
(int)lvgl_ppa_is_supported(), (int)lv_color_format);
}
ctx->has_swap_xy_cap = display_has_capability(device, DISPLAY_CAPABILITY_CAP_SWAP_XY); ctx->has_swap_xy_cap = display_has_capability(device, DISPLAY_CAPABILITY_CAP_SWAP_XY);
ctx->has_mirror_cap = display_has_capability(device, DISPLAY_CAPABILITY_CAP_MIRROR); ctx->has_mirror_cap = display_has_capability(device, DISPLAY_CAPABILITY_CAP_MIRROR);
ctx->has_set_gap_cap = display_has_capability(device, DISPLAY_CAPABILITY_CAP_SET_GAP); ctx->has_set_gap_cap = display_has_capability(device, DISPLAY_CAPABILITY_CAP_SET_GAP);
@@ -424,6 +482,9 @@ void lvgl_display_remove(lv_display_t* display) {
if (ctx->rotate_buf != NULL) { if (ctx->rotate_buf != NULL) {
lvgl_display_free_buffer(ctx->rotate_buf); lvgl_display_free_buffer(ctx->rotate_buf);
} }
if (ctx->ppa_handle != NULL) {
lvgl_ppa_delete(ctx->ppa_handle);
}
free(ctx); free(ctx);
} }
} }
+182
View File
@@ -0,0 +1,182 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/lvgl_ppa.h>
#include <tactility/log.h>
#ifdef ESP_PLATFORM
#include <soc/soc_caps.h>
#endif
#if defined(ESP_PLATFORM) && SOC_PPA_SUPPORTED
#include <driver/ppa.h>
#include <esp_heap_caps.h>
#include <inttypes.h>
#define TAG "lvgl_ppa"
// PPA output buffer alignment requirement (address AND size - see lvgl_ppa_get_or_create()):
// cache line size is hardware-specific, but 64 bytes covers every current PPA-capable target's
// L1/L2 cache line size, so a fixed constant avoids needing per-SoC Kconfig lookups here.
#define LVGL_PPA_ALIGN 64
#define LVGL_PPA_ALIGN_UP(n) (((n) + (LVGL_PPA_ALIGN - 1)) & ~(size_t)(LVGL_PPA_ALIGN - 1))
struct LvglPpa {
ppa_client_handle_t client;
uint8_t* out_buffer;
size_t out_buffer_size;
bool logged_first_rotate;
};
bool lvgl_ppa_is_supported(void) {
return true;
}
bool lvgl_ppa_supports_color_format(lv_color_format_t color_format) {
return color_format == LV_COLOR_FORMAT_RGB565 || color_format == LV_COLOR_FORMAT_RGB888;
}
static ppa_srm_color_mode_t lvgl_ppa_color_mode(lv_color_format_t color_format) {
return color_format == LV_COLOR_FORMAT_RGB888 ? PPA_SRM_COLOR_MODE_RGB888 : PPA_SRM_COLOR_MODE_RGB565;
}
void* lvgl_ppa_get_or_create(size_t out_buffer_size_bytes) {
struct LvglPpa* ppa = (struct LvglPpa*)calloc(1, sizeof(struct LvglPpa));
if (ppa == NULL) {
return NULL;
}
// PPA requires the output buffer's size, not just its address, to be cache-line aligned (see
// ppa_out_pic_blk_config_t's buffer_size doc) - round up before allocating so both match.
size_t aligned_size = LVGL_PPA_ALIGN_UP(out_buffer_size_bytes);
ppa->out_buffer = (uint8_t*)heap_caps_aligned_alloc(LVGL_PPA_ALIGN, aligned_size, MALLOC_CAP_DMA | MALLOC_CAP_8BIT);
if (ppa->out_buffer == NULL) {
ppa->out_buffer = (uint8_t*)heap_caps_aligned_alloc(LVGL_PPA_ALIGN, aligned_size, MALLOC_CAP_DEFAULT);
}
if (ppa->out_buffer == NULL) {
LOG_E(TAG, "Failed to allocate PPA output buffer (%u bytes)", (unsigned)aligned_size);
free(ppa);
return NULL;
}
ppa->out_buffer_size = aligned_size;
ppa_client_config_t client_config = {
.oper_type = PPA_OPERATION_SRM,
};
if (ppa_register_client(&client_config, &ppa->client) != ESP_OK) {
LOG_E(TAG, "Failed to register PPA client");
heap_caps_free(ppa->out_buffer);
free(ppa);
return NULL;
}
LOG_I(TAG, "PPA client created (out buffer: %u bytes)", (unsigned)aligned_size);
return ppa;
}
void lvgl_ppa_delete(void* ppa_handle) {
if (ppa_handle == NULL) {
return;
}
struct LvglPpa* ppa = (struct LvglPpa*)ppa_handle;
ppa_unregister_client(ppa->client);
heap_caps_free(ppa->out_buffer);
free(ppa);
}
void* lvgl_ppa_rotate(
void* ppa_handle,
const uint8_t* in_buff,
int32_t w,
int32_t h,
lv_display_rotation_t rotation,
lv_color_format_t color_format,
bool swap_bytes
) {
struct LvglPpa* ppa = (struct LvglPpa*)ppa_handle;
ppa_srm_color_mode_t color_mode = lvgl_ppa_color_mode(color_format);
bool swapped_wh = rotation == LV_DISPLAY_ROTATION_90 || rotation == LV_DISPLAY_ROTATION_270;
int32_t out_w = swapped_wh ? h : w;
int32_t out_h = swapped_wh ? w : h;
const ppa_srm_oper_config_t oper_config = {
.in = {
.buffer = in_buff,
.pic_w = (uint32_t)w,
.pic_h = (uint32_t)h,
.block_w = (uint32_t)w,
.block_h = (uint32_t)h,
.block_offset_x = 0,
.block_offset_y = 0,
.srm_cm = color_mode,
},
.out = {
.buffer = ppa->out_buffer,
.buffer_size = ppa->out_buffer_size,
.pic_w = (uint32_t)out_w,
.pic_h = (uint32_t)out_h,
.block_offset_x = 0,
.block_offset_y = 0,
.srm_cm = color_mode,
},
// LVGL rotation is CCW, same convention as ppa_srm_rotation_angle_t - see lvgl_ppa.h.
.rotation_angle = (ppa_srm_rotation_angle_t)rotation,
.scale_x = 1.0f,
.scale_y = 1.0f,
.byte_swap = swap_bytes,
.mode = PPA_TRANS_MODE_BLOCKING,
};
if (ppa_do_scale_rotate_mirror(ppa->client, &oper_config) != ESP_OK) {
LOG_E(TAG, "PPA rotate failed (%" PRId32 "x%" PRId32 " -> %" PRId32 "x%" PRId32 ")", w, h, out_w, out_h);
return NULL;
}
if (!ppa->logged_first_rotate) {
LOG_I(TAG, "PPA rotate in use (%" PRId32 "x%" PRId32 " -> %" PRId32 "x%" PRId32 ", angle=%d)", w, h, out_w, out_h, (int)rotation);
ppa->logged_first_rotate = true;
}
return ppa->out_buffer;
}
#else // !(ESP_PLATFORM && SOC_PPA_SUPPORTED)
bool lvgl_ppa_is_supported(void) {
return false;
}
bool lvgl_ppa_supports_color_format(lv_color_format_t color_format) {
(void)color_format;
return false;
}
void* lvgl_ppa_get_or_create(size_t out_buffer_size_bytes) {
(void)out_buffer_size_bytes;
return NULL;
}
void lvgl_ppa_delete(void* ppa_handle) {
(void)ppa_handle;
}
void* lvgl_ppa_rotate(
void* ppa_handle,
const uint8_t* in_buff,
int32_t w,
int32_t h,
lv_display_rotation_t rotation,
lv_color_format_t color_format,
bool swap_bytes
) {
(void)ppa_handle;
(void)in_buff;
(void)w;
(void)h;
(void)rotation;
(void)color_format;
(void)swap_bytes;
return NULL;
}
#endif