Skip to content

Fix ESP32 UART reader thread spinning at 100% CPU - #637

Open
TekTrails wants to merge 1 commit into
hd-zero:mainfrom
TekTrails:fix/esp32-select-timeout
Open

TekTrails wants to merge 1 commit into
hd-zero:mainfrom
TekTrails:fix/esp32-select-timeout

Conversation

@TekTrails

@TekTrails TekTrails commented Nov 3, 2026 •

Copy link
Copy Markdown

Problem

pthread_recv_esp32() initializes the select() timeout only once, before its receive loop. On Linux, select() updates that timeout with the time remaining. Once the initial 100 ms has elapsed, the loop keeps passing a zero timeout and spins instead of sleeping, consuming a CPU core while the ESP32 reader is enabled.

This was reproduced on HDZero Goggles 2 with the ELRS backpack enabled. The affected reader is shared by the builds in this repository.

Fix

  • Reset the 100 ms timeout before each call to select().
  • Check esp32_handler_timeout() after each successful wake-up, including after receiving data.

The second change preserves delayed DVR start/stop handling: checking the deadline only after a timeout would defer the command indefinitely while UART data arrives less than 100 ms apart. The original busy loop masked this problem.

One commit, one file; no changes to the UART protocol.

Validation

Goggles 2 hardware: an A/B measurement in our downstream firmware, at the idle main menu with elrs.enable=1, showed the ESP32 reader falling from approximately 98% of one CPU core to approximately 0%, with about 10 wake-ups per second. In that same downstream test, whole-process CPU was observed to fall from about 1.45 cores to 0.54 core; these figures are specific to that setup and do not imply a corresponding gain in frame rate. Per-thread CPU was sampled from /proc/<pid>/task/*/stat over five seconds; 375 of 400 program-counter samples before the fix were inside _newselect called by this reader.

Those hardware measurements cover the timeout reset alone, in a firmware containing other downstream patches. They do not establish temperature or battery-life gains.

Current revision:

  • Goggles 2 emulator Debug and ARM Release builds completed successfully. The ARM linker warning about a Softwinner dependency's liblog.so was also present on the unmodified base.
  • Repository clang-format and git diff --check passed during review.
  • A host regression harness extracts the actual receive loop and DVR deadline handler, substitutes a pipe for the UART, and sends one byte every 20 ms for 2.5 seconds. The reset-only version delayed the DVR command until traffic stopped; this revision handled the deadline during traffic. The latest rerun observed a 22 ms delay and 128 handler calls. This checks scheduling behavior on the host, not a real-time guarantee or the complete UART/parser/DVR pipeline on hardware.

The exact revision including both changes has not had a dedicated on-device test of delayed DVR commands under sustained backpack traffic.

pthread_recv_esp32() initialised its select() timeout once, before the
for(;;) loop. On Linux, select() overwrites the timeval with the time
remaining, so after the first 100 ms with no byte from the backpack the
timeout is {0, 0} and every subsequent select() returns immediately.
The thread then spins forever, burning a whole Cortex-A7 core whenever
the ELRS backpack option is enabled (the backpack only talks on events,
so the first timeout happens right after boot).

Measured on HDZero Goggles 2 (Linux 4.9, idle main menu, elrs.enable=1):

  - reader thread: 98% of one core, 85% in kernel, 0 voluntary sleeps
  - whole app: 1.45 cores -> 0.54 core after the fix
  - cpufreq: pinned at 1104 MHz 97% of the time -> 816 MHz 93% of the time

Re-arm the timeout on every iteration.

esp32_handler_timeout() fires the delayed DVR start/stop (record_time).
It only ran when select() timed out; the spinning loop hid that, as it
ran the handler continuously. With the thread sleeping again, a backpack
sending bytes less than 100 ms apart would hold the DVR command off, so
call the handler on every wake-up. It only compares record_time with
time().
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