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:
committed by
GitHub
parent
3b5a401594
commit
d896657bf9
@@ -35,6 +35,24 @@ struct LvglDisplayConfig {
|
||||
* Allocates one extra buffer sized like the primary draw buffer.
|
||||
*/
|
||||
bool sw_rotate;
|
||||
|
||||
/**
|
||||
* Endianness of the 2 bytes of each RGB565/BGR565 pixel sent to the panel. False (default)
|
||||
* keeps this little-endian CPU's native byte order (no-op). True swaps the 2 bytes of every
|
||||
* pixel (big-endian) in the flush callback, via lv_draw_sw_rgb565_swap() - for panels that
|
||||
* expect the opposite byte order over the bus. Ignored for color formats other than
|
||||
* RGB565/BGR565 (e.g. RGB888, MONOCHROME).
|
||||
*/
|
||||
bool swap_bytes;
|
||||
|
||||
/**
|
||||
* Forces LV_DISPLAY_RENDER_MODE_FULL with a full-resolution buffer, ignoring buffer_height,
|
||||
* and always flushes the entire display rather than per-tile dirty regions. Set this when the
|
||||
* device reports DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME - see that capability's doc comment.
|
||||
* Ignored when the device exposes its own frame buffer(s) (already always-full-frame) or uses
|
||||
* the LV_COLOR_FORMAT_I1 path (already always-full-frame).
|
||||
*/
|
||||
bool force_full_frame;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -37,8 +37,16 @@ void lvgl_devices_attach() {
|
||||
// sized to 1/10th of the vertical resolution; it fits comfortably everywhere,
|
||||
// including boards with PSRAM that could afford full-frame.
|
||||
uint16_t vres = display_get_resolution_y(kernel_display_device);
|
||||
// _SWAPPED variants need it outright; plain BGR565 panels have also been found on real
|
||||
// hardware to need it alongside their MADCTL BGR bit (see st7735-module/st7789-module).
|
||||
enum DisplayColorFormat color_format = display_get_color_format(kernel_display_device);
|
||||
bool swap_bytes = color_format == DISPLAY_COLOR_FORMAT_RGB565_SWAPPED ||
|
||||
color_format == DISPLAY_COLOR_FORMAT_BGR565_SWAPPED ||
|
||||
color_format == DISPLAY_COLOR_FORMAT_BGR565;
|
||||
struct LvglDisplayConfig lvgl_display_config = {
|
||||
.buffer_height = vres > 10 ? vres / 10 : vres
|
||||
.buffer_height = vres > 10 ? vres / 10 : vres,
|
||||
.swap_bytes = swap_bytes,
|
||||
.force_full_frame = display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME)
|
||||
};
|
||||
if (lvgl_display_add(kernel_display_device, &lvgl_display_config, &lvgl_display) == ERROR_NONE) {
|
||||
LOG_I(TAG, "Bound %s to LVGL", kernel_display_device->name);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/lvgl_display.h>
|
||||
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/drivers/display.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
@@ -17,10 +19,20 @@ struct LvglDisplayCtx {
|
||||
void* buf1;
|
||||
void* buf2;
|
||||
bool owns_buffers; // false when buf1/buf2 point at the device's own frame buffer(s)
|
||||
// Mirrors what lvgl_display_add() passed to lv_display_set_render_mode() - there's no
|
||||
// lv_display_get_render_mode() to query it back from LVGL, so it's cached here instead.
|
||||
lv_display_render_mode_t render_mode;
|
||||
// The device's swap_xy/mirror_x/mirror_y at bind time, queried once and treated as the LV_DISPLAY_ROTATION_0 baseline.
|
||||
bool base_swap_xy;
|
||||
bool base_mirror_x;
|
||||
bool base_mirror_y;
|
||||
// The device's configured gap at bind time, in the same LV_DISPLAY_ROTATION_0 baseline frame
|
||||
// as base_swap_xy above. set_gap() is a raw (x,y) offset applied to whatever coordinates the
|
||||
// panel is currently being drawn with - it has no idea about swap_xy, so a rotation that flips
|
||||
// swap_xy relative to this baseline must swap gap_x/gap_y too (see lvgl_display_apply_rotation()).
|
||||
int32_t base_gap_x;
|
||||
int32_t base_gap_y;
|
||||
bool has_set_gap_cap;
|
||||
// When true, rotation is done in software in the flush callback instead of via display_swap_xy()/
|
||||
// display_mirror(); rotate_buf holds the rotated pixels and is sized like buf1.
|
||||
bool sw_rotate;
|
||||
@@ -32,12 +44,8 @@ struct LvglDisplayCtx {
|
||||
// null on drivers that don't support them, so rotation handling must not call through blindly.
|
||||
bool has_swap_xy_cap;
|
||||
bool has_mirror_cap;
|
||||
// True for DISPLAY_COLOR_FORMAT_RGB565_SWAPPED: the panel wants each 16-bit pixel high-byte-first
|
||||
// over SPI, which this little-endian CPU's native RGB565 buffer isn't. LVGL's own
|
||||
// LV_COLOR_FORMAT_RGB565_SWAPPED display format looked like the fit but isn't fully supported by
|
||||
// the SW renderer (produced garbage on real hardware) - esp_lvgl_port's own approach is instead to
|
||||
// render normal RGB565 and byte-swap the flushed rect in place just before sending it out, via
|
||||
// lv_draw_sw_rgb565_swap(). Mirrored here.
|
||||
// Mirrors LvglDisplayConfig::swap_bytes: the panel is big endian while the OS is little endian,
|
||||
// so we fix it in software. In the future, the driver should probably expose endianness requirements instead.
|
||||
bool byte_swap;
|
||||
};
|
||||
|
||||
@@ -61,25 +69,28 @@ static void lvgl_display_free_buffer(void* buf) {
|
||||
#endif
|
||||
}
|
||||
|
||||
// out_byte_swap is set whenever the LVGL-side buffer format doesn't already match the panel's
|
||||
// wire byte order and needs a post-render swap in the flush callback (see LvglDisplayCtx::byte_swap).
|
||||
static bool lvgl_display_map_color_format(enum DisplayColorFormat in, lv_color_format_t* out, bool* out_byte_swap) {
|
||||
// Resolves the kernel-reported color format to an LVGL color format. RGB565 and BGR565 (and
|
||||
// their _SWAPPED variants) all render identically as far as LVGL is concerned - it has no native
|
||||
// concept of channel order, only byte order (see LvglDisplayConfig::swap_bytes for that axis).
|
||||
static bool lvgl_display_map_color_format(enum DisplayColorFormat in, lv_color_format_t* out) {
|
||||
switch (in) {
|
||||
case DISPLAY_COLOR_FORMAT_RGB565:
|
||||
*out = LV_COLOR_FORMAT_RGB565;
|
||||
*out_byte_swap = false;
|
||||
return true;
|
||||
case DISPLAY_COLOR_FORMAT_RGB565_SWAPPED:
|
||||
case DISPLAY_COLOR_FORMAT_BGR565:
|
||||
case DISPLAY_COLOR_FORMAT_BGR565_SWAPPED:
|
||||
*out = LV_COLOR_FORMAT_RGB565;
|
||||
*out_byte_swap = true;
|
||||
return true;
|
||||
case DISPLAY_COLOR_FORMAT_RGB888:
|
||||
*out = LV_COLOR_FORMAT_RGB888;
|
||||
*out_byte_swap = false;
|
||||
return true;
|
||||
case DISPLAY_COLOR_FORMAT_MONOCHROME:
|
||||
// Row-major, MSB-first 1bpp (matches LV_COLOR_FORMAT_I1's raw layout once the
|
||||
// palette header is stripped, see lvgl_display_flush_cb()) - any page/tile
|
||||
// reformatting a specific panel's GDDRAM needs is that driver's own concern
|
||||
// (e.g. ssd1306_draw_bitmap()'s row-to-page transpose).
|
||||
*out = LV_COLOR_FORMAT_I1;
|
||||
return true;
|
||||
default:
|
||||
// DISPLAY_COLOR_FORMAT_BGR565/_SWAPPED: no LVGL equivalent (channel order, not byte order).
|
||||
// DISPLAY_COLOR_FORMAT_MONOCHROME: unsupported for now, no 1bpp conversion buffer implemented.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -125,6 +136,14 @@ static void lvgl_display_apply_rotation(struct LvglDisplayCtx* ctx, lv_display_r
|
||||
if (ctx->has_mirror_cap) {
|
||||
display_mirror(ctx->device, mirror_x, mirror_y);
|
||||
}
|
||||
if (ctx->has_set_gap_cap) {
|
||||
// set_gap() takes its (x,y) in whatever axes the panel is currently drawn with, not the
|
||||
// baseline's - swap gap_x/gap_y whenever this rotation's swap_xy differs from the baseline.
|
||||
bool gap_axes_swapped = swap_xy != ctx->base_swap_xy;
|
||||
int32_t gap_x = gap_axes_swapped ? ctx->base_gap_y : ctx->base_gap_x;
|
||||
int32_t gap_y = gap_axes_swapped ? ctx->base_gap_x : ctx->base_gap_y;
|
||||
display_set_gap(ctx->device, gap_x, gap_y);
|
||||
}
|
||||
}
|
||||
|
||||
static void lvgl_display_rotation_event_cb(lv_event_t* e) {
|
||||
@@ -143,8 +162,9 @@ static void* lvgl_display_fb_base(struct LvglDisplayCtx* ctx, const uint8_t* col
|
||||
return ctx->buf1;
|
||||
}
|
||||
|
||||
static void lvgl_display_flush_cb(lv_display_t* disp, const lv_area_t* area, uint8_t* color_map) {
|
||||
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);
|
||||
bool is_i1 = lv_display_get_color_format(disp) == LV_COLOR_FORMAT_I1;
|
||||
|
||||
int32_t x1 = area->x1;
|
||||
int32_t y1 = area->y1;
|
||||
@@ -180,21 +200,40 @@ static void lvgl_display_flush_cb(lv_display_t* disp, const lv_area_t* area, uin
|
||||
lv_draw_sw_rgb565_swap(color_map, area_size_px);
|
||||
}
|
||||
|
||||
if (ctx->owns_buffers) {
|
||||
if (ctx->render_mode == LV_DISPLAY_RENDER_MODE_FULL) {
|
||||
// FULL mode always redraws (and flushes) the whole display, but a refresh cycle can still
|
||||
// call this flush_cb once per still-unjoined invalidated area (lv_refr.c's
|
||||
// refr_invalid_areas()), each writing its own tile into the *same* shared buffer - only
|
||||
// the last call actually holds the complete frame. This applies equally whether that
|
||||
// buffer is one we own (owns_buffers, e.g. an I1 e-paper/OLED panel) or a real hardware
|
||||
// frame buffer (fb-direct, where display_draw_bitmap() - see rgb_display_draw_bitmap() -
|
||||
// additionally blocks for a full scan-out period whenever frame_buffer_count > 0).
|
||||
// Presenting on every call would send partially-rendered frames, and for fb-direct would
|
||||
// also pay that scan-out wait N times per refresh instead of once; defer to the last
|
||||
// flush and present the whole buffer in one call, mirroring esp_lvgl_port_disp.c's own
|
||||
// lv_disp_flush_is_last() gate for its direct/full render mode.
|
||||
if (lv_display_flush_is_last(disp)) {
|
||||
uint8_t* fb_base;
|
||||
if (ctx->owns_buffers) {
|
||||
fb_base = (uint8_t*)ctx->buf1;
|
||||
if (is_i1) {
|
||||
// LVGL reserves an 8-byte palette (2 x lv_color32_t) at the front of every I1
|
||||
// draw buffer; it's on the caller to skip it before treating the rest as
|
||||
// pixel data.
|
||||
fb_base += 8;
|
||||
}
|
||||
} else {
|
||||
fb_base = (uint8_t*)lvgl_display_fb_base(ctx, color_map);
|
||||
}
|
||||
uint16_t hres = display_get_resolution_x(ctx->device);
|
||||
uint16_t vres = display_get_resolution_y(ctx->device);
|
||||
display_draw_bitmap(ctx->device, 0, 0, hres, vres, fb_base);
|
||||
}
|
||||
} else if (ctx->owns_buffers) {
|
||||
// PARTIAL mode: each flush_cb call is one independent, complete tile into a buffer that
|
||||
// gets reused for the next tile, so present it immediately rather than waiting.
|
||||
// LVGL's area is inclusive; DisplayApi's draw_bitmap wants an exclusive end.
|
||||
display_draw_bitmap(ctx->device, x1, y1, x2 + 1, y2 + 1, color_map);
|
||||
} else if (lv_display_flush_is_last(disp)) {
|
||||
// fb-direct: a refresh cycle can call this flush_cb once per still-unjoined invalidated area
|
||||
// (lv_refr.c's refr_invalid_areas()), each writing its own tile into the *same* shared frame
|
||||
// buffer - but display_draw_bitmap() (see rgb_display_draw_bitmap()) blocks for a full
|
||||
// scan-out period whenever frame_buffer_count > 0. Presenting after every tile would pay
|
||||
// that wait N times per refresh instead of once; defer to the last flush of the cycle and
|
||||
// present the whole buffer in one call, mirroring esp_lvgl_port_disp.c's
|
||||
// lv_disp_flush_is_last() gate for its own direct/full render mode.
|
||||
uint8_t* fb_base = (uint8_t*)lvgl_display_fb_base(ctx, color_map);
|
||||
uint16_t hres = display_get_resolution_x(ctx->device);
|
||||
uint16_t vres = display_get_resolution_y(ctx->device);
|
||||
display_draw_bitmap(ctx->device, 0, 0, hres, vres, fb_base);
|
||||
}
|
||||
// DisplayApi has no async completion callback, so draw_bitmap is synchronous.
|
||||
lv_display_flush_ready(disp);
|
||||
@@ -209,9 +248,8 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig*
|
||||
}
|
||||
|
||||
lv_color_format_t lv_color_format;
|
||||
bool byte_swap;
|
||||
enum DisplayColorFormat kernel_color_format = display_get_color_format(device);
|
||||
if (!lvgl_display_map_color_format(kernel_color_format, &lv_color_format, &byte_swap)) {
|
||||
if (!lvgl_display_map_color_format(kernel_color_format, &lv_color_format)) {
|
||||
LOG_E(TAG, "Unsupported color format %d (no LVGL equivalent)", (int)kernel_color_format);
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
@@ -226,31 +264,53 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig*
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
ctx->device = device;
|
||||
ctx->byte_swap = byte_swap;
|
||||
ctx->byte_swap = config->swap_bytes;
|
||||
ctx->sw_rotate = config->sw_rotate;
|
||||
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);
|
||||
// fb_count > 0 with capability false means a driver reports mirror/swap_xy unavailable
|
||||
// specifically because binding fb-direct would defeat them (see rgb_display_has_capability()).
|
||||
// We're about to fall back to an owned buffer instead (right below) precisely because of that,
|
||||
// at which point the concern doesn't apply anymore: frame_buffer_count > 0 mattering to
|
||||
// has_capability() at all implies the driver's mirror()/swap_xy() are real, non-null
|
||||
// implementations, and the copy-into-fb path honors them correctly once not fb-direct-bound.
|
||||
// sw_rotate is excluded too: it writes rotated pixels into ctx->rotate_buf, which
|
||||
// lvgl_display_fb_base() doesn't recognize, so fb-direct must stay off in that case as well.
|
||||
ctx->has_set_gap_cap = display_has_capability(device, DISPLAY_CAPABILITY_CAP_SET_GAP);
|
||||
// sw_rotate is excluded from fb-direct binding below: it writes rotated pixels into
|
||||
// ctx->rotate_buf, which lvgl_display_fb_base() doesn't recognize, so fb-direct must stay
|
||||
// off in that case as well.
|
||||
bool would_bind_fb_direct = fb_count > 0 && ctx->has_swap_xy_cap && ctx->has_mirror_cap && !ctx->sw_rotate;
|
||||
if (fb_count > 0 && !would_bind_fb_direct) {
|
||||
ctx->has_swap_xy_cap = true;
|
||||
ctx->has_mirror_cap = true;
|
||||
// Only re-enable capabilities the driver reported off above if it has a *dynamic*
|
||||
// has_capability() (DisplayApi.has_capability non-null, e.g. rgb_display_has_capability()/
|
||||
// st7701_has_capability()): that's specifically what a driver implements when a capability's
|
||||
// availability is state-dependent - here, off only because binding fb-direct would defeat
|
||||
// it (see rgb_display_has_capability()) - so once we're falling back to an owned buffer
|
||||
// instead (right below), the concern no longer applies. A driver with no dynamic
|
||||
// has_capability() reports fixed, hardware-level capabilities instead: if the bit's off,
|
||||
// swap_xy()/mirror() don't exist at all (null function pointers - see the DisplayApi
|
||||
// contract), so forcing them back on here would call through a null pointer.
|
||||
const struct DisplayApi* api = (const struct DisplayApi*)device_get_driver(device)->api;
|
||||
if (api->has_capability != NULL) {
|
||||
ctx->has_swap_xy_cap = true;
|
||||
ctx->has_mirror_cap = true;
|
||||
}
|
||||
}
|
||||
ctx->base_swap_xy = ctx->has_swap_xy_cap ? display_get_swap_xy(device) : false;
|
||||
ctx->base_mirror_x = ctx->has_mirror_cap ? display_get_mirror_x(device) : false;
|
||||
ctx->base_mirror_y = ctx->has_mirror_cap ? display_get_mirror_y(device) : false;
|
||||
ctx->base_gap_x = ctx->has_set_gap_cap ? display_get_gap_x(device) : 0;
|
||||
ctx->base_gap_y = ctx->has_set_gap_cap ? display_get_gap_y(device) : 0;
|
||||
|
||||
lv_display_render_mode_t render_mode;
|
||||
size_t buf_size_bytes;
|
||||
|
||||
if (would_bind_fb_direct) {
|
||||
if (lv_color_format == LV_COLOR_FORMAT_I1) {
|
||||
// I1 packs 8 pixels/byte row-wise and LVGL reserves an 8-byte palette header at the
|
||||
// buffer's start (see lvgl_display_flush_cb()). Always redraw the whole frame in one
|
||||
// owned buffer instead of computing partial-region byte offsets against that packing.
|
||||
buf_size_bytes = (size_t)((hres + 7) / 8) * vres + 8;
|
||||
ctx->buf1 = lvgl_display_alloc_buffer(buf_size_bytes);
|
||||
if (ctx->buf1 == NULL) {
|
||||
free(ctx);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
ctx->owns_buffers = true;
|
||||
render_mode = LV_DISPLAY_RENDER_MODE_FULL;
|
||||
} else if (would_bind_fb_direct) {
|
||||
display_get_frame_buffer(device, 0, &ctx->buf1);
|
||||
if (fb_count > 1) {
|
||||
display_get_frame_buffer(device, 1, &ctx->buf2);
|
||||
@@ -259,7 +319,8 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig*
|
||||
render_mode = LV_DISPLAY_RENDER_MODE_FULL;
|
||||
buf_size_bytes = (size_t)hres * vres * bpp;
|
||||
} else {
|
||||
uint16_t buf_height = config->buffer_height > 0 ? config->buffer_height : vres;
|
||||
uint16_t buf_height = config->force_full_frame || config->buffer_height == 0
|
||||
? vres : config->buffer_height;
|
||||
buf_size_bytes = (size_t)hres * buf_height * bpp;
|
||||
|
||||
ctx->buf1 = lvgl_display_alloc_buffer(buf_size_bytes);
|
||||
@@ -276,7 +337,7 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig*
|
||||
}
|
||||
}
|
||||
ctx->owns_buffers = true;
|
||||
render_mode = LV_DISPLAY_RENDER_MODE_PARTIAL;
|
||||
render_mode = config->force_full_frame ? LV_DISPLAY_RENDER_MODE_FULL : LV_DISPLAY_RENDER_MODE_PARTIAL;
|
||||
}
|
||||
|
||||
ctx->buf_size_bytes = buf_size_bytes;
|
||||
@@ -304,6 +365,7 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig*
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
ctx->render_mode = render_mode;
|
||||
lv_display_set_color_format(disp, lv_color_format);
|
||||
lv_display_set_buffers(disp, ctx->buf1, ctx->buf2, buf_size_bytes, render_mode);
|
||||
lv_display_set_flush_cb(disp, lvgl_display_flush_cb);
|
||||
|
||||
@@ -51,7 +51,6 @@ static void lvgl_pointer_calibration_apply(
|
||||
if (mapped_y < 0) mapped_y = 0;
|
||||
if (mapped_y > target_y_max) mapped_y = target_y_max;
|
||||
|
||||
LOG_I(TAG, "Calibration mapping: %d,%d -> %d,%d", *x, *y, (int)mapped_x, (int)mapped_y);
|
||||
*x = (uint16_t)mapped_x;
|
||||
*y = (uint16_t)mapped_y;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user