feat(esp_timer): Adds blocking stop function

This commit is contained in:
Konstantin Kondrashov
2026-09-10 10:45:24 +08:00
committed by BOT
parent db31a63351
commit 0382b59e21
4 changed files with 311 additions and 28 deletions
+50 -1
View File
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2017-2024 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2017-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
@@ -196,6 +196,12 @@ esp_err_t esp_timer_restart(esp_timer_handle_t timer, uint64_t timeout_us);
* This function stops the timer previously started using esp_timer_start_once()
* or esp_timer_start_periodic().
*
* This function disarms the timer (prevents future callbacks) but does NOT
* preempt an already-dispatched or currently-running callback.
*
* For a hard guarantee that no callback is running or will run after stop,
* use esp_timer_stop_blocking() or poll esp_timer_is_active() until it returns false.
*
* @param timer timer handle created using esp_timer_create()
* @return
* - ESP_OK on success
@@ -203,6 +209,49 @@ esp_err_t esp_timer_restart(esp_timer_handle_t timer, uint64_t timeout_us);
*/
esp_err_t esp_timer_stop(esp_timer_handle_t timer);
/**
* @brief Stop a running timer and wait for any in-flight callback to complete
*
* This function disarms the timer (prevents future callbacks). If the timer callback is
* currently executing, it waits for the callback to finish, taking into account the specified waiting time.
*
* If this function returns ESP_OK, the caller is guaranteed that the timer is disarmed and
* no callback is executing after the function returns (see the calling-context rules below).
*
* @note Behavior depends on calling context:
* - **User task context (normal case)**: Disarms the timer and waits up to timeout_ticks
* for any in-flight callback to complete. Returns ESP_OK when no callback is running,
* or ESP_ERR_TIMEOUT if the callback didn't finish in time.
*
* - **ISR context**: Never blocks. Disarms the timer and returns immediately.
* If the timer callback may still be running, returns ESP_ERR_NOT_FINISHED.
*
* - **Callback context**: Disarms the timer and returns immediately. Blocking from a callback
* would delay the esp_timer task and distort timing of other TASK-dispatch timers.
* Return value depends on the stopped timer state and the currently running callback:
* - Returns ESP_OK when the stop request is issued from the stopped timer’s own callback
* (the callback will complete naturally after it returns).
* - Returns ESP_ERR_NOT_FINISHED when stopping an ISR-dispatch timer from a foreign callback
* (i.e., the currently running callback belongs to another timer). In this case the caller
* cannot safely wait for ISR context to complete, so it is informed that the callback may
* still be running.
*
* If ESP_ERR_NOT_FINISHED is returned, it means that the timer is disarmed, but its callback
* may is still running or will run soon. In this case, you can wait for the callback to complete
* using esp_timer_is_active().
*
* @param timer Timer handle created using esp_timer_create()
* @param timeout_ticks Maximum time to wait for callback completion, in FreeRTOS ticks.
* Use portMAX_DELAY to wait indefinitely.
* @return
* - ESP_OK on success (timer is disarmed, no callback is running or will run)
* - ESP_ERR_TIMEOUT if the callback didn't complete within timeout_ticks
* - ESP_ERR_INVALID_ARG if the timer handle is invalid
* - ESP_ERR_INVALID_STATE if esp_timer is not initialized
* - ESP_ERR_NOT_FINISHED if the callback may still be running (e.g., ISR context, or TIMER_TASK while an ISR callback is in-flight)
*/
esp_err_t esp_timer_stop_blocking(esp_timer_handle_t timer, uint32_t timeout_ticks);
/**
* @brief Delete an esp_timer instance
*
+115 -25
View File
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2017-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2017-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
@@ -38,13 +38,13 @@
typedef enum {
FL_ISR_DISPATCH_METHOD = (1 << 0), //!< 0=Callback is called from timer task, 1=Callback is called from timer ISR
FL_SKIP_UNHANDLED_EVENTS = (1 << 1), //!< 0=NOT skip unhandled events for periodic timers, 1=Skip unhandled events for periodic timers
FL_CALLBACK_IS_RUNNING = (1 << 2), //!< 0=Callback is NOT running, 1=Callback is running
FL_CALLBACK_IS_RUNNING = (1 << 2), //!< 0=Callback is NOT running, 1=Callback is running
} flags_t;
struct esp_timer {
uint64_t alarm;
uint64_t period: 56;
flags_t flags: 8;
volatile flags_t flags: 8;
union {
esp_timer_cb_t callback;
uint32_t event_id;
@@ -62,10 +62,11 @@ struct esp_timer {
static inline bool is_initialized(void);
static esp_err_t timer_insert(esp_timer_handle_t timer, bool without_update_alarm);
static esp_err_t timer_remove(esp_timer_handle_t timer);
static void timer_remove(esp_timer_handle_t timer);
static bool timer_armed(esp_timer_handle_t timer);
static void timer_list_lock(esp_timer_dispatch_t timer_type);
static void timer_list_unlock(esp_timer_dispatch_t timer_type);
static esp_err_t timer_restart(esp_timer_handle_t timer, uint64_t timeout_us, uint64_t alarm_us);
#if WITH_PROFILING
static void timer_insert_inactive(esp_timer_handle_t timer);
@@ -134,6 +135,11 @@ esp_err_t esp_timer_create(const esp_timer_create_args_t* args,
* in IRAM when PM_SLP_IRAM_OPT = y and ESP_TASK_WDT USE ESP_TIMER = y.
*/
esp_err_t ESP_TIMER_IRAM_ATTR esp_timer_restart(esp_timer_handle_t timer, uint64_t timeout_us)
{
return timer_restart(timer, timeout_us, 0);
}
static esp_err_t ESP_TIMER_IRAM_ATTR timer_restart(esp_timer_handle_t timer, uint64_t timeout_us, uint64_t first_alarm_us)
{
esp_err_t ret = ESP_OK;
@@ -154,24 +160,23 @@ esp_err_t ESP_TIMER_IRAM_ATTR esp_timer_restart(esp_timer_handle_t timer, uint64
/* We need to remove the timer to the list of timers and reinsert it at
* the right position. In fact, the timers are sorted by their alarm value
* (earliest first) */
ret = timer_remove(timer);
timer_remove(timer);
if (ret == ESP_OK) {
/* Two cases here:
* - if the alarm was a periodic one, i.e. `period` is not 0, the given timeout_us becomes the new period
* - if the alarm was a one-shot one, i.e. `period` is 0, it remains non-periodic. */
if (period != 0) {
/* Remove function got rid of the alarm and period fields, restore them */
const uint64_t new_period = MAX(timeout_us, esp_timer_impl_get_min_period_us());
timer->alarm = now + new_period;
timer->period = new_period;
} else {
/* The new one-shot alarm shall be triggered timeout_us after the current time */
timer->alarm = now + timeout_us;
timer->period = 0;
}
ret = timer_insert(timer, false);
/* Two cases here:
* - if the alarm was a periodic one, i.e. `period` is not 0, the given timeout_us becomes the new period
* - if the alarm was a one-shot one, i.e. `period` is 0, it remains non-periodic. */
if (period != 0) {
/* Remove function got rid of the alarm and period fields, restore them */
const uint64_t min_period = esp_timer_impl_get_min_period_us();
const uint64_t new_period = MAX(timeout_us, min_period);
timer->alarm = (first_alarm_us != 0) ? first_alarm_us : now + new_period;
timer->period = new_period;
} else {
/* The new one-shot alarm shall be triggered timeout_us after the current time */
timer->alarm = (first_alarm_us != 0) ? first_alarm_us : now + timeout_us;
timer->period = 0;
}
ret = timer_insert(timer, false);
timer_list_unlock(dispatch_method);
@@ -250,7 +255,7 @@ esp_err_t ESP_TIMER_IRAM_ATTR esp_timer_stop(esp_timer_handle_t timer)
return ESP_ERR_INVALID_STATE;
}
esp_timer_dispatch_t dispatch_method = timer->flags & FL_ISR_DISPATCH_METHOD;
esp_err_t err;
esp_err_t err = ESP_OK;
timer_list_lock(dispatch_method);
@@ -258,12 +263,88 @@ esp_err_t ESP_TIMER_IRAM_ATTR esp_timer_stop(esp_timer_handle_t timer)
if (!timer_armed(timer)) {
err = ESP_ERR_INVALID_STATE;
} else {
err = timer_remove(timer);
timer_remove(timer);
}
timer_list_unlock(dispatch_method);
return err;
}
static inline bool is_callback_running(esp_timer_handle_t timer, esp_timer_dispatch_t dispatch_method)
{
timer_list_lock(dispatch_method);
bool callback_running = (timer != NULL) && (timer->flags & FL_CALLBACK_IS_RUNNING) != 0;
timer_list_unlock(dispatch_method);
return callback_running;
}
esp_err_t esp_timer_stop_blocking(esp_timer_handle_t timer, uint32_t timeout_ticks)
{
if (timer == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (!is_initialized()) {
return ESP_ERR_INVALID_STATE;
}
esp_timer_dispatch_t dispatch_method = timer->flags & FL_ISR_DISPATCH_METHOD;
timer_list_lock(dispatch_method);
bool callback_running = (timer->flags & FL_CALLBACK_IS_RUNNING) != 0;
/* Check if the timer is armed once the list is locked to avoid a data race */
if (timer_armed(timer)) {
timer_remove(timer);
}
timer_list_unlock(dispatch_method);
if (callback_running) {
// timer_process_alarm() releases the timer list lock while executing the callback.
// So it is possible that timer is disarmed but its callback is still running.
// To guarantee that the callback will not run after esp_timer_stop_blocking(),
// we need to wait for the callback to complete here.
// In ISR context: do not wait to avoid blocking
if (xPortInIsrContext()) {
return ESP_ERR_NOT_FINISHED;
}
if (xTaskGetCurrentTaskHandle() == s_timer_task) {
// Called from the esp_timer task context (i.e., the callback owner is a TASK-dispatch timer).
// Concurrency model:
// - TASK-dispatch callbacks are executed by a single esp_timer task and are strictly serialized.
// Therefore, only one TASK callback can be running at any time.
// - If CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD is enabled,
// ISR-dispatch callbacks and TASK-dispatch callbacks may be running at the same time.
if (dispatch_method == ESP_TIMER_TASK) {
// Only one ESP_TIMER_TASK callback can be running at any time.
// So we are stopping the timer from its own callback context:
// the callback will complete naturally after this function returns.
return ESP_OK;
}
// Stopping a running ISR-dispatch timer from a foreign callback:
// we cannot wait for ISR context to complete, and blocking the esp_timer task would
// stall TASK-dispatch callbacks. Report that the timer callback is still running.
return ESP_ERR_NOT_FINISHED;
}
TickType_t start_time = xTaskGetTickCount();
while (is_callback_running(timer, dispatch_method)) {
if (timeout_ticks != portMAX_DELAY) {
TickType_t elapsed = xTaskGetTickCount() - start_time;
if (elapsed >= timeout_ticks) {
return ESP_ERR_TIMEOUT;
}
}
vTaskDelay(1);
}
}
return ESP_OK;
}
esp_err_t esp_timer_delete(esp_timer_handle_t timer)
{
if (timer == NULL) {
@@ -320,7 +401,8 @@ static ESP_TIMER_IRAM_ATTR esp_err_t timer_insert(esp_timer_handle_t timer, bool
return ESP_OK;
}
static ESP_TIMER_IRAM_ATTR esp_err_t timer_remove(esp_timer_handle_t timer)
// It should be always called with the timer list locked
static ESP_TIMER_IRAM_ATTR void timer_remove(esp_timer_handle_t timer)
{
esp_timer_dispatch_t dispatch_method = timer->flags & FL_ISR_DISPATCH_METHOD;
esp_timer_handle_t first_timer = LIST_FIRST(&s_timers[dispatch_method]);
@@ -338,7 +420,6 @@ static ESP_TIMER_IRAM_ATTR esp_err_t timer_remove(esp_timer_handle_t timer)
#if WITH_PROFILING
timer_insert_inactive(timer);
#endif
return ESP_OK;
}
#if WITH_PROFILING
@@ -765,5 +846,14 @@ bool ESP_TIMER_IRAM_ATTR esp_timer_is_active(esp_timer_handle_t timer)
if (timer == NULL) {
return false;
}
return timer_armed(timer) || (timer->flags & FL_CALLBACK_IS_RUNNING);
esp_timer_dispatch_t dispatch_method = timer->flags & FL_ISR_DISPATCH_METHOD;
timer_list_lock(dispatch_method);
// Timer is active if it is armed or its callback is currently running
// After esp_timer_stop() timer is disarmed, but its callback may still be running
bool active = (timer_armed(timer) && timer->event_id != EVENT_ID_DELETE_TIMER)
|| ((timer->flags & FL_CALLBACK_IS_RUNNING) != 0);
timer_list_unlock(dispatch_method);
return active;
}
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
@@ -1337,3 +1337,146 @@ TEST_CASE("Test ISR dispatch callbacks are not blocked even if TASK callbacks ta
}
#endif // CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD
typedef struct {
SemaphoreHandle_t callback_started;
SemaphoreHandle_t stop_called_from_cb;
SemaphoreHandle_t callback_can_finish;
SemaphoreHandle_t callback_finished;
volatile int callback_count;
esp_timer_handle_t timer;
} test_stop_blocking_state_t;
static void test_stop_blocking_callback(void* arg)
{
test_stop_blocking_state_t* state = (test_stop_blocking_state_t*) arg;
state->callback_count++;
// Notify that callback has started
xSemaphoreGive(state->callback_started);
// Wait for permission to finish (simulating long-running callback)
xSemaphoreTake(state->callback_can_finish, portMAX_DELAY);
// Notify that callback has finished
xSemaphoreGive(state->callback_finished);
}
TEST_CASE("esp_timer_stop_blocking waits for callback completion", "[esp_timer]")
{
test_stop_blocking_state_t state = {
.callback_started = xSemaphoreCreateBinary(),
.stop_called_from_cb = NULL,
.callback_can_finish = xSemaphoreCreateBinary(),
.callback_finished = xSemaphoreCreateBinary(),
.callback_count = 0,
.timer = NULL,
};
esp_timer_create_args_t timer_args = {
.callback = test_stop_blocking_callback,
.arg = &state,
.name = "test_blocking",
};
esp_timer_handle_t timer;
TEST_ESP_OK(esp_timer_create(&timer_args, &timer));
// Start a one-shot timer
TEST_ESP_OK(esp_timer_start_once(timer, 1000));
// Wait for callback to start
TEST_ASSERT_TRUE(xSemaphoreTake(state.callback_started, pdMS_TO_TICKS(100)));
// At this point, callback is running
TEST_ASSERT_TRUE(esp_timer_is_active(timer));
// Try to stop with blocking - this should wait for callback
TEST_ESP_ERR(ESP_ERR_TIMEOUT, esp_timer_stop_blocking(timer, pdMS_TO_TICKS(100)));
// Now allow callback to finish
xSemaphoreGive(state.callback_can_finish);
// Wait for callback to complete
TEST_ASSERT_TRUE(xSemaphoreTake(state.callback_finished, pdMS_TO_TICKS(100)));
// Timer should no longer be active
TEST_ASSERT_FALSE(esp_timer_is_active(timer));
// Callback should have run exactly once
TEST_ASSERT_EQUAL(1, state.callback_count);
// Clean up
TEST_ESP_OK(esp_timer_delete(timer));
vSemaphoreDelete(state.callback_started);
vSemaphoreDelete(state.callback_can_finish);
vSemaphoreDelete(state.callback_finished);
}
static void test_stop_blocking_callback_stop_inside(void* arg)
{
test_stop_blocking_state_t* state = (test_stop_blocking_state_t*) arg;
state->callback_count++;
// Notify that callback has started (we are now in esp_timer task context for TASK dispatch)
xSemaphoreGive(state->callback_started);
// Call stop_blocking from the timer's own callback:
// This must NOT block (otherwise we deadlock waiting for ourselves).
TEST_ESP_OK(esp_timer_stop_blocking(state->timer, portMAX_DELAY));
// Signal to the test that stop_blocking has returned from inside callback
xSemaphoreGive(state->stop_called_from_cb);
// Keep callback running until the test allows us to finish
xSemaphoreTake(state->callback_can_finish, portMAX_DELAY);
xSemaphoreGive(state->callback_finished);
}
TEST_CASE("esp_timer_stop_blocking from timer callback and does not block", "[esp_timer]")
{
test_stop_blocking_state_t state = {
.callback_started = xSemaphoreCreateBinary(),
.stop_called_from_cb = xSemaphoreCreateBinary(),
.callback_can_finish = xSemaphoreCreateBinary(),
.callback_finished = xSemaphoreCreateBinary(),
.timer = NULL,
.callback_count = 0,
};
esp_timer_create_args_t timer_args = {
.callback = test_stop_blocking_callback_stop_inside,
.arg = &state,
.name = "stop_from_cb",
};
esp_timer_handle_t timer;
TEST_ESP_OK(esp_timer_create(&timer_args, &timer));
state.timer = timer;
TEST_ESP_OK(esp_timer_start_once(timer, 1000));
// Wait until callback starts
TEST_ASSERT_TRUE(xSemaphoreTake(state.callback_started, portMAX_DELAY));
// Ensure stop_blocking has returned from inside the callback (i.e., it did not block)
TEST_ASSERT_TRUE(xSemaphoreTake(state.stop_called_from_cb, pdMS_TO_TICKS(200)));
// While callback is still running, timer should still be considered active
TEST_ASSERT_TRUE(esp_timer_is_active(timer));
// Now let callback finish
xSemaphoreGive(state.callback_can_finish);
TEST_ASSERT_TRUE(xSemaphoreTake(state.callback_finished, pdMS_TO_TICKS(1000)));
// After callback finishes, timer should be inactive (one-shot already disarmed)
TEST_ASSERT_FALSE(esp_timer_is_active(timer));
TEST_ASSERT_EQUAL(1, state.callback_count);
TEST_ESP_OK(esp_timer_delete(timer));
vSemaphoreDelete(state.callback_started);
vSemaphoreDelete(state.stop_called_from_cb);
vSemaphoreDelete(state.callback_can_finish);
vSemaphoreDelete(state.callback_finished);
}