summaryrefslogtreecommitdiff
path: root/libhw_cr/rp2040_hwtimer.c
diff options
context:
space:
mode:
Diffstat (limited to 'libhw_cr/rp2040_hwtimer.c')
-rw-r--r--libhw_cr/rp2040_hwtimer.c153
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);
+}