Power: the main loop never rests and uses a whole core #40

Closed
opened 2026-10-05 23:04:28 +00:00 by twisla · 3 comments
Owner

What happens

The main loop never rests: loopTask uses about 81 % of core 1, averaged since boot, with the device idle on the Launcher and the screen off (tasks, v0.7.0+debug, 2026-10-06):

task             st   pri  stack  cpu% core
loopTask         R      1   2872    81 1
IDLE1            r      0    344    17 1
IDLE0            r      0    240    78 0
wifi             B     23   4064    19 0

Core 1 is busy four fifths of the time doing nothing visible. That's current the battery pays for, and heat (the chip reads 37 to 39 C at rest).

Why it matters

  • Battery life: an idle core can sleep between interrupts; a spinning one can't. How much it costs on this board isn't measured.
  • The LoRa radio's own noise (#20): a busy CPU is a candidate source.
  • From M4 the radio is always on, so idle consumption decides how long the device lasts as a mesh node.

What's known

  • loop() in src/main.cpp polls the keyboard, ticks the Services, runs the console and redraws when needed, then comes straight back.
  • Each Service declares how often it wants a tick (tickIntervalMs()), so the time until the next one due is known; the keyboard has an interrupt line (TCA8418, INT on GPIO 11); the consoles and the radio have their own tasks or interrupts.
  • So the loop could block until the next deadline or an event (a key, a console line, a redraw request) instead of spinning: vTaskDelay for the simplest version, a task notification from the interrupts for the proper one.
  • Risks: key response and typing feel, Toast and redraw timing, the 5 s loop watchdog, anything that silently relied on being polled fast (the serial console, GNSS at 50 ms).

