Skip to content

chore(color): reduce busy wait time on LCD refresh#7483

Open
philmoz wants to merge 7 commits into
mainfrom
philmoz/reduce-busy-wait
Open

chore(color): reduce busy wait time on LCD refresh#7483
philmoz wants to merge 7 commits into
mainfrom
philmoz/reduce-busy-wait

Conversation

@philmoz

@philmoz philmoz commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Version 2 - replacement for #7448.

Moved the call to tell LVGL that it can swap buffers and continue into the LDTC vertical refresh interrupt.
Seems to solves all the issues.
Tested widgets using lcd and lvgl API's with no issues.

This removes the busy wait loop completely in the firmware (still used in bootloader).

Note: PA01 does not have a vertical refresh interrupt so still uses a busy wait loop.

Summary by CodeRabbit

  • Refactor
    • Updated LCD refresh/interrupt handling so boot-time and normal runtime use different flush-completion signaling.
    • Centralized LVGL flush-ready notification and simplified non-boot completion behavior.
    • Improved LCD framebuffer alignment and updated buffering behavior for direct drawing and vertical-invert pipelines.
  • Bug Fixes
    • Reduced display stalls and inconsistent flush timing by removing reload busy-waits and flag-based completion tracking.
  • Chores
    • Modernized header structure (#pragma once) and cleaned up conditional simulation vs firmware callback declarations.

@philmoz philmoz added this to the 2.12.3 milestone Jun 23, 2026
@philmoz philmoz added color Related generally to color LCD radios house keeping 🧹 Cleanup of code and house keeping backport/2.12 To be backported to a 2.12 release also. labels Jun 23, 2026
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The LCD refresh path now routes completion through lvglFlushed() in runtime builds while BOOT-specific waiting remains in selected drivers. Shared GUI, boot menu, simulator hooks, headers, and board and target LCD drivers were updated for the new callback and framebuffer wiring.

Changes

LCD Flush Synchronization Refactor

Layer / File(s) Summary
API contracts and build guards
radio/src/boards/jumper-h750/lcd_driver.h, radio/src/boards/rm-h750/lcd_driver_480.h, radio/src/gui/colorlcd/lcd.h, radio/src/gui/common/stdlcd/lcd_common.h, radio/src/gui/common/stdlcd/lcd_common.cpp, radio/src/targets/taranis/board.h, radio/src/targets/simu/simulib.cpp
Updates LCD callback declarations and build guards, adds the shared lvglFlushed() declaration, conditionally exposes simulator-only lcdFlushed(), and replaces include guards with #pragma once in the LCD headers.
Shared GUI flush and framebuffer wiring
radio/src/gui/colorlcd/lcd.cpp, radio/src/gui/colorlcd/boot_lcd.cpp
Aligns the shared LCD implementation with aligned frame buffers, simulator-only wait callbacks, lvglFlushed() completion, and the updated boot display driver initialization.
Boot LCD object wiring
radio/src/gui/colorlcd/boot_menu.cpp
Switches boot menu rendering from BitmapBuffer * access to a direct lcd object and updates the drawing calls across the bootloader screens.
Board LTDC BOOT/runtime sync
radio/src/boards/jumper-h750/lcd_driver.cpp, radio/src/boards/rm-h750/lcd_driver_480.cpp, radio/src/boards/rm-h750/lcd_driver_800.cpp
Moves LTDC reload waiting to BOOT-only paths and runtime lvglFlushed() signaling across board drivers, while removing obsolete globals, init-time interrupt enables, and rm-h750 init indirection.
Target LTDC BOOT/runtime sync
radio/src/targets/horus/lcd_driver.cpp, radio/src/targets/horus/lcd_st7796s_driver.cpp, radio/src/targets/pa01/lcd_driver.cpp, radio/src/targets/pl18/lcd_driver.cpp, radio/src/targets/st16/lcd_driver.cpp, radio/src/targets/stm32h7s78-dk/lcd_driver.cpp
Moves LTDC reload completion to lvglFlushed() across target LCD drivers, with horus-specific deferred DMA region handling, framebuffer sizing updates, and init-time interrupt removal where changed.

Sequence Diagram(s)

sequenceDiagram
  participant startLcdRefresh
  participant LTDC_IRQHandler
  participant lvglFlushed
  participant lv_disp_flush_ready

  startLcdRefresh->>LTDC_IRQHandler: trigger LTDC reload
  LTDC_IRQHandler->>LTDC_IRQHandler: BOOT or runtime branch
  LTDC_IRQHandler->>lvglFlushed: runtime flush completion
  lvglFlushed->>lv_disp_flush_ready: mark flush ready
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

bug/regression

Suggested reviewers

  • pfeerick
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: reducing LCD refresh busy-waiting.
Description check ✅ Passed It summarizes the change, testing, and PA01 caveat, though it omits the template's Fixes # and Summary of changes headings.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch philmoz/reduce-busy-wait

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@3djc

3djc commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

tested on h7 hardware without issue I could spot

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
radio/src/targets/horus/lcd_driver.cpp (1)

169-193: 🩺 Stability & Availability | 🔴 Critical

DMA operations are asynchronous; data race when lvglFlushed() is signaled mid-copy.

DMACopyBitmap() calls LL_DMA2D_Start() and returns immediately without waiting for the transfer to complete. In non-BOOT builds, _update_frame_buffer_addr() enables LTDC_IT_LI but skips the busy-wait, so LTDC_IRQHandler fires at the next vertical blank—potentially while DMA2D is still writing to _back_buffer. When the IRQ calls lvglFlushed(), it signals to LVGL that the refresh is complete before the DMA copy operations have actually finished, creating a data race on the buffer contents.

Add DMAWait() after the invalidation area loop (after line 192) to ensure all pending DMA transfers complete before the IRQ can fire.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@radio/src/targets/horus/lcd_driver.cpp` around lines 169 - 193, The issue is
that `DMACopyBitmap()` initiates asynchronous DMA transfers but returns
immediately, while the loop iterates through multiple invalidation areas and
queues multiple DMA operations. The LTDC IRQ can fire before all DMA operations
complete, causing `lvglFlushed()` to signal refresh completion while DMA is
still writing to `_back_buffer`, creating a data race. After the for loop that
processes `disp->inv_p` invalidation areas and calls `DMACopyBitmap()`, add a
call to `DMAWait()` to ensure all pending DMA transfers finish before the IRQ
handler fires and signals the buffer is ready.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@radio/src/targets/horus/lcd_driver.cpp`:
- Around line 169-193: The issue is that `DMACopyBitmap()` initiates
asynchronous DMA transfers but returns immediately, while the loop iterates
through multiple invalidation areas and queues multiple DMA operations. The LTDC
IRQ can fire before all DMA operations complete, causing `lvglFlushed()` to
signal refresh completion while DMA is still writing to `_back_buffer`, creating
a data race. After the for loop that processes `disp->inv_p` invalidation areas
and calls `DMACopyBitmap()`, add a call to `DMAWait()` to ensure all pending DMA
transfers finish before the IRQ handler fires and signals the buffer is ready.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 505ba5a8-533c-4f46-9efa-aa96027da32b

📥 Commits

Reviewing files that changed from the base of the PR and between 66463ec and d8f3b90.

📒 Files selected for processing (18)
  • radio/src/boards/jumper-h750/lcd_driver.cpp
  • radio/src/boards/jumper-h750/lcd_driver.h
  • radio/src/boards/rm-h750/lcd_driver_480.cpp
  • radio/src/boards/rm-h750/lcd_driver_480.h
  • radio/src/boards/rm-h750/lcd_driver_800.cpp
  • radio/src/boards/rm-h750/lcd_driver_800.h
  • radio/src/gui/colorlcd/lcd.cpp
  • radio/src/gui/colorlcd/lcd.h
  • radio/src/gui/common/stdlcd/lcd_common.cpp
  • radio/src/gui/common/stdlcd/lcd_common.h
  • radio/src/targets/horus/lcd_driver.cpp
  • radio/src/targets/horus/lcd_st7796s_driver.cpp
  • radio/src/targets/pa01/lcd_driver.cpp
  • radio/src/targets/pl18/lcd_driver.cpp
  • radio/src/targets/simu/simulib.cpp
  • radio/src/targets/st16/lcd_driver.cpp
  • radio/src/targets/stm32h7s78-dk/lcd_driver.cpp
  • radio/src/targets/taranis/board.h
💤 Files with no reviewable changes (2)
  • radio/src/boards/rm-h750/lcd_driver_800.h
  • radio/src/targets/taranis/board.h

@philmoz

philmoz commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator Author

Reverted to using busy wait loop for radios with inverted LCD.

While the interrupt driven version works - it can result in some screen tearing while the inverted front buffer contents are being DMA copied to the back buffer.

Interrupt version works on F4 radios with non-inverted LCD and H7 radios (except PA01).

@philmoz
philmoz force-pushed the philmoz/reduce-busy-wait branch from f80393d to c83bfce Compare June 25, 2026 11:19

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@radio/src/targets/horus/lcd_driver.cpp`:
- Around line 129-133: The LTDC line interrupt can remain latched after the
handler disables it, so re-enabling LTDC_IT_LI in lcd_driver.cpp may trigger an
immediate interrupt instead of waiting for vertical blank. In the code path that
updates LTDC_Layer1->CFBAR and requests the shadow reload via LTDC->SRCR, clear
the latched line interrupt flag with the existing LTDC handle before calling
__HAL_LTDC_ENABLE_IT, using the same interrupt setup around
__HAL_LTDC_CLEAR_FLAG / __HAL_LTDC_ENABLE_IT to ensure the next interrupt fires
only after the intended reload.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 26a6a050-1d6f-4187-814b-323ba66ad2fb

📥 Commits

Reviewing files that changed from the base of the PR and between f80393d and c83bfce.

📒 Files selected for processing (20)
  • radio/src/boards/jumper-h750/lcd_driver.cpp
  • radio/src/boards/jumper-h750/lcd_driver.h
  • radio/src/boards/rm-h750/lcd_driver_480.cpp
  • radio/src/boards/rm-h750/lcd_driver_480.h
  • radio/src/boards/rm-h750/lcd_driver_800.cpp
  • radio/src/boards/rm-h750/lcd_driver_800.h
  • radio/src/gui/colorlcd/boot_lcd.cpp
  • radio/src/gui/colorlcd/boot_menu.cpp
  • radio/src/gui/colorlcd/lcd.cpp
  • radio/src/gui/colorlcd/lcd.h
  • radio/src/gui/common/stdlcd/lcd_common.cpp
  • radio/src/gui/common/stdlcd/lcd_common.h
  • radio/src/targets/horus/lcd_driver.cpp
  • radio/src/targets/horus/lcd_st7796s_driver.cpp
  • radio/src/targets/pa01/lcd_driver.cpp
  • radio/src/targets/pl18/lcd_driver.cpp
  • radio/src/targets/simu/simulib.cpp
  • radio/src/targets/st16/lcd_driver.cpp
  • radio/src/targets/stm32h7s78-dk/lcd_driver.cpp
  • radio/src/targets/taranis/board.h
💤 Files with no reviewable changes (2)
  • radio/src/targets/taranis/board.h
  • radio/src/boards/rm-h750/lcd_driver_800.h
✅ Files skipped from review due to trivial changes (2)
  • radio/src/gui/common/stdlcd/lcd_common.h
  • radio/src/gui/common/stdlcd/lcd_common.cpp
🚧 Files skipped from review as they are similar to previous changes (11)
  • radio/src/targets/stm32h7s78-dk/lcd_driver.cpp
  • radio/src/targets/simu/simulib.cpp
  • radio/src/targets/horus/lcd_st7796s_driver.cpp
  • radio/src/boards/rm-h750/lcd_driver_480.h
  • radio/src/boards/rm-h750/lcd_driver_480.cpp
  • radio/src/targets/pa01/lcd_driver.cpp
  • radio/src/gui/colorlcd/lcd.h
  • radio/src/boards/rm-h750/lcd_driver_800.cpp
  • radio/src/targets/st16/lcd_driver.cpp
  • radio/src/boards/jumper-h750/lcd_driver.cpp
  • radio/src/targets/pl18/lcd_driver.cpp

Comment thread radio/src/targets/horus/lcd_driver.cpp
@philmoz

philmoz commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator Author

Busy wait loop removed from F4 radios with inverted LCD display.
This also saves 255K of SDRAM since only one LVGL frame buffer is needed.

@philmoz
philmoz force-pushed the philmoz/reduce-busy-wait branch from 89822b0 to e71bdef Compare June 30, 2026 03:14
Comment thread radio/src/gui/colorlcd/lcd.h Outdated
void lcdRefresh();

void lcdFlushed();
extern "C" void lvglFlushed();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way, is there a specific reason for renaming this? That kind of "breaks" the naming convention and forces a few #if defined(SIMU).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wasn't renamed - 'lcdFlushed' has always been simulator only, now it's explicit.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, but that doesn't have to be something separate, right? (see #7483 (comment))

@raphaelcoeffic

Copy link
Copy Markdown
Member

Separate from the LTDC discussion — a thought on the lvglFlushed/lcdFlushed split. The PR adds lvglFlushed and walls lcdFlushed/refr_disp/lcd_wait_cb/lcdSetWaitCb behind #if defined(SIMU). I think it can mostly collapse back to one clean symbol, keeping the lcdFlushed name:

  1. One name. The driver-facing function does need extern "C" with separate firmware/boot defs (that link contract is fine) — but not a new name. lcdFlushed already exists and the sim callers + stdlcd stub use it, so just point the LTDC drivers at lcdFlushed() and drop lvglFlushed.
  2. Drop refr_disp. With a single static disp_drv it is always null or &disp_drv — it only holds the startup guard. Target &disp_drv directly and remove the refr_disp = disp_drv capture in flushLcd.
  3. lcd_wait_cb/lcdSetWaitCb do not need the SIMU guard. Only set from simu/simulcd.cpp, so on hardware it stays nullptr and disp_drv.wait_cb = lcd_wait_cb is a no-op = LVGL default. This is how main had it; --gc-sections drops the unused setter.
  4. Move the "not ready" guard to the simu boundary (where the race actually is — hardware has none, the ISR only fires from inside a flush). SDL refreshDisplay is already gated by simuLcdRefresh; the WASM export just needs:
    void simuLcdFlushed() { if (simu_running) lcdFlushed(); }
    (simu_running is set after lcdInitDisplayDriver in simuStart, and fails safe.)

Then lcdFlushed is identical in firmware and boot:

extern "C" void lcdFlushed() { lv_disp_flush_ready(&disp_drv); }

Net: removes lvglFlushed, refr_disp, the in-function guard, and three #ifdef SIMU blocks, keeping the lcd* naming and boot/runtime split. Shout if I am missing a reason for the split.

@philmoz

philmoz commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author
  1. lcd_wait_cb/lcdSetWaitCb do not need the SIMU guard.

What is the purpose of lcd_wait_cb / lcdSetWaitCb?

As far as I can see it serves no real use, and removing it completely does not seem to affect the simulator at all.

@raphaelcoeffic

raphaelcoeffic commented Jul 6, 2026

Copy link
Copy Markdown
Member
  1. lcd_wait_cb/lcdSetWaitCb do not need the SIMU guard.

What is the purpose of lcd_wait_cb / lcdSetWaitCb?

It's used to allow for a clean exit of the UI task when the simulator shuts down. Otherwise it could block on the refresh, causing the UI task to keep waiting. This is necessary in case the UI task is stopped after a frame flush but before it has been ACKed via lcdFlushed().

As far as I can see it serves no real use, and removing it completely does not seem to affect the simulator at all.

If you find a better way to allow for the UI task to finish cleanly, I have no problem with it.

The alternatives Claude comes up with are not really better:

  • Check simu_shutdown in the flush cb (simuRefreshLcd) and complete synchronously. If shutting down, call lv_disp_flush_ready(disp_drv) immediately instead of the async host handoff. Plus a one-shot lcdFlushed() from the shutdown path to drain a flush that's already in flight. Works, moves the awareness into one place — but it's more fragile than wait_cb: it doesn't cover the already-blocked thread on its own (hence the extra drain), and there's a re-entry window if LVGL starts another refresh before the task actually stops. wait_cb sidesteps all that because it fires on every wait iteration.

  • Replace LVGL's flushing-flag busy-wait with a real semaphore/condvar that both the host ack and the shutdown path post. Race-free and explicit — but it's the most machinery, and it steps away from LVGL's built-in wait model to hand-roll synchronization.

  • Fire-and-forget in the sim — have the flush cb call lv_disp_flush_ready right after simuLcdNotify() so there's never an async wait to get stuck in. Simplest, but it drops the host back-pressure: simuLcdBuf is LVGL's own buffer in direct mode, and the WASM simuLcdFlushed explicitly means "buffer can be reused." Letting LVGL redraw before the host finishes reading → tearing/corruption, especially in the WASM path. Not safe.

The implementation itself could however be made a bit simpler and better reflect the intention (UI task has been signalled to stop):

static void simuLcdExitHandler(lv_disp_drv_t* disp_drv)
{
  if (!task_running()) lv_disp_flush_ready(disp_drv);
}

@philmoz
philmoz force-pushed the philmoz/reduce-busy-wait branch 4 times, most recently from 92cbc25 to dc2a2e3 Compare July 13, 2026 05:47
@philmoz
philmoz force-pushed the philmoz/reduce-busy-wait branch from dc2a2e3 to 76db7b9 Compare July 16, 2026 04:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport/2.12 To be backported to a 2.12 release also. color Related generally to color LCD radios house keeping 🧹 Cleanup of code and house keeping

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants