Skip to content

Attempt to implement power saving features - #6292

Draft
kokroo wants to merge 4 commits into
esp-rs:mainfrom
Helius-Messenger:helius-esp-power
Draft

kokroo wants to merge 4 commits into
esp-rs:mainfrom
Helius-Messenger:helius-esp-power

Conversation

@kokroo

@kokroo kokroo commented Sep 9, 2026

Copy link
Copy Markdown

Submission Checklist 📝

  • I have updated existing examples or added new ones (if applicable).
  • I have used cargo xtask fmt command to ensure that all changed code is formatted correctly.
  • I have added changelog entries and/or migration guide notes in the sections below, or I will ask a maintainer to add the skip-changelog or manual-changelog label as appropriate.
  • My changes are in accordance to the esp-rs developer guidelines

Extra:

Pull Request Details 📖

Description

All of this was possible thanks to peeking into the ESP-IDF source.

Two power saving additions related to each other.

The first one is the "dynamic" frequency scaling from #5634. set_cpu_clock switches the CPU clock at runtime via ClockConfig::configure, which only applies the diff, so moving between two presets that share a PLL is just a divider change with no recalibration. PerfLevel and set_perf_levels define a Base and a High clock, and PerfLock::request(High) is a refcounted guard. The CPU runs at High while at least one is held and returns to Base on the last drop. The state sits behind esp_sync::NonReentrantMutex. Nothing scales the clock unless you ask it to, and APB is never affected since every CpuClock is PLL sourced and APB is derived independently of the CPU divider.

The second enables btdm controller modem sleep (sleep_mode MODE_1) on esp32c3 and esp32s3. It fills in the btdm_sleep_* OS callbacks, adds a BleSleepClock config to select the low-power clock, and manages the controller wake lock so the SoC can light-sleep between connection events. esp-rtos gets an optin to keep the main XTAL powered across light sleep, which is what boards without a 32.768 kHz crystal need to hold a connection while sleeping. The callbacks follow ESP-IDF's bt.c. Off by default (don't know what to do, best to keep it off by default I guess).

The two commits are independent if you would rather take only the clock change first.

Testing

Tested on a crystal-less ESP32-S3 (Heltec V3). Booted at 240 MHz, settled to a Base of 80 MHz, and ran an 80 -> 240 -> 80 round trip through PerfLock at runtime while a BLE connection and LoRa traffic stayed up, with no dropped events. The clock module builds for esp32, esp32s3, esp32c3, esp32c6 and esp32h2.

The BLE modem sleep is less mature. I have seen lower average current with a connection held by measuring battery drain over hours (so not accurate), but I do not have PPK2 measurements yet, which is why it is off by default.

Changelog

esp-hal

  • Added: set_cpu_clock to change the CPU clock at runtime, and PerfLevel/PerfLock/set_perf_levels for refcounted dynamic frequency scaling.

esp-radio

  • Added: BLE controller modem sleep (sleep_mode MODE_1) on esp32c3 and esp32s3, with a BleSleepClock option. Off by default.

esp-rtos

  • Added: set_main_xtal_powered_in_light_sleep to keep the main XTAL powered during automatic light sleep.

Runtime CPU clock switching plus a refcounted PerfLock guard (esp-rs#5634).

set_cpu_clock(CpuClock) switches via ClockConfig::configure, which applies
only the diff, so moving between two presets that share a PLL is
divider-only with no recalibration.

PerfLevel { Base, High } and set_perf_levels(base, high) set the two
levels; PerfLock::request(High) runs the CPU at High until dropped and
back to Base on the last release. State sits behind
esp_sync::NonReentrantMutex.

APB is untouched: every CpuClock is PLL-sourced and APB is derived
independently of the CPU divider. Opt-in. Builds on esp32, esp32s3,
esp32c3, esp32c6, esp32h2.
Enables btdm modem sleep (sleep_mode MODE_1) on esp32c3 and esp32s3:
implements the btdm_sleep_* OS callbacks, adds a selectable sleep clock
(BleSleepClock), turns sleep on at runtime with btdm_controller_enable_sleep,
and wakes a sleeping controller before each host->controller HCI send (else
the send hangs). Also lets esp-rtos keep the main XTAL powered across light
sleep, for boards without a 32 kHz crystal.

Ported from ESP-IDF bt.c. HW-validated on ESP32-S3: modem sleep engages
(check_duration fires, ~66 sleep cycles/s under a connection) and the BLE
link stays up. Off by default.
@github-actions github-actions Bot added the merge-conflict Merge conflict detected. Automatically added/removed by CI. label Sep 9, 2026
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

New commits in main have made this PR unmergeable. Please resolve the conflicts.

kokroo and others added 2 commits September 10, 2026 03:21
…bing

Light-sleep power work for the BLE (btdm) + esp-rtos path on ESP32-S3.

- rtc_cntl: WakeLock gains set/clear/sleep_deadline + holders(); a driver that
  releases its lock for a bounded gap can now bound how long the idle hook sleeps.
- sleep: enable/disable_bt_wakeup() exposes the Bt light-sleep wake source (c3/s3).
- uart: UartRx drops its lifetime wake lock once enable_wakeup arms the RX line as
  a wake source, so an always-armed serial no longer blocks light sleep.
- esp-rtos idle hook: honor the sleep deadline; add gate/residency diag counters.
- btdm: count active connections from the HCI event stream and arm the pre-event
  wake deadline + Bt wake source ONLY while connected. Idle advertising skips them
  and light-sleeps the whole gap (advertising CPU light-sleep ~54% -> ~66%), while
  a held connection keeps the SoC awake for its events (link survives). Deadline is
  set in enter_phase2 paired with the wakelock release so exit_phase3 always clears
  it. Phase callbacks gated to c3/s3 so esp32-classic still builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diagnostic for light-sleep power work: sleep_hist() returns 8 ms-bucketed counts
of the requested sleep length at each committed light sleep, so a bench can see
the wake cadence (what limits sleep length). Cheap relaxed counters in the hook.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Dominaezzz

Copy link
Copy Markdown
Collaborator

The two commits are independent if you would rather take only the clock change first.

For the sake of being able to revert broken PRs and also ease review burden, splitting this into two independent PRs makes sense to me.

@bugadani

Copy link
Copy Markdown
Contributor

This will most likely not be merged as is, I'll be cherry-picking the bits when I get to this topic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-conflict Merge conflict detected. Automatically added/removed by CI.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants