From 4b01f68488f97ea22cac00a0dc5647dff062829f Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Thu, 30 Jul 2026 21:55:52 -0500 Subject: [PATCH 1/8] feat(timer): Improve periodicity of the software timer --- .../timer/example/main/timer_example.cpp | 42 ++++++++++++----- components/timer/example/sdkconfig.defaults | 5 ++ components/timer/include/timer.hpp | 3 ++ components/timer/src/timer.cpp | 47 ++++++++++++++----- 4 files changed, 75 insertions(+), 22 deletions(-) diff --git a/components/timer/example/main/timer_example.cpp b/components/timer/example/main/timer_example.cpp index 072657d0b..c15ceaa92 100644 --- a/components/timer/example/main/timer_example.cpp +++ b/components/timer/example/main/timer_example.cpp @@ -51,6 +51,26 @@ extern "C" void app_main(void) { std::this_thread::sleep_for(num_seconds_to_run * 1s); } + // timer periodicity testing, with different durations + { + logger.info("Starting timer periodicity testing example"); + auto timer_fn = []() { + static size_t iterations{0}; + if (iterations % 50 == 0) { + fmt::print("[{:.3f}] #iterations = {}\n", elapsed(), iterations); + std::this_thread::sleep_for(9ms); // simulate a long callback + } + iterations++; + // we don't want to stop, so return false + return false; + }; + auto timer = espp::Timer({.name = "Timer 1", + .period = 10ms, + .callback = timer_fn, + .log_level = espp::Logger::Verbosity::WARN}); + std::this_thread::sleep_for(num_seconds_to_run * 1s); + } + // timer watchdog example { logger.info("Starting timer watchdog example"); @@ -67,7 +87,7 @@ extern "C" void app_main(void) { auto timer = espp::Timer({.name = "Timer 1", .period = 500ms, .callback = timer_fn, - .log_level = espp::Logger::Verbosity::DEBUG}); + .log_level = espp::Logger::Verbosity::INFO}); timer.start_watchdog(); // start the watchdog timer for this timer std::this_thread::sleep_for(500ms); std::error_code ec; @@ -101,7 +121,7 @@ extern "C" void app_main(void) { .delay = 500ms, .callback = timer_fn, .auto_start = false, // don't start the timer automatically, we'll call start() - .log_level = espp::Logger::Verbosity::DEBUG}); + .log_level = espp::Logger::Verbosity::INFO}); timer.start(); std::this_thread::sleep_for(2s); logger.info("Cancelling timer for 2 seconds"); @@ -132,7 +152,7 @@ extern "C" void app_main(void) { .period = 0ms, // one shot timer .delay = 500ms, .callback = timer_fn, - .log_level = espp::Logger::Verbosity::DEBUG}); + .log_level = espp::Logger::Verbosity::INFO}); //! [timer oneshot example] std::this_thread::sleep_for(num_seconds_to_run * 1s); } @@ -156,7 +176,7 @@ extern "C" void app_main(void) { .period = 500ms, .callback = timer_fn, .stack_size_bytes = 6192, - .log_level = espp::Logger::Verbosity::DEBUG}); + .log_level = espp::Logger::Verbosity::INFO}); //! [timer cancel itself example] std::this_thread::sleep_for(num_seconds_to_run * 1s); } @@ -177,7 +197,7 @@ extern "C" void app_main(void) { .delay = 500ms, .callback = timer_fn, .stack_size_bytes = 4096, - .log_level = espp::Logger::Verbosity::DEBUG}); + .log_level = espp::Logger::Verbosity::INFO}); std::this_thread::sleep_for(2s); timer.cancel(); // it will have already been cancelled by here, but this should be harmless timer.start(1s); // restart the timer with a 1 second delay @@ -200,7 +220,7 @@ extern "C" void app_main(void) { .period = 500ms, .callback = timer_fn, .stack_size_bytes = 4096, - .log_level = espp::Logger::Verbosity::DEBUG}); + .log_level = espp::Logger::Verbosity::INFO}); std::this_thread::sleep_for(2s); logger.info("Updating period to 100ms"); timer.set_period(100ms); @@ -224,11 +244,11 @@ extern "C" void app_main(void) { .task_config = { .name = "Advanced Config Timer", - .stack_size_bytes = 4096, + .stack_size_bytes = 4 * 1024, .priority = 10, .core_id = 1, }, - .log_level = espp::Logger::Verbosity::DEBUG}); + .log_level = espp::Logger::Verbosity::INFO}); //! [timer advanced config example] std::this_thread::sleep_for(num_seconds_to_run * 1s); } @@ -248,7 +268,7 @@ extern "C" void app_main(void) { auto high_resolution_timer = espp::HighResolutionTimer({.name = "High Resolution Timer", .callback = timer_fn, - .log_level = espp::Logger::Verbosity::DEBUG}); + .log_level = espp::Logger::Verbosity::INFO}); uint64_t period_us = 100; bool started = high_resolution_timer.start(period_us); logger.info("High resolution timer started: {}", started); @@ -294,7 +314,7 @@ extern "C" void app_main(void) { auto high_resolution_timer = espp::HighResolutionTimer({.name = "High Resolution Timer 1", .callback = timer_fn, - .log_level = espp::Logger::Verbosity::DEBUG}); + .log_level = espp::Logger::Verbosity::INFO}); uint64_t period_us = 100; bool started = high_resolution_timer.start(period_us); logger.info("High resolution timer 1 started: {}", started); @@ -309,7 +329,7 @@ extern "C" void app_main(void) { auto high_resolution_timer2 = espp::HighResolutionTimer({.name = "High Resolution Timer 2", .callback = timer2_fn, - .log_level = espp::Logger::Verbosity::DEBUG}); + .log_level = espp::Logger::Verbosity::INFO}); // configure the task watchdog static constexpr bool panic_on_watchdog_timeout = false; diff --git a/components/timer/example/sdkconfig.defaults b/components/timer/example/sdkconfig.defaults index 93db57dc7..670bbc974 100644 --- a/components/timer/example/sdkconfig.defaults +++ b/components/timer/example/sdkconfig.defaults @@ -1,8 +1,13 @@ +CONFIG_COMPILER_OPTIMIZATION_PERF=y + # Common ESP-related # CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 +# Set the FreeRTOS rate to 1ms (1000Hz) +CONFIG_FREERTOS_HZ=1000 + # Enable support for power management # NOTE: if you enable this the USB serial will not work in light sleep mode # CONFIG_PM_ENABLE=y diff --git a/components/timer/include/timer.hpp b/components/timer/include/timer.hpp index 609e66ef9..61c05fb36 100644 --- a/components/timer/include/timer.hpp +++ b/components/timer/include/timer.hpp @@ -157,9 +157,12 @@ class Timer : public BaseComponent { protected: bool timer_callback_fn(std::mutex &m, std::condition_variable &cv, bool &task_notified); + std::recursive_mutex mutex_; ///< Mutex to protect the timer state. std::chrono::microseconds period_{0}; ///< The period of the timer. If 0, the timer will run once. std::chrono::microseconds delay_{0}; ///< The delay before the timer starts. std::atomic running_{false}; ///< True if the timer is running, false otherwise. + std::chrono::time_point + wakeup_time_; ///< The time point when the timer will wake up. float period_float; float delay_float; callback_fn callback_; ///< The callback function to call when the timer expires. diff --git a/components/timer/src/timer.cpp b/components/timer/src/timer.cpp index f91bf1663..50fc0444c 100644 --- a/components/timer/src/timer.cpp +++ b/components/timer/src/timer.cpp @@ -54,6 +54,16 @@ Timer::~Timer() { cancel(); } void Timer::start() { logger_.info("starting with period {:.3f} s and delay {:.3f} s", period_float, delay_float); + { + std::lock_guard lock(mutex_); + wakeup_time_ = std::chrono::steady_clock::now(); + if (delay_float > 0) { + wakeup_time_ += delay_; + } + if (period_float > 0) { + wakeup_time_ += period_; + } + } running_ = true; // start the task task_->start(); @@ -68,8 +78,11 @@ void Timer::start(const std::chrono::duration &delay) { logger_.info("restarting with delay {:.3f} s", delay.count()); cancel(); } - delay_ = std::chrono::duration_cast(delay); - delay_float = std::chrono::duration(delay_).count(); + { + std::lock_guard lock(mutex_); + delay_ = std::chrono::duration_cast(delay); + delay_float = std::chrono::duration(delay_).count(); + } start(); } @@ -93,8 +106,11 @@ void Timer::set_period(const std::chrono::duration &period) { logger_.warn("period cannot be negative, not setting"); return; } - period_ = std::chrono::duration_cast(period); - period_float = std::chrono::duration(period_).count(); + { + std::lock_guard lock(mutex_); + period_ = std::chrono::duration_cast(period); + period_float = std::chrono::duration(period_).count(); + } logger_.info("setting period to {:.3f} s", period_float); } @@ -141,22 +157,31 @@ bool Timer::timer_callback_fn(std::mutex &m, std::condition_variable &cv, bool & return true; } auto end = std::chrono::steady_clock::now(); - float elapsed = std::chrono::duration(end - start_time).count(); - if (elapsed > period_float) { - // if the callback took longer than the period, then we should just - // return and run the callback again immediately - logger_.warn_rate_limited("callback took longer ({:.3f} s) than period ({:.3f} s)", elapsed, - period_float); + if (wakeup_time_ < end) { + // if the callback took longer than the period (so it is already past the + // next wakeup time), log a warning and ensure that the next wakeup time is + // the closest multiple of the period after the current + float elapsed = std::chrono::duration(end - start_time).count(); + if (elapsed > period_float) { + logger_.warn_rate_limited("callback took longer ({:.3f} s) than period ({:.3f} s)", elapsed, + period_float); + } + while (wakeup_time_ < end) { + wakeup_time_ += period_; + } return false; } // now wait for the period (taking into account the time it took to run // the callback) { std::unique_lock lock(m); - cv.wait_until(lock, start_time + period_, [&task_notified] { return task_notified; }); + cv.wait_until(lock, wakeup_time_, [&task_notified] { return task_notified; }); // reset the task_notified flag task_notified = false; } + // now that we've waited, make sure the next wakeup time is the next multiple + // of the period after the last + wakeup_time_ += period_; // keep the timer running return false; } From 0e270314d94afa0e7ba8f5cfe4dd8bd98af72dfe Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Fri, 31 Jul 2026 09:08:27 -0500 Subject: [PATCH 2/8] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- components/timer/src/timer.cpp | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/components/timer/src/timer.cpp b/components/timer/src/timer.cpp index 50fc0444c..12e3fae9e 100644 --- a/components/timer/src/timer.cpp +++ b/components/timer/src/timer.cpp @@ -162,12 +162,27 @@ bool Timer::timer_callback_fn(std::mutex &m, std::condition_variable &cv, bool & // next wakeup time), log a warning and ensure that the next wakeup time is // the closest multiple of the period after the current float elapsed = std::chrono::duration(end - start_time).count(); - if (elapsed > period_float) { + std::chrono::microseconds period; + float local_period_float; + { + std::lock_guard lock(mutex_); + period = period_; + local_period_float = period_float; + } + if (elapsed > local_period_float) { logger_.warn_rate_limited("callback took longer ({:.3f} s) than period ({:.3f} s)", elapsed, - period_float); + local_period_float); } - while (wakeup_time_ < end) { - wakeup_time_ += period_; + if (period.count() <= 0) { + // period changed to oneshot while running; stop after this callback + running_ = false; + return true; + } + { + std::lock_guard lock(mutex_); + while (wakeup_time_ < end) { + wakeup_time_ += period; + } } return false; } From 317eb804ac32a809cfc243665e8ebad3255ef22c Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Fri, 31 Jul 2026 09:15:35 -0500 Subject: [PATCH 3/8] address comments and improve api some more --- components/timer/include/timer.hpp | 8 ++++++-- components/timer/src/timer.cpp | 32 +++++++++++++++++++++++------- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/components/timer/include/timer.hpp b/components/timer/include/timer.hpp index 61c05fb36..ad1ccfad6 100644 --- a/components/timer/include/timer.hpp +++ b/components/timer/include/timer.hpp @@ -99,7 +99,9 @@ class Timer : public BaseComponent { /// @brief Start the timer. /// @details Starts the timer. Does nothing if the timer is already running. - void start(); + /// @return true if the timer was started or is already running, false if the + /// timer could not be started. + bool start(); /// @brief Start the timer with a delay. /// @details Starts the timer with a delay. If the timer is already running, @@ -108,7 +110,9 @@ class Timer : public BaseComponent { /// with the delay. Overwrites any previous delay that might have /// been set. /// @param delay The delay before the first execution of the timer callback. - void start(const std::chrono::duration &delay); + /// @return true if the timer was started or restarted, false if the timer + /// could not be started. + bool start(const std::chrono::duration &delay); /// @brief Stop the timer, same as cancel(). /// @details Stops the timer, same as cancel(). diff --git a/components/timer/src/timer.cpp b/components/timer/src/timer.cpp index 12e3fae9e..1f8dddf02 100644 --- a/components/timer/src/timer.cpp +++ b/components/timer/src/timer.cpp @@ -44,7 +44,17 @@ Timer::Timer(const Timer::AdvancedConfig &config) .log_level = config.log_level, }); period_float = std::chrono::duration(period_).count(); + if (period_float < 0) { + logger_.warn("period cannot be negative, setting to 0"); + period_ = std::chrono::microseconds(0); + period_float = 0; + } delay_float = std::chrono::duration(delay_).count(); + if (delay_float < 0) { + logger_.warn("delay cannot be negative, setting to 0"); + delay_ = std::chrono::microseconds(0); + delay_float = 0; + } if (config.auto_start) { start(); } @@ -52,7 +62,11 @@ Timer::Timer(const Timer::AdvancedConfig &config) Timer::~Timer() { cancel(); } -void Timer::start() { +bool Timer::start() { + if (is_running()) { + logger_.info("timer is already running, not starting"); + return true; + } logger_.info("starting with period {:.3f} s and delay {:.3f} s", period_float, delay_float); { std::lock_guard lock(mutex_); @@ -64,15 +78,18 @@ void Timer::start() { wakeup_time_ += period_; } } - running_ = true; - // start the task - task_->start(); + if (task_->start()) { + running_ = true; + return true; + } + logger_.error("failed to start timer task"); + return false; } -void Timer::start(const std::chrono::duration &delay) { +bool Timer::start(const std::chrono::duration &delay) { if (delay.count() < 0) { logger_.warn("delay cannot be negative, not starting"); - return; + return false; } if (is_running()) { logger_.info("restarting with delay {:.3f} s", delay.count()); @@ -83,7 +100,7 @@ void Timer::start(const std::chrono::duration &delay) { delay_ = std::chrono::duration_cast(delay); delay_float = std::chrono::duration(delay_).count(); } - start(); + return start(); } void Timer::stop() { cancel(); } @@ -196,6 +213,7 @@ bool Timer::timer_callback_fn(std::mutex &m, std::condition_variable &cv, bool & } // now that we've waited, make sure the next wakeup time is the next multiple // of the period after the last + std::lock_guard lock(mutex_); wakeup_time_ += period_; // keep the timer running return false; From cde59990e4c98bf3530d34071507346e81a2fe8e Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Fri, 31 Jul 2026 09:24:00 -0500 Subject: [PATCH 4/8] test(timer): Add a self-checking test suite to the example Run a set of pass/fail tests at the end of the timer example covering the periodicity improvement and the timer API: fixed-rate scheduling (no cumulative drift), overrunning-callback behavior, initial delay, one-shot, start()/ is_running()/cancel(), callback-requested stop, and negative-period clamping. Each test prints PASS/FAIL and the suite prints an overall PASS/FAIL summary. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../timer/example/main/timer_example.cpp | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) diff --git a/components/timer/example/main/timer_example.cpp b/components/timer/example/main/timer_example.cpp index c15ceaa92..2256c0ecf 100644 --- a/components/timer/example/main/timer_example.cpp +++ b/components/timer/example/main/timer_example.cpp @@ -1,6 +1,8 @@ #include +#include #include +#include #include #include @@ -365,6 +367,181 @@ extern "C" void app_main(void) { //! [high resolution timer watchdog example] } + // =========================================================================== + // Timer test suite + // + // A set of self-checking tests that run at the end of the example and print + // PASS/FAIL for each, then an overall PASS/FAIL summary for the suite. These + // exercise the timer's periodicity (fixed-rate scheduling / no drift), delay, + // one-shot behavior, start()/is_running()/cancel(), self-cancel, and input + // validation. Timings use generous tolerances so the suite is not flaky. + // =========================================================================== + { + using namespace std::chrono; + logger.info(""); + logger.info("======== Running Timer test suite ========"); + int passed = 0; + int failed = 0; + auto check = [&](const std::string &name, bool condition) { + fmt::print(" [{}] {}\n", condition ? "PASS" : "FAIL", name); + if (condition) { + ++passed; + } else { + ++failed; + } + }; + + // 1) Periodicity: a 20 ms periodic timer should fire ~50x/s and stay on a + // fixed schedule (the k-th callback lands near k*period, i.e. no + // cumulative drift). + { + std::atomic count{0}; + std::atomic last_fire{0.0f}; + auto t0 = steady_clock::now(); + auto timer = espp::Timer({.name = "test-periodicity", + .period = 20ms, + .callback = + [&count, &last_fire, t0]() { + last_fire = duration(steady_clock::now() - t0).count(); + ++count; + return false; + }, + .log_level = espp::Logger::Verbosity::WARN}); + std::this_thread::sleep_for(1s); + timer.cancel(); + const int n = count.load(); + // first fires immediately, then every 20 ms -> ~51 in 1 s + check("periodic timer fires ~50 times in 1 s (20 ms period)", n >= 45 && n <= 56); + // the last callback should land near (n-1)*20 ms if there is no drift + const float expected_last = (n - 1) * 0.020f; + const float actual_last = last_fire.load(); + check("periodic timer stays on schedule (no cumulative drift)", + n >= 2 && actual_last > expected_last - 0.030f && actual_last < expected_last + 0.030f); + } + + // 2) A callback that overruns the period should run back-to-back (bounded by + // the callback duration), not stall or spiral. + { + std::atomic count{0}; + auto timer = espp::Timer({.name = "test-overrun", + .period = 20ms, + .callback = + [&count]() { + ++count; + std::this_thread::sleep_for(30ms); // longer than the period + return false; + }, + .log_level = espp::Logger::Verbosity::WARN}); + std::this_thread::sleep_for(1s); + timer.cancel(); + const int n = count.load(); + // ~30 ms per callback -> ~33 in 1 s + check("timer with a long (overrunning) callback runs continuously", n >= 27 && n <= 40); + } + + // 3) Delay: the first callback fires at ~the configured delay, not before. + { + std::atomic count{0}; + std::atomic first_fire{-1.0f}; + auto t0 = steady_clock::now(); + auto timer = espp::Timer({.name = "test-delay", + .period = 50ms, + .delay = 300ms, + .callback = + [&count, &first_fire, t0]() { + if (count.fetch_add(1) == 0) { + first_fire = + duration(steady_clock::now() - t0).count(); + } + return false; + }, + .log_level = espp::Logger::Verbosity::WARN}); + std::this_thread::sleep_for(200ms); // still within the 300 ms delay + const bool none_before_delay = (count.load() == 0); + std::this_thread::sleep_for(400ms); // total 600 ms, past the delay + timer.cancel(); + const float ff = first_fire.load(); + check("delayed timer does not fire before the delay", none_before_delay); + check("delayed timer first fires at ~the delay", ff > 0.25f && ff < 0.40f); + } + + // 4) One-shot (period 0) fires exactly once. + { + std::atomic count{0}; + auto timer = espp::Timer({.name = "test-oneshot", + .period = 0ms, + .delay = 100ms, + .callback = + [&count]() { + ++count; + return false; + }, + .log_level = espp::Logger::Verbosity::WARN}); + std::this_thread::sleep_for(400ms); + check("one-shot timer (period 0) fires exactly once", count.load() == 1); + } + + // 5) start() / is_running() / cancel(). + { + std::atomic count{0}; + auto timer = espp::Timer({.name = "test-startstop", + .period = 50ms, + .callback = + [&count]() { + ++count; + return false; + }, + .auto_start = false, + .log_level = espp::Logger::Verbosity::WARN}); + check("timer is not running before start()", !timer.is_running()); + const bool started = timer.start(); + check("start() returns true and the timer is running", started && timer.is_running()); + check("start() on an already-running timer returns true", timer.start()); + std::this_thread::sleep_for(200ms); + timer.cancel(); + const int after_cancel = count.load(); + std::this_thread::sleep_for(150ms); + check("cancel() stops the timer (no more callbacks)", + !timer.is_running() && count.load() == after_cancel); + } + + // 6) A callback that returns true stops the timer. + { + std::atomic count{0}; + auto timer = espp::Timer({.name = "test-selfstop", + .period = 50ms, + .callback = [&count]() { return ++count >= 3; }, + .log_level = espp::Logger::Verbosity::WARN}); + std::this_thread::sleep_for(400ms); + check("callback returning true stops the timer", count.load() == 3 && !timer.is_running()); + } + + // 7) Input validation: a negative period is clamped (behaves as one-shot). + { + std::atomic count{0}; + auto timer = espp::Timer({.name = "test-negative-period", + .period = -50ms, + .callback = + [&count]() { + ++count; + return false; + }, + .log_level = espp::Logger::Verbosity::WARN}); + std::this_thread::sleep_for(300ms); + timer.cancel(); + check("negative period is clamped (runs once, not repeatedly)", count.load() == 1); + } + + // ---- summary ---- + fmt::print("\n"); + logger.info("======== Timer test suite: {}/{} passed ========", passed, passed + failed); + if (failed == 0) { + logger.info("TIMER TESTS RESULT: PASS ({} tests)", passed); + } else { + logger.error("TIMER TESTS RESULT: FAIL ({} passed, {} failed)", passed, failed); + } + } + logger.info("Example complete!"); while (true) { From 94f8bc4997ffa9b9813ffec9b8c66418af3f0bf8 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Fri, 31 Jul 2026 15:46:08 -0500 Subject: [PATCH 5/8] address comments, improve API, improve testing --- .../timer/example/main/timer_example.cpp | 10 +- components/timer/include/timer.hpp | 4 + components/timer/src/timer.cpp | 120 ++++++++++++------ 3 files changed, 92 insertions(+), 42 deletions(-) diff --git a/components/timer/example/main/timer_example.cpp b/components/timer/example/main/timer_example.cpp index 2256c0ecf..c1672a860 100644 --- a/components/timer/example/main/timer_example.cpp +++ b/components/timer/example/main/timer_example.cpp @@ -56,11 +56,11 @@ extern "C" void app_main(void) { // timer periodicity testing, with different durations { logger.info("Starting timer periodicity testing example"); - auto timer_fn = []() { - static size_t iterations{0}; + size_t iterations{0}; + auto timer_fn = [&iterations]() { if (iterations % 50 == 0) { fmt::print("[{:.3f}] #iterations = {}\n", elapsed(), iterations); - std::this_thread::sleep_for(9ms); // simulate a long callback + std::this_thread::sleep_for(12ms); // simulate a long callback } iterations++; // we don't want to stop, so return false @@ -436,7 +436,9 @@ extern "C" void app_main(void) { timer.cancel(); const int n = count.load(); // ~30 ms per callback -> ~33 in 1 s - check("timer with a long (overrunning) callback runs continuously", n >= 27 && n <= 40); + auto msg = fmt::format( + "Timer with a long (overrunning) callback runs continuously 27 <= {} <= 40", n); + check(msg, n >= 27 && n <= 40); } // 3) Delay: the first callback fires at ~the configured delay, not before. diff --git a/components/timer/include/timer.hpp b/components/timer/include/timer.hpp index ad1ccfad6..d28ca5d1d 100644 --- a/components/timer/include/timer.hpp +++ b/components/timer/include/timer.hpp @@ -165,6 +165,10 @@ class Timer : public BaseComponent { std::chrono::microseconds period_{0}; ///< The period of the timer. If 0, the timer will run once. std::chrono::microseconds delay_{0}; ///< The delay before the timer starts. std::atomic running_{false}; ///< True if the timer is running, false otherwise. + std::chrono::time_point + start_time_; ///< The time point when the timer was started. + std::chrono::time_point + delay_wakeup_time_; ///< The time point when the timer will wake up after the delay if any. std::chrono::time_point wakeup_time_; ///< The time point when the timer will wake up. float period_float; diff --git a/components/timer/src/timer.cpp b/components/timer/src/timer.cpp index 1f8dddf02..c2ef89536 100644 --- a/components/timer/src/timer.cpp +++ b/components/timer/src/timer.cpp @@ -67,21 +67,36 @@ bool Timer::start() { logger_.info("timer is already running, not starting"); return true; } - logger_.info("starting with period {:.3f} s and delay {:.3f} s", period_float, delay_float); + // set the flag here to avoid race condition + running_ = true; + float local_period_float; + float local_delay_float; + std::chrono::time_point local_wakeup_time; + std::chrono::time_point local_start_time; { std::lock_guard lock(mutex_); - wakeup_time_ = std::chrono::steady_clock::now(); + start_time_ = std::chrono::steady_clock::now(); + wakeup_time_ = start_time_; if (delay_float > 0) { wakeup_time_ += delay_; + delay_wakeup_time_ = wakeup_time_; } if (period_float > 0) { wakeup_time_ += period_; } + local_period_float = period_float; + local_delay_float = delay_float; + local_wakeup_time = wakeup_time_; + local_start_time = start_time_; } if (task_->start()) { - running_ = true; + logger_.info("Started with period {:.3f} s and delay {:.3f} s. Will wake up in {:.3f} s", + local_period_float, local_delay_float, + std::chrono::duration(local_wakeup_time - local_start_time).count()); return true; } + // reset the flag if the task failed to start + running_ = false; logger_.error("failed to start timer task"); return false; } @@ -128,7 +143,7 @@ void Timer::set_period(const std::chrono::duration &period) { period_ = std::chrono::duration_cast(period); period_float = std::chrono::duration(period_).count(); } - logger_.info("setting period to {:.3f} s", period_float); + logger_.info("Period set to {:.3f} s", period.count()); } bool Timer::is_running() const { return running_ && task_->is_running(); } @@ -146,75 +161,104 @@ bool Timer::timer_callback_fn(std::mutex &m, std::condition_variable &cv, bool & running_ = false; return true; } + // initial delay, if any - this is only used the first time the timer // runs - if (delay_float > 0) { - auto start_time = std::chrono::steady_clock::now(); - logger_.debug("waiting for delay {:.3f} s", delay_float); - std::unique_lock lock(m); - cv.wait_until(lock, start_time + delay_, [&task_notified] { return task_notified; }); - // reset the task_notified flag - task_notified = false; + float local_delay_float; + { + std::lock_guard lock(mutex_); + local_delay_float = delay_float; + } + if (local_delay_float > 0) { + std::chrono::time_point local_delay_wakeup_time; + { + std::lock_guard lock(mutex_); + local_delay_wakeup_time = delay_wakeup_time_; + } + logger_.debug("waiting for delay {:.3f} s", local_delay_float); + { + std::unique_lock lock(m); + cv.wait_until(lock, local_delay_wakeup_time, [&task_notified] { return task_notified; }); + // reset the task_notified flag + task_notified = false; + } if (!running_) { logger_.debug("delay canceled, stopping"); return true; } // now set the delay to 0 - delay_ = std::chrono::microseconds(0); - delay_float = 0; + { + std::lock_guard lock(mutex_); + delay_ = std::chrono::microseconds(0); + delay_float = 0; + } } + // now run the callback - auto start_time = std::chrono::steady_clock::now(); logger_.debug("running callback"); + auto start_time = std::chrono::steady_clock::now(); bool requested_stop = callback_(); - if (requested_stop || period_float <= 0) { + auto end = std::chrono::steady_clock::now(); + + std::chrono::time_point local_wakeup_time; + std::chrono::microseconds local_period; + float local_period_float; + { + std::lock_guard lock(mutex_); + local_wakeup_time = wakeup_time_; + local_period = period_; + local_period_float = period_float; + } + + if (requested_stop || local_period_float <= 0) { // stop the timer if requested or if the period is <= 0 logger_.debug("callback requested stop or period is <= 0, stopping"); running_ = false; return true; } - auto end = std::chrono::steady_clock::now(); - if (wakeup_time_ < end) { - // if the callback took longer than the period (so it is already past the - // next wakeup time), log a warning and ensure that the next wakeup time is - // the closest multiple of the period after the current + + if (local_wakeup_time <= end) { + // if the callback took longer (or just as long) than the period (so it is + // already past the next wakeup time), log a warning and ensure that the + // next wakeup time is the closest multiple of the period after the current float elapsed = std::chrono::duration(end - start_time).count(); - std::chrono::microseconds period; - float local_period_float; - { - std::lock_guard lock(mutex_); - period = period_; - local_period_float = period_float; - } - if (elapsed > local_period_float) { - logger_.warn_rate_limited("callback took longer ({:.3f} s) than period ({:.3f} s)", elapsed, + if (elapsed >= local_period_float) { + logger_.warn_rate_limited("callback took ~longer ({:.3f} s) than period ({:.3f} s)", elapsed, local_period_float); } - if (period.count() <= 0) { + if (local_period.count() <= 0) { // period changed to oneshot while running; stop after this callback running_ = false; return true; } - { - std::lock_guard lock(mutex_); - while (wakeup_time_ < end) { - wakeup_time_ += period; - } + // update the next wakeup time to the closest multiple of the period after the current time + size_t n = (end - local_wakeup_time) / local_period + 1; + // only log if we are skipping more than one period + if (n > 1) { + logger_.warn("Already passed expected wakeup time, skipping {} periods", n); } + std::lock_guard lock(mutex_); + wakeup_time_ += n * local_period; + // return immediately to execute the next callback iteration return false; } + // now wait for the period (taking into account the time it took to run // the callback) { std::unique_lock lock(m); - cv.wait_until(lock, wakeup_time_, [&task_notified] { return task_notified; }); + cv.wait_until(lock, local_wakeup_time, [&task_notified] { return task_notified; }); // reset the task_notified flag task_notified = false; } + // now that we've waited, make sure the next wakeup time is the next multiple // of the period after the last - std::lock_guard lock(mutex_); - wakeup_time_ += period_; + { + std::lock_guard lock(mutex_); + wakeup_time_ += period_; + } + // keep the timer running return false; } From e824a639edb0f7fbb99a4e3e23d001df2215280d Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Fri, 31 Jul 2026 16:06:37 -0500 Subject: [PATCH 6/8] test(timer): Expand the example test suite for thorough coverage Add pass/fail tests covering: callback time absorbed into the period (fixed-rate scheduling, the key improvement), delay+period anchoring, the start(delay) overload, cancel-then-restart and the stop() alias, set_period() on a running timer, set_period(0) becoming one-shot, negative-delay rejection, null-callback self-stop, and watchdog reporting of an overrunning callback. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../timer/example/main/timer_example.cpp | 233 +++++++++++++++++- 1 file changed, 228 insertions(+), 5 deletions(-) diff --git a/components/timer/example/main/timer_example.cpp b/components/timer/example/main/timer_example.cpp index c1672a860..43db02e78 100644 --- a/components/timer/example/main/timer_example.cpp +++ b/components/timer/example/main/timer_example.cpp @@ -393,17 +393,28 @@ extern "C" void app_main(void) { // 1) Periodicity: a 20 ms periodic timer should fire ~50x/s and stay on a // fixed schedule (the k-th callback lands near k*period, i.e. no - // cumulative drift). + // cumulative drift) with small per-fire jitter. { std::atomic count{0}; std::atomic last_fire{0.0f}; + // worst-case deviation of any fire from its ideal time (k * period). + // Single writer (the timer task), so a plain load/store RMW is race-free. + std::atomic max_dev{0.0f}; + const float period_s = 0.020f; auto t0 = steady_clock::now(); auto timer = espp::Timer({.name = "test-periodicity", .period = 20ms, .callback = - [&count, &last_fire, t0]() { - last_fire = duration(steady_clock::now() - t0).count(); - ++count; + [&count, &last_fire, &max_dev, period_s, t0]() { + const int k = count.fetch_add(1); + const float t = + duration(steady_clock::now() - t0).count(); + last_fire = t; + const float expected = k * period_s; + const float dev = t > expected ? t - expected : expected - t; + if (dev > max_dev.load()) { + max_dev.store(dev); + } return false; }, .log_level = espp::Logger::Verbosity::WARN}); @@ -413,10 +424,16 @@ extern "C" void app_main(void) { // first fires immediately, then every 20 ms -> ~51 in 1 s check("periodic timer fires ~50 times in 1 s (20 ms period)", n >= 45 && n <= 56); // the last callback should land near (n-1)*20 ms if there is no drift - const float expected_last = (n - 1) * 0.020f; + const float expected_last = (n - 1) * period_s; const float actual_last = last_fire.load(); check("periodic timer stays on schedule (no cumulative drift)", n >= 2 && actual_last > expected_last - 0.030f && actual_last < expected_last + 0.030f); + // periodicity is "good enough" if no single fire strays far from its + // ideal slot (bounds worst-case jitter, not just the endpoint). + const float worst_ms = max_dev.load() * 1000.0f; + auto jmsg = fmt::format("periodic timer jitter is small (worst deviation {:.1f} ms < 15 ms)", + worst_ms); + check(jmsg, n >= 2 && max_dev.load() < 0.015f); } // 2) A callback that overruns the period should run back-to-back (bounded by @@ -481,6 +498,7 @@ extern "C" void app_main(void) { .log_level = espp::Logger::Verbosity::WARN}); std::this_thread::sleep_for(400ms); check("one-shot timer (period 0) fires exactly once", count.load() == 1); + check("one-shot timer is not running after it completes", !timer.is_running()); } // 5) start() / is_running() / cancel(). @@ -534,6 +552,211 @@ extern "C" void app_main(void) { check("negative period is clamped (runs once, not repeatedly)", count.load() == 1); } + // 8) Fixed-rate: a callback that does work but finishes within the period + // must not stretch the period (its run time is absorbed). A naive "sleep + // for the period after the callback" would fire at ~1/(period + work). + { + std::atomic count{0}; + auto timer = espp::Timer({.name = "test-absorb", + .period = 40ms, + .callback = + [&count]() { + ++count; + std::this_thread::sleep_for(15ms); // < period + return false; + }, + .log_level = espp::Logger::Verbosity::WARN}); + std::this_thread::sleep_for(1s); + timer.cancel(); + const int n = count.load(); + // 40 ms period with the 15 ms callback absorbed -> ~25 in 1 s (not ~18) + auto msg = + fmt::format("callback shorter than the period does not stretch it: 22 <= {} <= 28", n); + check(msg, n >= 22 && n <= 28); + } + + // 9) Delay + period together: the first callback fires at ~the delay, the + // second one period later. + { + std::atomic count{0}; + std::atomic first_fire{-1.0f}; + std::atomic second_fire{-1.0f}; + auto t0 = steady_clock::now(); + auto timer = espp::Timer({.name = "test-delay-period", + .period = 100ms, + .delay = 200ms, + .callback = + [&count, &first_fire, &second_fire, t0]() { + float t = duration(steady_clock::now() - t0).count(); + int c = count.fetch_add(1); + if (c == 0) { + first_fire = t; + } else if (c == 1) { + second_fire = t; + } + return false; + }, + .log_level = espp::Logger::Verbosity::WARN}); + std::this_thread::sleep_for(500ms); + timer.cancel(); + const float f = first_fire.load(); + const float s = second_fire.load(); + check("delay+period: first fires at ~the delay", f > 0.15f && f < 0.28f); + check("delay+period: second fires ~one period after the first", + s - f > 0.07f && s - f < 0.14f); + } + + // 10) start(delay) overload: starts with the given initial delay. + { + std::atomic count{0}; + std::atomic first_fire{-1.0f}; + auto t0 = steady_clock::now(); + auto timer = espp::Timer({.name = "test-start-delay", + .period = 50ms, + .callback = + [&count, &first_fire, t0]() { + if (count.fetch_add(1) == 0) { + first_fire = + duration(steady_clock::now() - t0).count(); + } + return false; + }, + .auto_start = false, + .log_level = espp::Logger::Verbosity::WARN}); + const bool started = timer.start(200ms); + std::this_thread::sleep_for(400ms); + timer.cancel(); + const float f = first_fire.load(); + check("start(delay) returns true", started); + check("start(delay) delays the first fire", f > 0.15f && f < 0.30f); + } + + // 11) cancel() then start() again resumes; stop() is an alias for cancel(). + { + std::atomic count{0}; + auto timer = espp::Timer({.name = "test-restart", + .period = 40ms, + .callback = + [&count]() { + ++count; + return false; + }, + .log_level = espp::Logger::Verbosity::WARN}); + std::this_thread::sleep_for(150ms); + timer.stop(); // alias for cancel() + const bool stopped = !timer.is_running(); + const int c_paused = count.load(); + std::this_thread::sleep_for(150ms); + const bool no_fire_while_stopped = (count.load() == c_paused); + const bool restarted = timer.start(); + std::this_thread::sleep_for(150ms); + timer.cancel(); + const bool resumed = (count.load() > c_paused); + check("stop() stops the timer (no more callbacks)", stopped && no_fire_while_stopped); + check("start() after cancel resumes the timer", restarted && resumed); + } + + // 12) set_period() changes the rate of a running timer. + { + std::atomic count{0}; + auto timer = espp::Timer({.name = "test-set-period", + .period = 100ms, + .callback = + [&count]() { + ++count; + return false; + }, + .log_level = espp::Logger::Verbosity::WARN}); + std::this_thread::sleep_for(250ms); // ~2-3 fires at 100 ms + timer.set_period(20ms); // speed up + count = 0; + std::this_thread::sleep_for(250ms); // now ~10-12 fires at 20 ms + const int n_fast = count.load(); + auto msg = fmt::format("set_period() speeds up a running timer: {} >= 8", n_fast); + check(msg, n_fast >= 8); + timer.set_period(200ms); // slow down + count = 0; + std::this_thread::sleep_for(300ms); // at most ~2 fires at 200 ms + const int n_slow = count.load(); + timer.cancel(); + auto slow_msg = fmt::format("set_period() slows down a running timer: {} <= 3", n_slow); + check(slow_msg, n_slow <= 3); + } + + // 13) set_period(0) turns a running periodic timer into a one-shot (it stops + // after the current callback). + { + std::atomic count{0}; + auto timer = espp::Timer({.name = "test-set-period-zero", + .period = 50ms, + .callback = + [&count]() { + ++count; + return false; + }, + .log_level = espp::Logger::Verbosity::WARN}); + std::this_thread::sleep_for(120ms); + timer.set_period(0ms); // -> one-shot + std::this_thread::sleep_for(200ms); + const bool stopped = !timer.is_running(); + const int c = count.load(); + std::this_thread::sleep_for(120ms); + check("set_period(0) stops a running timer", stopped && count.load() == c); + } + + // 14) Input validation: start() with a negative delay is rejected. + { + std::atomic count{0}; + auto timer = espp::Timer({.name = "test-neg-delay", + .period = 50ms, + .callback = + [&count]() { + ++count; + return false; + }, + .auto_start = false, + .log_level = espp::Logger::Verbosity::WARN}); + const bool result = timer.start(-100ms); + std::this_thread::sleep_for(150ms); + check("start() with a negative delay is rejected", + !result && count.load() == 0 && !timer.is_running()); + } + + // 15) A timer with a null callback stops itself without crashing. + { + auto timer = espp::Timer({.name = "test-null-callback", + .period = 50ms, + .callback = nullptr, + .log_level = espp::Logger::Verbosity::WARN}); + std::this_thread::sleep_for(150ms); + check("timer with a null callback stops itself (no crash)", !timer.is_running()); + } + + // 16) Watchdog: a timer whose callback overruns the watchdog is reported by + // get_watchdog_info(). + { + espp::Task::configure_task_watchdog(50ms, false); // 50 ms, don't panic + std::atomic count{0}; + auto timer = espp::Timer({.name = "test-watchdog", + .period = 200ms, + .callback = + [&count]() { + ++count; + std::this_thread::sleep_for(120ms); // > 50 ms watchdog + return false; + }, + .log_level = espp::Logger::Verbosity::WARN}); + const bool wd_started = timer.start_watchdog(); + std::this_thread::sleep_for(300ms); + std::error_code ec; + const std::string info = espp::Task::get_watchdog_info(ec); + const bool flagged = !ec && info.find("test-watchdog") != std::string::npos; + timer.stop_watchdog(); + timer.cancel(); + check("start_watchdog() returns true", wd_started); + check("watchdog reports an overrunning timer's task", flagged); + } + // ---- summary ---- fmt::print("\n"); logger.info("======== Timer test suite: {}/{} passed ========", passed, passed + failed); From 190d4c92052046f4996efd313bee005d5c532570 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Fri, 31 Jul 2026 20:11:09 -0500 Subject: [PATCH 7/8] improve and add warnings --- components/timer/include/timer.hpp | 10 +++++++ components/timer/src/timer.cpp | 46 +++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/components/timer/include/timer.hpp b/components/timer/include/timer.hpp index d28ca5d1d..b7f15ae54 100644 --- a/components/timer/include/timer.hpp +++ b/components/timer/include/timer.hpp @@ -161,6 +161,16 @@ class Timer : public BaseComponent { protected: bool timer_callback_fn(std::mutex &m, std::condition_variable &cv, bool &task_notified); + /// @brief Warn if a period/delay is at or near the FreeRTOS tick period. + /// @details On ESP / FreeRTOS the scheduler can only resolve timing to a + /// single tick (1 / CONFIG_FREERTOS_HZ), so a period or delay that + /// is shorter than - or similar to - the tick period cannot be + /// honored accurately. This logs a warning in that case. + /// @param duration The period or delay to check. + /// @param what A short label ("period" or "delay") used in the warning. + /// @note Does nothing off ESP_PLATFORM or when the duration is <= 0. + void warn_if_below_tick_period(const std::chrono::microseconds &duration, const char *what) const; + std::recursive_mutex mutex_; ///< Mutex to protect the timer state. std::chrono::microseconds period_{0}; ///< The period of the timer. If 0, the timer will run once. std::chrono::microseconds delay_{0}; ///< The delay before the timer starts. diff --git a/components/timer/src/timer.cpp b/components/timer/src/timer.cpp index c2ef89536..b638b71d5 100644 --- a/components/timer/src/timer.cpp +++ b/components/timer/src/timer.cpp @@ -1,7 +1,34 @@ #include "timer.hpp" +#if defined(ESP_PLATFORM) +#include +#endif + using namespace espp; +void Timer::warn_if_below_tick_period(const std::chrono::microseconds &duration, + const char *what) const { +#if defined(ESP_PLATFORM) && defined(CONFIG_FREERTOS_HZ) + if (duration.count() <= 0) { + // 0 period means one-shot; 0 delay means no delay - nothing to warn about. + return; + } + // The FreeRTOS scheduler resolves timing to a single tick, so a period/delay + // shorter than - or within ~2 ticks of - the tick period cannot be honored + // accurately (it will be rounded up and/or jitter by up to a full tick). + static constexpr int64_t tick_period_us = 1000000 / CONFIG_FREERTOS_HZ; + if (duration.count() < 2 * tick_period_us) { + logger_.warn("Requested {} of {} us is at or near the FreeRTOS tick period " + "({} us at CONFIG_FREERTOS_HZ={}); timing cannot be honored " + "accurately - use a longer {} or increase CONFIG_FREERTOS_HZ", + what, duration.count(), tick_period_us, CONFIG_FREERTOS_HZ, what); + } +#else + (void)duration; + (void)what; +#endif +} + Timer::Timer(const Timer::Config &config) : BaseComponent(config.name, config.log_level) , period_(std::chrono::duration_cast(config.period)) @@ -24,6 +51,18 @@ Timer::Timer(const Timer::Config &config) }); period_float = std::chrono::duration(period_).count(); delay_float = std::chrono::duration(delay_).count(); + if (period_float < 0) { + logger_.warn("period cannot be negative, setting to 0"); + period_ = std::chrono::microseconds(0); + period_float = 0; + } + if (delay_float < 0) { + logger_.warn("delay cannot be negative, setting to 0"); + delay_ = std::chrono::microseconds(0); + delay_float = 0; + } + warn_if_below_tick_period(period_, "period"); + warn_if_below_tick_period(delay_, "delay"); if (config.auto_start) { start(); } @@ -44,17 +83,19 @@ Timer::Timer(const Timer::AdvancedConfig &config) .log_level = config.log_level, }); period_float = std::chrono::duration(period_).count(); + delay_float = std::chrono::duration(delay_).count(); if (period_float < 0) { logger_.warn("period cannot be negative, setting to 0"); period_ = std::chrono::microseconds(0); period_float = 0; } - delay_float = std::chrono::duration(delay_).count(); if (delay_float < 0) { logger_.warn("delay cannot be negative, setting to 0"); delay_ = std::chrono::microseconds(0); delay_float = 0; } + warn_if_below_tick_period(period_, "period"); + warn_if_below_tick_period(delay_, "delay"); if (config.auto_start) { start(); } @@ -115,6 +156,7 @@ bool Timer::start(const std::chrono::duration &delay) { delay_ = std::chrono::duration_cast(delay); delay_float = std::chrono::duration(delay_).count(); } + warn_if_below_tick_period(std::chrono::duration_cast(delay), "delay"); return start(); } @@ -143,6 +185,8 @@ void Timer::set_period(const std::chrono::duration &period) { period_ = std::chrono::duration_cast(period); period_float = std::chrono::duration(period_).count(); } + warn_if_below_tick_period(std::chrono::duration_cast(period), + "period"); logger_.info("Period set to {:.3f} s", period.count()); } From 34128d91b56132cef48c1c543f3e472e37fd4177 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Fri, 31 Jul 2026 22:03:17 -0500 Subject: [PATCH 8/8] add more tests, address comments, and improve docs --- components/timer/include/timer.hpp | 14 ++++++++++++++ components/timer/src/timer.cpp | 8 +++++--- doc/en/core/timer.rst | 22 ++++++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/components/timer/include/timer.hpp b/components/timer/include/timer.hpp index b7f15ae54..2c436a8be 100644 --- a/components/timer/include/timer.hpp +++ b/components/timer/include/timer.hpp @@ -35,6 +35,20 @@ namespace espp { /// long time, then the timer will not be able to keep up with the /// period. /// +/// @note Timing resolution. On ESP / FreeRTOS the timer waits on the +/// scheduler, which can only resolve time to a single tick +/// (1 / CONFIG_FREERTOS_HZ seconds; e.g. 10 ms at the 100 Hz default, +/// 1 ms at 1000 Hz). A period or delay that is shorter than - or within +/// a couple of ticks of - the tick period cannot be honored accurately: +/// it will be rounded up to a whole number of ticks and can jitter by up +/// to a full tick. The constructor, set_period() and start(delay) log a +/// warning when the requested period/delay is at or near the tick +/// period. For sub-tick or highly accurate periodic work, either raise +/// CONFIG_FREERTOS_HZ or use the esp_timer-based HighResolutionTimer +/// instead. The timer schedules against an absolute wake-up time (the +/// k-th callback targets start + k*period), so it does not accumulate +/// drift even when individual iterations jitter. +/// /// \section timer_ex1 Timer Example 1 /// \snippet timer_example.cpp timer example /// \section timer_ex2 Timer Watchdog Example diff --git a/components/timer/src/timer.cpp b/components/timer/src/timer.cpp index b638b71d5..e8264478e 100644 --- a/components/timer/src/timer.cpp +++ b/components/timer/src/timer.cpp @@ -104,12 +104,14 @@ Timer::Timer(const Timer::AdvancedConfig &config) Timer::~Timer() { cancel(); } bool Timer::start() { - if (is_running()) { + // Atomically claim the start: only one caller can flip running_ from false to + // true. This closes the race where two concurrent start() calls both observe + // "not running" and both go on to start the task. + bool expected = false; + if (!running_.compare_exchange_strong(expected, true)) { logger_.info("timer is already running, not starting"); return true; } - // set the flag here to avoid race condition - running_ = true; float local_period_float; float local_delay_float; std::chrono::time_point local_wakeup_time; diff --git a/doc/en/core/timer.rst b/doc/en/core/timer.rst index 3d2bf7bc5..4a4b2c3b0 100644 --- a/doc/en/core/timer.rst +++ b/doc/en/core/timer.rst @@ -12,6 +12,28 @@ callback is executed. The timer can be configured to run once or repeatedly. The timer API is implemented using the `Task` component, and the timer callback is executed in the context of the timer task. +The timer schedules against an absolute wake-up time (the k-th callback targets +``start + k * period``) rather than sleeping for ``period`` after each callback, +so it does not accumulate drift even when an individual callback runs long or +the scheduler jitters. If a callback overruns the period, subsequent callbacks +run back-to-back to catch up (a rate-limited warning is logged) instead of the +schedule slipping permanently. + +Timing and resolution +^^^^^^^^^^^^^^^^^^^^^^^ + +On ESP / FreeRTOS the timer waits on the scheduler, which can only resolve time +to a single tick (``1 / CONFIG_FREERTOS_HZ`` seconds - e.g. 10 ms at the 100 Hz +default, or 1 ms at 1000 Hz). A period or delay that is shorter than - or within +a couple of ticks of - the tick period cannot be honored accurately: it is +rounded up to a whole number of ticks and can jitter by up to a full tick. The +constructor, :cpp:func:`set_period` and :cpp:func:`start` log a warning when the +requested period/delay is at or near the tick period. + +For sub-tick periods or highly accurate periodic work, either raise +``CONFIG_FREERTOS_HZ`` (menuconfig: ``FreeRTOS`` → ``Tick rate (Hz)``) or use the +esp_timer-based ``HighResolutionTimer`` described below. + Code examples for the task API are provided in the `timer` example folder. .. ------------------------------- Example -------------------------------------