Drivers, device migrations and other improvements (#566)

- RGB display driver added
- display_driver API:
  - Added software-based display rotation for screens without hardware rotation support.
  - Improved display capability detection across supported display drivers.
- lvgl_display driver:
  - Improved framebuffer handling for smoother, more reliable display updates.
  - Display gap and rotation behavior now adapts more accurately to hardware capabilities.
- Migration Crowpanel 5" basic & advance to kernel drivers
- Improve KernelDisplay app: brightness controls are hidden for displays that only support on/off operation
- device.py now allows for (non-ambiguous) partial device IDs
- TactilityKernel: Implement more tests
This commit is contained in:
Ken Van Hoeylandt
2026-07-15 22:56:21 +02:00
committed by GitHub
parent bd8fdfd858
commit 6fb2bb736c
16 changed files with 627 additions and 86 deletions
+9 -18
View File
@@ -54,11 +54,7 @@ constexpr uint8_t MADCTL_BIT_PAGE_COLUMN_ORDER = 5; // swap-xy
constexpr uint8_t MADCTL_BIT_COLUMN_ADDRESS_ORDER = 6; // mirror-x
constexpr uint8_t MADCTL_BIT_PAGE_ADDRESS_ORDER = 7; // mirror-y
// Bring-up command list, ported verbatim (byte-for-byte) from the deprecated HAL's hx8357.c
// initd[] table (Devices/unphone/Source/hx8357/hx8357.c) - the HX8357B variant (initb[]) is
// dead code there (displayType is hardcoded to HX8357D) and was not ported. Format matches the
// original: cmd, then a length byte (bit7 set means "this is actually a delay in 5ms units, no
// data follows"), then that many data bytes. A single 0 cmd byte ends the list.
// Bring-up command list
constexpr uint8_t INIT_CMDS[] = {
HX8357_SWRESET, 0x80 + 100 / 5, // Soft reset, then delay 100 ms
HX8357D_SETC, 3,
@@ -118,11 +114,6 @@ struct Hx8357Internal {
spi_device_handle_t spi_handle;
gpio_num_t dc_pin;
size_t max_transfer_size;
// Live MADCTL orientation bits, seeded from config at start() and updated in place by
// mirror()/swap_xy() - NOT reconstructed from the static config each call. swap_xy() and
// mirror() are invoked back-to-back by lvgl_display_apply_rotation() on every rotation
// change, so rebuilding from config would silently discard whichever bit the other call
// just set.
bool swap_xy;
bool mirror_x;
bool mirror_y;
@@ -379,13 +370,6 @@ static bool hx8357_get_mirror_y(Device* device) {
return GET_CONFIG(device)->mirror_y;
}
static error_t hx8357_set_gap(Device*, int32_t, int32_t) {
// Not supported by this controller's fixed CASET/PASET-per-draw protocol beyond the static
// gap-x/gap-y devicetree config already folded into draw_bitmap(); matches the deprecated
// HAL, which never exposed a runtime gap either.
return ERROR_NOT_SUPPORTED;
}
static error_t hx8357_invert_color(Device* device, bool invert_color_data) {
auto* internal = static_cast<Hx8357Internal*>(device_get_driver_data(device));
send_cmd(internal, invert_color_data ? HX8357_INVON : HX8357_INVOFF);
@@ -436,6 +420,9 @@ static error_t hx8357_get_backlight(Device* device, Device** backlight) {
// endregion
static const DisplayApi hx8357_display_api = {
.capabilities = DISPLAY_CAPABILITY_CAP_MIRROR | DISPLAY_CAPABILITY_CAP_SWAP_XY |
DISPLAY_CAPABILITY_INVERT_COLOR | DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_SLEEP |
DISPLAY_CAPABILITY_BACKLIGHT,
.reset = hx8357_reset,
.init = hx8357_init,
.draw_bitmap = hx8357_draw_bitmap,
@@ -444,7 +431,10 @@ static const DisplayApi hx8357_display_api = {
.get_swap_xy = hx8357_get_swap_xy,
.get_mirror_x = hx8357_get_mirror_x,
.get_mirror_y = hx8357_get_mirror_y,
.set_gap = hx8357_set_gap,
// set_gap is not exposed: this controller's fixed CASET/PASET-per-draw protocol only supports the
// static gap-x/gap-y devicetree config already folded into draw_bitmap(); matches the deprecated
// HAL, which never exposed a runtime gap either.
.set_gap = nullptr,
.invert_color = hx8357_invert_color,
.disp_on_off = hx8357_disp_on_off,
.disp_sleep = hx8357_disp_sleep,
@@ -454,6 +444,7 @@ static const DisplayApi hx8357_display_api = {
.get_frame_buffer = hx8357_get_frame_buffer,
.get_frame_buffer_count = hx8357_get_frame_buffer_count,
.get_backlight = hx8357_get_backlight,
.has_capability = nullptr,
};
Driver hx8357_driver = {
@@ -300,6 +300,9 @@ static error_t ili9341_get_backlight(Device* device, Device** backlight) {
// endregion
static const DisplayApi ili9341_display_api = {
.capabilities = DISPLAY_CAPABILITY_CAP_MIRROR | DISPLAY_CAPABILITY_CAP_SWAP_XY |
DISPLAY_CAPABILITY_CAP_SET_GAP | DISPLAY_CAPABILITY_INVERT_COLOR | DISPLAY_CAPABILITY_ON_OFF |
DISPLAY_CAPABILITY_SLEEP | DISPLAY_CAPABILITY_BACKLIGHT,
.reset = ili9341_reset,
.init = ili9341_init,
.draw_bitmap = ili9341_draw_bitmap,
@@ -318,6 +321,7 @@ static const DisplayApi ili9341_display_api = {
.get_frame_buffer = ili9341_get_frame_buffer,
.get_frame_buffer_count = ili9341_get_frame_buffer_count,
.get_backlight = ili9341_get_backlight,
.has_capability = nullptr,
};
Driver ili9341_driver = {
@@ -291,6 +291,9 @@ static error_t ili9488_get_backlight(Device* device, Device** backlight) {
// endregion
static const DisplayApi ili9488_display_api = {
.capabilities = DISPLAY_CAPABILITY_CAP_MIRROR | DISPLAY_CAPABILITY_CAP_SWAP_XY |
DISPLAY_CAPABILITY_CAP_SET_GAP | DISPLAY_CAPABILITY_INVERT_COLOR | DISPLAY_CAPABILITY_ON_OFF |
DISPLAY_CAPABILITY_SLEEP | DISPLAY_CAPABILITY_BACKLIGHT,
.reset = ili9488_reset,
.init = ili9488_init,
.draw_bitmap = ili9488_draw_bitmap,
@@ -309,6 +312,7 @@ static const DisplayApi ili9488_display_api = {
.get_frame_buffer = ili9488_get_frame_buffer,
.get_frame_buffer_count = ili9488_get_frame_buffer_count,
.get_backlight = ili9488_get_backlight,
.has_capability = nullptr,
};
Driver ili9488_driver = {
@@ -34,8 +34,11 @@ struct RgbDisplayInternal {
esp_lcd_panel_handle_t panel_handle;
void* frame_buffers[MAX_CACHED_FRAME_BUFFERS];
uint8_t frame_buffer_count;
// Size of each buffer in frame_buffers, in bytes - used to range-check whether a given
// draw_bitmap() color_data pointer is actually one of them (see rgb_display_draw_bitmap()).
size_t frame_buffer_size_bytes;
// Signaled by on_frame_buf_complete once per real DMA scan-out of a whole frame. Only
// waited on in draw_bitmap() when frame_buffer_count > 0 - see the comment there for why.
// waited on in draw_bitmap() when color_data is one of frame_buffers - see the comment there for why.
SemaphoreHandle_t frame_complete_semaphore;
};
@@ -89,6 +92,8 @@ static error_t perform_hardware_reset(const RgbDisplayConfig* config) {
static error_t cache_frame_buffers(RgbDisplayInternal* internal, const RgbDisplayConfig* config) {
internal->frame_buffer_count = 0;
internal->frame_buffer_size_bytes = (size_t)config->horizontal_resolution * config->vertical_resolution *
((config->bits_per_pixel + 7) / 8);
if (config->num_fbs == 0) {
return ERROR_NONE;
}
@@ -277,16 +282,26 @@ static error_t rgb_display_init(Device* device) {
return esp_lcd_panel_init(internal->panel_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
// Only block for scan-out completion when color_data is actually one of the panel's own frame
// buffers (see on_frame_buf_complete's comment above for why that matters) - i.e. this specific
// call is a zero-copy flip, not a plain CPU copy into the panel's buffer from a caller-owned one
// (e.g. LVGL bound in owned-buffer mode), which has no reuse race to guard against and shouldn't
// pay the up-to-one-frame latency cost for every partial update.
static bool rgb_display_color_data_is_frame_buffer(const RgbDisplayInternal* internal, const void* color_data) {
const auto* ptr = static_cast<const uint8_t*>(color_data);
for (uint8_t i = 0; i < internal->frame_buffer_count; i++) {
const auto* base = static_cast<const uint8_t*>(internal->frame_buffers[i]);
if (ptr >= base && ptr < base + internal->frame_buffer_size_bytes) {
return true;
}
}
return false;
}
static error_t rgb_display_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<RgbDisplayInternal*>(device_get_driver_data(device));
// Only block for scan-out completion when the caller could be writing straight into one of
// the panel's own frame buffers (see on_frame_buf_complete's comment above for why that
// matters). With no real frame buffer of ours involved (frame_buffer_count == 0, e.g. LVGL
// partial-render mode with its own separate buffer), draw_bitmap does a real memcpy into the
// panel's buffer and there's no reuse race to guard against, so don't pay the up-to-one-frame
// latency cost for every small partial update.
bool wait_for_scanout = internal->frame_buffer_count > 0;
bool wait_for_scanout = rgb_display_color_data_is_frame_buffer(internal, color_data);
if (wait_for_scanout) {
xSemaphoreTake(internal->frame_complete_semaphore, 0); // clear any already-pending signal
}
@@ -324,11 +339,8 @@ static bool rgb_display_get_mirror_y(Device* device) {
return GET_CONFIG(device)->mirror_y;
}
// RGB panels are raw scan-out framebuffers with no addressable-window concept the way MIPI/SPI
// panels have, so there's no gap to set.
static error_t rgb_display_set_gap(Device*, int32_t, int32_t) {
return ERROR_NOT_SUPPORTED;
}
// set_gap is not exposed: RGB panels are raw scan-out framebuffers with no addressable-window
// concept the way MIPI/SPI panels have, so there's no gap to set.
static error_t rgb_display_invert_color(Device* device, bool invert_color_data) {
auto* internal = static_cast<RgbDisplayInternal*>(device_get_driver_data(device));
@@ -340,10 +352,8 @@ static error_t rgb_display_disp_on_off(Device* device, bool on_off) {
return esp_lcd_panel_disp_on_off(internal->panel_handle, on_off) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
// RGB panels have no MIPI DCS command interface, so there's no sleep mode to enter.
static error_t rgb_display_disp_sleep(Device*, bool) {
return ERROR_NOT_SUPPORTED;
}
// disp_sleep is not exposed: RGB panels have no MIPI DCS command interface, so there's no sleep
// mode to enter.
static enum DisplayColorFormat rgb_display_get_color_format(Device*) {
return DISPLAY_COLOR_FORMAT_RGB565;
@@ -376,9 +386,28 @@ static error_t rgb_display_get_backlight(Device* device, Device** backlight) {
return ERROR_NONE;
}
constexpr uint32_t RGB_DISPLAY_CAPABILITIES = DISPLAY_CAPABILITY_CAP_MIRROR | DISPLAY_CAPABILITY_CAP_SWAP_XY |
DISPLAY_CAPABILITY_INVERT_COLOR | DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_BACKLIGHT;
// esp_lcd_panel_rgb only applies mirror()/swap_xy()'s rotate_mask when draw_bitmap()'s color_data
// is copied into the frame buffer by CPU. When frame_buffer_count > 0, LVGL is bound directly onto
// the panel's own frame buffers (see lvgl_display.c), so color_data always already *is* the frame
// buffer and that copy - and with it the rotation - never happens. Report those two capabilities as
// unavailable in that configuration so callers (e.g. lvgl_display.c) don't rely on a rotation that
// silently does nothing.
static bool rgb_display_has_capability(Device* device, uint32_t capability) {
auto* internal = static_cast<RgbDisplayInternal*>(device_get_driver_data(device));
uint32_t capabilities = RGB_DISPLAY_CAPABILITIES;
if (internal->frame_buffer_count > 0) {
capabilities &= ~(DISPLAY_CAPABILITY_CAP_MIRROR | DISPLAY_CAPABILITY_CAP_SWAP_XY);
}
return (capabilities & capability) == capability;
}
// endregion
static const DisplayApi rgb_display_api = {
.capabilities = RGB_DISPLAY_CAPABILITIES,
.reset = rgb_display_reset,
.init = rgb_display_init,
.draw_bitmap = rgb_display_draw_bitmap,
@@ -387,16 +416,17 @@ static const DisplayApi rgb_display_api = {
.get_swap_xy = rgb_display_get_swap_xy,
.get_mirror_x = rgb_display_get_mirror_x,
.get_mirror_y = rgb_display_get_mirror_y,
.set_gap = rgb_display_set_gap,
.set_gap = nullptr,
.invert_color = rgb_display_invert_color,
.disp_on_off = rgb_display_disp_on_off,
.disp_sleep = rgb_display_disp_sleep,
.disp_sleep = nullptr,
.get_color_format = rgb_display_get_color_format,
.get_resolution_x = rgb_display_get_resolution_x,
.get_resolution_y = rgb_display_get_resolution_y,
.get_frame_buffer = rgb_display_get_frame_buffer,
.get_frame_buffer_count = rgb_display_get_frame_buffer_count,
.get_backlight = rgb_display_get_backlight,
.has_capability = rgb_display_has_capability,
};
Driver rgb_display_driver = {
@@ -324,6 +324,9 @@ static error_t st7789_i8080_get_backlight(Device* device, Device** backlight) {
// endregion
static const DisplayApi st7789_i8080_display_api = {
.capabilities = DISPLAY_CAPABILITY_CAP_MIRROR | DISPLAY_CAPABILITY_CAP_SWAP_XY |
DISPLAY_CAPABILITY_CAP_SET_GAP | DISPLAY_CAPABILITY_INVERT_COLOR | DISPLAY_CAPABILITY_ON_OFF |
DISPLAY_CAPABILITY_SLEEP | DISPLAY_CAPABILITY_BACKLIGHT,
.reset = st7789_i8080_reset,
.init = st7789_i8080_init,
.draw_bitmap = st7789_i8080_draw_bitmap,
@@ -342,6 +345,7 @@ static const DisplayApi st7789_i8080_display_api = {
.get_frame_buffer = st7789_i8080_get_frame_buffer,
.get_frame_buffer_count = st7789_i8080_get_frame_buffer_count,
.get_backlight = st7789_i8080_get_backlight,
.has_capability = nullptr,
};
Driver st7789_i8080_driver = {
+4
View File
@@ -294,6 +294,9 @@ static error_t st7789_get_backlight(Device* device, Device** backlight) {
// endregion
static const DisplayApi st7789_display_api = {
.capabilities = DISPLAY_CAPABILITY_CAP_MIRROR | DISPLAY_CAPABILITY_CAP_SWAP_XY |
DISPLAY_CAPABILITY_CAP_SET_GAP | DISPLAY_CAPABILITY_INVERT_COLOR | DISPLAY_CAPABILITY_ON_OFF |
DISPLAY_CAPABILITY_SLEEP | DISPLAY_CAPABILITY_BACKLIGHT,
.reset = st7789_reset,
.init = st7789_init,
.draw_bitmap = st7789_draw_bitmap,
@@ -312,6 +315,7 @@ static const DisplayApi st7789_display_api = {
.get_frame_buffer = st7789_get_frame_buffer,
.get_frame_buffer_count = st7789_get_frame_buffer_count,
.get_backlight = st7789_get_backlight,
.has_capability = nullptr,
};
Driver st7789_driver = {