Austin Schuh | 208337d | 2022-01-01 14:29:11 -0800 | [diff] [blame^] | 1 | /* |
| 2 | * Copyright (c) 2020 Raspberry Pi (Trading) Ltd. |
| 3 | * |
| 4 | * SPDX-License-Identifier: BSD-3-Clause |
| 5 | */ |
| 6 | |
| 7 | #include <limits.h> |
| 8 | #include <inttypes.h> |
| 9 | #include <stdio.h> |
| 10 | #include <stdlib.h> |
| 11 | #include "pico.h" |
| 12 | #include "pico/time.h" |
| 13 | #include "pico/util/pheap.h" |
| 14 | #include "pico/sync.h" |
| 15 | |
| 16 | const absolute_time_t ABSOLUTE_TIME_INITIALIZED_VAR(nil_time, 0); |
| 17 | const absolute_time_t ABSOLUTE_TIME_INITIALIZED_VAR(at_the_end_of_time, INT64_MAX); |
| 18 | |
| 19 | typedef struct alarm_pool_entry { |
| 20 | absolute_time_t target; |
| 21 | alarm_callback_t callback; |
| 22 | void *user_data; |
| 23 | } alarm_pool_entry_t; |
| 24 | |
| 25 | typedef struct alarm_pool { |
| 26 | pheap_t *heap; |
| 27 | spin_lock_t *lock; |
| 28 | alarm_pool_entry_t *entries; |
| 29 | // one byte per entry, used to provide more longevity to public IDs than heap node ids do |
| 30 | // (this is increment every time the heap node id is re-used) |
| 31 | uint8_t *entry_ids_high; |
| 32 | alarm_id_t alarm_in_progress; // this is set during a callback from the IRQ handler... it can be cleared by alarm_cancel to prevent repeats |
| 33 | uint8_t hardware_alarm_num; |
| 34 | } alarm_pool_t; |
| 35 | |
| 36 | #if !PICO_TIME_DEFAULT_ALARM_POOL_DISABLED |
| 37 | // To avoid bringing in calloc, we statically allocate the arrays and the heap |
| 38 | PHEAP_DEFINE_STATIC(default_alarm_pool_heap, PICO_TIME_DEFAULT_ALARM_POOL_MAX_TIMERS); |
| 39 | static alarm_pool_entry_t default_alarm_pool_entries[PICO_TIME_DEFAULT_ALARM_POOL_MAX_TIMERS]; |
| 40 | static uint8_t default_alarm_pool_entry_ids_high[PICO_TIME_DEFAULT_ALARM_POOL_MAX_TIMERS]; |
| 41 | static lock_core_t sleep_notifier; |
| 42 | |
| 43 | static alarm_pool_t default_alarm_pool = { |
| 44 | .heap = &default_alarm_pool_heap, |
| 45 | .entries = default_alarm_pool_entries, |
| 46 | .entry_ids_high = default_alarm_pool_entry_ids_high, |
| 47 | }; |
| 48 | |
| 49 | static inline bool default_alarm_pool_initialized(void) { |
| 50 | return default_alarm_pool.lock != NULL; |
| 51 | } |
| 52 | #endif |
| 53 | |
| 54 | static alarm_pool_t *pools[NUM_TIMERS]; |
| 55 | static void alarm_pool_post_alloc_init(alarm_pool_t *pool, uint hardware_alarm_num); |
| 56 | |
| 57 | |
| 58 | static inline alarm_pool_entry_t *get_entry(alarm_pool_t *pool, pheap_node_id_t id) { |
| 59 | assert(id && id <= pool->heap->max_nodes); |
| 60 | return pool->entries + id - 1; |
| 61 | } |
| 62 | |
| 63 | static inline uint8_t *get_entry_id_high(alarm_pool_t *pool, pheap_node_id_t id) { |
| 64 | assert(id && id <= pool->heap->max_nodes); |
| 65 | return pool->entry_ids_high + id - 1; |
| 66 | } |
| 67 | |
| 68 | bool timer_pool_entry_comparator(void *user_data, pheap_node_id_t a, pheap_node_id_t b) { |
| 69 | alarm_pool_t *pool = (alarm_pool_t *)user_data; |
| 70 | return to_us_since_boot(get_entry(pool, a)->target) < to_us_since_boot(get_entry(pool, b)->target); |
| 71 | } |
| 72 | |
| 73 | static inline alarm_id_t make_public_id(uint8_t id_high, pheap_node_id_t id) { |
| 74 | return (alarm_id_t)(((uint)id_high << 8u * sizeof(id)) | id); |
| 75 | } |
| 76 | |
| 77 | void alarm_pool_init_default() { |
| 78 | #if !PICO_TIME_DEFAULT_ALARM_POOL_DISABLED |
| 79 | // allow multiple calls for ease of use from host tests |
| 80 | if (!default_alarm_pool_initialized()) { |
| 81 | ph_post_alloc_init(default_alarm_pool.heap, PICO_TIME_DEFAULT_ALARM_POOL_MAX_TIMERS, |
| 82 | timer_pool_entry_comparator, &default_alarm_pool); |
| 83 | alarm_pool_post_alloc_init(&default_alarm_pool, |
| 84 | PICO_TIME_DEFAULT_ALARM_POOL_HARDWARE_ALARM_NUM); |
| 85 | } |
| 86 | lock_init(&sleep_notifier, PICO_SPINLOCK_ID_TIMER); |
| 87 | #endif |
| 88 | } |
| 89 | |
| 90 | #if !PICO_TIME_DEFAULT_ALARM_POOL_DISABLED |
| 91 | alarm_pool_t *alarm_pool_get_default() { |
| 92 | assert(default_alarm_pool_initialized()); |
| 93 | return &default_alarm_pool; |
| 94 | } |
| 95 | #endif |
| 96 | |
| 97 | static pheap_node_id_t add_alarm_under_lock(alarm_pool_t *pool, absolute_time_t time, alarm_callback_t callback, |
| 98 | void *user_data, pheap_node_id_t reuse_id, bool create_if_past, bool *missed) { |
| 99 | pheap_node_id_t id; |
| 100 | if (reuse_id) { |
| 101 | assert(!ph_contains_node(pool->heap, reuse_id)); |
| 102 | id = reuse_id; |
| 103 | } else { |
| 104 | id = ph_new_node(pool->heap); |
| 105 | } |
| 106 | if (id) { |
| 107 | alarm_pool_entry_t *entry = get_entry(pool, id); |
| 108 | entry->target = time; |
| 109 | entry->callback = callback; |
| 110 | entry->user_data = user_data; |
| 111 | if (id == ph_insert_node(pool->heap, id)) { |
| 112 | bool is_missed = hardware_alarm_set_target(pool->hardware_alarm_num, time); |
| 113 | if (is_missed && !create_if_past) { |
| 114 | ph_remove_and_free_node(pool->heap, id); |
| 115 | } |
| 116 | if (missed) *missed = is_missed; |
| 117 | } |
| 118 | } |
| 119 | return id; |
| 120 | } |
| 121 | |
| 122 | static void alarm_pool_alarm_callback(uint alarm_num) { |
| 123 | // note this is called from timer IRQ handler |
| 124 | alarm_pool_t *pool = pools[alarm_num]; |
| 125 | bool again; |
| 126 | do { |
| 127 | absolute_time_t now = get_absolute_time(); |
| 128 | alarm_callback_t callback = NULL; |
| 129 | absolute_time_t target = nil_time; |
| 130 | void *user_data = NULL; |
| 131 | uint8_t id_high; |
| 132 | again = false; |
| 133 | uint32_t save = spin_lock_blocking(pool->lock); |
| 134 | pheap_node_id_t next_id = ph_peek_head(pool->heap); |
| 135 | if (next_id) { |
| 136 | alarm_pool_entry_t *entry = get_entry(pool, next_id); |
| 137 | if (absolute_time_diff_us(now, entry->target) <= 0) { |
| 138 | // we don't free the id in case we need to re-add the timer |
| 139 | pheap_node_id_t __unused removed_id = ph_remove_head(pool->heap, false); |
| 140 | assert(removed_id == next_id); // will be true under lock |
| 141 | target = entry->target; |
| 142 | callback = entry->callback; |
| 143 | user_data = entry->user_data; |
| 144 | assert(callback); |
| 145 | id_high = *get_entry_id_high(pool, next_id); |
| 146 | pool->alarm_in_progress = make_public_id(id_high, removed_id); |
| 147 | } else { |
| 148 | if (hardware_alarm_set_target(alarm_num, entry->target)) { |
| 149 | again = true; |
| 150 | } |
| 151 | } |
| 152 | } |
| 153 | spin_unlock(pool->lock, save); |
| 154 | if (callback) { |
| 155 | int64_t repeat = callback(make_public_id(id_high, next_id), user_data); |
| 156 | save = spin_lock_blocking(pool->lock); |
| 157 | // todo think more about whether we want to keep calling |
| 158 | if (repeat < 0 && pool->alarm_in_progress) { |
| 159 | assert(pool->alarm_in_progress == make_public_id(id_high, next_id)); |
| 160 | add_alarm_under_lock(pool, delayed_by_us(target, (uint64_t)-repeat), callback, user_data, next_id, true, NULL); |
| 161 | } else if (repeat > 0 && pool->alarm_in_progress) { |
| 162 | assert(pool->alarm_in_progress == make_public_id(id_high, next_id)); |
| 163 | add_alarm_under_lock(pool, delayed_by_us(get_absolute_time(), (uint64_t)repeat), callback, user_data, next_id, |
| 164 | true, NULL); |
| 165 | } else { |
| 166 | // need to return the id to the heap |
| 167 | ph_free_node(pool->heap, next_id); |
| 168 | (*get_entry_id_high(pool, next_id))++; // we bump it for next use of id |
| 169 | } |
| 170 | pool->alarm_in_progress = 0; |
| 171 | spin_unlock(pool->lock, save); |
| 172 | again = true; |
| 173 | } |
| 174 | } while (again); |
| 175 | } |
| 176 | |
| 177 | // note the timer is create with IRQs on this core |
| 178 | alarm_pool_t *alarm_pool_create(uint hardware_alarm_num, uint max_timers) { |
| 179 | alarm_pool_t *pool = (alarm_pool_t *) malloc(sizeof(alarm_pool_t)); |
| 180 | pool->heap = ph_create(max_timers, timer_pool_entry_comparator, pool); |
| 181 | pool->entries = (alarm_pool_entry_t *)calloc(max_timers, sizeof(alarm_pool_entry_t)); |
| 182 | pool->entry_ids_high = (uint8_t *)calloc(max_timers, sizeof(uint8_t)); |
| 183 | alarm_pool_post_alloc_init(pool, hardware_alarm_num); |
| 184 | return pool; |
| 185 | } |
| 186 | |
| 187 | void alarm_pool_post_alloc_init(alarm_pool_t *pool, uint hardware_alarm_num) { |
| 188 | hardware_alarm_claim(hardware_alarm_num); |
| 189 | hardware_alarm_cancel(hardware_alarm_num); |
| 190 | hardware_alarm_set_callback(hardware_alarm_num, alarm_pool_alarm_callback); |
| 191 | pool->lock = spin_lock_instance(next_striped_spin_lock_num()); |
| 192 | pool->hardware_alarm_num = (uint8_t) hardware_alarm_num; |
| 193 | pools[hardware_alarm_num] = pool; |
| 194 | } |
| 195 | |
| 196 | void alarm_pool_destroy(alarm_pool_t *pool) { |
| 197 | #if !PICO_TIME_DEFAULT_ALARM_POOL_DISABLED |
| 198 | if (pool == &default_alarm_pool) { |
| 199 | assert(false); // attempt to delete default alarm pool |
| 200 | return; |
| 201 | } |
| 202 | #endif |
| 203 | assert(pools[pool->hardware_alarm_num] == pool); |
| 204 | pools[pool->hardware_alarm_num] = NULL; |
| 205 | // todo clear out timers |
| 206 | ph_destroy(pool->heap); |
| 207 | hardware_alarm_set_callback(pool->hardware_alarm_num, NULL); |
| 208 | hardware_alarm_unclaim(pool->hardware_alarm_num); |
| 209 | free(pool->entry_ids_high); |
| 210 | free(pool->entries); |
| 211 | free(pool); |
| 212 | } |
| 213 | |
| 214 | alarm_id_t alarm_pool_add_alarm_at(alarm_pool_t *pool, absolute_time_t time, alarm_callback_t callback, |
| 215 | void *user_data, bool fire_if_past) { |
| 216 | bool missed = false; |
| 217 | |
| 218 | alarm_id_t public_id; |
| 219 | do { |
| 220 | uint8_t id_high = 0; |
| 221 | uint32_t save = spin_lock_blocking(pool->lock); |
| 222 | |
| 223 | pheap_node_id_t id = add_alarm_under_lock(pool, time, callback, user_data, 0, false, &missed); |
| 224 | if (id) id_high = *get_entry_id_high(pool, id); |
| 225 | |
| 226 | spin_unlock(pool->lock, save); |
| 227 | |
| 228 | if (!id) { |
| 229 | // no space in pheap to allocate an alarm |
| 230 | return -1; |
| 231 | } |
| 232 | |
| 233 | // note that if missed was true, then the id was never added to the pheap (because we |
| 234 | // passed false for create_if_past arg above) |
| 235 | public_id = missed ? 0 : make_public_id(id_high, id); |
| 236 | if (missed && fire_if_past) { |
| 237 | // ... so if fire_if_past == true we call the callback |
| 238 | int64_t repeat = callback(public_id, user_data); |
| 239 | // if not repeated we have no id to return so set public_id to 0, |
| 240 | // otherwise we need to repeat, but will assign a new id next time |
| 241 | // todo arguably this does mean that the id passed to the first callback may differ from subsequent calls |
| 242 | if (!repeat) { |
| 243 | public_id = 0; |
| 244 | break; |
| 245 | } else if (repeat < 0) { |
| 246 | time = delayed_by_us(time, (uint64_t)-repeat); |
| 247 | } else { |
| 248 | time = delayed_by_us(get_absolute_time(), (uint64_t)repeat); |
| 249 | } |
| 250 | } else { |
| 251 | // either: |
| 252 | // a) missed == false && public_id is > 0 |
| 253 | // b) missed == true && fire_if_past == false && public_id = 0 |
| 254 | // but we are done in either case |
| 255 | break; |
| 256 | } |
| 257 | } while (true); |
| 258 | return public_id; |
| 259 | } |
| 260 | |
| 261 | bool alarm_pool_cancel_alarm(alarm_pool_t *pool, alarm_id_t alarm_id) { |
| 262 | bool rc = false; |
| 263 | uint32_t save = spin_lock_blocking(pool->lock); |
| 264 | pheap_node_id_t id = (pheap_node_id_t) alarm_id; |
| 265 | if (ph_contains_node(pool->heap, id)) { |
| 266 | assert(alarm_id != pool->alarm_in_progress); // it shouldn't be in the heap if it is in progress |
| 267 | // check we have the right high value |
| 268 | uint8_t id_high = (uint8_t)((uint)alarm_id >> 8u * sizeof(pheap_node_id_t)); |
| 269 | if (id_high == *get_entry_id_high(pool, id)) { |
| 270 | rc = ph_remove_and_free_node(pool->heap, id); |
| 271 | // note we don't bother to remove the actual hardware alarm timeout... |
| 272 | // it will either do callbacks or not depending on other alarms, and reset the next timeout itself |
| 273 | assert(rc); |
| 274 | } |
| 275 | } else { |
| 276 | if (alarm_id == pool->alarm_in_progress) { |
| 277 | // make sure the alarm doesn't repeat |
| 278 | pool->alarm_in_progress = 0; |
| 279 | } |
| 280 | } |
| 281 | spin_unlock(pool->lock, save); |
| 282 | return rc; |
| 283 | } |
| 284 | |
| 285 | uint alarm_pool_hardware_alarm_num(alarm_pool_t *pool) { |
| 286 | return pool->hardware_alarm_num; |
| 287 | } |
| 288 | |
| 289 | static void alarm_pool_dump_key(pheap_node_id_t id, void *user_data) { |
| 290 | alarm_pool_t *pool = (alarm_pool_t *)user_data; |
| 291 | #if PICO_ON_DEVICE |
| 292 | printf("%lld (hi %02x)", to_us_since_boot(get_entry(pool, id)->target), *get_entry_id_high(pool, id)); |
| 293 | #else |
| 294 | printf("%"PRIu64, to_us_since_boot(get_entry(pool, id)->target)); |
| 295 | #endif |
| 296 | } |
| 297 | |
| 298 | static int64_t repeating_timer_callback(__unused alarm_id_t id, void *user_data) { |
| 299 | repeating_timer_t *rt = (repeating_timer_t *)user_data; |
| 300 | assert(rt->alarm_id == id); |
| 301 | if (rt->callback(rt)) { |
| 302 | return rt->delay_us; |
| 303 | } else { |
| 304 | rt->alarm_id = 0; |
| 305 | return 0; |
| 306 | } |
| 307 | } |
| 308 | |
| 309 | bool alarm_pool_add_repeating_timer_us(alarm_pool_t *pool, int64_t delay_us, repeating_timer_callback_t callback, void *user_data, repeating_timer_t *out) { |
| 310 | if (!delay_us) delay_us = 1; |
| 311 | out->pool = pool; |
| 312 | out->callback = callback; |
| 313 | out->delay_us = delay_us; |
| 314 | out->user_data = user_data; |
| 315 | out->alarm_id = alarm_pool_add_alarm_at(pool, make_timeout_time_us((uint64_t)(delay_us >= 0 ? delay_us : -delay_us)), |
| 316 | repeating_timer_callback, out, true); |
| 317 | // note that if out->alarm_id is 0, then the callback was called during the above call (fire_if_past == true) |
| 318 | // and then the callback removed itself. |
| 319 | return out->alarm_id >= 0; |
| 320 | } |
| 321 | |
| 322 | bool cancel_repeating_timer(repeating_timer_t *timer) { |
| 323 | bool rc = false; |
| 324 | if (timer->alarm_id) { |
| 325 | rc = alarm_pool_cancel_alarm(timer->pool, timer->alarm_id); |
| 326 | timer->alarm_id = 0; |
| 327 | } |
| 328 | return rc; |
| 329 | } |
| 330 | |
| 331 | void alarm_pool_dump(alarm_pool_t *pool) { |
| 332 | uint32_t save = spin_lock_blocking(pool->lock); |
| 333 | ph_dump(pool->heap, alarm_pool_dump_key, pool); |
| 334 | spin_unlock(pool->lock, save); |
| 335 | } |
| 336 | |
| 337 | #if !PICO_TIME_DEFAULT_ALARM_POOL_DISABLED |
| 338 | static int64_t sleep_until_callback(__unused alarm_id_t id, __unused void *user_data) { |
| 339 | uint32_t save = spin_lock_blocking(sleep_notifier.spin_lock); |
| 340 | lock_internal_spin_unlock_with_notify(&sleep_notifier, save); |
| 341 | return 0; |
| 342 | } |
| 343 | #endif |
| 344 | |
| 345 | void sleep_until(absolute_time_t t) { |
| 346 | #if PICO_ON_DEVICE && !defined(NDEBUG) |
| 347 | if (__get_current_exception()) { |
| 348 | panic("Attempted to sleep inside of an exception handler; use busy_wait if you must"); |
| 349 | } |
| 350 | #endif |
| 351 | #if !PICO_TIME_DEFAULT_ALARM_POOL_DISABLED |
| 352 | uint64_t t_us = to_us_since_boot(t); |
| 353 | uint64_t t_before_us = t_us - PICO_TIME_SLEEP_OVERHEAD_ADJUST_US; |
| 354 | // needs to work in the first PICO_TIME_SLEEP_OVERHEAD_ADJUST_US of boot |
| 355 | if (t_before_us > t_us) t_before_us = 0; |
| 356 | absolute_time_t t_before; |
| 357 | update_us_since_boot(&t_before, t_before_us); |
| 358 | if (absolute_time_diff_us(get_absolute_time(), t_before) > 0) { |
| 359 | if (add_alarm_at(t_before, sleep_until_callback, NULL, false) >= 0) { |
| 360 | // able to add alarm for just before the time |
| 361 | while (!time_reached(t_before)) { |
| 362 | uint32_t save = spin_lock_blocking(sleep_notifier.spin_lock); |
| 363 | lock_internal_spin_unlock_with_wait(&sleep_notifier, save); |
| 364 | } |
| 365 | } |
| 366 | } |
| 367 | #else |
| 368 | // hook in case we're in RTOS; note we assume using the alarm pool is better always if available. |
| 369 | sync_internal_yield_until_before(t); |
| 370 | #endif |
| 371 | // now wait until the exact time |
| 372 | busy_wait_until(t); |
| 373 | } |
| 374 | |
| 375 | void sleep_us(uint64_t us) { |
| 376 | #if !PICO_TIME_DEFAULT_ALARM_POOL_DISABLED |
| 377 | sleep_until(make_timeout_time_us(us)); |
| 378 | #else |
| 379 | if (us < PICO_TIME_SLEEP_OVERHEAD_ADJUST_US) { |
| 380 | busy_wait_us(us); |
| 381 | } else { |
| 382 | // hook in case we're in RTOS; note we assume using the alarm pool is better always if available. |
| 383 | absolute_time_t t = make_timeout_time_us(us - PICO_TIME_SLEEP_OVERHEAD_ADJUST_US); |
| 384 | sync_internal_yield_until_before(t); |
| 385 | |
| 386 | // then wait the rest of thw way |
| 387 | busy_wait_until(t); |
| 388 | } |
| 389 | #endif |
| 390 | } |
| 391 | |
| 392 | void sleep_ms(uint32_t ms) { |
| 393 | sleep_us(ms * 1000ull); |
| 394 | } |
| 395 | |
| 396 | bool best_effort_wfe_or_timeout(absolute_time_t timeout_timestamp) { |
| 397 | #if !PICO_TIME_DEFAULT_ALARM_POOL_DISABLED |
| 398 | alarm_id_t id; |
| 399 | id = add_alarm_at(timeout_timestamp, sleep_until_callback, NULL, false); |
| 400 | if (id <= 0) { |
| 401 | tight_loop_contents(); |
| 402 | return time_reached(timeout_timestamp); |
| 403 | } else { |
| 404 | __wfe(); |
| 405 | // we need to clean up if it wasn't us that caused the wfe; if it was this will be a noop. |
| 406 | cancel_alarm(id); |
| 407 | return time_reached(timeout_timestamp); |
| 408 | } |
| 409 | #else |
| 410 | tight_loop_contents(); |
| 411 | return time_reached(timeout_timestamp); |
| 412 | #endif |
| 413 | } |