From 838f276d763244eef093e8e8b2580cd7abca8b21 Mon Sep 17 00:00:00 2001 From: Bitcoin3us <115934595+bitcoin3us@users.noreply.github.com> Date: Thu, 24 Sep 2026 21:00:14 +0100 Subject: [PATCH 1/3] lcd_bus: add DSIBus, a MIPI-DSI video-mode bus (ESP32-P4) esp32_src/dsi_bus.c existed but was neither compiled nor registered, and did not build against ESP-IDF 5.5 (esp_lcd_new_panel_io_dsi is esp_lcd_new_panel_io_dbi there, plus a few typos). This wires it in, guarded by SOC_MIPI_DSI_SUPPORTED, and reworks it around how the IDF DPI panel actually works: - The DPI panel driver allocates screen-sized frame buffers itself and its DMA scans them out continuously. allocate_framebuffer() therefore hands out real, correctly sized buffers that init() repoints at the panel's frame buffers (so len() is right for the display framework and LVGL renders straight into the scanned-out memory); partial buffers are refused with a clear error. - esp_lcd_panel_init() (the start of the video stream) is deferred to the first tx_color, so the display driver can configure the panel over DCS (tx_param/rx_param through the DBI panel IO) first, the order Espressif's own MIPI panel drivers use. - A flush is a cache write-back plus, when the flushed buffer is not the one being scanned out, a swap at the next frame boundary; the refresh-done interrupt reports completion (flush_ready) once the panel has picked the buffer up. Intermediate areas of a multi-area update complete immediately. LVGL's inclusive area coordinates are converted for esp_lcd_panel_draw_bitmap(). - The DSI PHY's internal LDO (ESP32-P4: channel 3, 2500 mV) can be acquired by the bus (phy_ldo_channel / phy_ldo_voltage_mv kwargs), dpi_clock_freq (MHz) is separate from the lane bit rate, and del/free paths no longer leak or double-declare. Verified on the Waveshare ESP32-P4-WIFI6-Touch-LCD-4.3 (ST7701 480x800, 2 lanes at 500 Mbps, DPI 30 MHz, RGB565, two PSRAM frame buffers): DCS reads answer (RDDPM 0x9C, RDDMADCTL 0x00, RDDCOLMOD 0x50), LVGL renders into the panel buffers and full-screen refreshes complete in one frame period (14-17 ms) with no DMA underruns at 200 MHz PSRAM. Co-Authored-By: Claude Fable 5.1 --- ext_mod/lcd_bus/esp32_include/dsi_bus.h | 17 +- ext_mod/lcd_bus/esp32_src/dsi_bus.c | 338 +++++++++++++++++------- ext_mod/lcd_bus/micropython.cmake | 1 + ext_mod/lcd_bus/modlcd_bus.c | 4 + 4 files changed, 254 insertions(+), 106 deletions(-) diff --git a/ext_mod/lcd_bus/esp32_include/dsi_bus.h b/ext_mod/lcd_bus/esp32_include/dsi_bus.h index 44e63115..9b68b7c3 100644 --- a/ext_mod/lcd_bus/esp32_include/dsi_bus.h +++ b/ext_mod/lcd_bus/esp32_include/dsi_bus.h @@ -20,17 +20,20 @@ // esp-idf includes #include "esp_lcd_panel_io.h" #include "esp_lcd_panel_interface.h" - #include "esp_lcd_panel_io.h" #include "esp_lcd_mipi_dsi.h" + #include "esp_ldo_regulator.h" typedef struct _mp_lcd_dsi_bus_obj_t { + // The first members mirror mp_lcd_bus_obj_t (lcd_types.h): the + // generic bus methods cast to it. mp_obj_base_t base; mp_obj_t callback; - void *buf1; - void *buf2; + mp_obj_array_t *view1; + mp_obj_array_t *view2; + uint32_t buffer_flags; bool trans_done; @@ -45,10 +48,12 @@ esp_lcd_dpi_panel_config_t panel_config; uint32_t buffer_size; - mp_obj_array_t *view1; - mp_obj_array_t *view2; - void *transmitting_buf; + bool panel_started; + + int phy_ldo_channel; + int phy_ldo_voltage_mv; + esp_ldo_channel_handle_t phy_ldo_handle; } mp_lcd_dsi_bus_obj_t; diff --git a/ext_mod/lcd_bus/esp32_src/dsi_bus.c b/ext_mod/lcd_bus/esp32_src/dsi_bus.c index f826de6e..15a8309a 100644 --- a/ext_mod/lcd_bus/esp32_src/dsi_bus.c +++ b/ext_mod/lcd_bus/esp32_src/dsi_bus.c @@ -1,5 +1,16 @@ // Copyright (c) 2024 - 2025 Kevin G. Schlosser +// MIPI-DSI bus (video mode) on top of ESP-IDF's esp_lcd MIPI DSI driver. +// +// Unlike the SPI/I80/RGB buses this bus owns no pixel transfer of its own: the +// DPI panel driver in ESP-IDF allocates screen-sized frame buffers in PSRAM +// and its DMA scans them out to the panel continuously. The memoryviews +// handed out by allocate_framebuffer() are pointed at those frame buffers when +// the bus is initialized, so LVGL renders straight into them and a flush only +// has to write the CPU cache back and, when the flush is the last one of an +// update, swap the buffer being scanned out. Panel commands (DCS) travel over +// the same link through the DBI panel IO (tx_param / rx_param). + // local includes #include "lcd_types.h" #include "modlcd_bus.h" @@ -9,6 +20,8 @@ #include "mphalport.h" #include "py/obj.h" #include "py/runtime.h" +#include "py/objarray.h" +#include "py/binary.h" // stdlib includes #include @@ -24,8 +37,15 @@ #include "esp_heap_caps.h" #include "hal/lcd_types.h" #include "esp_lcd_mipi_dsi.h" + #include "esp_ldo_regulator.h" + + + // Prefix of ESP-IDF's private esp_lcd_dpi_panel_t (esp_lcd/dsi/esp_lcd_panel_dpi.c), + // just enough to reach the frame buffers the DPI panel allocated. + #ifndef DPI_PANEL_MAX_FB_NUM + #define DPI_PANEL_MAX_FB_NUM 3 // esp_lcd/dsi/mipi_dsi_priv.h + #endif - typedef struct { esp_lcd_panel_t base; // Base class of generic lcd panel esp_lcd_dsi_bus_handle_t bus; // DSI bus handle @@ -39,23 +59,33 @@ mp_lcd_err_t dsi_del(mp_obj_t obj); mp_lcd_err_t dsi_init(mp_obj_t obj, uint16_t width, uint16_t height, uint8_t bpp, uint32_t buffer_size, bool rgb565_byte_swap, uint8_t cmd_bits, uint8_t param_bits); mp_lcd_err_t dsi_get_lane_count(mp_obj_t obj, uint8_t *lane_count); - mp_lcd_err_t dsi_tx_color(mp_obj_t obj, int lcd_cmd, void *color, size_t color_size, int x_start, int y_start, int x_end, int y_end, , uint8_t rotation, bool last_update); + mp_lcd_err_t dsi_tx_color(mp_obj_t obj, int lcd_cmd, void *color, size_t color_size, int x_start, int y_start, int x_end, int y_end, uint8_t rotation, bool last_update); mp_obj_t dsi_allocate_framebuffer(mp_obj_t obj, uint32_t size, uint32_t caps); mp_obj_t dsi_free_framebuffer(mp_obj_t obj, mp_obj_t buf); - static bool dsi_bus_trans_done_cb(esp_lcd_panel_handle_t panel, esp_lcd_dpi_panel_event_data_t *edata, void *user_ctx) + static inline bool dsi_buf_in_fb(mp_lcd_dsi_bus_obj_t *self, const uint8_t *fb, const void *buf) + { + const uint8_t *p = (const uint8_t *)buf; + return fb != NULL && p != NULL && p >= fb && p < fb + self->buffer_size; + } + + + // Runs from the DPI panel's end-of-frame interrupt: the DMA has just + // restarted its scan-out from fbs[cur_fb_index]. Once that is the buffer + // LVGL last flushed, the other buffer is free to be drawn into. + static bool dsi_bus_refresh_done_cb(esp_lcd_panel_handle_t panel, esp_lcd_dpi_panel_event_data_t *edata, void *user_ctx) { LCD_UNUSED(edata); dpi_panel_t *dpi_panel = __containerof(panel, dpi_panel_t, base); mp_lcd_dsi_bus_obj_t *self = (mp_lcd_dsi_bus_obj_t *)user_ctx; - if (!self->trans_done && dpi_panel->fbs[dpi_panel->cur_fb_index] == self->transmitting_buf) { - if (self->callback != mp_const_none && mp_obj_is_callable(self->callback)) { - cb_isr(self->callback); - } - self->trans_done = true; + if (!self->trans_done && dsi_buf_in_fb(self, dpi_panel->fbs[dpi_panel->cur_fb_index], self->transmitting_buf)) { + if (self->callback != mp_const_none && mp_obj_is_callable(self->callback)) { + cb_isr(self->callback); + } + self->trans_done = true; } return false; @@ -74,22 +104,28 @@ ARG_hsync_pulse_width, ARG_vsync_front_porch, ARG_vsync_back_porch, - ARG_vsync_pulse_width + ARG_vsync_pulse_width, + ARG_dpi_clock_freq, + ARG_phy_ldo_channel, + ARG_phy_ldo_voltage_mv }; const mp_arg_t make_new_args[] = { { MP_QSTR_bus_id, MP_ARG_INT | MP_ARG_KW_ONLY | MP_ARG_REQUIRED }, { MP_QSTR_data_lanes, MP_ARG_INT | MP_ARG_KW_ONLY | MP_ARG_REQUIRED }, - { MP_QSTR_freq, MP_ARG_INT | MP_ARG_KW_ONLY | MP_ARG_REQUIRED }, + { MP_QSTR_freq, MP_ARG_INT | MP_ARG_KW_ONLY | MP_ARG_REQUIRED }, // lane bit rate, Mbps { MP_QSTR_virtual_channel, MP_ARG_INT | MP_ARG_KW_ONLY | MP_ARG_REQUIRED }, { MP_QSTR_hsync_front_porch, MP_ARG_INT | MP_ARG_KW_ONLY, { .u_int = 0 } }, { MP_QSTR_hsync_back_porch, MP_ARG_INT | MP_ARG_KW_ONLY, { .u_int = 0 } }, { MP_QSTR_hsync_pulse_width, MP_ARG_INT | MP_ARG_KW_ONLY, { .u_int = 1 } }, { MP_QSTR_vsync_front_porch, MP_ARG_INT | MP_ARG_KW_ONLY, { .u_int = 0 } }, { MP_QSTR_vsync_back_porch, MP_ARG_INT | MP_ARG_KW_ONLY, { .u_int = 0 } }, - { MP_QSTR_vsync_pulse_width, MP_ARG_INT | MP_ARG_KW_ONLY, { .u_int = 1 } } + { MP_QSTR_vsync_pulse_width, MP_ARG_INT | MP_ARG_KW_ONLY, { .u_int = 1 } }, + { MP_QSTR_dpi_clock_freq, MP_ARG_INT | MP_ARG_KW_ONLY, { .u_int = 0 } }, // DPI pixel clock, MHz; 0 = same number as freq + { MP_QSTR_phy_ldo_channel, MP_ARG_INT | MP_ARG_KW_ONLY, { .u_int = -1 } }, // internal LDO channel powering the DSI PHY (ESP32-P4 boards: 3), -1 = not managed here + { MP_QSTR_phy_ldo_voltage_mv, MP_ARG_INT | MP_ARG_KW_ONLY, { .u_int = 2500 } } }; - + mp_arg_val_t args[MP_ARRAY_SIZE(make_new_args)]; mp_arg_parse_all_kw_array( n_args, @@ -102,8 +138,9 @@ // create new object mp_lcd_dsi_bus_obj_t *self = m_new_obj(mp_lcd_dsi_bus_obj_t); + memset(self, 0, sizeof(mp_lcd_dsi_bus_obj_t)); self->base.type = &mp_lcd_dsi_bus_type; - + self->callback = mp_const_none; self->bus_config.bus_id = (int)args[ARG_bus_id].u_int; @@ -115,8 +152,7 @@ self->panel_config.virtual_channel = (uint8_t)args[ARG_virtual_channel].u_int; self->panel_config.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT; - - self->panel_config.dpi_clock_freq_mhz = (uint32_t)args[ARG_freq].u_int; + self->panel_config.dpi_clock_freq_mhz = args[ARG_dpi_clock_freq].u_int > 0 ? (uint32_t)args[ARG_dpi_clock_freq].u_int : (uint32_t)args[ARG_freq].u_int; self->panel_config.video_timing.hsync_back_porch = (uint32_t)args[ARG_hsync_back_porch].u_int; self->panel_config.video_timing.hsync_pulse_width = (uint32_t)args[ARG_hsync_pulse_width].u_int; @@ -127,14 +163,13 @@ self->panel_config.num_fbs = 0; - self->bus_config.pclk_hz = (uint32_t)args[ARG_freq].u_int; + self->phy_ldo_channel = (int)args[ARG_phy_ldo_channel].u_int; + self->phy_ldo_voltage_mv = (int)args[ARG_phy_ldo_voltage_mv].u_int; LCD_DEBUG_PRINT("bus_id=%d\n", self->bus_config.bus_id) LCD_DEBUG_PRINT("num_data_lanes=%d\n", self->bus_config.num_data_lanes) LCD_DEBUG_PRINT("lane_bit_rate_mbps=%d\n",self->bus_config.lane_bit_rate_mbps) - LCD_DEBUG_PRINT("phy_clk_src=%d\n", self->bus_config.phy_clk_src) LCD_DEBUG_PRINT("virtual_channel=%d\n", self->panel_io_config.virtual_channel) - LCD_DEBUG_PRINT("dpi_clk_src=%d\n", self->panel_config.dpi_clk_src) LCD_DEBUG_PRINT("dpi_clock_freq_mhz=%d\n", self->panel_config.dpi_clock_freq_mhz) LCD_DEBUG_PRINT("hsync_front_porch=%d\n", self->panel_config.video_timing.hsync_front_porch) LCD_DEBUG_PRINT("hsync_back_porch=%d\n", self->panel_config.video_timing.hsync_back_porch) @@ -142,7 +177,7 @@ LCD_DEBUG_PRINT("vsync_front_porch=%d\n", self->panel_config.video_timing.vsync_front_porch) LCD_DEBUG_PRINT("vsync_back_porch=%d\n", self->panel_config.video_timing.vsync_back_porch) LCD_DEBUG_PRINT("vsync_pulse_width=%d\n", self->panel_config.video_timing.vsync_pulse_width) - LCD_DEBUG_PRINT("pclk_hz[10]=%d\n", self->bus_config.pclk_hz) + LCD_DEBUG_PRINT("phy_ldo_channel=%d\n", self->phy_ldo_channel) self->panel_io_handle.get_lane_count = &dsi_get_lane_count; self->panel_io_handle.del = &dsi_del; @@ -153,32 +188,53 @@ return MP_OBJ_FROM_PTR(self); } - + mp_lcd_err_t dsi_init(mp_obj_t obj, uint16_t width, uint16_t height, uint8_t bpp, uint32_t buffer_size, bool rgb565_byte_swap, uint8_t cmd_bits, uint8_t param_bits) { LCD_DEBUG_PRINT("dsi_init(self, width=%i, height=%i, bpp=%i, buffer_size=%lu, rgb565_byte_swap=%i, cmd_bits=%i, param_bits=%i)\n", width, height, bpp, buffer_size, (uint8_t)rgb565_byte_swap, cmd_bits, param_bits) + LCD_UNUSED(buffer_size); + mp_lcd_dsi_bus_obj_t *self = (mp_lcd_dsi_bus_obj_t *)obj; + if (self->panel_handle != NULL) { + mp_raise_msg(&mp_type_ValueError, MP_ERROR_TEXT("DSIBus is already initialized")); + return LCD_ERR_INVALID_STATE; + } + switch(bpp) { case 16: self->panel_config.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; - self->rgb565_byte_swap = rgb565_byte_swap; break; case 18: self->panel_config.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB666; - self->rgb565_byte_swap = false; break; case 24: self->panel_config.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB888; - self->rgb565_byte_swap = false; break; default: - mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("unsopported bits per pixel.(%d)"), bpp); + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("unsupported bits per pixel (%d)"), bpp); return LCD_ERR_INVALID_ARG; } + // The DPI panel refreshes straight from the frame buffers, so they have + // to hold the whole screen; partial (strip) buffers cannot work here. + uint32_t full_frame_size = (uint32_t)width * (uint32_t)height * (uint32_t)bpp / 8; + + if (self->view1 == NULL) { + mp_raise_msg(&mp_type_ValueError, MP_ERROR_TEXT("DSIBus frame buffers must be created with allocate_framebuffer()")); + return LCD_ERR_INVALID_ARG; + } + + if (self->buffer_size != full_frame_size) { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("DSIBus needs screen sized frame buffers (%lu bytes, got %lu)"), full_frame_size, self->buffer_size); + return LCD_ERR_INVALID_ARG; + } + + // LVGL renders in the frame buffers' own byte order; nothing here to swap. + self->rgb565_byte_swap = false; + self->panel_config.video_timing.h_size = (uint32_t)width; self->panel_config.video_timing.v_size = (uint32_t)height; @@ -190,21 +246,44 @@ LCD_DEBUG_PRINT("h_size=%d\n", self->panel_config.video_timing.h_size) LCD_DEBUG_PRINT("v_size=%d\n", self->panel_config.video_timing.v_size) LCD_DEBUG_PRINT("pixel_format=%d\n", self->panel_config.pixel_format) + LCD_DEBUG_PRINT("num_fbs=%d\n", self->panel_config.num_fbs) + + esp_err_t ret; + + if (self->phy_ldo_channel >= 0 && self->phy_ldo_handle == NULL) { + // The MIPI DSI PHY is powered by one of the ESP32-P4's internal + // LDOs (channel 3 on the Espressif and Waveshare boards); without + // it the bus never comes up. + esp_ldo_channel_config_t ldo_cfg = { + .chan_id = self->phy_ldo_channel, + .voltage_mv = self->phy_ldo_voltage_mv, + }; + ret = esp_ldo_acquire_channel(&ldo_cfg, &self->phy_ldo_handle); + if (ret != 0) { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("%d(esp_ldo_acquire_channel)"), ret); + return ret; + } + } - esp_err_t ret = esp_lcd_new_dsi_bus(&self->bus_config, &self->bus_handle); + ret = esp_lcd_new_dsi_bus(&self->bus_config, &self->bus_handle); if (ret != 0) { mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("%d(esp_lcd_new_dsi_bus)"), ret); return ret; } - ret = esp_lcd_new_panel_io_dsi(self->bus_handle, &self->panel_io_config, &self->panel_io_handle.panel_io); + // DCS commands and parameters go through the DBI panel IO + ret = esp_lcd_new_panel_io_dbi(self->bus_handle, &self->panel_io_config, &self->panel_io_handle.panel_io); if (ret != 0) { - mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("%d(esp_lcd_new_panel_io_dsi)"), ret); + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("%d(esp_lcd_new_panel_io_dbi)"), ret); return ret; } + // Creating the DPI panel allocates its frame buffers (in PSRAM); the + // video stream itself is not started here but by the first tx_color, + // so the display driver can configure the panel over DCS first, the + // order Espressif's own MIPI panel drivers use. ret = esp_lcd_new_panel_dpi(self->bus_handle, &self->panel_config, &self->panel_handle); if (ret != 0) { @@ -212,15 +291,8 @@ return ret; } - ret = esp_lcd_panel_init(self->panel_handle); - - if (ret != 0) { - mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("%d(esp_lcd_panel_init)"), ret); - return ret; - } - esp_lcd_dpi_panel_event_callbacks_t callbacks = { - .on_refresh_done = &dsi_bus_trans_done_cb + .on_refresh_done = &dsi_bus_refresh_done_cb }; ret = esp_lcd_dpi_panel_register_event_callbacks(self->panel_handle, &callbacks, self); @@ -230,21 +302,23 @@ return ret; } + // Point the buffers handed out by allocate_framebuffer() at the DPI + // panel's frame buffers and drop the interim allocations. dpi_panel_t *dpi_panel = __containerof((esp_lcd_panel_t *)self->panel_handle, dpi_panel_t, base); - void *buf1 = self->view1->items; + heap_caps_free(self->view1->items); self->view1->items = (void *)dpi_panel->fbs[0]; - self->view1->len = buffer_size; - heap_caps_free(buf1); + self->view1->len = self->buffer_size; - if (self->panel_config.num_fbs == 2) { - void *buf2 = self->view2->items; + if (self->view2 != NULL) { + heap_caps_free(self->view2->items); self->view2->items = (void *)dpi_panel->fbs[1]; - self->view2->len = buffer_size; - heap_caps_free(buf2); + self->view2->len = self->buffer_size; } - return ret; + LCD_DEBUG_PRINT("fb1=%p fb2=%p\n", self->view1->items, self->view2 != NULL ? self->view2->items : NULL) + + return LCD_OK; } @@ -253,29 +327,57 @@ LCD_DEBUG_PRINT("dsi_del(self)\n") mp_lcd_dsi_bus_obj_t *self = (mp_lcd_dsi_bus_obj_t *)obj; + mp_lcd_err_t ret; - mp_lcd_err_t ret = esp_lcd_panel_del(self->panel_handle); - if (ret != 0) { - mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("%d(esp_lcd_panel_del)"), ret); - return ret; + if (self->panel_handle != NULL) { + ret = esp_lcd_panel_del(self->panel_handle); + if (ret != 0) { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("%d(esp_lcd_panel_del)"), ret); + return ret; + } + self->panel_handle = NULL; + self->panel_started = false; + + // the frame buffers went with the panel + if (self->view1 != NULL) { + self->view1->items = NULL; + self->view1->len = 0; + self->view1 = NULL; + } + if (self->view2 != NULL) { + self->view2->items = NULL; + self->view2->len = 0; + self->view2 = NULL; + } } + if (self->panel_io_handle.panel_io != NULL) { + ret = esp_lcd_panel_io_del(self->panel_io_handle.panel_io); + if (ret != 0) { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("%d(esp_lcd_panel_io_del)"), ret); + return ret; + } + self->panel_io_handle.panel_io = NULL; + } - mp_lcd_err_t ret = esp_lcd_panel_io_del(self->panel_io_handle.panel_io); - if (ret != 0) { - mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("%d(esp_lcd_panel_io_del)"), ret); - return ret; + if (self->bus_handle != NULL) { + ret = esp_lcd_del_dsi_bus(self->bus_handle); + if (ret != 0) { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("%d(esp_lcd_del_dsi_bus)"), ret); + return ret; + } + self->bus_handle = NULL; } - ret = esp_lcd_del_dsi_bus(self->bus_handle); - if (ret != 0) { - mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("%d(esp_lcd_del_dsi_bus)"), ret); - return ret; + if (self->phy_ldo_handle != NULL) { + esp_ldo_release_channel(self->phy_ldo_handle); + self->phy_ldo_handle = NULL; } - return ret; + return LCD_OK; } + mp_lcd_err_t dsi_get_lane_count(mp_obj_t obj, uint8_t *lane_count) { mp_lcd_dsi_bus_obj_t *self = (mp_lcd_dsi_bus_obj_t *)obj; @@ -292,6 +394,7 @@ mp_lcd_dsi_bus_obj_t *self = (mp_lcd_dsi_bus_obj_t *)obj; if (self->panel_handle != NULL) { + // once initialized the buffers belong to the DPI panel (see dsi_del) mp_raise_msg(&mp_type_ValueError, MP_ERROR_TEXT("Unable to free buffer")); return mp_const_none; } @@ -301,15 +404,24 @@ if (array_buf == self->view1) { heap_caps_free(item_buf); - self->view1 = NULL; + array_buf->items = NULL; + array_buf->len = 0; + self->view1 = self->view2; + self->view2 = NULL; LCD_DEBUG_PRINT("dsi_free_framebuffer(self, buf=1)\n") } else if (array_buf == self->view2) { heap_caps_free(item_buf); + array_buf->items = NULL; + array_buf->len = 0; self->view2 = NULL; LCD_DEBUG_PRINT("dsi_free_framebuffer(self, buf=2)\n") } else { mp_raise_msg(&mp_type_MemoryError, MP_ERROR_TEXT("No matching buffer found")); + return mp_const_none; } + + self->panel_config.num_fbs = self->view1 == NULL ? 0 : (self->view2 == NULL ? 1 : 2); + return mp_const_none; } @@ -320,86 +432,112 @@ mp_lcd_dsi_bus_obj_t *self = (mp_lcd_dsi_bus_obj_t *)obj; - if ((caps | MALLOC_CAP_DMA) == caps) { - #if SOC_DMA2D_SUPPORTED - self->panel_config.flags.use_dma2d = true; - #else - mp_raise_msg(&mp_type_MemoryError, MP_ERROR_TEXT("DMA is not supported")); + if (self->panel_handle != NULL) { + mp_raise_msg(&mp_type_ValueError, MP_ERROR_TEXT("DSIBus is already initialized")); + return mp_const_none; + } + + if (self->view1 != NULL && self->view2 != NULL) { + mp_raise_msg(&mp_type_MemoryError, MP_ERROR_TEXT("There is a maximum of 2 frame buffers allowed")); return mp_const_none; - #endif } - void *buf = heap_caps_calloc(1, 1, MALLOC_CAP_INTERNAL); + if (self->view1 != NULL && self->buffer_size != size) { + mp_raise_msg_varg(&mp_type_MemoryError, MP_ERROR_TEXT("Frame buffer sizes do not match (%lu)"), size); + return mp_const_none; + } - mp_obj_array_t *view = MP_OBJ_TO_PTR(mp_obj_new_memoryview(BYTEARRAY_TYPECODE, 1, buf)); - view->typecode |= 0x80; // used to indicate writable buffer + // A real buffer for now, so the memoryview is always valid; once the + // bus is initialized it is repointed at the frame buffer the DPI panel + // driver allocated itself and this one is freed again (dsi_init). + void *buf = heap_caps_calloc(1, size, caps); - uint32_t available = (uint32_t)heap_caps_get_largest_free_block(caps); - if (available < size) { - heap_caps_free(buf); - mp_raise_msg_varg( - &mp_type_MemoryError, - MP_ERROR_TEXT("Not enough memory available (%d)"), - size - ); + if (buf == NULL) { + mp_raise_msg_varg(&mp_type_MemoryError, MP_ERROR_TEXT("Not enough memory available (%lu)"), size); return mp_const_none; } + mp_obj_array_t *view = MP_OBJ_TO_PTR(mp_obj_new_memoryview(BYTEARRAY_TYPECODE, size, buf)); + view->typecode |= 0x80; // used to indicate writable buffer + if (self->view1 == NULL) { self->buffer_size = size; + self->buffer_flags = caps; self->view1 = view; self->panel_config.num_fbs = 1; - } else if (self->buffer_size != size) { - heap_caps_free(buf); - mp_raise_msg_varg( - &mp_type_MemoryError, - MP_ERROR_TEXT("Frame buffer sizes do not match (%d)"), - size - ); - return mp_const_none; - } else if (self->view2 == NULL) { + } else { self->view2 = view; self->panel_config.num_fbs = 2; - } else { - heap_caps_free(buf); - mp_raise_msg(&mp_type_MemoryError, - MP_ERROR_TEXT("There is a maximum of 2 frame buffers allowed")); - return mp_const_none; } return MP_OBJ_FROM_PTR(view); } - + mp_lcd_err_t dsi_tx_color(mp_obj_t obj, int lcd_cmd, void *color, size_t color_size, int x_start, int y_start, int x_end, int y_end, uint8_t rotation, bool last_update) { - LCD_DEBUG_PRINT("dsi_tx_color(self, lcd_cmd=%d, color, color_size=%d, x_start=%d, y_start=%d, x_end=%d, y_end=%d)\n", lcd_cmd, color_size, x_start, y_start, x_end, y_end) + LCD_DEBUG_PRINT("dsi_tx_color(self, lcd_cmd=%d, color, color_size=%d, x_start=%d, y_start=%d, x_end=%d, y_end=%d, last_update=%d)\n", lcd_cmd, color_size, x_start, y_start, x_end, y_end, (int)last_update) + LCD_UNUSED(lcd_cmd); + LCD_UNUSED(color_size); LCD_UNUSED(rotation); - LCD_UNUSED(last_update); mp_lcd_dsi_bus_obj_t *self = (mp_lcd_dsi_bus_obj_t *)obj; + esp_err_t ret; - self->trans_done = false; - self->transmitting_buf = color; + if (self->panel_handle == NULL) { + mp_raise_msg(&mp_type_ValueError, MP_ERROR_TEXT("DSIBus is not initialized")); + return LCD_ERR_INVALID_STATE; + } + + if (!self->panel_started) { + // First flush: the panel has been configured by now, start the + // DPI video stream. + ret = esp_lcd_panel_init(self->panel_handle); + + if (ret != 0) { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("%d(esp_lcd_panel_init)"), ret); + return ret; + } + self->panel_started = true; + } - esp_err_t ret = esp_lcd_panel_draw_bitmap( + self->trans_done = false; + self->transmitting_buf = NULL; + + // LVGL renders into the frame buffer itself, so for the DPI panel this + // is a cache write-back of the touched lines plus, if the buffer is + // not the one being scanned out, a swap at the next frame boundary. + // LVGL areas are inclusive, esp_lcd_panel_draw_bitmap() wants + // exclusive end coordinates. + ret = esp_lcd_panel_draw_bitmap( self->panel_handle, x_start, y_start, - x_end, - y_end, + x_end + 1, + y_end + 1, color ); if (ret != 0) { mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("%d(esp_lcd_panel_draw_bitmap)"), ret); - return LCD_OK; + return ret; } - if (self->callback == mp_const_none || self->panel_config.num_fbs != 2) { - while (!self->trans_done) {} - self->trans_done = false; + if (!last_update) { + // One of several areas of the same update: the pixels are in the + // frame buffer, LVGL can carry on with the next area right away. + // Only the last area waits for the panel to pick the buffer up. + self->trans_done = true; + + if (self->callback != mp_const_none && mp_obj_is_callable(self->callback)) { + mp_call_function_n_kw(self->callback, 0, 0, NULL); + } + } else { + // Completion is reported from the refresh-done interrupt + // (dsi_bus_refresh_done_cb); with no callback registered the + // generic tx_color method busy-waits on trans_done. + self->transmitting_buf = color; } return LCD_OK; diff --git a/ext_mod/lcd_bus/micropython.cmake b/ext_mod/lcd_bus/micropython.cmake index 8feb1ff2..a5c27113 100644 --- a/ext_mod/lcd_bus/micropython.cmake +++ b/ext_mod/lcd_bus/micropython.cmake @@ -19,6 +19,7 @@ if(ESP_PLATFORM) ${CMAKE_CURRENT_LIST_DIR}/esp32_src/rgb_bus.c ${CMAKE_CURRENT_LIST_DIR}/esp32_src/rgb_bus_rotation.c ${CMAKE_CURRENT_LIST_DIR}/esp32_src/rgb565_dither.c + ${CMAKE_CURRENT_LIST_DIR}/esp32_src/dsi_bus.c ) # gets esp_lcd include paths diff --git a/ext_mod/lcd_bus/modlcd_bus.c b/ext_mod/lcd_bus/modlcd_bus.c index 1da883e7..c37f4f21 100644 --- a/ext_mod/lcd_bus/modlcd_bus.c +++ b/ext_mod/lcd_bus/modlcd_bus.c @@ -6,6 +6,7 @@ #include "i2c_bus.h" #include "i80_bus.h" #include "rgb_bus.h" +#include "dsi_bus.h" #ifdef MP_PORT_UNIX #include "sdl_bus.h" @@ -291,6 +292,9 @@ static const mp_rom_map_elem_t mp_module_lcd_bus_globals_table[] = { { MP_ROM_QSTR(MP_QSTR_SPIBus), MP_ROM_PTR(&mp_lcd_spi_bus_type) }, { MP_ROM_QSTR(MP_QSTR_I2CBus), MP_ROM_PTR(&mp_lcd_i2c_bus_type) }, { MP_ROM_QSTR(MP_QSTR_I80Bus), MP_ROM_PTR(&mp_lcd_i80_bus_type) }, + #if defined(ESP_IDF_VERSION) && SOC_MIPI_DSI_SUPPORTED + { MP_ROM_QSTR(MP_QSTR_DSIBus), MP_ROM_PTR(&mp_lcd_dsi_bus_type) }, + #endif { MP_ROM_QSTR(MP_QSTR__pump_main_thread), MP_ROM_PTR(&mp_lcd_bus__pump_main_thread_obj) }, #ifdef MP_PORT_UNIX From 18d6f14f073d768c7b5146ea032c865b628f53c4 Mon Sep 17 00:00:00 2001 From: Bitcoin3us <115934595+bitcoin3us@users.noreply.github.com> Date: Fri, 25 Sep 2026 18:01:04 +0100 Subject: [PATCH 2/3] lcd_bus: DSIBus rotation through the PPA (ESP32-P4) Panels on the DSI video interface cannot swap rows and columns, so a landscape UI on a portrait panel needs the picture rotated on the way to the frame buffer. DSIBus(rotation=90|180|270) does that with the ESP32-P4's PPA (pixel processing accelerator): LVGL renders the rotated (logical) picture into the bus's own screen-sized buffers, and on the last flush of an update the whole picture is rotated by the PPA into the panel's back buffer, which is then swapped in at the next frame boundary (the refresh-done interrupt reports completion as before). The panel side is always double-buffered in this mode; intermediate areas of a multi-area update complete immediately. The angle follows LVGL's convention, so pass the same rotation to lv_display. Frame buffers are now allocated cache-line aligned (the PPA and the DPI DMA both need it), the PPA driver's include path is added where the chip has one, and rotation is refused on chips without a PPA. Verified on the Waveshare ESP32-P4-WIFI6-Touch-LCD-4.3 (480x800 panel, rotation=90): LVGL runs at 800x480, a forced full-screen refresh takes ~29 ms (render + PPA rotation + vsync) against ~15 ms unrotated. Co-Authored-By: Claude Fable 5.1 --- ext_mod/lcd_bus/esp32_include/dsi_bus.h | 18 +++ ext_mod/lcd_bus/esp32_src/dsi_bus.c | 199 ++++++++++++++++++++++-- ext_mod/lcd_bus/micropython.cmake | 11 ++ 3 files changed, 214 insertions(+), 14 deletions(-) diff --git a/ext_mod/lcd_bus/esp32_include/dsi_bus.h b/ext_mod/lcd_bus/esp32_include/dsi_bus.h index 9b68b7c3..d6c8365f 100644 --- a/ext_mod/lcd_bus/esp32_include/dsi_bus.h +++ b/ext_mod/lcd_bus/esp32_include/dsi_bus.h @@ -23,6 +23,10 @@ #include "esp_lcd_mipi_dsi.h" #include "esp_ldo_regulator.h" + #if SOC_PPA_SUPPORTED + #include "driver/ppa.h" + #endif + typedef struct _mp_lcd_dsi_bus_obj_t { // The first members mirror mp_lcd_bus_obj_t (lcd_types.h): the @@ -55,6 +59,20 @@ int phy_ldo_voltage_mv; esp_ldo_channel_handle_t phy_ldo_handle; + // rotation (0/90/180/270 degrees, LVGL's sense): LVGL renders the + // rotated (logical) picture into its own buffers and every update + // is rotated into the panel's frame buffers by the PPA + int rotation; + uint8_t bpp; + uint32_t lvgl_width; + uint32_t lvgl_height; + uint8_t *panel_fbs[2]; + uint8_t back_fb_index; + uint32_t fb_size_aligned; + #if SOC_PPA_SUPPORTED + ppa_client_handle_t ppa_client; + #endif + } mp_lcd_dsi_bus_obj_t; extern const mp_obj_type_t mp_lcd_dsi_bus_type; diff --git a/ext_mod/lcd_bus/esp32_src/dsi_bus.c b/ext_mod/lcd_bus/esp32_src/dsi_bus.c index 15a8309a..73390610 100644 --- a/ext_mod/lcd_bus/esp32_src/dsi_bus.c +++ b/ext_mod/lcd_bus/esp32_src/dsi_bus.c @@ -38,6 +38,12 @@ #include "hal/lcd_types.h" #include "esp_lcd_mipi_dsi.h" #include "esp_ldo_regulator.h" + #include "hal/cache_hal.h" + #include "hal/cache_ll.h" + + #if SOC_PPA_SUPPORTED + #include "driver/ppa.h" + #endif // Prefix of ESP-IDF's private esp_lcd_dpi_panel_t (esp_lcd/dsi/esp_lcd_panel_dpi.c), @@ -71,6 +77,14 @@ } + // frame buffers (and the PPA's output) have to be cache-line aligned + static uint32_t dsi_cache_line_size(void) + { + uint32_t line = cache_hal_get_cache_line_size(CACHE_LL_LEVEL_EXT_MEM, CACHE_TYPE_DATA); + return line ? line : 64; + } + + // Runs from the DPI panel's end-of-frame interrupt: the DMA has just // restarted its scan-out from fbs[cur_fb_index]. Once that is the buffer // LVGL last flushed, the other buffer is free to be drawn into. @@ -107,7 +121,8 @@ ARG_vsync_pulse_width, ARG_dpi_clock_freq, ARG_phy_ldo_channel, - ARG_phy_ldo_voltage_mv + ARG_phy_ldo_voltage_mv, + ARG_rotation }; const mp_arg_t make_new_args[] = { @@ -123,7 +138,8 @@ { MP_QSTR_vsync_pulse_width, MP_ARG_INT | MP_ARG_KW_ONLY, { .u_int = 1 } }, { MP_QSTR_dpi_clock_freq, MP_ARG_INT | MP_ARG_KW_ONLY, { .u_int = 0 } }, // DPI pixel clock, MHz; 0 = same number as freq { MP_QSTR_phy_ldo_channel, MP_ARG_INT | MP_ARG_KW_ONLY, { .u_int = -1 } }, // internal LDO channel powering the DSI PHY (ESP32-P4 boards: 3), -1 = not managed here - { MP_QSTR_phy_ldo_voltage_mv, MP_ARG_INT | MP_ARG_KW_ONLY, { .u_int = 2500 } } + { MP_QSTR_phy_ldo_voltage_mv, MP_ARG_INT | MP_ARG_KW_ONLY, { .u_int = 2500 } }, + { MP_QSTR_rotation, MP_ARG_INT | MP_ARG_KW_ONLY, { .u_int = 0 } } // 0/90/180/270 degrees (LVGL's sense); the PPA rotates every frame into the panel }; mp_arg_val_t args[MP_ARRAY_SIZE(make_new_args)]; @@ -166,6 +182,18 @@ self->phy_ldo_channel = (int)args[ARG_phy_ldo_channel].u_int; self->phy_ldo_voltage_mv = (int)args[ARG_phy_ldo_voltage_mv].u_int; + self->rotation = (int)args[ARG_rotation].u_int; + if (self->rotation != 0 && self->rotation != 90 && self->rotation != 180 && self->rotation != 270) { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("rotation must be 0, 90, 180 or 270 (%d)"), self->rotation); + return mp_const_none; + } + #if !SOC_PPA_SUPPORTED + if (self->rotation != 0) { + mp_raise_msg(&mp_type_ValueError, MP_ERROR_TEXT("rotation needs a PPA (pixel processing accelerator), this chip has none")); + return mp_const_none; + } + #endif + LCD_DEBUG_PRINT("bus_id=%d\n", self->bus_config.bus_id) LCD_DEBUG_PRINT("num_data_lanes=%d\n", self->bus_config.num_data_lanes) LCD_DEBUG_PRINT("lane_bit_rate_mbps=%d\n",self->bus_config.lane_bit_rate_mbps) @@ -178,6 +206,7 @@ LCD_DEBUG_PRINT("vsync_back_porch=%d\n", self->panel_config.video_timing.vsync_back_porch) LCD_DEBUG_PRINT("vsync_pulse_width=%d\n", self->panel_config.video_timing.vsync_pulse_width) LCD_DEBUG_PRINT("phy_ldo_channel=%d\n", self->phy_ldo_channel) + LCD_DEBUG_PRINT("rotation=%d\n", self->rotation) self->panel_io_handle.get_lane_count = &dsi_get_lane_count; self->panel_io_handle.del = &dsi_del; @@ -234,6 +263,25 @@ // LVGL renders in the frame buffers' own byte order; nothing here to swap. self->rgb565_byte_swap = false; + self->bpp = bpp; + + if (self->rotation == 90 || self->rotation == 270) { + self->lvgl_width = (uint32_t)height; + self->lvgl_height = (uint32_t)width; + } else { + self->lvgl_width = (uint32_t)width; + self->lvgl_height = (uint32_t)height; + } + + if (self->rotation != 0) { + if (bpp == 18) { + mp_raise_msg(&mp_type_ValueError, MP_ERROR_TEXT("rotation works with 16 and 24 bits per pixel, not 18")); + return LCD_ERR_INVALID_ARG; + } + // the rotated frame goes into the panel's back buffer while the + // front one is scanned out, whatever LVGL's own buffering is + self->panel_config.num_fbs = 2; + } self->panel_config.video_timing.h_size = (uint32_t)width; self->panel_config.video_timing.v_size = (uint32_t)height; @@ -302,21 +350,45 @@ return ret; } - // Point the buffers handed out by allocate_framebuffer() at the DPI - // panel's frame buffers and drop the interim allocations. dpi_panel_t *dpi_panel = __containerof((esp_lcd_panel_t *)self->panel_handle, dpi_panel_t, base); + self->panel_fbs[0] = dpi_panel->fbs[0]; + self->panel_fbs[1] = self->panel_config.num_fbs == 2 ? dpi_panel->fbs[1] : NULL; + + if (self->rotation == 0) { + // Point the buffers handed out by allocate_framebuffer() at the DPI + // panel's frame buffers and drop the interim allocations: LVGL + // renders straight into what the panel scans out. + heap_caps_free(self->view1->items); + self->view1->items = (void *)self->panel_fbs[0]; + self->view1->len = self->buffer_size; + + if (self->view2 != NULL) { + heap_caps_free(self->view2->items); + self->view2->items = (void *)self->panel_fbs[1]; + self->view2->len = self->buffer_size; + } + } else { + #if SOC_PPA_SUPPORTED + // LVGL keeps its own (logical, rotated) buffers; each finished + // update is rotated into the panel's back buffer by the PPA. + ppa_client_config_t ppa_cfg = { + .oper_type = PPA_OPERATION_SRM, + .max_pending_trans_num = 1, + }; + ret = ppa_register_client(&ppa_cfg, &self->ppa_client); - heap_caps_free(self->view1->items); - self->view1->items = (void *)dpi_panel->fbs[0]; - self->view1->len = self->buffer_size; + if (ret != 0) { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("%d(ppa_register_client)"), ret); + return ret; + } - if (self->view2 != NULL) { - heap_caps_free(self->view2->items); - self->view2->items = (void *)dpi_panel->fbs[1]; - self->view2->len = self->buffer_size; + uint32_t line = dsi_cache_line_size(); + self->fb_size_aligned = (full_frame_size + line - 1) / line * line; + self->back_fb_index = 1; + #endif } - LCD_DEBUG_PRINT("fb1=%p fb2=%p\n", self->view1->items, self->view2 != NULL ? self->view2->items : NULL) + LCD_DEBUG_PRINT("fb1=%p fb2=%p rotation=%d\n", self->view1->items, self->view2 != NULL ? self->view2->items : NULL, self->rotation) return LCD_OK; } @@ -337,20 +409,35 @@ } self->panel_handle = NULL; self->panel_started = false; + self->panel_fbs[0] = NULL; + self->panel_fbs[1] = NULL; - // the frame buffers went with the panel + // unrotated: the frame buffers went with the panel; rotated: they are ours if (self->view1 != NULL) { + if (self->rotation != 0) { + heap_caps_free(self->view1->items); + } self->view1->items = NULL; self->view1->len = 0; self->view1 = NULL; } if (self->view2 != NULL) { + if (self->rotation != 0) { + heap_caps_free(self->view2->items); + } self->view2->items = NULL; self->view2->len = 0; self->view2 = NULL; } } + #if SOC_PPA_SUPPORTED + if (self->ppa_client != NULL) { + ppa_unregister_client(self->ppa_client); + self->ppa_client = NULL; + } + #endif + if (self->panel_io_handle.panel_io != NULL) { ret = esp_lcd_panel_io_del(self->panel_io_handle.panel_io); if (ret != 0) { @@ -450,7 +537,7 @@ // A real buffer for now, so the memoryview is always valid; once the // bus is initialized it is repointed at the frame buffer the DPI panel // driver allocated itself and this one is freed again (dsi_init). - void *buf = heap_caps_calloc(1, size, caps); + void *buf = heap_caps_aligned_calloc(dsi_cache_line_size(), 1, size, caps); if (buf == NULL) { mp_raise_msg_varg(&mp_type_MemoryError, MP_ERROR_TEXT("Not enough memory available (%lu)"), size); @@ -502,6 +589,90 @@ self->panel_started = true; } + #if SOC_PPA_SUPPORTED + if (self->rotation != 0) { + if (!last_update) { + // more areas of this update follow; LVGL's buffer is complete + // only after the last one, nothing to rotate yet + self->trans_done = true; + + if (self->callback != mp_const_none && mp_obj_is_callable(self->callback)) { + mp_call_function_n_kw(self->callback, 0, 0, NULL); + } + return LCD_OK; + } + + // LVGL's buffer (the one this area lives in) holds the whole + // logical picture: rotate all of it into the panel's back buffer. + const uint8_t *base = NULL; + if (self->view1 != NULL && dsi_buf_in_fb(self, (const uint8_t *)self->view1->items, color)) { + base = (const uint8_t *)self->view1->items; + } else if (self->view2 != NULL && dsi_buf_in_fb(self, (const uint8_t *)self->view2->items, color)) { + base = (const uint8_t *)self->view2->items; + } else { + mp_raise_msg(&mp_type_ValueError, MP_ERROR_TEXT("flushed buffer is not a DSIBus frame buffer")); + return LCD_ERR_INVALID_ARG; + } + + uint8_t *dst = self->panel_fbs[self->back_fb_index]; + ppa_srm_color_mode_t cm = self->bpp == 16 ? PPA_SRM_COLOR_MODE_RGB565 : PPA_SRM_COLOR_MODE_RGB888; + + ppa_srm_oper_config_t op = { 0 }; + op.in.buffer = base; + op.in.pic_w = self->lvgl_width; + op.in.pic_h = self->lvgl_height; + op.in.block_w = self->lvgl_width; + op.in.block_h = self->lvgl_height; + op.in.block_offset_x = 0; + op.in.block_offset_y = 0; + op.in.srm_cm = cm; + op.out.buffer = dst; + op.out.buffer_size = self->fb_size_aligned; + op.out.pic_w = self->panel_config.video_timing.h_size; + op.out.pic_h = self->panel_config.video_timing.v_size; + op.out.block_offset_x = 0; + op.out.block_offset_y = 0; + op.out.srm_cm = cm; + // LVGL's rotation and the PPA's are both counter-clockwise + op.rotation_angle = self->rotation == 90 ? PPA_SRM_ROTATION_ANGLE_90 : + self->rotation == 180 ? PPA_SRM_ROTATION_ANGLE_180 : + PPA_SRM_ROTATION_ANGLE_270; + op.scale_x = 1.0f; + op.scale_y = 1.0f; + op.mode = PPA_TRANS_MODE_BLOCKING; + + ret = ppa_do_scale_rotate_mirror(self->ppa_client, &op); + + if (ret != 0) { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("%d(ppa_do_scale_rotate_mirror)"), ret); + return ret; + } + + self->trans_done = false; + self->transmitting_buf = NULL; + + // whole-buffer "draw": cache write-back plus the swap to this buffer + // at the next frame boundary; the refresh-done interrupt reports it + ret = esp_lcd_panel_draw_bitmap( + self->panel_handle, + 0, + 0, + (int)self->panel_config.video_timing.h_size, + (int)self->panel_config.video_timing.v_size, + dst + ); + + if (ret != 0) { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("%d(esp_lcd_panel_draw_bitmap)"), ret); + return ret; + } + + self->transmitting_buf = dst; + self->back_fb_index ^= 1; + return LCD_OK; + } + #endif + self->trans_done = false; self->transmitting_buf = NULL; diff --git a/ext_mod/lcd_bus/micropython.cmake b/ext_mod/lcd_bus/micropython.cmake index a5c27113..1250153a 100644 --- a/ext_mod/lcd_bus/micropython.cmake +++ b/ext_mod/lcd_bus/micropython.cmake @@ -32,6 +32,17 @@ if(ESP_PLATFORM) list(APPEND LCD_INCLUDES ${ESP_LCD_INCLUDES}) endif() + # the DSI bus rotates with the PPA where the chip has one (ESP32-P4) + if(CONFIG_SOC_PPA_SUPPORTED) + idf_component_get_property(ESP_PPA_INCLUDES esp_driver_ppa INCLUDE_DIRS) + idf_component_get_property(ESP_PPA_DIR esp_driver_ppa COMPONENT_DIR) + + if(ESP_PPA_INCLUDES) + list(TRANSFORM ESP_PPA_INCLUDES PREPEND ${ESP_PPA_DIR}/) + list(APPEND LCD_INCLUDES ${ESP_PPA_INCLUDES}) + endif() + endif() + else() set(LCD_INCLUDES ${CMAKE_CURRENT_LIST_DIR} From 145be6281c22508fe94b7d503486ba1eb2555bbb Mon Sep 17 00:00:00 2001 From: Bitcoin3us <115934595+bitcoin3us@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:31:38 +0100 Subject: [PATCH 3/3] Add build-time patches: absolute USER_C_MODULES, RISC-V frozen native alignment Two build fixes as .patch files applied by MicroPythonOS's scripts/build_mpos.sh (see CONTRIBUTING.md), rather than edits to builder/ in this branch. esp32_user_c_modules_abspath.patch (apply in this repo's root): builder/esp32.py passed USER_C_MODULES=../../../../../ext_mod/micropython.cmake, which CMake resolves against lib/micropython/ports/esp32's real location. When lib/ is symlinked to another checkout (git worktrees sharing one MicroPython tree and build directory) that silently compiles the OTHER checkout's ext_mod. The patch passes the absolute path instead. esp32_riscv_frozen_native_align.patch (apply in lib/micropython): frozen @micropython.native / viper functions crashed with an illegal instruction on their first call on the ESP32-P4. tools/mpy-tool.py emits their machine code in a code section with aligned(2) for RISC-V, but the RISC-V assembler never pads a 2-byte alignment inside a code section, so every array following an odd-sized one starts at an odd address; jalr clears the low bit and the CPU decodes garbage. A 4-byte alignment is padded, but only when the file is assembled without linker relaxation (with relaxation the padding is R_RISCV_ALIGN NOPs, which fail on odd sizes: "3 bytes required for alignment ... can't relax section"). So the patch makes mpy-tool.py use aligned(4) for RV32/RV64, as it already does for Xtensa, and compiles frozen_content.c with -mno-relax on RISC-V targets. Xtensa builds are unchanged. Both were previously done by builder/esp32.py code in this PR. Co-Authored-By: Claude Opus 5.5 --- esp32_riscv_frozen_native_align.patch | 47 +++++++++++++++++++++++++++ esp32_user_c_modules_abspath.patch | 15 +++++++++ 2 files changed, 62 insertions(+) create mode 100644 esp32_riscv_frozen_native_align.patch create mode 100644 esp32_user_c_modules_abspath.patch diff --git a/esp32_riscv_frozen_native_align.patch b/esp32_riscv_frozen_native_align.patch new file mode 100644 index 00000000..14744dcf --- /dev/null +++ b/esp32_riscv_frozen_native_align.patch @@ -0,0 +1,47 @@ +diff --git a/ports/esp32/esp32_common.cmake b/ports/esp32/esp32_common.cmake +index e3b1b81..6ec1f26 100644 +--- a/ports/esp32/esp32_common.cmake ++++ b/ports/esp32/esp32_common.cmake +@@ -311,6 +311,15 @@ endforeach() + # Include the main MicroPython cmake rules. + include(${MICROPY_DIR}/py/mkrules.cmake) + ++if(CONFIG_IDF_TARGET_ARCH_RISCV AND MICROPY_FROZEN_CONTENT) ++ # Frozen native code is emitted as byte arrays in a code section with a ++ # 4-byte alignment (tools/mpy-tool.py). With linker relaxation on, the ++ # assembler pads that alignment with R_RISCV_ALIGN NOPs, which cannot ++ # cover odd sizes ("can't relax section"); the file holds no code that ++ # could be relaxed, so assemble it without relaxation. ++ set_source_files_properties(${MICROPY_FROZEN_CONTENT} PROPERTIES COMPILE_OPTIONS "-mno-relax") ++endif() ++ + # Generate source files for named pins (requires mkrules.cmake for MICROPY_GENHDR_DIR). + + set(GEN_PINS_PREFIX "${MICROPY_PORT_DIR}/boards/pins_prefix.c") +diff --git a/tools/mpy-tool.py b/tools/mpy-tool.py +index 67e2cbf..a61b0cc 100644 +--- a/tools/mpy-tool.py ++++ b/tools/mpy-tool.py +@@ -1254,13 +1254,19 @@ class RawCodeNative(RawCode): + MP_NATIVE_ARCH_ARMV6, + MP_NATIVE_ARCH_XTENSA, + MP_NATIVE_ARCH_XTENSAWIN, ++ MP_NATIVE_ARCH_RV32IMC, ++ MP_NATIVE_ARCH_RV64IMC, + ): +- # ARMV6 or Xtensa -- four byte align. ++ # ARMV6, Xtensa or RISC-V -- four byte align. RISC-V needs only two, ++ # but the assembler never pads a 2-byte alignment inside a code ++ # section, so arrays after an odd-sized one would start at an odd ++ # address (a 4-byte one is padded once relaxation is off, see ++ # ports/esp32/esp32_common.cmake). + self.fun_data_attributes += " __attribute__ ((aligned (4)))" + elif ( + MP_NATIVE_ARCH_ARMV6M <= config.native_arch <= MP_NATIVE_ARCH_ARMV7EMDP +- ) or MP_NATIVE_ARCH_RV32IMC <= config.native_arch <= MP_NATIVE_ARCH_RV64IMC: +- # ARMVxxM or RV{32,64}IMC -- two byte align. ++ ): ++ # ARMVxxM -- two byte align. + self.fun_data_attributes += " __attribute__ ((aligned (2)))" + + def disassemble(self): diff --git a/esp32_user_c_modules_abspath.patch b/esp32_user_c_modules_abspath.patch new file mode 100644 index 00000000..ce3d16c6 --- /dev/null +++ b/esp32_user_c_modules_abspath.patch @@ -0,0 +1,15 @@ +diff --git a/builder/esp32.py b/builder/esp32.py +index 3f5fd15..bfc442d 100644 +--- a/builder/esp32.py ++++ b/builder/esp32.py +@@ -612,7 +612,9 @@ def build_commands(_, extra_args, script_dir, lv_cflags, ___): + f'LV_CFLAGS="{lv_cflags}"', + f'LV_PORT=esp32', + f'BOARD={board}', +- 'USER_C_MODULES=../../../../../ext_mod/micropython.cmake' ++ # absolute: the relative form resolves against lib/micropython's real ++ # location, which is a different tree when lib/ is symlinked (git worktrees) ++ f'USER_C_MODULES={SCRIPT_DIR}/ext_mod/micropython.cmake' + ]) + + # esp_cmd.extend(extra_args)