diff --git a/smtc_rac_lib/radio_planner/src/radio_planner.c b/smtc_rac_lib/radio_planner/src/radio_planner.c index 2c44c68..e221f4f 100644 --- a/smtc_rac_lib/radio_planner/src/radio_planner.c +++ b/smtc_rac_lib/radio_planner/src/radio_planner.c @@ -186,6 +186,16 @@ static void rp_consumption_statistics_updated( radio_planner_t* rp, const uint8_ */ static void rp_timer_irq_callback( void* obj ); +#if( RP_LAZY_SLEEP_DELAY_MS > 0 ) +/** + * @brief rp_lazy_sleep_flush put a standby-held radio to sleep if a lazy-sleep + * hold is pending (no-op otherwise) + * + * @param rp pointer to the radioplanner object itself + */ +static void rp_lazy_sleep_flush( radio_planner_t* rp ); +#endif + /** * @brief rp_hook_callback call the callback associated to the id * @@ -236,6 +246,14 @@ void rp_init( radio_planner_t* rp, const ralf_t* radio ) rp->next_state_status = RP_STATUS_NO_MORE_TASK_SCHEDULE; rp->margin_delay = RP_MARGIN_DELAY; rp->disable_failsafe = 0; +#if( RP_LAZY_SLEEP_DELAY_MS > 0 ) + rp->lazy_sleep_pending = false; + rp->lazy_fallback_configured = false; + rp->lazy_sleep_deadline_ms = 0; + rp->lazy_sleep_target = rp->radio; + SMTC_MODEM_HAL_TRACE_PRINTF( "RP: lazy-sleep hysteresis enabled: %d ms\n", + RP_LAZY_SLEEP_DELAY_MS ); +#endif print_hook_ids( ); } rp_hook_status_t rp_attach_new_radio( radio_planner_t* rp, const ralf_t* radio, const uint8_t hook_id ) @@ -444,8 +462,15 @@ rp_stats_t rp_get_stats( const radio_planner_t* rp ) void rp_callback( radio_planner_t* rp ) { + // UNLOCK_RADIO_ACCESS must be exempt like LOCK_RADIO_ACCESS: a lock task + // held open longer than the failsafe window (e.g. continuous RX under the + // raw RAC) keeps its original start_time_ms, and unlock_radio_access + // retypes the still-RUNNING task to UNLOCK before the engine processes + // it — the very next rp_callback would evaluate the failsafe against the + // stale start time and panic at the moment the client releases the lock. if( ( rp->tasks[rp->radio_task_id].state == RP_TASK_STATE_RUNNING ) && ( rp->tasks[rp->radio_task_id].type != RP_TASK_TYPE_LOCK_RADIO_ACCESS ) && + ( rp->tasks[rp->radio_task_id].type != RP_TASK_TYPE_UNLOCK_RADIO_ACCESS ) && ( rp->disable_failsafe != RP_DISABLE_FAILSAFE_KEY ) && ( ( int32_t ) ( rp->tasks[rp->radio_task_id].start_time_ms + 128000 - smtc_modem_hal_get_time_in_ms( ) ) < 0 ) ) { @@ -521,7 +546,30 @@ void rp_callback( radio_planner_t* rp ) // the arbiter rp->radio_is_free = false; // this is a kind of critical section rp_task_free( rp, &rp->tasks[rp->radio_task_id] ); +#if( RP_LAZY_SLEEP_DELAY_MS > 0 ) + // Lazy sleep: leave the radio in standby instead of sleeping it. + // If the hook callback (or a later enqueue inside the hysteresis + // window) launches a new task, it starts from standby with no + // sleep-wakeup cost. The arbiter below arms the hysteresis + // deadline; on expiry with no task the radio sleeps as before. + // Configure the transceiver (once) to fall back to STDBY_XOSC by + // itself after TX/RX so the oscillator/TCXO stays running between + // back-to-back tasks; a synchronous SetStandby(XOSC) here would + // just re-pay the TCXO startup on the critical path instead + // (the chip drops to STDBY_RC on TX done and restarting the + // oscillator blocks the next SPI command for the TCXO delay). + // Best-effort: not all radios support a fallback mode. + if( rp->lazy_fallback_configured == false ) + { + rp->lazy_fallback_configured = true; + ( void ) ral_set_rx_tx_fallback_mode( TARGET_RAL, RAL_FALLBACK_STDBY_XOSC ); + } + rp->lazy_sleep_pending = true; + rp->lazy_sleep_target = TARGET_RADIO; + rp->lazy_sleep_deadline_ms = smtc_modem_hal_get_time_in_ms( ) + RP_LAZY_SLEEP_DELAY_MS; +#else SMTC_MODEM_HAL_PANIC_ON_FAILURE( ral_set_sleep( TARGET_RAL, true ) == RAL_STATUS_OK ); +#endif rp->radio = TARGET_RADIO; rp_hook_callback( rp, rp->radio_task_id ); @@ -534,6 +582,10 @@ void rp_callback( radio_planner_t* rp ) { // A radio irq happened and no rp task is on going, put radio to sleep // even in case multiple radio put only main radio in sleep +#if( RP_LAZY_SLEEP_DELAY_MS > 0 ) + rp->lazy_sleep_pending = false; // all radios sleep below + rp->lazy_fallback_configured = false; +#endif for( int i = 0; i < RP_NB_HOOKS; i++ ) { SMTC_MODEM_HAL_PANIC_ON_FAILURE( @@ -776,6 +828,15 @@ static void rp_task_arbiter( radio_planner_t* rp, const char* caller_func_name ) { if( rp->timer_value > rp->margin_delay ) { +#if( RP_LAZY_SLEEP_DELAY_MS > 0 ) + // The alarm now belongs to a scheduled future task: give up a + // pending lazy-sleep hold and sleep the radio immediately, as + // the legacy code would (the future launch pays the wakeup). + if( rp->tasks[rp->radio_task_id].state != RP_TASK_STATE_RUNNING ) + { + rp_lazy_sleep_flush( rp ); + } +#endif rp_set_alarm( rp, rp->timer_value - rp->margin_delay ); } else @@ -783,10 +844,46 @@ static void rp_task_arbiter( radio_planner_t* rp, const char* caller_func_name ) rp->timer_irq_flag = true; } } +#if( RP_LAZY_SLEEP_DELAY_MS > 0 ) + else if( ( rp->lazy_sleep_pending == true ) && + ( rp->tasks[rp->radio_task_id].state != RP_TASK_STATE_RUNNING ) ) + { + // Priority task exists but no timer to set and nothing launched: + // arm/expire the lazy-sleep hysteresis deadline. + int32_t lazy_remaining_ms = + ( int32_t ) ( rp->lazy_sleep_deadline_ms - smtc_modem_hal_get_time_in_ms( ) ); + if( lazy_remaining_ms > 0 ) + { + rp_set_alarm( rp, ( uint32_t ) lazy_remaining_ms ); + } + else + { + rp_lazy_sleep_flush( rp ); + } + } +#endif } else { // No more tasks in the radio planner rp_task_call_aborted( rp ); +#if( RP_LAZY_SLEEP_DELAY_MS > 0 ) + if( ( rp->lazy_sleep_pending == true ) && + ( rp->tasks[rp->radio_task_id].state != RP_TASK_STATE_RUNNING ) ) + { + int32_t lazy_remaining_ms = + ( int32_t ) ( rp->lazy_sleep_deadline_ms - smtc_modem_hal_get_time_in_ms( ) ); + if( lazy_remaining_ms > 0 ) + { + // Re-arm the planner timer for the remainder of the hysteresis + // window; expiry re-enters this arbiter and sleeps the radio. + rp_set_alarm( rp, ( uint32_t ) lazy_remaining_ms ); + } + else + { + rp_lazy_sleep_flush( rp ); + } + } +#endif SMTC_MODEM_HAL_RP_TRACE_PRINTF( " RP: No more active tasks\n" ); } } @@ -969,6 +1066,13 @@ static void rp_task_launch_current( radio_planner_t* rp ) } else { +#if( RP_LAZY_SLEEP_DELAY_MS > 0 ) + // The task takes over the radio: cancel any pending lazy-sleep hold so + // the hysteresis expiry cannot sleep a radio a task is now using. The + // launch proceeds from STDBY_XOSC with no wakeup cost (the HAL wake + // path only runs when the radio is actually asleep). + rp->lazy_sleep_pending = false; +#endif rp_task_print( rp, &rp->tasks[id] ); rp->radio = TARGET_RADIO; rp->tasks[id].launch_task_callbacks( ( void* ) rp ); @@ -1169,6 +1273,21 @@ static void rp_timer_irq( radio_planner_t* rp ) rp_task_arbiter( rp, __func__ ); } +#if( RP_LAZY_SLEEP_DELAY_MS > 0 ) +static void rp_lazy_sleep_flush( radio_planner_t* rp ) +{ + if( rp->lazy_sleep_pending == true ) + { + rp->lazy_sleep_pending = false; + // Re-apply the fallback mode on the first hold after the next wakeup: + // it is not guaranteed to survive the sleep period on all radios. + rp->lazy_fallback_configured = false; + SMTC_MODEM_HAL_PANIC_ON_FAILURE( ral_set_sleep( &( rp->lazy_sleep_target->ral ), true ) == RAL_STATUS_OK ); + SMTC_MODEM_HAL_RP_TRACE_PRINTF( " RP: lazy-sleep hysteresis expired, radio to sleep\n" ); + } +} +#endif + static void rp_task_call_aborted( radio_planner_t* rp ) { for( int32_t i = 0; i < RP_NB_HOOKS; i++ ) diff --git a/smtc_rac_lib/radio_planner/src/radio_planner.h b/smtc_rac_lib/radio_planner/src/radio_planner.h index 99058bd..313ab12 100644 --- a/smtc_rac_lib/radio_planner/src/radio_planner.h +++ b/smtc_rac_lib/radio_planner/src/radio_planner.h @@ -98,6 +98,13 @@ typedef struct radio_planner_s const ralf_t* radio; const ralf_t* radio_target_attached_to_this_hook[RP_NB_HOOKS]; uint32_t margin_delay; + // Lazy-sleep hysteresis state (see RP_LAZY_SLEEP_DELAY_MS): radio is held + // in STDBY_XOSC after a task completes, and only put to sleep once the + // hysteresis deadline expires with no new task launched. + bool lazy_sleep_pending; + bool lazy_fallback_configured; + uint32_t lazy_sleep_deadline_ms; + const ralf_t* lazy_sleep_target; } radio_planner_t; /* diff --git a/smtc_rac_lib/radio_planner/src/radio_planner_types.h b/smtc_rac_lib/radio_planner/src/radio_planner_types.h index 4eb1a7f..f0bb3ae 100644 --- a/smtc_rac_lib/radio_planner/src/radio_planner_types.h +++ b/smtc_rac_lib/radio_planner/src/radio_planner_types.h @@ -113,6 +113,23 @@ extern "C" { */ #define RP_DISABLE_FAILSAFE_KEY 0xF00D4BEE +/*! + * @brief Lazy-sleep hysteresis window in ms. + * + * When a task completes and no other task is ready to launch, the radio is + * held in STDBY_XOSC (oscillator still running) for this long before being + * put to sleep. A task launched inside the window starts from standby and + * skips the full sleep-wakeup sequence (NSS wake glitch, busy-settle wait, + * TCXO restart), which otherwise re-pays the TCXO startup delay on every + * back-to-back transmission. On expiry with the queue still empty the radio + * is put to sleep exactly as before. + * + * Set to 0 to disable and compile the legacy immediate-sleep behavior. + */ +#ifndef RP_LAZY_SLEEP_DELAY_MS +#define RP_LAZY_SLEEP_DELAY_MS 200 +#endif + /* clang-format on */ /*