[lvgl] Implement rotation with PPA (#15453)

This commit is contained in:
Clyde Stubbs
2026-04-08 11:19:29 +10:00
committed by GitHub
parent d20d613c1d
commit 88f4067dd6
5 changed files with 160 additions and 27 deletions
+8 -5
View File
@@ -187,7 +187,6 @@ def final_validation(config_list):
for config in config_list: for config in config_list:
if (pages := config.get(CONF_PAGES)) and all(p[df.CONF_SKIP] for p in pages): if (pages := config.get(CONF_PAGES)) and all(p[df.CONF_SKIP] for p in pages):
raise cv.Invalid("At least one page must not be skipped") raise cv.Invalid("At least one page must not be skipped")
uses_rotation = CONF_ROTATION in config
for display_id in config[df.CONF_DISPLAYS]: for display_id in config[df.CONF_DISPLAYS]:
path = global_config.get_path_for_id(display_id)[:-1] path = global_config.get_path_for_id(display_id)[:-1]
display = global_config.get_config_for_path(path) display = global_config.get_config_for_path(path)
@@ -196,9 +195,9 @@ def final_validation(config_list):
"Using lambda: or pages: in display config is not compatible with LVGL" "Using lambda: or pages: in display config is not compatible with LVGL"
) )
# treating 0 as false is intended here. # treating 0 as false is intended here.
if uses_rotation and display.get(CONF_ROTATION): if display.get(CONF_ROTATION):
df.LOGGER.warning( raise cv.Invalid(
"use of 'rotation' in both LVGL and the display config is not recommended" "use of 'rotation' in the display config is not compatible with LVGL, please set rotation in the LVGL config instead"
) )
if display.get(CONF_AUTO_CLEAR_ENABLED) is True: if display.get(CONF_AUTO_CLEAR_ENABLED) is True:
raise cv.Invalid( raise cv.Invalid(
@@ -262,6 +261,7 @@ async def to_code(configs):
df.add_define("LV_USE_STDLIB_SPRINTF", "LV_STDLIB_CLIB") df.add_define("LV_USE_STDLIB_SPRINTF", "LV_STDLIB_CLIB")
df.add_define("LV_USE_STDLIB_STRING", "LV_STDLIB_CLIB") df.add_define("LV_USE_STDLIB_STRING", "LV_STDLIB_CLIB")
df.add_define("LV_USE_STDLIB_MALLOC", "LV_STDLIB_CUSTOM") df.add_define("LV_USE_STDLIB_MALLOC", "LV_STDLIB_CUSTOM")
df.add_define("LV_DEF_REFR_PERIOD", "16")
cg.add_define("USE_LVGL") cg.add_define("USE_LVGL")
# suppress default enabling of extra widgets # suppress default enabling of extra widgets
# cg.add_define("LV_KCONFIG_PRESENT") # cg.add_define("LV_KCONFIG_PRESENT")
@@ -341,7 +341,10 @@ async def to_code(configs):
df.LOGGER.info("LVGL will use hardware rotation via display driver") df.LOGGER.info("LVGL will use hardware rotation via display driver")
else: else:
rotation_type = RotationType.ROTATION_SOFTWARE rotation_type = RotationType.ROTATION_SOFTWARE
df.LOGGER.info("LVGL will use software rotation") if get_esp32_variant() == VARIANT_ESP32P4:
df.LOGGER.info("LVGL will use software rotation (PPA accelerated)")
else:
df.LOGGER.info("LVGL will use software rotation")
lv_component = cg.new_Pvariable( lv_component = cg.new_Pvariable(
config[CONF_ID], config[CONF_ID],
displays, displays,
+124 -20
View File
@@ -158,8 +158,15 @@ void LvglComponent::dump_config() {
" Draw rounding: %d", " Draw rounding: %d",
this->width_, this->height_, 100 / this->buffer_frac_, this->rotation_, (int) this->draw_rounding); this->width_, this->height_, 100 / this->buffer_frac_, this->rotation_, (int) this->draw_rounding);
if (this->rotation_type_ != ROTATION_UNUSED) { if (this->rotation_type_ != ROTATION_UNUSED) {
ESP_LOGCONFIG(TAG, " Rotation type: %s", const char *rot_type = "hardware via display driver";
this->rotation_type_ == RotationType::ROTATION_SOFTWARE ? "software" : "hardware via display driver"); if (this->rotation_type_ == RotationType::ROTATION_SOFTWARE) {
#ifdef USE_ESP32_VARIANT_ESP32P4
rot_type = this->ppa_client_ != nullptr ? "software (PPA accelerated)" : "software";
#else
rot_type = "software";
#endif
}
ESP_LOGCONFIG(TAG, " Rotation type: %s", rot_type);
} }
} }
@@ -252,21 +259,120 @@ void LvglComponent::show_prev_page(lv_screen_load_anim_t anim, uint32_t time) {
size_t LvglComponent::get_current_page() const { return this->current_page_; } size_t LvglComponent::get_current_page() const { return this->current_page_; }
bool LvPageType::is_showing() const { return this->parent_->get_current_page() == this->index; } bool LvPageType::is_showing() const { return this->parent_->get_current_page() == this->index; }
#ifdef USE_ESP32_VARIANT_ESP32P4
bool LvglComponent::ppa_rotate_(const lv_color_data *src, lv_color_data *dst, uint16_t width, uint16_t height,
uint32_t height_rounded) {
ppa_srm_rotation_angle_t angle;
uint16_t out_w, out_h;
// Map ESPHome clockwise display rotation to PPA counter-clockwise angles
switch (this->rotation_) {
case display::DISPLAY_ROTATION_90_DEGREES:
angle = PPA_SRM_ROTATION_ANGLE_270; // 270° CCW = 90° CW
out_w = height_rounded;
out_h = width;
break;
case display::DISPLAY_ROTATION_180_DEGREES:
angle = PPA_SRM_ROTATION_ANGLE_180;
out_w = width;
out_h = height;
break;
case display::DISPLAY_ROTATION_270_DEGREES:
angle = PPA_SRM_ROTATION_ANGLE_90; // 90° CCW = 270° CW
out_w = height_rounded;
out_h = width;
break;
default:
return false; // No rotation needed
}
// Align buffer size to cache line (LV_DRAW_BUF_ALIGN) as required by PPA DMA
// the underlying buffer will be large enough as the size is also padded when allocating.
size_t out_buf_size = out_w * out_h * sizeof(lv_color_data);
out_buf_size = LV_ROUND_UP(out_buf_size, LV_DRAW_BUF_ALIGN);
ppa_srm_oper_config_t srm_config{};
srm_config.in.buffer = src;
srm_config.in.pic_w = width;
srm_config.in.pic_h = height;
srm_config.in.block_w = width;
srm_config.in.block_h = height;
#if LV_COLOR_DEPTH == 16
srm_config.in.srm_cm = PPA_SRM_COLOR_MODE_RGB565;
#elif LV_COLOR_DEPTH == 32
srm_config.in.srm_cm = PPA_SRM_COLOR_MODE_ARGB8888;
#endif
srm_config.out.buffer = dst;
srm_config.out.buffer_size = out_buf_size;
srm_config.out.pic_w = out_w;
srm_config.out.pic_h = out_h;
#if LV_COLOR_DEPTH == 16
srm_config.out.srm_cm = PPA_SRM_COLOR_MODE_RGB565;
#elif LV_COLOR_DEPTH == 32
srm_config.out.srm_cm = PPA_SRM_COLOR_MODE_ARGB8888;
#endif
srm_config.rotation_angle = angle;
srm_config.scale_x = 1.0f;
srm_config.scale_y = 1.0f;
srm_config.mode = PPA_TRANS_MODE_BLOCKING;
esp_err_t ret = ppa_do_scale_rotate_mirror(this->ppa_client_, &srm_config);
if (ret != ESP_OK) {
ESP_LOGW(TAG, "PPA rotation failed: %s", esp_err_to_name(ret));
ESP_LOGW(TAG, "PPA SRM: in=%ux%u src=%p, out=%ux%u dst=%p size=%zu, angle=%d", width, height, src, out_w, out_h,
dst, out_buf_size, (int) angle);
return false;
}
return true;
}
#endif // USE_ESP32_VARIANT_ESP32P4
void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_data *ptr) { void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_data *ptr) {
auto width = lv_area_get_width(area); auto width = lv_area_get_width(area);
auto height = lv_area_get_height(area); auto height = lv_area_get_height(area);
auto height_rounded = (height + this->draw_rounding - 1) / this->draw_rounding * this->draw_rounding; auto height_rounded = (height + this->draw_rounding - 1) / this->draw_rounding * this->draw_rounding;
auto x1 = area->x1; auto x1 = area->x1;
auto y1 = area->y1; auto y1 = area->y1;
if (this->rotation_type_ == RotationType::ROTATION_SOFTWARE) { if (this->rotation_type_ == ROTATION_SOFTWARE) {
lv_color_data *dst = reinterpret_cast<lv_color_data *>(this->rotate_buf_); lv_color_data *dst = reinterpret_cast<lv_color_data *>(this->rotate_buf_);
#ifdef USE_ESP32_VARIANT_ESP32P4
bool ppa_done = this->ppa_client_ != nullptr && this->ppa_rotate_(ptr, dst, width, height, height_rounded);
if (!ppa_done)
#endif
{
switch (this->rotation_) {
case display::DISPLAY_ROTATION_90_DEGREES:
for (lv_coord_t x = height; x-- != 0;) {
for (lv_coord_t y = 0; y != width; y++) {
dst[y * height_rounded + x] = *ptr++;
}
}
break;
case display::DISPLAY_ROTATION_180_DEGREES:
for (lv_coord_t y = height; y-- != 0;) {
for (lv_coord_t x = width; x-- != 0;) {
dst[y * width + x] = *ptr++;
}
}
break;
case display::DISPLAY_ROTATION_270_DEGREES:
for (lv_coord_t x = 0; x != height; x++) {
for (lv_coord_t y = width; y-- != 0;) {
dst[y * height_rounded + x] = *ptr++;
}
}
break;
default:
dst = ptr;
break;
}
}
// Coordinate adjustments apply regardless of PPA or SW rotation
switch (this->rotation_) { switch (this->rotation_) {
case display::DISPLAY_ROTATION_90_DEGREES: case display::DISPLAY_ROTATION_90_DEGREES:
for (lv_coord_t x = height; x-- != 0;) {
for (lv_coord_t y = 0; y != width; y++) {
dst[y * height_rounded + x] = *ptr++;
}
}
y1 = x1; y1 = x1;
x1 = this->width_ - area->y1 - height; x1 = this->width_ - area->y1 - height;
height = width; height = width;
@@ -274,21 +380,11 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_data *ptr) {
break; break;
case display::DISPLAY_ROTATION_180_DEGREES: case display::DISPLAY_ROTATION_180_DEGREES:
for (lv_coord_t y = height; y-- != 0;) {
for (lv_coord_t x = width; x-- != 0;) {
dst[y * width + x] = *ptr++;
}
}
x1 = this->width_ - x1 - width; x1 = this->width_ - x1 - width;
y1 = this->height_ - y1 - height; y1 = this->height_ - y1 - height;
break; break;
case display::DISPLAY_ROTATION_270_DEGREES: case display::DISPLAY_ROTATION_270_DEGREES:
for (lv_coord_t x = 0; x != height; x++) {
for (lv_coord_t y = width; y-- != 0;) {
dst[y * height_rounded + x] = *ptr++;
}
}
x1 = y1; x1 = y1;
y1 = this->height_ - area->x1 - width; y1 = this->height_ - area->x1 - width;
height = width; height = width;
@@ -296,7 +392,6 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_data *ptr) {
break; break;
default: default:
dst = ptr;
break; break;
} }
ptr = dst; ptr = dst;
@@ -664,6 +759,15 @@ void LvglComponent::setup() {
this->mark_failed(); this->mark_failed();
return; return;
} }
#ifdef USE_ESP32_VARIANT_ESP32P4
ppa_client_config_t ppa_config{};
ppa_config.oper_type = PPA_OPERATION_SRM;
ppa_config.max_pending_trans_num = 1;
if (ppa_register_client(&ppa_config, &this->ppa_client_) != ESP_OK) {
ESP_LOGW(TAG, "PPA client registration failed, using software rotation");
this->ppa_client_ = nullptr;
}
#endif
} }
if (this->draw_start_callback_ != nullptr) { if (this->draw_start_callback_ != nullptr) {
lv_display_add_event_cb(this->disp_, render_start_cb, LV_EVENT_RENDER_START, this); lv_display_add_event_cb(this->disp_, render_start_cb, LV_EVENT_RENDER_START, this);
@@ -804,7 +908,7 @@ static unsigned cap_bits = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; // NOLINT
static void *lv_alloc_draw_buf(size_t size, bool internal) { static void *lv_alloc_draw_buf(size_t size, bool internal) {
void *buffer; void *buffer;
size = ((size + LV_DRAW_BUF_ALIGN - 1) / LV_DRAW_BUF_ALIGN) * LV_DRAW_BUF_ALIGN; size = LV_ROUND_UP(size, LV_DRAW_BUF_ALIGN);
buffer = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, size, internal ? MALLOC_CAP_8BIT : cap_bits); // NOLINT buffer = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, size, internal ? MALLOC_CAP_8BIT : cap_bits); // NOLINT
if (buffer == nullptr) if (buffer == nullptr)
ESP_LOGW(esphome::lvgl::TAG, "Failed to allocate %zu bytes for %sdraw buffer", size, internal ? "internal " : ""); ESP_LOGW(esphome::lvgl::TAG, "Failed to allocate %zu bytes for %sdraw buffer", size, internal ? "internal " : "");
+14
View File
@@ -26,6 +26,10 @@
#include <utility> #include <utility>
#include <vector> #include <vector>
#ifdef USE_ESP32_VARIANT_ESP32P4
#include "driver/ppa.h"
#endif
#ifdef USE_FONT #ifdef USE_FONT
#include "esphome/components/font/font.h" #include "esphome/components/font/font.h"
#endif // USE_LVGL_FONT #endif // USE_LVGL_FONT
@@ -229,6 +233,9 @@ class LvglComponent : public PollingComponent {
display::DisplayRotation get_rotation() const { return this->rotation_; } display::DisplayRotation get_rotation() const { return this->rotation_; }
void rotate_coordinates(int32_t &x, int32_t &y) const; void rotate_coordinates(int32_t &x, int32_t &y) const;
uint16_t get_width() const { return lv_display_get_horizontal_resolution(this->disp_); }
uint16_t get_height() const { return lv_display_get_vertical_resolution(this->disp_); }
protected: protected:
void set_resolution_() const; void set_resolution_() const;
void draw_end_(); void draw_end_();
@@ -238,6 +245,10 @@ class LvglComponent : public PollingComponent {
void write_random_(); void write_random_();
void draw_buffer_(const lv_area_t *area, lv_color_data *ptr); void draw_buffer_(const lv_area_t *area, lv_color_data *ptr);
#ifdef USE_ESP32_VARIANT_ESP32P4
bool ppa_rotate_(const lv_color_data *src, lv_color_data *dst, uint16_t width, uint16_t height,
uint32_t height_rounded);
#endif
void flush_cb_(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p); void flush_cb_(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p);
std::vector<display::Display *> displays_{}; std::vector<display::Display *> displays_{};
@@ -266,6 +277,9 @@ class LvglComponent : public PollingComponent {
void *rotate_buf_{}; void *rotate_buf_{};
display::DisplayRotation rotation_{display::DISPLAY_ROTATION_0_DEGREES}; display::DisplayRotation rotation_{display::DISPLAY_ROTATION_0_DEGREES};
RotationType rotation_type_; RotationType rotation_type_;
#ifdef USE_ESP32_VARIANT_ESP32P4
ppa_client_handle_t ppa_client_{};
#endif
}; };
class IdleTrigger : public Trigger<> { class IdleTrigger : public Trigger<> {
+2 -2
View File
@@ -21,7 +21,7 @@ binary_sensor:
ignore_strapping_warning: true ignore_strapping_warning: true
display: display:
- platform: ili9xxx - platform: mipi_spi
spi_id: spi_bus spi_id: spi_bus
model: st7789v model: st7789v
id: second_display id: second_display
@@ -41,7 +41,7 @@ display:
invert_colors: false invert_colors: false
update_interval: never update_interval: never
- platform: ili9xxx - platform: mipi_spi
spi_id: spi_bus spi_id: spi_bus
model: st7789v model: st7789v
id: tft_display id: tft_display
@@ -0,0 +1,12 @@
display:
- platform: mipi_dsi
model: WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-3.4C
lvgl:
byte_order: little_endian
rotation: 90
psram:
esp_ldo:
- channel: 3
voltage: 2.5V