fix(m5stack-tab5): rotation audit, experimental opt-in ST7121 panel rotation (default off), and DSI underrun fixes - #738
fix(m5stack-tab5): rotation audit, experimental opt-in ST7121 panel rotation (default off), and DSI underrun fixes#738finger563 wants to merge 15 commits into
Conversation
…21 variant All three Tab5 display variants previously rotated every non-zero LVGL orientation at flush time via the ESP32-P4 PPA (with an lv_draw_sw_rotate fallback); the espp::Display rotation_callback was left null, so the display driver's set_rotation() (MADCTL) support was never used. Route rotation through the espp::Display machinery for the ST7121: - Wire the Display rotation_callback to a new on_display_rotation() which, for the ST7121, forwards 0/180 to the driver's set_rotation() (MADCTL GS/SS scan-direction flip done by the panel itself) and restores the natural scan direction for 90/270. - Skip the PPA/software buffer rotation in flush() when the panel handles the current rotation (new panel_handles_rotation()); the logical frame is then written unrotated and the panel flips it at scan-out, which lands partial areas exactly where LVGL's rotated mapping expects them. - 90/270 keep the PPA path: the Tab5 panels are MIPI-DSI DPI (video mode) panels streaming a fixed 720x1280 raster, so the panel cannot swap axes. The ILI9881 and ST7123 variants keep their existing PPA rotation path, and the touch transform is unchanged: touchpad_convert() maps physical to logical coordinates from the LVGL rotation, which is independent of how the display rotation is implemented. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
✅Static analysis result - no issues found! ✅ |
There was a problem hiding this comment.
Pull request overview
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
This PR updates M5Stack Tab5 rotation handling so the ST7121 (TDDI) panel uses the panel’s MADCTL scan-direction flip for 0°/180° rotation, avoiding PPA/software rotation artifacts with partial flush areas.
Changes:
- Wire
Display::LvglConfig::rotation_callbacktoM5StackTab5::on_display_rotation()to forward 0°/180° rotation to the ST7121 driver. - Add
panel_handles_rotation()to decide when to skip PPA/software rotation. - Update
flush()to bypass rotation when the panel applies it (ST7121, 180°).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| components/m5stack-tab5/src/video.cpp | Adds rotation callback handling, panel-rotation decision logic, and adjusts flush rotation path accordingly. |
| components/m5stack-tab5/include/m5stack-tab5.hpp | Declares the new rotation callback handler and helper used by the updated flush logic. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…ply initial rotation, document MADCTL ordering - Pin the espp::DisplayRotation <-> lv_display_rotation_t value correspondence with a static_assert and route all conversions through explicit to_lv_rotation()/to_display_rotation() helpers; the panel-vs-PPA decision in on_display_rotation() now uses the exact same panel_handles_rotation() predicate flush() uses. - Explicitly (re)apply the current rotation at the end of initialize_lcd() so the panel MADCTL matches flush()'s expectation from the first frame even if initialize_display() was called before initialize_lcd() (the normal order is already covered: LVGL sends LV_EVENT_RESOLUTION_CHANGED synchronously from lv_display_set_rotation() in Display's constructor, before any flush can run). - Document why the runtime MADCTL write is safe w.r.t. the DPI pipeline: it travels on the DSI generic/DBI command channel, hardware-arbitrated against the video stream and independent of the framebuffer/DMA path; worst case is a single transient frame during a rotation change, which is stated and accepted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ntal Kconfig (default off) Hardware testing showed the MADCTL GS/SS 180-degree scan flip renders corrupted on (at least some) ST7121 units. Root-cause analysis: the init sequence programs full TDDI gate/source mux tables (the 0xAC block), which vendor inits pair to a specific scan direction - a MADCTL GS flip alone reorders gate scanning without swapping those tables, producing interleaved garbage. (The bit positions themselves match Espressif's esp_lcd_st7121 mirror implementation, whose generic template was likely never validated on this glass either; ESPHome's mipi_dsi driver documents the same 0/180-only constraint for DSI panels generally, so the design was sound - this panel's init tables are what make runtime flips unsafe.) Default behavior is now EXACTLY the pre-branch known-good path: PPA/software rotation for all orientations and ZERO runtime MADCTL writes (the rotation callback is a full no-op), which also makes A/B comparison for unrelated display artifacts clean. The panel-side path remains available for experimentation via CONFIG_M5STACK_TAB5_ST7121_HW_ROTATION, and the rest of the branch (init-order hardening, single-predicate rotation decision, enum unification, documented DSI command-channel safety) stands. Both configurations build clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Hardware test result (ST7121 unit): the MADCTL 180° scan flip renders corrupted — addressed in 33eb080. Root cause analysis: the ST7121 init sequence programs full TDDI gate/source mux tables (the Resolution: panel-side rotation is now gated behind |
…n-out (display streaking) The Tab5's DPI (video mode) panel continuously scans its PSRAM framebuffer at ~140 MB/s via DW-GDMA; when concurrent PSRAM/AXI traffic starves that read stream the DSI bridge FIFO underruns and the panel shows streaks along the physical scan-line axis. The flush path was generating far more PSRAM traffic than needed: - On ESP-IDF 6.0 the esp_lcd_dpi_panel_config_t::flags.use_dma2d flag no longer exists (replaced by esp_lcd_dpi_panel_enable_dma2d()), so every esp_lcd_panel_draw_bitmap silently regressed to a CPU memcpy through the cache (~1.8 MB read + ~1.8 MB write + ~1.8 MB writeback per full frame). Call esp_lcd_dpi_panel_enable_dma2d() so copies run on the 2D-DMA engine again (matches M5Stack's UserDemo and esp-bsp's m5stack_tab5). - The PPA rotation path rotated into a PSRAM scratch buffer and then draw_bitmap-copied that into the framebuffer. The PPA output supports placing a block inside a larger picture, so rotate directly into the DPI framebuffer at the rotated offset instead, halving that path's PSRAM traffic. The CPU software-rotate fallback keeps the scratch buffer. - Register both PPA clients (display rotation and camera preview) with data_burst_length = 64 instead of the default 128: full-length PPA bursts are known to starve the DSI scan-out DMA even with 200 MHz hex PSRAM (lvgl/lvgl#9590; LVGL exposes the same knob as LV_PPA_BURST_LENGTH). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Display streaking: root cause + fixes (513f565)The streaking is consistent with DSI bridge FIFO underrun from PSRAM read contention: the DPI (video-mode) panel scans its PSRAM framebuffer continuously at ~140 MB/s via DW-GDMA, and when competing PSRAM/AXI traffic starves that stream the bridge discards pixels, which renders as streaks along the physical scan-line axis in any orientation. This matches "worse with Three traffic reductions landed in 513f565:
If streaks persist, please try in this order
If a serial console is attached during streaking, any |
…_rotation() call on_display_rotation()'s ordering comment claimed it runs on the LVGL thread, but initialize_lcd() also calls it directly on the init thread. Scope the LVGL-thread claim to the rotation-callback invocation and spell out at the call site why the direct init-path call is safe in either init order: in the documented order display_ is still null (no LVGL display, flush callback, or handler task exists, and the Display constructor re-fires the callback synchronously before any flush); in the reversed order the MADCTL write rides the DSI command channel, which never touches the DPI framebuffer or its DMA, so at worst one transient frame — and with CONFIG_M5STACK_TAB5_ST7121_HW_ROTATION off (default) the call is a no-op entirely. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rently flushing LVGL thread initialize_lcd() writes lcd_handles_.panel early (before panel->init(), DMA2D enable, and caching the dpi_framebuffer_/dpi_framebuffer_bytes_ pair), while flush() gated only on a plain null-check of that pointer. With the reversed init order (initialize_display() first, the app already pumping LVGL on another thread), flush() could race those plain writes: undefined behavior, and observably a not-yet-initialized panel or a torn framebuffer pointer/size pair. Publish the LCD state through a single std::atomic<bool> lcd_initialized_ gate: initialize_lcd() clears it on entry, writes every field (lcd_handles_, dpi_framebuffer_ + bytes, display_driver_, display_controller_), and store-releases it true as its final step; flush(), write_lcd_lines() and on_display_rotation() load-acquire it and no-op until it is true, so every plain write happens-before any read that observes the gate open. The init-path rotation call runs after the release store, and a rotation event that arrives while the gate is closed is harmless: initialize_lcd() re-applies the current LVGL rotation once the driver is up. Also restructure on_display_rotation()'s Kconfig-disabled early-return into #if/#else/#endif, fixing the cppcheck unreachableCode finding from static analysis. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- flush(): rotate a local copy of the LVGL area instead of mutating the const area pointer via const_cast (lv_display_rotate_area on a copy) - Tie the direct-to-framebuffer PPA rotation to the single-framebuffer panel configuration: name the num_fbs value (kNumDpiFramebuffers) and static_assert it is 1 where the framebuffer pointer is cached, since with multiple DPI framebuffers the cached pointer could target a non-scanned buffer - Replace the hard-coded 128-byte cache-line alignment with the value the PPA driver itself validates against, queried via esp_cache_get_alignment(MALLOC_CAP_SPIRAM | MALLOC_CAP_DMA) (used for both the framebuffer alignment check and the scratch-buffer allocation); add esp_mm to the component requirements for the header Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
components/m5stack-tab5/src/video.cpp:498
- Publishing
truehere exposes the panel before its initial MADCTL state is applied below. In the supported reversed-init order, an already-running LVGL thread can flush in this window; with experimental 180° rotation,panel_handles_rotation()skips PPA even though the panel is still in its old scan direction. Apply the initial panel rotation before opening the gate (using an init-only helper that does not require the gate).
lcd_initialized_.store(true, std::memory_order_release);
components/m5stack-tab5/src/video.cpp:154
- Clearing an atomic flag does not wait for readers that already observed the previous
true. A concurrentflush()orwrite_lcd_lines()can pass its acquire-load, then race the re-init writes todisplay_driver_,display_controller_, or framebuffer state despite the stated guarantee. Either prohibit re-initialization while readers run, or use reader/writer synchronization that waits for in-flight operations before rebuilding this state.
lcd_initialized_.store(false, std::memory_order_release);
…ublication-gate lifecycle - Skip esp_lcd_dpi_panel_enable_dma2d() on the ILI9881 variant: the repo documents (esp32-p4-function-ev-board, esp32-p4-nano) that the DMA2D draw_bitmap path corrupts RGB565 channel order on ILI9881C-family DSI panels; keep it for the ST7121/ST7123 TDDI variants (no corruption reported there, the underrun fix targets them, and M5Stack's own demo ships DMA2D-enabled on mostly-ST71xx units). Update the stale cross-reference in the function-ev-board note. - Apply the initial panel rotation BEFORE store-releasing lcd_initialized_: with the reversed init order an already-running LVGL thread could flush in the window between the gate opening and the MADCTL apply, skipping PPA rotation with the panel still in its old scan direction. The init path now uses a gate-free apply_panel_rotation() helper (same thread as the state writes) and the gate opens last. - Refuse LCD re-initialization once the gate has opened: clearing the atomic would not wait for readers that already observed true, so a concurrent flush()/write_lcd_lines() could race the re-init writes; the gate is now monotonic (false -> true, never back). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Addressed the two suppressed comments from the latest Copilot review in b85dd72 (same commit as the DMA2D gating fix). 1. Valid: in the reversed init order ( Fixed by restructuring so the gate opens last:
2. Also valid: Of the two options offered, I took "prohibit re-initialization":
Both 🤖 Generated with Claude Code |
…c Kconfig cache-line constant The PPA output-buffer alignment was queried at runtime via esp_cache_get_alignment(), which is only declared in the private esp_private/esp_cache_private.h header and can move or break across IDF releases. On the ESP32-P4 that call just reads back the L2 cache line size that cpu_start.c programs into the cache HAL from CONFIG_CACHE_L2_CACHE_LINE_SIZE (64 or 128), so use that public compile-time constant directly (with a conservative 128-byte fallback if the symbol is ever absent). This also removes the component's only use of esp_mm, so drop the esp_mm REQUIRES again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Suppressed comments (1)
components/m5stack-tab5/src/video.cpp:1
- The rotation path is hard-coded to
PPA_SRM_COLOR_MODE_RGB565(and elsewhere the scratch-buffer sizing usessizeof(uint16_t)), whileinitialize_lcd()configuresdpi_cfg.pixel_formatvia#if/#else. If this component can be built in a non-RGB565 mode (e.g., RGB888), these PPA operations and buffer sizing will produce incorrect results or memory corruption. Suggested fix: either enforce RGB565 for Tab5 at compile time (assert/panic early if not), or branch the PPA + buffer-size logic based on the active pixel format /sizeof(Pixel)and use the correct PPA color mode.
#include "m5stack-tab5.hpp"
The static-analysis CI flagged the new cache-line alignment checks (video.cpp:514): cppcheck's --force explores a preprocessor configuration where CONFIG_CACHE_L2_CACHE_LINE_SIZE folds to 1, trivializing the modulo checks (knownConditionTrueFalse/moduloofone); on real configurations the constant is 64 or 128 and both checks are meaningful. Also suppressed the flush()-path knownConditionTrueFalse where panel_handles_rotation() is a compile-time false in the (default) HW-rotation-disabled configuration. Inline suppressions with justifications; both build configurations still compile clean and local cppcheck reports no findings at either site. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… video path Address the remaining PR #738 review feedback on video.cpp: - Reset dpi_framebuffer_/dpi_framebuffer_bytes_ at the start of every initialize_lcd() attempt: a retry after a failed attempt that then cannot re-cache the framebuffer (query or alignment failure) could otherwise open the publication gate with a stale pointer from the previous attempt still cached, and flush()'s direct-to-framebuffer PPA path would write through it. - Make the kNumDpiFramebuffers commentary match the static_assert: the single-framebuffer assumption is an intentional compile-time guard (raising the count requires teaching the PPA path to track the active framebuffer), not a runtime fallback as the old comment claimed. - Enforce the RGB565-only video path at compile time: the DPI panel config, the PPA rotation color modes, and the buffer sizing all hard-code RGB565, so static_assert LV_COLOR_DEPTH == 16 (and a 2-byte Pixel) instead of corrupting output if LVGL were configured with a different color depth, and size the rotation scratch buffer with sizeof(Pixel) rather than a bare sizeof(uint16_t). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Addressing Copilot's suppressed comment (video.cpp: the PPA rotation hard-codes Fixed in 2d69b61, taking the suggested enforce-at-compile-time option. The Tab5 video path is RGB565-only by design (the DPI panel config, both PPA SRM color modes, and all buffer sizing assume 2-byte pixels), so video.cpp now carries a file-level Both example flag states ( |
… config site, Pixel-typed buffer Round-8 review fixes + static-analysis repair: - The rotation scratch buffer is allocated with MALLOC_CAP_SPIRAM | MALLOC_CAP_DMA - it is a PPA (DMA) output target and these are exactly the caps kPpaOutBufferAlignment is derived for (was SPIRAM|8BIT). - The DPI pixel-format selection is consolidated to ONE shared site for all three controller variants, with a comment noting the preprocessor branch only picks IDF-version field names for the single supported RGB565 format (the file-level static_assert already enforces RGB565-only). - third_buffer is typed as M5StackTab5::Pixel* to match the sizeof(Pixel) sizing introduced earlier. - Removed the now-stale knownConditionTrueFalse inline suppression on the flush() rotation gate: CI's cppcheck no longer emits that finding there, so the suppression itself failed the static-analysis run as unmatchedSuppression (cppcheck version divergence; same repair as the coredump component). Both Kconfig states build clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
components/m5stack-tab5/Kconfig:32
- This adds a user-facing opt-in behavior, but neither the component README nor the example README documents the option, its default, or the known corruption risk. The repository guidance requires both READMEs to be updated when an existing component's behavior changes; please add the same enablement and warning information there.
config M5STACK_TAB5_ST7121_HW_ROTATION
bool "ST7121: apply 0/180 rotation in the panel (EXPERIMENTAL)"
default n
…tial panel rotation initialize_lcd()'s init-path apply_panel_rotation() called lv_display_get_rotation() from the init thread when display_ already existed (reversed init order) - reading LVGL state without the app's LVGL lock while the GUI task may be running (LV_USE_OS == LV_OS_NONE). Use the configured initial 'rotation' instead; runtime rotation changes are still applied by on_display_rotation() (the LVGL rotation callback, on the GUI task). Both Kconfig states build clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
components/m5stack-tab5/src/video.cpp:794
- With the experimental option enabled, this drops every rotation event that occurs after
initialize_display()but before the LCD gate opens.initialize_lcd()then applies the compile-time initialrotation, not the current/pending value; for example, if LVGL changes to 180° during the documented reversed-init window, the panel is programmed to 0°, while the first flush observes 180° andpanel_handles_rotation()skips PPA rotation. Preserve pending rotations and synchronize the final apply/publication step so an event cannot be lost between applying MADCTL and opening the gate.
if (!lcd_initialized_.load(std::memory_order_acquire)) {
return;
}
…e global LVGL default flush() and rotated_display_width()/height() read the rotation from lv_display_get_default() rather than the display actually being used (the flush callback's disp argument / the BSP's own display). Correct in a single-display setup but wrong if another LVGL display is the default; matches the fix already applied to the sibling P4 BSPs. Builds clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s single-flight-safe With the IDF6 DMA2D hook, esp_lcd_panel_draw_bitmap() is asynchronous and single-flight (a second call while one is in flight returns ESP_ERR_INVALID_STATE). Both flush() and the public cross-thread write_lcd_lines() issued draws without serialization or checking the return, so a concurrent direct write could make an LVGL flush's draw fail with no on_color_trans_done -> LVGL waits forever; and a direct write's completion could be taken for an LVGL flush completion. Now every panel draw goes through draw_and_wait() under panel_op_mutex_: the ISR only gives a completion semaphore (no longer signals LVGL); each draw holds the mutex across the draw and its completion wait (only one transfer in flight); draw_and_wait() checks the return and skips the wait on failure, and flush() ALWAYS calls lv_display_flush_ready() afterwards so a failed draw never hangs LVGL (frame dropped); the PPA direct-to-FB path takes the same mutex around its framebuffer write. Both Kconfig states build clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
Audit of how the M5Stack Tab5's three display variants (ILI9881 / ST7123 / ST7121) handle rotation, plus fixes for DSI scan-out underrun (display streaking) found while testing on hardware.
The branch originally routed 0°/180° rotation on the ST7121 TDDI variant to the panel itself (MADCTL GS/SS scan-direction flip) instead of the ESP32-P4 PPA. Hardware testing showed the 180° MADCTL flip renders corrupted on (at least some) ST7121 units — the init sequence programs full TDDI gate/source mux tables (the
0xACblock) matched to the normal scan direction, and a MADCTL GS flip alone reorders gate scanning without swapping them. Panel-side rotation is therefore experimental and opt-in, gated behindCONFIG_M5STACK_TAB5_ST7121_HW_ROTATION(default off). The default build keeps the pre-branch known-good behavior: PPA rotation for all orientations and zero runtime MADCTL writes.What landed
Rotation audit + plumbing (default behavior unchanged)
Display::LvglConfig::rotation_callbackto a newM5StackTab5::on_display_rotation(); the rotation decision is centralized in a single predicate (panel_handles_rotation()) shared by the flush path and the callback.DisplayRotation<->lv_display_rotation_tmapping pinned down with explicit converters) and init-order hardening: the current rotation is (re)applied when the display driver comes up, correct in eitherinitialize_lcd()/initialize_display()order, with the thread-safety of the init-path call documented at the call site.CONFIG_M5STACK_TAB5_ST7121_HW_ROTATION(defaultn) with the corruption warning in its Kconfig help. When enabled, the ST7121 applies 0/180 via the display driver'sset_rotation()(MADCTL) andflush()skips the PPA for those orientations.touchpad_convert) is invariant to the transform mechanism; camera PPA pass unaffected.DSI underrun / streaking fixes (active by default)
Streaking traced to DSI bridge FIFO underrun from PSRAM read contention against the DPI panel's continuous ~140 MB/s framebuffer scan-out. Three traffic reductions:
draw_bitmap: IDF 6.0 removedflags.use_dma2din favor ofesp_lcd_dpi_panel_enable_dma2d(), which was never called — every flush had silently regressed to a CPU memcpy (~5.4 MB of PSRAM/cache traffic per full frame).draw_bitmapcopy, halving the rotation path's PSRAM traffic.Hardware testing
Testing
CONFIG_M5STACK_TAB5_ST7121_HW_ROTATIONoff and on) with ESP-IDF 6.0, esp32p4.🤖 Generated with Claude Code