diff options
Diffstat (limited to 'libhw_cr/rp2040_hwtimer.c')
-rw-r--r-- | libhw_cr/rp2040_hwtimer.c | 153 |
1 files changed, 153 insertions, 0 deletions
diff --git a/libhw_cr/rp2040_hwtimer.c b/libhw_cr/rp2040_hwtimer.c new file mode 100644 index 0000000..8227abb --- /dev/null +++ b/libhw_cr/rp2040_hwtimer.c @@ -0,0 +1,153 @@ +/* libhw_cr/rp2040_hwtimer.c - <libhw/generic/alarmclock.h> implementation for the RP2040's hardware timer + * + * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#include <hardware/irq.h> /* pico-sdk:hardware_irq */ +#include <hardware/timer.h> /* pico-sdk:hardware_timer */ + +#include <libcr/coroutine.h> +#include <libmisc/assert.h> + +#define IMPLEMENTATION_FOR_LIBHW_GENERIC_ALARMCLOCK_H YES +#include <libhw/generic/alarmclock.h> + +#include <libhw/rp2040_hwtimer.h> + +/******************************************************************************/ + +/** Conflict with pico-sdk:pico_time:!PICO_TIME_DEFAULT_ALARM_POOL_DISABLED. */ +void add_alarm_at(void) {}; + +/* Types **********************************************************************/ + +struct rp2040_hwtimer { + enum rp2040_hwalarm_instance alarm_num; + bool initialized; + struct alarmclock_trigger *queue; +}; +LO_IMPLEMENTATION_H(alarmclock, struct rp2040_hwtimer, rp2040_hwtimer); +LO_IMPLEMENTATION_C(alarmclock, struct rp2040_hwtimer, rp2040_hwtimer, static); + +/* Globals ********************************************************************/ + +static struct rp2040_hwtimer hwtimers[] = { + { .alarm_num = 0 }, + { .alarm_num = 1 }, + { .alarm_num = 2 }, + { .alarm_num = 3 }, +}; +static_assert(sizeof(hwtimers)/sizeof(hwtimers[0]) == _RP2040_HWALARM_NUM); + +/* Main implementation ********************************************************/ + +lo_interface alarmclock rp2040_hwtimer(enum rp2040_hwalarm_instance alarm_num) { + assert(alarm_num < _RP2040_HWALARM_NUM); + return lo_box_rp2040_hwtimer_as_alarmclock(&hwtimers[alarm_num]); +} + + +static uint64_t rp2040_hwtimer_get_time_ns(struct rp2040_hwtimer *) { + return timer_time_us_64(timer_hw) * (NS_PER_S/US_PER_S); +} + +#define NS_TO_US_ROUNDUP(x) LM_CEILDIV(x, NS_PER_S/US_PER_S) + +static void rp2040_hwtimer_intrhandler(void) { + uint irq_num = __get_current_exception() - VTABLE_FIRST_IRQ; + enum rp2040_hwalarm_instance alarm_num = TIMER_ALARM_NUM_FROM_IRQ(irq_num); + assert(alarm_num < _RP2040_HWALARM_NUM); + + struct rp2040_hwtimer *alarmclock = &hwtimers[alarm_num]; + + while (alarmclock->queue && + NS_TO_US_ROUNDUP(alarmclock->queue->fire_at_ns) <= timer_time_us_64(timer_hw)) { + struct alarmclock_trigger *trigger = alarmclock->queue; + trigger->cb(trigger->cb_arg); + alarmclock->queue = trigger->next; + trigger->alarmclock = NULL; + trigger->next = NULL; + trigger->prev = NULL; + } + + hw_clear_bits(&timer_hw->intf, 1 << alarm_num); /* Clear "force"ing the interrupt. */ + hw_clear_bits(&timer_hw->intr, 1 << alarm_num); /* Clear natural firing of the alarm. */ + if (alarmclock->queue) + timer_hw->alarm[alarm_num] = (uint32_t)NS_TO_US_ROUNDUP(alarmclock->queue->fire_at_ns); +} + +static bool rp2040_hwtimer_add_trigger(struct rp2040_hwtimer *alarmclock, + struct alarmclock_trigger *trigger, + uint64_t fire_at_ns, + void (*cb)(void *), + void *cb_arg) { + assert(alarmclock); + assert(trigger); + assert(fire_at_ns); + assert(cb); + + uint64_t now_us = timer_time_us_64(timer_hw); + if (NS_TO_US_ROUNDUP(fire_at_ns) > now_us && + (NS_TO_US_ROUNDUP(fire_at_ns) - now_us) > UINT32_MAX) + /* Too far in the future. */ + return true; + + trigger->alarmclock = alarmclock; + trigger->fire_at_ns = fire_at_ns; + trigger->cb = cb; + trigger->cb_arg = cb_arg; + + bool saved = cr_save_and_disable_interrupts(); + struct alarmclock_trigger **dst = &alarmclock->queue; + while (*dst && fire_at_ns >= (*dst)->fire_at_ns) + dst = &(*dst)->next; + trigger->next = *dst; + trigger->prev = *dst ? (*dst)->prev : NULL; + if (*dst) + (*dst)->prev = trigger; + *dst = trigger; + if (!alarmclock->initialized) { + hw_set_bits(&timer_hw->inte, 1 << alarmclock->alarm_num); + irq_set_exclusive_handler(TIMER_ALARM_IRQ_NUM(timer_hw, alarmclock->alarm_num), + rp2040_hwtimer_intrhandler); + irq_set_enabled(TIMER_ALARM_IRQ_NUM(timer_hw, alarmclock->alarm_num), true); + alarmclock->initialized = true; + } + if (alarmclock->queue == trigger) { + /* "Force" the interrupt handler to trigger as soon as + * we enable interrupts. This handles the case of + * when fire_at_ns is before when we called + * cr_save_and_disable_interrupts(). We could check + * timer_time_us_64() again after calling + * cr_save_and_disable_interrupts() and do this + * conditionally, but I don't think that would be any + * more efficient than just letting the interrupt + * fire. */ + hw_set_bits(&timer_hw->intf, 1 << alarmclock->alarm_num); + } + cr_restore_interrupts(saved); + + return false; +} + +static void rp2040_hwtimer_del_trigger(struct rp2040_hwtimer *alarmclock, + struct alarmclock_trigger *trigger) { + assert(alarmclock); + assert(trigger); + + bool saved = cr_save_and_disable_interrupts(); + if (trigger->alarmclock == alarmclock) { + if (!trigger->prev) + alarmclock->queue = trigger->next; + else + trigger->prev->next = trigger->next; + if (trigger->next) + trigger->next->prev = trigger->prev; + trigger->alarmclock = NULL; + trigger->prev = NULL; + trigger->next = NULL; + } else + assert(!trigger->alarmclock); + cr_restore_interrupts(saved); +} |