diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index b69f8ef57bf..f6f6204f4c4 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -187,7 +187,6 @@ def final_validation(config_list): for config in config_list: 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") - uses_rotation = CONF_ROTATION in config for display_id in config[df.CONF_DISPLAYS]: path = global_config.get_path_for_id(display_id)[:-1] 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" ) # treating 0 as false is intended here. - if uses_rotation and display.get(CONF_ROTATION): - df.LOGGER.warning( - "use of 'rotation' in both LVGL and the display config is not recommended" + if display.get(CONF_ROTATION): + raise cv.Invalid( + "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: 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_STRING", "LV_STDLIB_CLIB") df.add_define("LV_USE_STDLIB_MALLOC", "LV_STDLIB_CUSTOM") + df.add_define("LV_DEF_REFR_PERIOD", "16") cg.add_define("USE_LVGL") # suppress default enabling of extra widgets # 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") else: 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( config[CONF_ID], displays, diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 0ab49d0a101..0c4e7a3425d 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -158,8 +158,15 @@ void LvglComponent::dump_config() { " Draw rounding: %d", this->width_, this->height_, 100 / this->buffer_frac_, this->rotation_, (int) this->draw_rounding); if (this->rotation_type_ != ROTATION_UNUSED) { - ESP_LOGCONFIG(TAG, " Rotation type: %s", - this->rotation_type_ == RotationType::ROTATION_SOFTWARE ? "software" : "hardware via display driver"); + const char *rot_type = "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_; } 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) { auto width = lv_area_get_width(area); auto height = lv_area_get_height(area); auto height_rounded = (height + this->draw_rounding - 1) / this->draw_rounding * this->draw_rounding; auto x1 = area->x1; auto y1 = area->y1; - if (this->rotation_type_ == RotationType::ROTATION_SOFTWARE) { + if (this->rotation_type_ == ROTATION_SOFTWARE) { lv_color_data *dst = reinterpret_cast(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_) { 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; x1 = this->width_ - area->y1 - height; height = width; @@ -274,21 +380,11 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_data *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++; - } - } x1 = this->width_ - x1 - width; y1 = this->height_ - y1 - height; 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++; - } - } x1 = y1; y1 = this->height_ - area->x1 - width; height = width; @@ -296,7 +392,6 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_data *ptr) { break; default: - dst = ptr; break; } ptr = dst; @@ -664,6 +759,15 @@ void LvglComponent::setup() { this->mark_failed(); 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) { 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) { 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 if (buffer == nullptr) ESP_LOGW(esphome::lvgl::TAG, "Failed to allocate %zu bytes for %sdraw buffer", size, internal ? "internal " : ""); diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 4a4c11d3834..3433aaa5272 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -26,6 +26,10 @@ #include #include +#ifdef USE_ESP32_VARIANT_ESP32P4 +#include "driver/ppa.h" +#endif + #ifdef USE_FONT #include "esphome/components/font/font.h" #endif // USE_LVGL_FONT @@ -229,6 +233,9 @@ class LvglComponent : public PollingComponent { display::DisplayRotation get_rotation() const { return this->rotation_; } 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: void set_resolution_() const; void draw_end_(); @@ -238,6 +245,10 @@ class LvglComponent : public PollingComponent { void write_random_(); 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); std::vector displays_{}; @@ -266,6 +277,9 @@ class LvglComponent : public PollingComponent { void *rotate_buf_{}; display::DisplayRotation rotation_{display::DISPLAY_ROTATION_0_DEGREES}; RotationType rotation_type_; +#ifdef USE_ESP32_VARIANT_ESP32P4 + ppa_client_handle_t ppa_client_{}; +#endif }; class IdleTrigger : public Trigger<> { diff --git a/tests/components/lvgl/test.esp32-idf.yaml b/tests/components/lvgl/test.esp32-idf.yaml index e6025e17fc6..79ea06f16af 100644 --- a/tests/components/lvgl/test.esp32-idf.yaml +++ b/tests/components/lvgl/test.esp32-idf.yaml @@ -21,7 +21,7 @@ binary_sensor: ignore_strapping_warning: true display: - - platform: ili9xxx + - platform: mipi_spi spi_id: spi_bus model: st7789v id: second_display @@ -41,7 +41,7 @@ display: invert_colors: false update_interval: never - - platform: ili9xxx + - platform: mipi_spi spi_id: spi_bus model: st7789v id: tft_display diff --git a/tests/components/lvgl/test.esp32-p4-idf.yaml b/tests/components/lvgl/test.esp32-p4-idf.yaml new file mode 100644 index 00000000000..5fd93702554 --- /dev/null +++ b/tests/components/lvgl/test.esp32-p4-idf.yaml @@ -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