Skip to content

Conversation

@MichaelHopfengaertner
Copy link

@MichaelHopfengaertner MichaelHopfengaertner commented Nov 26, 2025

Scope

This PR adds a backwards-compatible feature, that allows the registration of a custom idle-hook function for the thread mode executor.

By enabling the idle-hook feature for embassy-executor an additional executor method is provided to the application:

#[cfg(feature = "idle-hook")]
pub fn with_idle_hook(mut self, idle_hook: fn(&Executor)) -> Self

This feature is also added to embassy-executor-macros for convenient custom idle-hook registration:

#[embassy_executor::main(idle_hook = "my_idle_hook")]

The feature is supported on the following architectures: arch-cortex-m, arch-cortex-ar, arch-riscv32, arch-avr, arch-std.

The executor's default idle implementation is provided to the application Executor::default_idle(&self), e.g. if the custom idle-hook wants/needs to use it.

Goal

Previously, the idle behavior for each Executor implementation was pre-defined, e.g. WFE/WFI/SLEEP instruction.
If an application or chip requires a different/custom idle implementation or pre-idle/post-idle steps (as in embassy-stm32/src/low_power.rs), a custom Executor has to be implemented.

The goal is to enable developers to use the existing executor implementations while being able to customize the idle behavior very easily (if necessary).
This reduces the overhead and duplication of implementing a custom Executor.

Examples

Example embassy-nrf

use embassy_executor::{Executor, Spawner};
use embassy_time::Timer;

fn my_custom_idle_hook(executor: &Executor) {
    // TODO: Optionally, execute custom pre-idle steps, e.g. power-management, stop mode configuration, etc.

    // Put chip into idle state, e.g. by using the exposed executor's default idle implementation
    executor.default_idle();

    // TODO: Optionally, execute custom post-idle steps, e.g. clock re-initialization, etc.
}

#[embassy_executor::main(idle_hook = "my_custom_idle_hook")]
async fn main(_spawner: Spawner) {
    let _p = embassy_nrf::init(embassy_nrf::config::Config::default());

    loop {
        Timer::after_millis(1000).await;
    }
}

Example: embassy-nrf + nrf-softdevice

When using nrf-softdevice the arch-cortex-m executor's default idle implementation is not able to put the system into a low power mode.

use embassy_executor::{Executor, Spawner};
use embassy_nrf::interrupt::Priority;
use embassy_time::Timer;
use nrf_softdevice::{raw, Softdevice};

fn my_custom_idle_hook(_executor: &Executor) {
    // ~3uA consumption (when using nrf-softdevice's idle implementation)
    unsafe { raw::sd_app_evt_wait() };

    // ~830uA consumption (when using executor's default idle implementation)
    // _executor.default_idle();
}

#[embassy_executor::main(idle_hook = "my_custom_idle_hook")]
async fn main(_spawner: Spawner) {
    let mut config = embassy_nrf::config::Config::default();
    config.time_interrupt_priority = Priority::P2;
    let _p = embassy_nrf::init(config);
    let _sd = Softdevice::enable(&nrf_softdevice::Config::default());

    loop {
        Timer::after_millis(1000).await;
    }
}

@ivmarkov
Copy link
Contributor

Actually, I would need to use this with... arch-std. Is there any reason why this can't be supported with that platform?

@Christopher-06
Copy link

That would be a cool and practical addition.
It would also make it much easier to measure sleep time for estimating CPU usage. So far, you have to copy the existing executor and just save a timer value briefly before going to sleep and after waking up. With this PR, this would also be possible directly with the existing executor!

@MichaelHopfengaertner
Copy link
Author

Actually, I would need to use this with... arch-std. Is there any reason why this can't be supported with that platform?

Yes, we could also add the feature to arch-std. I wanted to keep it simple in the first place by just exposing Executor::default_idle() as function, without passing a reference to the executor instance around (which would be required to access the arch-std signaler within the default_idle implementation).

But I think we could easily change this from

fn my_custom_idle_hook() {
    Executor::default_idle();
}

to something like this

fn my_custom_idle_hook(executor: &Executor) {
    executor.default_idle();
}

That would probably even be cleaner and more flexible.

I will change it.

@MichaelHopfengaertner
Copy link
Author

Added idle-hook feature for arch-std and updated PR description.

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.

3 participants