@@ -136,7 +136,9 @@ static void alarm_pool_irq_handler(void);
136136static void alarm_pool_irq_handler (void ) {
137137 // This IRQ handler does the main work, as it always (assuming the IRQ hasn't been enabled on both cores
138138 // which is unsupported) run on the alarm pool's core, and can't be preempted by itself, meaning
139- // that it doesn't need locks except to protect against linked list access
139+ // that it doesn't need locks except to protect against linked list, or other state access.
140+ // This simplifies the code considerably, and makes it much faster in general, even though we are forced to take
141+ // two IRQs per alarm.
140142 uint timer_alarm_num ;
141143 alarm_pool_timer_t * timer = ta_from_current_irq (& timer_alarm_num );
142144 uint timer_num = ta_timer_num (timer );
@@ -263,9 +265,17 @@ static void alarm_pool_irq_handler(void) {
263265 // need to wait
264266 alarm_pool_entry_t * earliest_entry = & pool -> entries [earliest_index ];
265267 earliest_target = earliest_entry -> target ;
266- ta_set_timeout (timer , timer_alarm_num , earliest_target );
267- // check we haven't now past the target time; if not we don't want to loop again
268+ // we are leaving a timeout every 2^32 microseconds anyway if there is no valid target, so we can choose any value.
269+ // best_effort_wfe_or_timeout now relies on it being the last value set, and arguably this is the
270+ // best value anyway, as it is the furthest away from the last fire.
271+ if (earliest_target != -1 ) { // cancelled alarm has target of -1
272+ ta_set_timeout (timer , timer_alarm_num , earliest_target );
273+ }
274+ // check we haven't now passed the target time; if not we don't want to loop again
268275 } while ((earliest_target - (int64_t )ta_time_us_64 (timer )) <= 0 );
276+ // We always want the timer IRQ to wake a WFE so that best_effort_wfe_or_timeout() will wake up. It will wake
277+ // a WFE on its own core by nature of having taken an IRQ, but we do an explicit SEV so it wakes the other core
278+ __sev ();
269279}
270280
271281void alarm_pool_post_alloc_init (alarm_pool_t * pool , alarm_pool_timer_t * timer , uint hardware_alarm_num , uint max_timers ) {
@@ -437,26 +447,48 @@ bool best_effort_wfe_or_timeout(absolute_time_t timeout_timestamp) {
437447 return time_reached (timeout_timestamp );
438448 } else {
439449 alarm_id_t id ;
440- id = add_alarm_at (timeout_timestamp , sleep_until_callback , NULL , false);
441- if (id <= 0 ) {
442- tight_loop_contents ();
450+ // note that as of SDK 2.0.0 calling add_alarm_at always causes a SEV. What we really
451+ // want to do is cause an IRQ at the specified time in the future if there is not
452+ // an IRQ already happening before then. The problem is that the IRQ may be happening on the
453+ // other core, so taking an IRQ is the only way to get the state protection.
454+ //
455+ // Therefore, we make a compromise; we will set the alarm, if we won't wake up before the right time
456+ // already. This means that repeated calls to this function with the same timeout will work correctly
457+ // after the first one! This is fine, because we ask callers to use a polling loop on another
458+ // event variable when using this function.
459+ //
460+ // For this to work, we require that once we have set an alarm, an SEV happens no later than that, even
461+ // if we cancel the alarm as we do below. Therefore, the IRQ handler (which is always enabled) will
462+ // never set its wakeup time to a later value, but instead wake up once and then wake up again.
463+ //
464+ // This overhead when canceling alarms is a small price to pay for the much simpler/faster/cleaner
465+ // implementation that relies on the IRQ handler (on a single core) being the only state accessor.
466+ //
467+ // Note also, that the use of software spin locks on RP2350 to access state would always cause a SEV
468+ // due to use of LDREX etc., so actually using spin locks to protect the state would be worse.
469+ if (ta_wakes_up_on_or_before (alarm_pool_get_default ()-> timer , alarm_pool_get_default ()-> timer_alarm_num ,
470+ (int64_t )to_us_since_boot (timeout_timestamp ))) {
471+ // we already are waking up at or before when we want to (possibly due to us having been called
472+ // before in a loop), so we can do an actual WFE. Note we rely on the fact that the alarm pool IRQ
473+ // handler always does an explicit SEV, since it may be on the other core.
474+ __wfe ();
443475 return time_reached (timeout_timestamp );
444476 } else {
445- // the above alarm add now may force an IRQ which will wake us up,
446- // so we want to consume one __wfe.. we do an explicit __sev
447- // just to make sure there is one
448- __sev (); // make sure there is an event sow ee don't block
449- __wfe ();
450- if (!time_reached (timeout_timestamp ))
451- {
452- // ^ at the point above the timer hadn't fired, so it is safe
453- // to wait; the event will happen due to IRQ at some point between
454- // then and the correct wakeup time
455- __wfe ();
477+ id = add_alarm_at (timeout_timestamp , sleep_until_callback , NULL , false);
478+ if (id <= 0 ) {
479+ tight_loop_contents ();
480+ return time_reached (timeout_timestamp );
481+ } else {
482+ if (!time_reached (timeout_timestamp )) {
483+ // ^ at the point above the timer hadn't fired, so it is safe
484+ // to wait; the event will happen due to IRQ at some point between
485+ // then and the correct wakeup time
486+ __wfe ();
487+ }
488+ // we need to clean up if it wasn't us that caused the wfe; if it was this will be a noop.
489+ cancel_alarm (id );
490+ return time_reached (timeout_timestamp );
456491 }
457- // we need to clean up if it wasn't us that caused the wfe; if it was this will be a noop.
458- cancel_alarm (id );
459- return time_reached (timeout_timestamp );
460492 }
461493 }
462494#else
0 commit comments