Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion components/m5stack-tab5/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# ESP-IDF 6.1 adds esp_driver_dma's async color-convert driver (DMA2D window
# copies), which the back-buffer flush uses when present (see video.cpp).
set(m5stack_tab5_extra_requires "")
if("${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}" VERSION_GREATER_EQUAL "6.1")
list(APPEND m5stack_tab5_extra_requires esp_driver_dma)
endif()

idf_component_register(
INCLUDE_DIRS "include"
SRC_DIRS "src"
REQUIRES driver esp_driver_i2s esp_driver_ppa esp_driver_sdmmc esp_driver_spi esp_lcd esp_video esp_cam_sensor fatfs base_component bmi270 codec display display_drivers gt911 i2c ina226 input_drivers interrupt pi4ioe5v rx8130ce sdcard st7123touch task touch
REQUIRES driver esp_driver_i2s esp_driver_ppa esp_driver_sdmmc esp_driver_spi esp_lcd esp_video esp_cam_sensor fatfs base_component bmi270 codec display display_drivers gt911 i2c ina226 input_drivers interrupt pi4ioe5v rx8130ce sdcard st7123touch task touch ${m5stack_tab5_extra_requires}
REQUIRED_IDF_TARGETS "esp32p4"
)
18 changes: 18 additions & 0 deletions components/m5stack-tab5/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,22 @@ menu "M5Stack Tab5 Configuration"
application that drives the frame buffers itself. Extra buffers without
DIRECT mode buy nothing: LVGL keeps copying into the scanned-out buffer.

config M5STACK_TAB5_BACK_BUFFER_FLUSH
bool "Compile in the back-buffer LVGL flush (tear-free with rotation)"
depends on M5STACK_TAB5_DPI_NUM_FBS > 1
default n
help
Compile in DisplayConfig::flush_to_back_buffer_enabled: LVGL keeps its
draw buffers, every flushed area is copied (DMA2D; PPA when rotated)
into the frame buffer the panel is not scanning out, and the panel
switches to it at the frame boundary once the frame is complete, so
frames never tear and the display rotation keeps working (unlike
LVGL's DIRECT mode). Off, nothing of it is built: the flush path is
unchanged and initialize_display() refuses the option. On, it is still
opt-in per display (the DisplayConfig flag defaults to off; nothing is
allocated or installed until it is set). Needs 2 frame buffers (1.8 MB
of PSRAM for the second) and the PPA; a full-screen redraw then takes
two panel periods (the frame-done wait) and the draw buffers can be
small strips: see the README's measurements.

endmenu
68 changes: 68 additions & 0 deletions components/m5stack-tab5/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,74 @@ the 1.8 MB rotation scratch buffer is not needed either). The frame buffers are
cache-line aligned, so they also satisfy a raised `LV_DRAW_BUF_ALIGN` (e.g. 128
for the PPA draw unit), which a plain `heap_caps_malloc()` draw buffer may not.

## Tear-free flushes with rotation: the back-buffer flush

DIRECT mode cannot rotate. The back-buffer flush keeps LVGL's normal partial
rendering into draw buffers (so the PPA rotation of each area, and the panel's
own 180-degree flip, keep working) and gets rid of tearing differently:

- Compiled in with `CONFIG_M5STACK_TAB5_BACK_BUFFER_FLUSH` (off by default:
an application that does not enable it carries none of the code, and its
flush path is unchanged). Then `initialize_display({.flush_to_back_buffer_enabled
= true})` (needs `CONFIG_M5STACK_TAB5_DPI_NUM_FBS` >= 2 and the PPA) turns it
on per display; nothing is allocated or installed until it is set. Every flushed area is
copied, rotated when needed, by the PPA into the frame buffer the panel is
*not* scanning out; nothing is drawn into the buffer on screen.
- When LVGL's last area of a frame has landed, the flush switches the panel to
that buffer with `present_frame()` at the frame boundary, then has the PPA
copy the region this frame changed from the new front buffer into the new
back buffer, left in flight while LVGL renders the next frame, so the back
buffer is current when the next frame's areas are written into it. (With
three buffers each one is brought up to date when it becomes the back
buffer.)
- The panel never shows a half-drawn frame, whatever the draw-buffer size.
That is what makes small draw buffers viable again: with a single frame
buffer a full-screen redraw flushes strip by strip into the buffer on
screen, visible as a progressive wipe, which is why the example used two
whole-frame draw buffers (3.7 MB); with the back-buffer flush the strips are
assembled off screen and shown at once.

