third_party.pigweed.src/pw_chrono_freertos/system_timer.cc
Ewout van Bekkum 1f43db3080 pw_chrono_freertos: disable scheduler during timer callbacks
Turns out the scheduler isn't always disabled while timer callbacks
are invoked depending on the version. Given the minimal cost this
change goes ahead and always locks the scheduler.

No-Docs-Update-Reason: No API change.
Change-Id: I8a4dc5945bf35dc7eadf3fa379ebfd4755a30639
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/52565
Reviewed-by: Ewout van Bekkum <ewout@google.com>
Reviewed-by: Kevin Zeng <zengk@google.com>
Reviewed-by: Wyatt Hepler <hepler@google.com>
Commit-Queue: Ewout van Bekkum <ewout@google.com>
Pigweed-Auto-Submit: Ewout van Bekkum <ewout@google.com>
2021-07-09 22:41:35 +00:00

199 lines
7.4 KiB
C++

// Copyright 2021 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
#include "pw_chrono/system_timer.h"
#include <algorithm>
#include <mutex>
#include "FreeRTOS.h"
#include "pw_assert/check.h"
#include "pw_chrono_freertos/system_clock_constants.h"
#include "task.h"
#include "timers.h"
namespace pw::chrono {
namespace {
using State = backend::NativeSystemTimer::State;
// Instead of adding targeted locks to each instance, simply use the global
// scheduler critical section lock.
class SchedulerLock {
public:
static void lock() { vTaskSuspendAll(); }
static void unlock() { xTaskResumeAll(); }
};
SchedulerLock global_timer_lock;
void HandleTimerCallback(TimerHandle_t timer_handle) {
// The FreeRTOS timer service is always handled by a thread, ergo to ensure
// this API is threadsafe we simply disable task switching.
std::unique_lock lock(global_timer_lock);
// Because the timer control block, AKA what the timer handle points at, is
// the first member of the NativeSystemTimer struct we play a trick to cheaply
// get the native handle reference.
backend::NativeSystemTimer& native_type =
*reinterpret_cast<backend::NativeSystemTimer*>(timer_handle);
PW_CHECK_UINT_EQ(xTimerIsTimerActive(timer_handle),
pdFALSE,
"The timer is still active while being executed");
if (native_type.state == State::kCancelled) {
// Do nothing, we were invoked while the stop command was in the queue.
return;
}
const SystemClock::duration time_until_deadline =
native_type.expiry_deadline - SystemClock::now();
if (time_until_deadline <= SystemClock::duration::zero()) {
// We have met the deadline, cancel the current state and execute the user's
// callback. Note we cannot update the state later as the user's callback
// may alter the desired state through the Invoke*() API.
native_type.state = State::kCancelled;
// Release the scheduler lock once we won't modify native_state any further.
lock.unlock();
native_type.user_callback(native_type.expiry_deadline);
return;
}
// We haven't met the deadline yet, reschedule as far out as possible.
const SystemClock::duration period =
std::min(pw::chrono::freertos::kMaxTimeout, time_until_deadline);
PW_CHECK_UINT_EQ(
xTimerChangePeriod(
&native_type.tcb, static_cast<TickType_t>(period.count()), 0),
pdPASS,
"Timer command queue overflowed");
PW_CHECK_UINT_EQ(xTimerStart(&native_type.tcb, 0),
pdPASS,
"Timer command queue overflowed");
}
constexpr TickType_t kInvalidPeriod = 1;
constexpr UBaseType_t kOneShotMode = pdFALSE; // Do not use auto reload.
} // namespace
#if configUSE_TIMERS != 1
#error \
"Backend requires your FreeRTOS configuration to have configUSE_TIMERS == 1"
#endif
#if configSUPPORT_STATIC_ALLOCATION != 1
#error \
"Backend requires your FreeRTOS configuration to have configSUPPORT_STATIC_ALLOCATION == 1"
#endif
SystemTimer::SystemTimer(ExpiryCallback callback)
: native_type_{.tcb{},
.state = State::kCancelled,
.expiry_deadline = SystemClock::time_point(),
.user_callback = std::move(callback)} {
// Note that timer "creation" is not enqueued through the command queue and
// is ergo safe to do before the scheduler is running.
const TimerHandle_t handle =
xTimerCreateStatic("", // "pw::chrono::SystemTimer",
kInvalidPeriod,
kOneShotMode,
this,
HandleTimerCallback,
&native_type_.tcb);
// This should never fail since the pointer provided was not null and it
// should return a pointer to the StaticTimer_t.
PW_DCHECK_PTR_EQ(handle, &native_type_.tcb);
}
SystemTimer::~SystemTimer() {
Cancel();
// WARNING: This enqueues the request to delete the timer through a queue, it
// does not synchronously delete and disable the timer here! This means that
// if the timer is about to expire and the timer service thread is a lower
// priority that it may use the native_type_ after it is free'd.
PW_CHECK_UINT_EQ(pdPASS,
xTimerDelete(&native_type_.tcb, 0),
"Timer command queue overflowed");
// In case the timer is still active as warned above, busy yield loop until it
// has been removed. Note that this is safe before the scheduler has been
// started because the timer cannot have been added to the queue yet and ergo
// it shouldn't attempt to yield.
while (xTimerIsTimerActive(&native_type_.tcb)) {
taskYIELD();
}
}
void SystemTimer::InvokeAt(SystemClock::time_point timestamp) {
// The FreeRTOS timer service is always handled by a thread, ergo to ensure
// this API is threadsafe we simply disable task switching.
std::lock_guard lock(global_timer_lock);
// We don't want to call Cancel which would enqueue a stop command instead of
// synchronously updating the state. Instead we update the expiry deadline
// and update the state where the one shot only fires if the expiry deadline
// is exceeded and the callback is executed once.
native_type_.expiry_deadline = timestamp;
// Schedule the timer as far out as possible. Note that the timeout might be
// clamped and it may be rescheduled internally.
const SystemClock::duration time_until_deadline =
timestamp - SystemClock::now();
const SystemClock::duration period =
std::clamp(SystemClock::duration::zero(),
time_until_deadline,
pw::chrono::freertos::kMaxTimeout);
PW_CHECK_UINT_EQ(
xTimerChangePeriod(
&native_type_.tcb, static_cast<TickType_t>(period.count()), 0),
pdPASS,
"Timer command queue overflowed");
// Don't enqueue the start multiple times, schedule it once and let the
// callback cancel.
if (native_type_.state == State::kCancelled) {
PW_CHECK_UINT_EQ(xTimerStart(&native_type_.tcb, 0),
pdPASS,
"Timer command queue overflowed");
native_type_.state = State::kScheduled;
}
}
void SystemTimer::Cancel() {
// The FreeRTOS timer service is always handled by a thread, ergo to ensure
// this API is threadsafe we simply disable task switching.
std::lock_guard lock(global_timer_lock);
// The stop command may not be executed until later in case we're in a
// critical section. For this reason update the internal state in case the
// callback gets invoked.
//
// Note that xTimerIsTimerActive cannot be used here as the timer service
// daemon may be a lower priority and ergo may still execute the callback
// after Cancel() was invoked. This is because a single expired timer may be
// processed before the entire command queue is emptied.
native_type_.state = State::kCancelled;
PW_CHECK_UINT_EQ(xTimerStop(&native_type_.tcb, 0),
pdPASS,
"Timer command queue overflowed");
}
} // namespace pw::chrono