summaryrefslogtreecommitdiff
path: root/libhw/rp2040_hwtimer.c
blob: ada624646ec1bf68ccde4c340db2345330d9e38a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/* libhw/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);
}