blob: 620cd2ace5a9020ed64316c21528a71ada00b253 [file] [log] [blame]
Ewout van Bekkum4f97fdd2021-06-24 10:37:37 -07001// Copyright 2021 The Pigweed Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License"); you may not
4// use this file except in compliance with the License. You may obtain a copy of
5// the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12// License for the specific language governing permissions and limitations under
13// the License.
14
15#include "pw_chrono/system_timer.h"
16
17#include <algorithm>
18
19#include "FreeRTOS.h"
20#include "pw_assert/check.h"
21#include "pw_chrono_freertos/system_clock_constants.h"
22#include "task.h"
23#include "timers.h"
24
25namespace pw::chrono {
26namespace {
27
28using State = backend::NativeSystemTimer::State;
29
30void HandleTimerCallback(TimerHandle_t timer_handle) {
31 // NOTE: FreeRTOS invokes all timer callbacks inside of a context where the
32 // scheduler is suspended (vTaskSuspendAll & xTaskResumeAll). Ergo we do not
33 // add a redundant unnecessary layer doing the exact same thing here.
34 PW_DCHECK_UINT_EQ(xTaskGetSchedulerState(),
35 taskSCHEDULER_SUSPENDED,
36 "Scheduler must be suspended during the timer callback");
37
38 // Because the timer control block, AKA what the timer handle points at, is
39 // the first member of the NativeSystemTimer struct we play a trick to cheaply
40 // get the native handle reference.
41 backend::NativeSystemTimer& native_type =
42 *reinterpret_cast<backend::NativeSystemTimer*>(timer_handle);
43
44 PW_CHECK_UINT_EQ(xTimerIsTimerActive(timer_handle),
45 pdFALSE,
46 "The timer is still active while being executed");
47
48 if (native_type.state == State::kCancelled) {
49 // Do nothing, we were invoked while the stop command was in the queue.
50 return;
51 }
52
53 const SystemClock::duration time_until_deadline =
54 native_type.expiry_deadline - SystemClock::now();
55 if (time_until_deadline <= SystemClock::duration::zero()) {
56 // We have met the deadline, cancel the current state and execute the user's
57 // callback. Note we cannot update the state later as the user's callback
58 // may alter the desired state through the Invoke*() API.
59 native_type.state = State::kCancelled;
60 native_type.user_callback(native_type.expiry_deadline);
61 return;
62 }
63
64 // We haven't met the deadline yet, reschedule as far out as possible.
65 const SystemClock::duration period =
66 std::min(pw::chrono::freertos::kMaxTimeout, time_until_deadline);
67 PW_CHECK_UINT_EQ(
68 xTimerChangePeriod(
69 &native_type.tcb, static_cast<TickType_t>(period.count()), 0),
70 pdPASS,
71 "Timer command queue overflowed");
72 PW_CHECK_UINT_EQ(xTimerStart(&native_type.tcb, 0),
73 pdPASS,
74 "Timer command queue overflowed");
75}
76
77constexpr TickType_t kInvalidPeriod = 1;
78constexpr UBaseType_t kOneShotMode = pdFALSE; // Do not use auto reload.
79
80} // namespace
81
82#if configUSE_TIMERS != 1
83#error \
84 "Backend requires your FreeRTOS configuration to have configUSE_TIMERS == 1"
85#endif
86
87#if configSUPPORT_STATIC_ALLOCATION != 1
88#error \
89 "Backend requires your FreeRTOS configuration to have configSUPPORT_STATIC_ALLOCATION == 1"
90#endif
91
92SystemTimer::SystemTimer(ExpiryCallback callback)
93 : native_type_{.tcb{},
94 .state = State::kCancelled,
95 .expiry_deadline = SystemClock::time_point(),
96 .user_callback = std::move(callback)} {
97 // Note that timer "creation" is not enqueued through the command queue and
98 // is ergo safe to do before the scheduler is running.
99 const TimerHandle_t handle =
100 xTimerCreateStatic("", // "pw::chrono::SystemTimer",
101 kInvalidPeriod,
102 kOneShotMode,
103 this,
104 HandleTimerCallback,
105 &native_type_.tcb);
106
107 // This should never fail since the pointer provided was not null and it
108 // should return a pointer to the StaticTimer_t.
109 PW_DCHECK_PTR_EQ(handle, &native_type_.tcb);
110}
111
112SystemTimer::~SystemTimer() {
113 Cancel();
114
115 // WARNING: This enqueues the request to delete the timer through a queue, it
116 // does not synchronously delete and disable the timer here! This means that
117 // if the timer is about to expire and the timer service thread is a lower
118 // priority that it may use the native_type_ after it is free'd.
119 PW_CHECK_UINT_EQ(pdPASS,
120 xTimerDelete(&native_type_.tcb, 0),
121 "Timer command queue overflowed");
122
123 // In case the timer is still active as warned above, busy yield loop until it
124 // has been removed. Note that this is safe before the scheduler has been
125 // started because the timer cannot have been added to the queue yet and ergo
126 // it shouldn't attempt to yield.
127 while (xTimerIsTimerActive(&native_type_.tcb)) {
128 taskYIELD();
129 }
130}
131
132void SystemTimer::InvokeAt(SystemClock::time_point timestamp) {
133 // The FreeRTOS timer service is always handled by a thread, ergo to ensure
134 // this API is threadsafe we simply disable task switching.
135 vTaskSuspendAll();
136
137 // We don't want to call Cancel which would enqueue a stop command instead of
138 // synchronously updating the state. Instead we update the expiry deadline
139 // and update the state where the one shot only fires if the expiry deadline
140 // is exceeded and the callback is executed once.
141 native_type_.expiry_deadline = timestamp;
142
143 // Schedule the timer as far out as possible. Note that the timeout might be
144 // clamped and it may be rescheduled internally.
145 const SystemClock::duration time_until_deadline =
146 timestamp - SystemClock::now();
147 const SystemClock::duration period =
148 std::clamp(SystemClock::duration::zero(),
149 time_until_deadline,
150 pw::chrono::freertos::kMaxTimeout);
151
152 PW_CHECK_UINT_EQ(
153 xTimerChangePeriod(
154 &native_type_.tcb, static_cast<TickType_t>(period.count()), 0),
155 pdPASS,
156 "Timer command queue overflowed");
157
158 // Don't enqueue the start multiple times, schedule it once and let the
159 // callback cancel.
160 if (native_type_.state == State::kCancelled) {
161 PW_CHECK_UINT_EQ(xTimerStart(&native_type_.tcb, 0),
162 pdPASS,
163 "Timer command queue overflowed");
164 native_type_.state = State::kScheduled;
165 }
166
167 xTaskResumeAll(); // Leave the critical section.
168}
169
170void SystemTimer::Cancel() {
171 // The FreeRTOS timer service is always handled by a thread, ergo to ensure
172 // this API is threadsafe we simply disable task switching.
173 vTaskSuspendAll();
174
175 // The stop command may not be executed until later in case we're in a
176 // critical section. For this reason update the internal state in case the
177 // callback gets invoked.
178 //
179 // Note that xTimerIsTimerActive cannot be used here as the timer service
180 // daemon may be a lower priority and ergo may still execute the callback
181 // after Cancel() was invoked. This is because a single expired timer may be
182 // processed before the entire command queue is emptied.
183 native_type_.state = State::kCancelled;
184
185 PW_CHECK_UINT_EQ(xTimerStop(&native_type_.tcb, 0),
186 pdPASS,
187 "Timer command queue overflowed");
188
189 xTaskResumeAll(); // Leave the critical section.
190}
191
192} // namespace pw::chrono