To do

  1. Measure first: current draw, or failing that battery percentage over an hour, idle and with IRC connected. The System Monitor (#11) shows the per-core load live.
  2. Try the simplest change (a short delay when nothing is due) and measure again: load, key latency, typing.
  3. Then decide whether the event-driven version is worth it.

Related

#11 (System Monitor, which makes this visible), #20 (radio noise), src/main.cpp (loop), lib/core/src/service_manager.cpp, docs/milestones/S1.md (Q127).

## What happens The main loop never rests: `loopTask` uses about 81 % of core 1, averaged since boot, with the device idle on the Launcher and the screen off (`tasks`, v0.7.0+debug, 2026-10-06): ``` task st pri stack cpu% core loopTask R 1 2872 81 1 IDLE1 r 0 344 17 1 IDLE0 r 0 240 78 0 wifi B 23 4064 19 0 ``` Core 1 is busy four fifths of the time doing nothing visible. That's current the battery pays for, and heat (the chip reads 37 to 39 C at rest). ## Why it matters - Battery life: an idle core can sleep between interrupts; a spinning one can't. How much it costs on this board isn't measured. - The LoRa radio's own noise (#20): a busy CPU is a candidate source. - From M4 the radio is always on, so idle consumption decides how long the device lasts as a mesh node. ## What's known - `loop()` in `src/main.cpp` polls the keyboard, ticks the Services, runs the console and redraws when needed, then comes straight back. - Each Service declares how often it wants a tick (`tickIntervalMs()`), so the time until the next one due is known; the keyboard has an interrupt line (TCA8418, INT on GPIO 11); the consoles and the radio have their own tasks or interrupts. - So the loop could block until the next deadline or an event (a key, a console line, a redraw request) instead of spinning: `vTaskDelay` for the simplest version, a task notification from the interrupts for the proper one. - Risks: key response and typing feel, Toast and redraw timing, the 5 s loop watchdog, anything that silently relied on being polled fast (the serial console, GNSS at 50 ms). ## To do 1. Measure first: current draw, or failing that battery percentage over an hour, idle and with IRC connected. The System Monitor (#11) shows the per-core load live. 2. Try the simplest change (a short delay when nothing is due) and measure again: load, key latency, typing. 3. Then decide whether the event-driven version is worth it. ## Related #11 (System Monitor, which makes this visible), #20 (radio noise), `src/main.cpp` (`loop`), `lib/core/src/service_manager.cpp`, `docs/milestones/S1.md` (Q127).
twisla added this to the S1 System basics milestone 2026-10-05 23:04:28 +00:00
twisla added the
kind
bug
area/power
status
needs-design
priority
medium
labels 2026-10-05 23:04:28 +00:00
Author
Owner

Correction: it's 100 %, not 81 %. The 81 % was an average since boot, and FreeRTOS only adds to a task's run time when the task is switched out, which under-counts a task that has its core to itself. Measured properly (the System App and tasks, on the s1 branch): at rest, core 1's idle task gets 0.0 % and loopTask has the whole core.

task             st   pri  stack   cpu% core
loopTask         R      1   2872  100.0 1
wifi             B     23   4068    0.8 0
IDLE0            r      0    232   98.1 0
IDLE1            r      0    352    0.0 1
load: core 0 2 %, core 1 100 %
**Correction: it's 100 %, not 81 %.** The 81 % was an average since boot, and FreeRTOS only adds to a task's run time when the task is switched out, which under-counts a task that has its core to itself. Measured properly (the System App and `tasks`, on the `s1` branch): at rest, core 1's idle task gets 0.0 % and `loopTask` has the whole core. ``` task st pri stack cpu% core loopTask R 1 2872 100.0 1 wifi B 23 4068 0.8 0 IDLE0 r 0 232 98.1 0 IDLE1 r 0 352 0.0 1 load: core 0 2 %, core 1 100 % ```
twisla changed title from Power: the main loop never rests and uses 81 % of a core to Power: the main loop never rests and uses a whole core 2026-10-05 23:28:28 +00:00
Author
Owner

Fixed on the s1 branch (06a5932), not merged yet. After each pass the loop rests: 5 ms with the screen on, 20 ms with it off, and not at all during a serial file transfer. Safe Mode's loop rests too.

Spinning Resting
Passes a second, screen off 50,160 50
Core 1 load, screen off 100 % 1 %
Passes a second, screen on (Launcher) 1,203 167
Core 1 load, screen on 62 % 10 %
Chip temperature at rest, settled 38.3 C 34.3 C
A 1.8 MB upload over the Debug Console about 230 KB/s 288 KB/s

Checked at the new pace: GNSS, a Gemini fetch, an upload read back by SHA-256, the Sweep, the radio's interrupt. tasks now prints the loop's passes; Debug Builds have loop spin on|off to compare.

Not measured

  • The current drawn: there's no meter on the battery line. The temperature drop is the only evidence of the saving.
  • How typing feels on the real keyboard: a key now waits up to 5 ms for the loop, 20 ms if it's the one that wakes the screen. Worth five minutes of typing in IRC before closing this.

Not done: real sleep. The framework is built without power management (CONFIG_PM_ENABLE off), so an idle core only halts until the next interrupt. Automatic light sleep means rebuilding the framework with it, Wi-Fi in modem sleep, and checking what the USB serial port does. A separate issue if battery life calls for it.

For #20: the radio's noise floor is the same with the loop spinning or resting. It wasn't the source.

**Fixed on the `s1` branch** (`06a5932`), not merged yet. After each pass the loop rests: 5 ms with the screen on, 20 ms with it off, and not at all during a serial file transfer. Safe Mode's loop rests too. | | Spinning | Resting | |---|---|---| | Passes a second, screen off | 50,160 | 50 | | Core 1 load, screen off | 100 % | 1 % | | Passes a second, screen on (Launcher) | 1,203 | 167 | | Core 1 load, screen on | 62 % | 10 % | | Chip temperature at rest, settled | 38.3 C | 34.3 C | | A 1.8 MB upload over the Debug Console | about 230 KB/s | 288 KB/s | Checked at the new pace: GNSS, a Gemini fetch, an upload read back by SHA-256, the Sweep, the radio's interrupt. `tasks` now prints the loop's passes; Debug Builds have `loop spin on|off` to compare. **Not measured** - The current drawn: there's no meter on the battery line. The temperature drop is the only evidence of the saving. - How typing feels on the real keyboard: a key now waits up to 5 ms for the loop, 20 ms if it's the one that wakes the screen. Worth five minutes of typing in IRC before closing this. **Not done: real sleep.** The framework is built without power management (`CONFIG_PM_ENABLE` off), so an idle core only halts until the next interrupt. Automatic light sleep means rebuilding the framework with it, Wi-Fi in modem sleep, and checking what the USB serial port does. A separate issue if battery life calls for it. **For #20:** the radio's noise floor is the same with the loop spinning or resting. It wasn't the source.
twisla added
status
ready
and removed
status
needs-design
labels 2026-10-06 01:45:21 +00:00
Author
Owner

Shipped in v0.8.1 (s1 merged into main). Typing feel on the real keyboard is still to be judged by hand; reopen if a key ever feels late.

Shipped in v0.8.1 (`s1` merged into `main`). Typing feel on the real keyboard is still to be judged by hand; reopen if a key ever feels late.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: twisla/roro9stack#40