Skip to content

perf(power): scale CPU to 80MHz when radios idle (AI FYI, -25% idle) - #118

Open
johnbuckman wants to merge 1 commit into
mainfrom
ai-fyi/cpu-freq-scaling-idle-power
Open

perf(power): scale CPU to 80MHz when radios idle (AI FYI, -25% idle)#118
johnbuckman wants to merge 1 commit into
mainfrom
ai-fyi/cpu-freq-scaling-idle-power

Conversation

@johnbuckman

@johnbuckman johnbuckman commented Aug 10, 2026

Copy link
Copy Markdown

⚠️ AI-written, FYI only. This entire change and its testing were done by an AI agent (Claude) driving a hardware-in-the-loop bench. It is posted for review and discussion, not as something that has to be merged. Treat the numbers as a starting point, not gospel — they come from a single V8.1 bench unit.

What this changes

The scale's CPU is pinned at 240 MHz all the time — there is no setCpuFrequencyMhz() call anywhere in the tree. But most of the time the scale is idle and BLE-only (WiFi off by default), just polling a load cell. This drops the core to 80 MHz when radios are idle and restores 240 MHz whenever WiFi comes up:

  • setup(): if !b_wifiOnBoot, setCpuFrequencyMhz(80) after init.
  • connectToWifi() / setupAP(): setCpuFrequencyMhz(240) before bringing WiFi up.
  • stopWifi(): back to setCpuFrequencyMhz(80).

80 MHz is the safe floor: APB stays at 80 MHz, so the UART console and the BLE radio keep working; it is the documented minimum with Bluetooth active. Total change: 8 lines.

How it was found

Reading the ON/idle path (power.h, the loop() in hds.ino, docs/AI_GPIO_NOTES.md): the deep-sleep OFF path is already well engineered (both rails gated, GPIO holds, EXT1 wake), but the idle-ON path runs wide open — CPU at 240 MHz, a ~200 Hz busy-poll loop, no light sleep, OLED + load cell up for the full 15-minute auto-off. The CPU clock stood out as the one large, zero-risk lever.

How it was measured

Bench: a ChargerLAB POWER-Z KM003C USB-C power analyzer inline on the scale's USB (VBUS current at ~5.8 kHz, ~0.1 mA resolution), read over its vendor bulk interface. All measurements at 5.19 V, battery full (4.23 V) so charge current had terminated — otherwise it swamps the operating draw (V8.1 has no firmware charge-disable, so "charge to full first" is the only clean option).

State Current vs baseline
baseline idle-ON (240 MHz, OLED + rails on) 110.6 mA
baseline recheck (reproducibility) 110.4 mA −0.2 (noise)
this change — CPU 80 MHz 82.6 mA −28.0 mA (−25.3%)
soft-sleep (OLED + both rails off), for reference 107.9 mA −2.7 (−2.5%)
soft-sleep + 80 MHz (deepest idle) 78.3 mA −32.3 (−29%)

Two findings worth noting: (1) the peripherals (ADS1232 + ADS1115 + a sparse OLED) draw only ~3 mA — the CPU dominates idle-ON; (2) the 15-min auto-off and any idle auto-timers are gated if(!b_is_charging), and on V8.1 b_is_charging means "USB connected" — so those timers only run on battery and can't be metered over USB. This change is not so gated: it applies whenever radios are idle, on USB or battery.

How it was tested

USB-serial functional regression (BLE/WiFi host tooling is unavailable non-interactively on the test Mac), run identically on baseline and on this build:

Check baseline cpu80
boot without panic
weight stream present
setup completes
loop alive (continuous weight frames)
v → battery voltage
oledoff/oledon → no crash
tare → loop survives
post-command stability
Total 8/8 8/8

Boot log on this build confirms it is active: [power] CPU 80 MHz (BLE-only idle).

Known gap / what has NOT been verified

  • BLE over-the-air was not tested. The serial regression proves the SoC, loop, UART, load cell, OLED and command handling all work at 80 MHz, but not BLE connect/throughput. This should be fine (80 MHz floor, APB unchanged, boots and advertises without crashing), but someone should confirm pairing + weight streaming from a phone/Decent app before trusting it.
  • Single bench unit (a V8.1 that reports as N16R8-class), one battery, room temperature.
  • The [power] serial line is left in for verification; drop it if merging.

Update / scope clarification: this change keys the CPU frequency on WiFi only — a BLE-connected scale with WiFi off still runs at 80 MHz here. That's fine for normal weight streaming (it's light work), but the upcoming on-scale extension mechanism wants full speed during active use. That "pin 240 MHz when connected + live" behavior is implemented on the DFS/light-sleep path in #121 (via a PM lock), not here. This PR remains the simple, precompiled-build (no from-source rebuild) option; #121 is the fuller DFS-based alternative.

Idle-ON draw is dominated by the CPU running at a pinned 240MHz. Drop to
80MHz at end of setup() when WiFi is not on at boot, and restore 240MHz on
any WiFi bring-up (setupAP / connectToWifi); return to 80MHz in stopWifi().
80MHz is the safe floor: APB stays 80MHz so serial and the BLE radio keep
working. Measured -28mA / -25% idle on a V8.1 unit (see PR for method).

AI-written change, posted as an FYI for review; not necessarily to merge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@johnbuckman

johnbuckman commented Aug 10, 2026

Copy link
Copy Markdown
Author

@tadelv and @Sofronio wanted to send you the initial result of my power optimisation work. I bought a Km003c and am driving Claude to try things. This change is a 25% power reduction.

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.

2 participants