Skip to content

Commit bd5523c

Browse files
authored
Improve best_effort_wfe_or_timeout (#1822)
* #1812 improvements to best_effort_wfe_or_timeout * Fix best_effort_wfe_or_timeout further by not having the IRQ ever move the alarm target backwards
1 parent cf8301f commit bd5523c

File tree

5 files changed

+81
-21
lines changed

5 files changed

+81
-21
lines changed

src/common/pico_time/include/pico/time.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,8 @@ void sleep_ms(uint32_t ms);
284284
* return false; // timed out
285285
* }
286286
* ```
287+
* NOTE: This method should always be used in a loop associated with checking another "event" variable, since
288+
* processor events are a shared resource and can happen for a large number of reasons.
287289
*
288290
* @param timeout_timestamp the timeout time
289291
* @return true if the target time is reached, false otherwise

src/common/pico_time/time.c

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,9 @@ static void alarm_pool_irq_handler(void);
136136
static 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

271281
void 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

src/host/pico_time_adapter/include/pico/time_adapter.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ void ta_clear_force_irq(alarm_pool_timer_t *timer, uint hardware_alarm_num);
1919
void ta_clear_irq(alarm_pool_timer_t *timer, uint hardware_alarm_num);
2020
void ta_force_irq(alarm_pool_timer_t *timer, uint hardware_alarm_num);
2121
void ta_set_timeout(alarm_pool_timer_t *timer, uint hardware_alarm_num, int64_t target);
22+
void ta_wakes_up_on_or_before(alarm_pool_timer_t *timer, uint alarm_num, int64_t target);
2223
void ta_enable_irq_handler(alarm_pool_timer_t *timer, uint hardware_alarm_num, void (*irq_handler)(void));
2324
void ta_disable_irq_handler(alarm_pool_timer_t *timer, uint hardware_alarm_num, void (*irq_handler)(void));
2425
void ta_hardware_alarm_claim(alarm_pool_timer_t *timer, uint hardware_alarm_num);

src/host/pico_time_adapter/time_adapter.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ PICO_WEAK_FUNCTION_DEF(ta_set_timeout)
2727
void PICO_WEAK_FUNCTION_IMPL_NAME(ta_set_timeout)(alarm_pool_timer_t *timer, uint hardware_alarm_num, int64_t target) {
2828
panic_unsupported();
2929
}
30+
PICO_WEAK_FUNCTION_DEF(ta_wakes_up_on_or_before)
31+
void PICO_WEAK_FUNCTION_IMPL_NAME(ta_wakes_up_on_or_before)(alarm_pool_timer_t *timer, uint hardware_alarm_num, int64_t target) {
32+
panic_unsupported();
33+
}
3034
PICO_WEAK_FUNCTION_DEF(ta_enable_irq_handler)
3135
void PICO_WEAK_FUNCTION_IMPL_NAME(ta_enable_irq_handler)(alarm_pool_timer_t *timer, uint hardware_alarm_num, void (*irq_handler)(void)) {
3236
panic_unsupported();

src/rp2_common/pico_time_adapter/include/pico/time_adapter.h

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,28 @@ static inline alarm_pool_timer_t *ta_from_current_irq(uint *alarm_num) {
3636
}
3737

3838
static inline void ta_set_timeout(alarm_pool_timer_t *timer, uint alarm_num, int64_t target) {
39-
timer_hw_from_timer(timer)->alarm[alarm_num] = (uint32_t) target;
39+
// We never want to set the timeout to be later than our current one.
40+
uint32_t current = timer_time_us_32(timer_hw_from_timer(timer));
41+
uint32_t time_til_target = (uint32_t) target - current;
42+
uint32_t time_til_alarm = timer_hw_from_timer(timer)->alarm[alarm_num] - current;
43+
// Note: we are only dealing with the low 32 bits of the timer values,
44+
// so there is some opportunity to make wrap-around errors.
45+
//
46+
// 1. If we just passed the alarm time, then time_til_alarm will be high, meaning we will
47+
// likely do the update, but this is OK since the alarm will have just fired
48+
// 2. If we just passed the target time, then time_til_target will be high, meaning we will
49+
// likely not do the update, but this is OK since the caller who has the full 64 bits
50+
// must check if the target time has passed when we return anyway to avoid races.
51+
if (time_til_target < time_til_alarm) {
52+
timer_hw_from_timer(timer)->alarm[alarm_num] = (uint32_t) target;
53+
}
54+
}
55+
56+
static inline bool ta_wakes_up_on_or_before(alarm_pool_timer_t *timer, uint alarm_num, int64_t target) {
57+
uint32_t current = timer_time_us_32(timer_hw_from_timer(timer));
58+
uint32_t time_til_target = (uint32_t) target - current;
59+
uint32_t time_til_alarm = timer_hw_from_timer(timer)->alarm[alarm_num] - current;
60+
return time_til_alarm <= time_til_target;
4061
}
4162

4263
static inline uint64_t ta_time_us_64(alarm_pool_timer_t *timer) {

0 commit comments

Comments
 (0)