Skip to content

task_handler: fix LVGL time running ~2x fast (double tick_inc) - #607

Open
bitcoin3us wants to merge 1 commit into
lvgl-micropython:mainfrom
bitcoin3us:fix/lvgl-tick-single-source-upstream
Open

bitcoin3us wants to merge 1 commit into
lvgl-micropython:mainfrom
bitcoin3us:fix/lvgl-tick-single-source-upstream

Conversation

@bitcoin3us

Copy link
Copy Markdown

Problem

LVGL time runs roughly 2x faster than wall clock on ESP32 builds that use the frozen task_handler.TaskHandler. task_handler.py feeds lv.tick_inc() from two places at once:

  • _timer_cb adds the nominal duration on every machine.Timer tick, and
  • _task_handler adds the elapsed time since its previous pass on every scheduled run, plus a third increment covering the TASK_HANDLER_FINISHED callbacks.

Every lv.timer, animation, scroll throw and long-press threshold therefore fires early. It was noticed because a video player's lv.timer_create(cb, 1000 // 12) produced 14 frames per second.

Measurements

Waveshare ESP32-S3-Touch-LCD-3.5, LVGL 9 build, TaskHandler(duration=2). Values are lv.tick_get() advance divided by time.ticks_ms() advance over the same window:

Condition Stock This PR
Idle 1.834 1.000
Under flash-read load ~1.07 1.000
TaskHandler.disable() (timer path only) 1.000 n/a
LVGL ms per 500 ms sleep 1026 500
Handler timer deinit()'d 0 (no other tick source)

The stock timer path alone loses ~20% of ticks under load because timer callbacks are delivered through the scheduler and coalesce or drop; the handler-side increment was masking that loss by double counting. The effect is the same at the default duration=33, since both sources are added regardless of period.

Fix

Make _timer_cb the only place ticks are added, and have it add the real elapsed milliseconds since its previous run instead of the nominal period. LVGL time then equals wall time regardless of load, and a stalled scheduler (long C call, raw-REPL transfer) catches up afterwards instead of losing time. Net diff: 8 insertions, 14 deletions in one file; no API change.

Verification

  • Hot-loaded on the board: ratio 1.000 idle and under load, exactly 500 ms per 500 ms sleep.
  • Frozen into a rebuilt firmware and flashed: ratio 1.000 in every settled window across two boots; a 12 fps lv.timer now paces at 11.4 fps instead of running free at 14.2.
  • A second, independent session on the same board measured 1026 ms per 500 ms on the unpatched firmware.

Same change submitted to the MicroPythonOS fork as MicroPythonOS#13.

🤖 Generated with Claude Code

With thanks to the scientists and engineers who did the hard, unglamorous work that got us here.

LVGL time ran ~1.8x faster than wall clock on ESP32 builds: _timer_cb
added the nominal period on every machine.Timer tick while
_task_handler also added the elapsed time on every scheduled pass (plus
a third increment covering the FINISHED callbacks). Every lv timer,
animation, scroll throw and long-press threshold therefore fired early;
a 12 fps lv.timer produced 14 frames per second.

Measured on a Waveshare ESP32-S3-Touch-LCD-3.5 (MicroPythonOS 0.18,
TaskHandler duration=2): lv.tick_get() advanced 1.834 ms per wall-clock
ms when idle. With TaskHandler.disable() (timer path only) the ratio
was exactly 1.000, and under load the timer path alone lost ~20% of
ticks because scheduled callbacks coalesce.

Make _timer_cb the only place ticks are added, and have it add the real
elapsed milliseconds since its previous run instead of the nominal
period, so LVGL time equals wall time regardless of load. Verified
1.000 idle and under flash-read load; a stalled scheduler now catches
up instead of losing time.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant