// SPDX-License-Identifier: Apache-2.0 #pragma once #include "Thread.h" #include "freertoscompat/Timers.h" #ifdef ESP_PLATFORM #include #else #include #endif #include #include #include namespace tt { /** * Wrapper class for xTimer functions. * @warning Cannot be called from an ISR context except for ::setPendingCallback() */ class Timer final { public: enum class Type { Once = 0, // Timer triggers once after time has passed Periodic = 1 // Timer triggers repeatedly after time has passed }; typedef std::function Callback; typedef void (*PendingCallback)(void* context, uint32_t arg); private: struct TimerHandleDeleter { void operator()(TimerHandle_t handleToDelete) const { xTimerDelete(handleToDelete, kernel::FREERTOS_MAX_TICKS); } }; Callback callback; std::unique_ptr, TimerHandleDeleter> handle; // Set for the duration of a callback invocation. xTimerStop()/xTimerDelete() only prevent // *future* dispatches: if a callback was already dispatched by the timer daemon task, they // return immediately without waiting for it to finish. Callers that stop a timer and then // immediately destroy state the callback reads/writes (e.g. a Service destructing itself // right after stopping its own update timer) can otherwise race an in-flight callback against // that destruction. stop() below spins on this flag so it only returns once no callback is // executing and none can start afterward. std::atomic callbackRunning {false}; static TimerHandle_t createTimer(Type type, TickType_t ticks, void* timerId, TimerCallbackFunction_t callback) { assert(timerId != nullptr); assert(callback != nullptr); BaseType_t auto_reload = (type == Type::Once) ? pdFALSE : pdTRUE; return xTimerCreate(nullptr, ticks, auto_reload, timerId, callback); } static void onCallback(TimerHandle_t hTimer) { auto* timer = static_cast(pvTimerGetTimerID(hTimer)); if (timer != nullptr) { timer->callbackRunning.store(true, std::memory_order_release); timer->callback(); timer->callbackRunning.store(false, std::memory_order_release); } } // Signals a SemaphoreHandle_t (passed as context) from the timer daemon task. Used by stop() // as a barrier: FreeRTOS timer commands are processed FIFO, so queuing this via // xTimerPendFunctionCall right after xTimerStop() guarantees it only runs once the daemon has // drained everything queued ahead of it - including an expiry command that raced the stop. static void onStopBarrier(void* context, uint32_t /*arg*/) { xSemaphoreGive(static_cast(context)); } public: /** * @param[in] type The timer type * @param[in] callback The callback function */ Timer(Type type, TickType_t ticks, Callback callback) : callback(callback), handle(createTimer(type, ticks, this, onCallback)) { assert(xPortInIsrContext() == pdFALSE); assert(handle != nullptr); } ~Timer() { assert(xPortInIsrContext() == pdFALSE); } /** * Start the timer * @return success result */ bool start() const { assert(xPortInIsrContext() == pdFALSE); return xTimerStart(handle.get(), kernel::FREERTOS_MAX_TICKS) == pdPASS; } /** * Stop the timer. Unlike a bare xTimerStop(), this blocks until any * callback invocation already queued or dispatched by the timer daemon * task at the time of the call has finished running, so it is safe to * destroy state the callback reads or writes as soon as this returns. * @warning Do not call this from within the timer's own callback - it * would deadlock waiting on itself. * @return success result */ bool stop() const { assert(xPortInIsrContext() == pdFALSE); bool result = xTimerStop(handle.get(), kernel::FREERTOS_MAX_TICKS) == pdPASS; if (result) { // xTimerStop() only queues tmrCOMMAND_STOP - the daemon may not have processed it yet, // and an expiry command already ahead of it in the queue can still dispatch a callback // after this returns. Queuing a pend-function-call barrier right after the stop command // guarantees (FIFO command processing) that it only runs once everything queued ahead // of it - including such an expiry - has been handled. SemaphoreHandle_t barrier = xSemaphoreCreateBinary(); assert(barrier != nullptr); if (setPendingCallback(onStopBarrier, barrier, 0, kernel::FREERTOS_MAX_TICKS)) { xSemaphoreTake(barrier, kernel::FREERTOS_MAX_TICKS); } vSemaphoreDelete(barrier); } while (callbackRunning.load(std::memory_order_acquire)) { vTaskDelay(1); } return result; } /** * Set a new interval and reset the timer * @param[in] interval The new timer interval * @return success result */ bool reset(TickType_t interval) const { assert(xPortInIsrContext() == pdFALSE); return xTimerChangePeriod(handle.get(), interval, kernel::FREERTOS_MAX_TICKS) == pdPASS && xTimerReset(handle.get(), kernel::FREERTOS_MAX_TICKS) == pdPASS; } /** * Reset the timer * @return success result */ bool reset() const { assert(xPortInIsrContext() == pdFALSE); return xTimerReset(handle.get(), kernel::FREERTOS_MAX_TICKS) == pdPASS; } /** @return true when the timer is running */ bool isRunning() const { assert(xPortInIsrContext() == pdFALSE); return xTimerIsTimerActive(handle.get()) == pdTRUE; } /** @return the expiry time in ticks */ TickType_t getExpiryTime() const { assert(xPortInIsrContext() == pdFALSE); return xTimerGetExpiryTime(handle.get()); } /** * Calls xTimerPendFunctionCall internally. * @param[in] callback the function to call * @param[in] callbackContext the first function argument * @param[in] callbackArg the second function argument * @param[in] timeout the function timeout (must set to 0 in ISR mode) * @return true on success */ bool setPendingCallback(PendingCallback newCallback, void* callbackContext, uint32_t callbackArg, TickType_t timeout) const { if (xPortInIsrContext() == pdTRUE) { assert(timeout == 0); return xTimerPendFunctionCallFromISR(newCallback, callbackContext, callbackArg, nullptr) == pdPASS; } else { return xTimerPendFunctionCall(newCallback, callbackContext, callbackArg, timeout) == pdPASS; } } /** Set callback priority * @param[in] priority The priority */ void setCallbackPriority(Thread::Priority priority) const { assert(xPortInIsrContext() == pdFALSE); TaskHandle_t task_handle = xTimerGetTimerDaemonTaskHandle(); assert(task_handle); // Don't call this method before timer task start vTaskPrioritySet(task_handle, static_cast(priority)); } }; } // namespace