zephyr/mphalport: Scheduled callbacks not processed during time.sleep_ms() on TrustZone NS builds
Port, board and/or hardware
Zephyr port. Confirmed on nrf5340dk/nrf5340/cpuapp/ns (TrustZone non-secure mode with CONFIG_ARM_TRUSTZONE_M=y and CONFIG_BUILD_WITH_TFM=y).
May also affect other TrustZone NS targets (nrf9160, etc.). Standard non-TrustZone builds may not be affected if k_poll_signal_raise works reliably from ISR context.
MicroPython version
MicroPython v1.27.0 (tag: v1.27.0)
The mp_hal_wait_sem-based mp_hal_delay_ms was introduced in commit d12085985 (Apr 2021) — "zephyr: Run scheduled callbacks at REPL and during mp_hal_delay_ms."
Reproduction
Soft IRQ timer callbacks scheduled via mp_sched_schedule() are not executed during time.sleep_ms() on TrustZone NS builds.
from machine import Timer
import time
fired = [False]
def callback(t):
fired[0] = True
t = Timer(-1)
t.init(period=100, mode=Timer.ONE_SHOT, callback=callback)
time.sleep_ms(200)
t.deinit()
print('Timer callback fired:', fired[0]) # Prints False on NS builds
How it should work (but doesn't on NS builds):
The Zephyr port has a signal mechanism wired up:
- Timer ISR fires
machine_timer_callback() mp_irq_dispatch()withishard=falsecallsmp_sched_schedule()mp_sched_schedule()triggersMICROPY_SCHED_HOOK_SCHEDULED=mp_hal_signal_event()mp_hal_signal_event()callsk_poll_signal_raise(&wait_signal, 0)- This should wake
k_poll()inmp_hal_wait_sem(), causing the loop to iterate and callmp_handle_pending(true)
Why it fails on TrustZone NS builds:
On boards built with CONFIG_ARM_TRUSTZONE_M=y and CONFIG_BUILD_WITH_TFM=y, the MicroPython code runs in the non-secure partition. k_poll_signal_raise() called from ISR context in the non-secure partition does not reliably wake k_poll() — TFM manages secure/non-secure interrupt routing and the signal never reaches the waiting thread.
The result is that k_poll() blocks for the full timeout, dt >= timeout_ms is true, and the function returns without processing the queued callback.
Expected behaviour
Timer callback fires at 100ms and is processed during the 200ms sleep. fired[0] should be True after time.sleep_ms(200) returns.
Observed behaviour
On TrustZone NS builds: the callback is queued by the ISR but never processed during time.sleep_ms(). fired[0] is still False after the sleep returns. The callback only executes later when the REPL processes pending callbacks.
On non-TrustZone builds: may work correctly if k_poll_signal_raise reliably wakes k_poll from ISR context (not verified).
Additional Information
Proposed fix: Replace the inline mp_hal_delay_ms with a standalone function that sleeps in 1ms increments, calling mp_handle_pending(true) every iteration. This avoids relying on k_poll_signal_raise working from ISR context, which is not reliable across all Zephyr security configurations.
mphalport.h — change inline to extern declaration:
// Implemented in mphalport.c - processes scheduled callbacks during delay
void mp_hal_delay_ms(mp_uint_t ms);
mphalport.c — add standalone implementation:
// Process scheduled callbacks during delay by sleeping in 1ms increments.
// This ensures soft IRQ timer callbacks are executed during time.sleep_ms()
// regardless of whether k_poll_signal_raise works in the target's security context.
void mp_hal_delay_ms(mp_uint_t ms) {
uint64_t dt;
uint32_t t0 = mp_hal_ticks_ms();
for (;;) {
mp_handle_pending(true);
MP_THREAD_GIL_EXIT();
uint32_t t1 = mp_hal_ticks_ms();
dt = t1 - t0;
if (dt + 1 >= ms) {
k_yield();
MP_THREAD_GIL_ENTER();
t1 = mp_hal_ticks_ms();
dt = t1 - t0;
break;
} else {
k_msleep(1);
MP_THREAD_GIL_ENTER();
}
}
if (dt < ms) {
mp_hal_delay_us(500);
}
}
Trade-off: 1ms wake-ups increase CPU activity during delays compared to a single k_poll. This is acceptable because time.sleep_ms() is an active user-requested delay where callback responsiveness matters more than power savings. mp_hal_wait_sem() is left unchanged — it is still used for REPL input waiting, where the REPL loop processes pending callbacks on each iteration.
Note: mp_hal_wait_sem() has the same underlying issue (no mp_handle_pending after k_poll timeout return), but the impact is lower since the REPL loop naturally processes pending callbacks. This fix targets mp_hal_delay_ms specifically where the impact is observable.
I can submit a PR with this fix if the approach is acceptable.
Code of Conduct
- Yes, I agree to follow the MicroPython Code of Conduct
zephyr: run async/scheduled events during REPL and sleep
This uses Zephyr's k_poll API to wait efficiently for an event signal, and an optional semaphore.