feat(esp_timer): Adds blocking stop function

This commit is contained in:
Konstantin Kondrashov
2025-12-30 21:04:16 +02:00
parent 3e74ff2b33
commit 8569da1ba8
4 changed files with 305 additions and 29 deletions

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
*/
@@ -249,6 +249,12 @@ esp_err_t esp_timer_restart_at(esp_timer_handle_t timer, uint64_t period_us, uin
* 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
@@ -256,6 +262,49 @@ esp_err_t esp_timer_restart_at(esp_timer_handle_t timer, uint64_t period_us, uin
*/
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 timers 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
*

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,7 +62,7 @@ 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);
@@ -169,25 +169,23 @@ static esp_err_t ESP_TIMER_IRAM_ATTR timer_restart(esp_timer_handle_t timer, uin
/* 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 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);
/* 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);
@@ -268,7 +266,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);
@@ -276,12 +274,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) {
@@ -338,7 +412,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]);
@@ -356,7 +431,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
@@ -789,5 +863,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;
}

View File

@@ -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
*/
@@ -1334,3 +1334,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);
}

View File

@@ -215,7 +215,8 @@ The general procedure to create, start, stop, and delete a timer is as follows:
3. Stop the timer
- To stop the running timer, call the function :cpp:func:`esp_timer_stop`.
- To stop the running timer, call the function :cpp:func:`esp_timer_stop`. But it does not guarantee that after this call, the callback will not be running one or more times. To check if the callback is not running after stopping the timer, you can use :cpp:func:`esp_timer_is_active`. Another approach is to use a blocking stop API.
- Blocking the timer stop operation until any in-flight callback completes can be done using :cpp:func:`esp_timer_stop_blocking`.
4. Delete the timer