```cpp
// menuconfig: M5Stack Tab5 Configuration -> Number of DPI frame buffers = 2,
// Compile in the back-buffer LVGL flush = y
auto &tab5 = espp::M5StackTab5::get();
tab5.initialize_lcd();
tab5.initialize_display({.pixel_buffer_size = 720 * 128, // 128-row strips (184 KB each)
.flush_to_back_buffer_enabled = true});
```

Costs: the flush of a frame's last area waits for the frame-done event (one
refresh, like DIRECT mode), and the PPA does one extra copy of the changed
region per frame. Pixels written with `write_lcd_lines()` land in the buffer on
screen and are not carried over to the next frame: draw through LVGL.

Measured with the example's display benchmark (a local test aid, not in the
repository: four phases of 8 s at 0 and at 90 degrees, same unit as the earlier
tables) on a Tab5 (ST71xx, ESP-IDF v6.1). "Stock" is one frame buffer with two
whole-frame draw buffers (the example's default); the back-buffer flush uses two
frame buffers, with whole-frame or 128-row (184 KB) draw buffers:

| phase | stock (tears) | back-buffer, whole-frame draw buffers | back-buffer, 128-row strips |
| --- | --- | --- | --- |
| small update, 0 deg | 993 fps (unthrottled) | 57.8 (locked to the 58 Hz panel) | 57.8 |
| sweeping bar, 0 deg | 350 | 57.7, no tearing | 57.5, no tearing |
| full-screen redraw, 0 deg | 38.2 | 28.9 | 28.9 |
| heavy animation, 0 deg | 15.7 | 11.5 | 11.5 |
| small update, 90 deg | 589 | 57.8 | 57.8 |
| sweeping bar, 90 deg | 260 | 56.7 | 56.8 |
| full-screen redraw, 90 deg | 16.2 | 11.6 | 14.4 |
| heavy animation, 90 deg | 9.4 | 6.4 | 7.2 |
| free PSRAM after display init | 26.36 MB | 24.52 MB | 27.89 MB |

A full-screen redraw at 0 degrees takes two panel periods (34.5 ms): render plus
copy is about 26 ms, which misses one frame-done event and waits for the next -
the price of never showing a half-drawn frame, as in DIRECT mode. At 90 degrees
the strips are faster than whole-frame draw buffers because the PPA rotation of
one strip overlaps the rendering of the next. The 128-row configuration frees
1.5 MB of PSRAM against the stock example (two frame buffers plus two 184 KB
draw buffers, 4.1 MB, against one frame buffer plus two whole-frame draw
buffers, 5.5 MB).

## Example

The [example](./example) shows how to use the `espp::M5StackTab5` hardware abstraction component to initialize and use various subsystems of the Tab5.
Expand Down
31 changes: 31 additions & 0 deletions components/m5stack-tab5/example/main/Kconfig.projbuild
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,35 @@ menu "M5Stack Tab5 Example Configuration"
The rotate button does nothing in this mode: LVGL cannot rotate in
DIRECT mode.

config M5STACK_TAB5_EXAMPLE_FLUSH_TO_BACK_BUFFER
bool "Flush LVGL into the back frame buffer (tear-free, rotation works)"
depends on M5STACK_TAB5_BACK_BUFFER_FLUSH && !M5STACK_TAB5_EXAMPLE_RENDER_TO_FRAME_BUFFERS
default n
help
Initialize the display with
DisplayConfig::flush_to_back_buffer_enabled (compiled in with
CONFIG_M5STACK_TAB5_BACK_BUFFER_FLUSH, M5Stack Tab5 Configuration): every LVGL flush is
copied (and rotated) by the PPA into the frame buffer the panel is
not scanning out, and the panel switches to it at the frame
boundary once the frame is complete. Partial frames are never on
screen, so the draw buffers can be strips (see
M5STACK_TAB5_EXAMPLE_DRAW_BUFFER_LINES) and a full-screen redraw
still appears at once. The rotate button works. Needs
CONFIG_M5STACK_TAB5_DPI_NUM_FBS >= 2 (M5Stack Tab5 Configuration).

config M5STACK_TAB5_EXAMPLE_DRAW_BUFFER_LINES
int "Rows per LVGL draw buffer"
depends on !M5STACK_TAB5_EXAMPLE_RENDER_TO_FRAME_BUFFERS
range 8 1280
default 1280
help
Height, in panel rows of 720 pixels, of each of LVGL's two draw
buffers; 1280 is a whole frame (1.8 MB of PSRAM each). With one
frame buffer a full-screen redraw flushes strip by strip into the
buffer on screen, so a whole-frame draw buffer is what makes it
appear at once; with the back-buffer flush the strips are
assembled off screen and a far smaller draw buffer (e.g. 128 rows,
184 KB, the BSP's default strip) looks the same and leaves the
PSRAM to the application.

endmenu
3 changes: 2 additions & 1 deletion components/m5stack-tab5/example/main/gui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,8 @@ bool Gui::update(std::mutex &m, std::condition_variable &cv) {
update_audio_label();
}
std::unique_lock<std::mutex> lock(m);
#if CONFIG_M5STACK_TAB5_EXAMPLE_RENDER_TO_FRAME_BUFFERS
#if CONFIG_M5STACK_TAB5_EXAMPLE_RENDER_TO_FRAME_BUFFERS || \
CONFIG_M5STACK_TAB5_EXAMPLE_FLUSH_TO_BACK_BUFFER
// The flush already waits for the panel's frame-done event, which paces a
// frame that has something to draw; here only sleep until LVGL's next timer
// is due (at least 1 ms so an idle LVGL never spins). lv_timer_handler()
Expand Down
34 changes: 19 additions & 15 deletions components/m5stack-tab5/example/main/m5stack_tab5_example.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,28 +87,32 @@ extern "C" void app_main(void) {
return;
}

// initialize the display with a full-frame pixel buffer (Tab5 is 1280x720 with
// 2 bytes per pixel). A full-frame LVGL draw buffer (allocated in PSRAM, see
// initialize_display) lets a full-screen redraw - e.g. a rotation change -
// flush in a single pass instead of ~72 ten-line strips, so the screen
// repaints at once rather than wiping progressively. Partial updates still
// only render/flush their dirty area, so the larger buffer costs nothing
// there (just PSRAM).
logger.info("Initializing display ({} DPI frame buffers)...", tab5.frame_buffers().size());
#if CONFIG_M5STACK_TAB5_EXAMPLE_RENDER_TO_FRAME_BUFFERS
// LVGL renders straight into the panel's frame buffers (DIRECT mode): no
// draw buffers, no rotation scratch buffer, and no copy per flush.
// LVGL renders in DIRECT mode straight into the panel's frame buffers (no
// draw buffers); see the menuconfig help.
if (!tab5.initialize_display({.render_to_frame_buffers_enabled = true})) {
logger.error("Failed to initialize display!");
return;
}
#else
auto pixel_buffer_size = tab5.display_width() * tab5.display_height();
if (!tab5.initialize_display(pixel_buffer_size)) {
// LVGL draw buffers of CONFIG_M5STACK_TAB5_EXAMPLE_DRAW_BUFFER_LINES panel
// rows each. With one frame buffer a whole-frame draw buffer (the default, in
// PSRAM) lets a full-screen redraw - e.g. a rotation change - flush in a
// single pass instead of ten 128-row strips, so the screen repaints at once
// rather than wiping progressively. With the back-buffer flush
// (CONFIG_M5STACK_TAB5_EXAMPLE_FLUSH_TO_BACK_BUFFER) a full redraw is
// assembled off screen whatever the strip size, so a small draw buffer
// looks the same and frees the PSRAM.
const size_t pixel_buffer_size =
tab5.display_width() * static_cast<size_t>(CONFIG_M5STACK_TAB5_EXAMPLE_DRAW_BUFFER_LINES);
if (!tab5.initialize_display({
.pixel_buffer_size = pixel_buffer_size,
#if CONFIG_M5STACK_TAB5_EXAMPLE_FLUSH_TO_BACK_BUFFER
.flush_to_back_buffer_enabled = true,
#endif
})) {
#endif
logger.error("Failed to initialize display!");
return;
}
#endif

// make the filter we'll use for the IMU to compute the orientation
static constexpr float angle_noise = 0.001f;
Expand Down
71 changes: 71 additions & 0 deletions components/m5stack-tab5/include/m5stack-tab5.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
#include <driver/i2s_std.h>
#include <driver/i2s_tdm.h>
#include <driver/ppa.h>
#include <sdkconfig.h>
#if CONFIG_M5STACK_TAB5_BACK_BUFFER_FLUSH && __has_include(<esp_async_color_convert.h>)
// ESP-IDF 6.1+: DMA2D window copies (esp_driver_dma), used by the back-buffer flush
#include <esp_async_color_convert.h>
#define M5STACK_TAB5_HAS_ASYNC_FBCPY 1
#endif
#include <driver/sdmmc_host.h>
#include <driver/spi_master.h>
#include <driver/uart.h>
Expand Down Expand Up @@ -237,6 +243,27 @@ class M5StackTab5 : public BaseComponent {
/// callback with lv_display_set_flush_wait_cb(); the transfer-done
/// interrupt no longer does it.
bool render_to_frame_buffers_enabled{false};
/// Flush into the frame buffer the panel is NOT scanning out, and switch
/// the panel to it once the frame is complete (tear-free, and unlike
/// render_to_frame_buffers_enabled the display rotation keeps working).
/// Every flushed area is copied - rotated when needed - by the PPA into
/// the back frame buffer; when LVGL's last area of a frame has landed,
/// present_frame() switches the panel to that buffer at the frame
/// boundary, and the region this frame changed is copied into the new
/// back buffer (by the PPA, overlapping LVGL's next render) so it is
/// current for the next frame. Partial frames are never on screen, so
/// the draw buffers can be strips (pixel_buffer_size) instead of whole
/// frames: a full-screen redraw is assembled off screen and shown at
/// once. Needs initialize_lcd() first, CONFIG_M5STACK_TAB5_DPI_NUM_FBS >= 2
/// and a working PPA; ignored with render_to_frame_buffers_enabled. The
/// mode is compiled in with CONFIG_M5STACK_TAB5_BACK_BUFFER_FLUSH (off by
/// default: without it nothing of it is built and this flag is refused).
/// \note Costs: the flush of a frame's last area waits for the panel's
/// frame-done event (one refresh, like DIRECT mode), and the PPA
/// does an extra copy of the changed region per frame. Pixels
/// written with write_lcd_lines() land in the buffer on screen and
/// are not carried over to the next frame: draw through LVGL.
bool flush_to_back_buffer_enabled{false};
};

/// Initialize the LVGL display
Expand Down Expand Up @@ -1233,6 +1260,50 @@ class M5StackTab5 : public BaseComponent {
// initialize_display() set LVGL up in DIRECT mode on the frame buffers
// (DisplayConfig::render_to_frame_buffers_enabled)
bool direct_render_enabled_{false};
// Queue a non-blocking PPA copy of a block of a source picture into a frame
// buffer, rotated by `angle`, completed through notify_ppa_trans_done().
// Caller holds panel_op_mutex_ with no transfer in flight (and has set
// lvgl_flush_pending_ as it wants); sets transfer_in_flight_, cleared again
// if the operation is not queued (then returns false).
bool queue_ppa_block(const void *src, uint32_t src_pic_w, uint32_t src_pic_h, uint32_t src_x,
uint32_t src_y, uint32_t block_w, uint32_t block_h, void *dst_frame_buffer,
uint32_t dst_x, uint32_t dst_y, ppa_srm_rotation_angle_t angle);
// Index of a pointer inside one of frame_buffers(), or -1
int frame_buffer_index(const void *p) const;
#if CONFIG_M5STACK_TAB5_BACK_BUFFER_FLUSH
// initialize_display() set flush() up to land in the back frame buffer
// and switch at the frame boundary (DisplayConfig::flush_to_back_buffer_enabled)
bool back_buffer_flush_enabled_{false};
// Back-buffer flush bookkeeping, touched by the LVGL task only (flush()
// runs there): the panel-space region the frame in progress has changed so
// far, and per frame buffer the region it has missed since it was last on
// screen - what the sync copy after a switch brings over.
lv_area_t frame_dirty_area_{};
bool frame_dirty_valid_{false};
std::array<lv_area_t, max_frame_buffers> stale_area_{};
std::array<bool, max_frame_buffers> stale_valid_{};
// flush() for the back-buffer mode (see flush_to_back_buffer_enabled)
void flush_to_back_buffer(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map,
lv_display_rotation_t rotation);
// Queue a non-blocking unrotated window copy of a source picture into a
// frame buffer: on the DMA2D engine (ESP-IDF 6.1+, about five times the
// PPA's throughput) when available, else the PPA at angle 0. Same contract
// as queue_ppa_block().
bool queue_block_copy(const void *src, uint32_t src_stride_px, uint32_t src_height,
uint32_t src_x, uint32_t src_y, uint32_t w, uint32_t h,
void *dst_frame_buffer, uint32_t dst_x, uint32_t dst_y);
#if M5STACK_TAB5_HAS_ASYNC_FBCPY
// DMA2D copy-done callback (ISR) for queue_block_copy(); cb_args is the
// M5StackTab5 instance.
static bool notify_block_copy_done(async_color_convert_handle_t handle,
async_color_convert_event_data_t *edata, void *cb_args);
#endif
#endif // CONFIG_M5STACK_TAB5_BACK_BUFFER_FLUSH
// present_frame() with the number of frame-buffer rows whose cache is
// written back before the switch (the driver syncs the rows of the draw):
// the public call syncs the whole buffer for CPU-rendered frames; the
// back-buffer flush, whose frame buffers only DMA engines write, syncs one.
bool present_frame(const void *frame_buffer, uint32_t timeout_ms, int cache_sync_rows);

// Display controller detection
DisplayController display_controller_{DisplayController::UNKNOWN};
Expand Down
Loading
Loading