From ea1628e48776b469c69ba7545a29e5a34cb50ed8 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Fri, 26 Jun 2026 09:08:04 -0500 Subject: [PATCH] feat(task/interrupt): Allow runtime changes to task priority/core_id. Expose interrupt task methods for runtime control --- ...interrupt_example.cpp.CC43D66CE96D9977.idx | Bin 0 -> 3476 bytes .../example/main/interrupt_example.cpp | 38 ++++++++- components/interrupt/include/interrupt.hpp | 75 ++++++++++++++++++ components/task/example/main/task_example.cpp | 35 ++++++++ components/task/include/task.hpp | 36 +++++++++ components/task/src/task.cpp | 55 +++++++++++++ 6 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 components/interrupt/example/.cache/clangd/index/interrupt_example.cpp.CC43D66CE96D9977.idx diff --git a/components/interrupt/example/.cache/clangd/index/interrupt_example.cpp.CC43D66CE96D9977.idx b/components/interrupt/example/.cache/clangd/index/interrupt_example.cpp.CC43D66CE96D9977.idx new file mode 100644 index 0000000000000000000000000000000000000000..529c237f0dda9e2b0ce50abb31c6117470fa34c3 GIT binary patch literal 3476 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 literal 0 HcmV?d00001 diff --git a/components/interrupt/example/main/interrupt_example.cpp b/components/interrupt/example/main/interrupt_example.cpp index 71ce21e42..bd11e833e 100644 --- a/components/interrupt/example/main/interrupt_example.cpp +++ b/components/interrupt/example/main/interrupt_example.cpp @@ -139,9 +139,43 @@ extern "C" void app_main(void) { logger.info("Minimum queue size over last 4 seconds: {}", min_queue_size); } + //! [interrupt example] + + //! [interrupt task config example] + // The interrupt handler task is started automatically in the constructor, but + // its priority and core affinity (configured here, or via Kconfig in the BSP + // components) can be changed at runtime. + { + espp::Interrupt interrupt({ + .interrupts = {io0}, + .task_config = + { + .name = "Interrupt task", + .stack_size_bytes = 6192, + .priority = 5, + .core_id = 0, + }, + .log_level = espp::Logger::Verbosity::DEBUG, + }); + + // raise the handler task priority - this is applied to the running task + // immediately (returns true), and is also used if the task is restarted + bool applied = interrupt.set_task_priority(10); + logger.info("Raised interrupt task priority to 10, applied live: {}", applied); + + // changing the core affinity only takes effect when the task is (re)started, + // because the default ESP-IDF FreeRTOS port fixes a task's core at creation + interrupt.set_task_core_id(1); + // so stop and start the task to re-create it pinned to the new core + interrupt.stop_task(); + interrupt.start_task(); + logger.info("Restarted interrupt task on core 1"); + + std::this_thread::sleep_for(2s); + } + //! [interrupt task config example] + #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 2, 0) esp_intr_dump(stdout); #endif // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 2, 0) - - //! [interrupt example] } diff --git a/components/interrupt/include/interrupt.hpp b/components/interrupt/include/interrupt.hpp index 46337d465..8ac4c49b7 100644 --- a/components/interrupt/include/interrupt.hpp +++ b/components/interrupt/include/interrupt.hpp @@ -65,6 +65,8 @@ namespace espp { /// /// \section interrupt_ex0 Interrupt Example /// \snippet interrupt_example.cpp interrupt example +/// \section interrupt_ex1 Interrupt Task Reconfiguration Example +/// \snippet interrupt_example.cpp interrupt task config example class Interrupt : public BaseComponent { public: /// \brief The event for the interrupt @@ -210,6 +212,79 @@ class Interrupt : public BaseComponent { /// with hardware filtering). size_t get_min_queue_size() const { return min_queue_size_; } + /// \brief Start the interrupt handler task + /// \return true if the task was started, false if it was already running or + /// there is no task + /// \details The handler task is started automatically in the constructor, so + /// this is only needed after a matching stop_task() - for example to + /// re-create the task so that a new core id set via set_task_core_id() + /// takes effect (the default ESP-IDF FreeRTOS port fixes a task's + /// core affinity when the task is created). + /// \sa Task::start() + bool start_task() { + if (!task_) { + logger_.error("No task to start"); + return false; + } + return task_->start(); + } + + /// \brief Stop the interrupt handler task + /// \return true if the task was stopped, false if it was not running or there + /// is no task + /// \details While the task is stopped, interrupt events are not serviced, but + /// the GPIO ISRs still run and keep enqueueing events. Those events + /// are not dropped: they remain in the queue and are serviced when + /// start_task() is called - up until the queue fills (event_queue_size + /// entries), after which further events are lost until the task drains + /// it. Pair with start_task() to resume. This is primarily useful to + /// apply a new core id from set_task_core_id() by stopping and + /// restarting the task. + /// \sa Task::stop() + bool stop_task() { + if (!task_) { + logger_.error("No task to stop"); + return false; + } + return task_->stop(); + } + + /// \brief Set the priority of the interrupt handler task + /// \param priority New FreeRTOS priority (0 is lowest priority) + /// \return true if the change was applied to the running task, false otherwise + /// \details Forwards to Task::set_priority(). The new priority is always + /// stored and used the next time the task is started; if the task is + /// already running it is also applied immediately. This lets callers + /// override the priority configured at construction (e.g. via Kconfig + /// in the BSP components) at runtime. + /// \sa Task::set_priority() + bool set_task_priority(size_t priority) { + if (!task_) { + logger_.error("No task to set priority on"); + return false; + } + return task_->set_priority(priority); + } + + /// \brief Set the core affinity (core ID) of the interrupt handler task + /// \param core_id Core to pin the task to (0 or 1), or -1 to leave it unpinned + /// \return true if the change was applied to the running task, false otherwise + /// \details Forwards to Task::set_core_id(). The new core ID is always stored + /// and used the next time the task is started. On the default + /// ESP-IDF FreeRTOS port a running task's core affinity cannot be + /// changed, so for an already-started handler this only takes effect + /// after the task is restarted - call stop_task() then start_task() + /// to apply it. This lets callers override the core configured at + /// construction (e.g. via Kconfig in the BSP components). + /// \sa Task::set_core_id() + bool set_task_core_id(int core_id) { + if (!task_) { + logger_.error("No task to set core id on"); + return false; + } + return task_->set_core_id(core_id); + } + /// \brief Add an interrupt to the interrupt handler /// \param interrupt The interrupt to add void add_interrupt(const PinConfig &interrupt) { diff --git a/components/task/example/main/task_example.cpp b/components/task/example/main/task_example.cpp index 0366516be..62fde1248 100644 --- a/components/task/example/main/task_example.cpp +++ b/components/task/example/main/task_example.cpp @@ -469,6 +469,41 @@ extern "C" void app_main(void) { test_duration = std::chrono::duration(test_end - test_start).count(); logger.debug("Test ran for {:.03f} seconds", test_duration); + /** + * Show an example of changing a task's priority and core affinity at runtime. + */ + { + logger.info("Task priority / core affinity example"); + //! [Task Priority and Core example] + auto task_fn = [](std::mutex &m, std::condition_variable &cv) { + std::unique_lock lk(m); + cv.wait_for(lk, 100ms); + return false; // keep running + }; + auto task = espp::Task({.callback = task_fn, + .task_config = {.name = "Reconfig Task", .priority = 5, .core_id = 0}, + .log_level = espp::Logger::Verbosity::DEBUG}); + task.start(); + fmt::println("Task started on core {} at priority {}", espp::Task::get_core_id(task), + espp::Task::get_priority(task)); + + // priority changes apply to the running task immediately + bool applied = task.set_priority(10); + fmt::println("set_priority(10) applied live: {}, priority now {}", applied, + espp::Task::get_priority(task)); + + // core affinity changes are stored but only take effect when the task is + // (re)started on the default ESP-IDF FreeRTOS port (a task's core is fixed + // when it is created), so stop() then start() to re-create it on core 1 + task.set_core_id(1); + task.stop(); + task.start(); + fmt::println("After restart, task running on core {}", espp::Task::get_core_id(task)); + + std::this_thread::sleep_for(500ms); + //! [Task Priority and Core example] + } + /** * Show an example of a task which is stopped by multiple other tasks * (ensuring that stop can be called multiple times from multiple other diff --git a/components/task/include/task.hpp b/components/task/include/task.hpp index 333e7721e..2eb02bf3a 100644 --- a/components/task/include/task.hpp +++ b/components/task/include/task.hpp @@ -43,6 +43,8 @@ namespace espp { * \snippet task_example.cpp Task Info example * \section task_ex7 Task Request Stop Example * \snippet task_example.cpp Task Request Stop example + * \section task_ex8 Task Priority and Core Affinity Example + * \snippet task_example.cpp Task Priority and Core example * * \section run_on_core_ex1 Run on Core Example * \snippet task_example.cpp run on core example @@ -270,6 +272,40 @@ class Task : public espp::BaseComponent { */ bool is_running() const; + /** + * @brief Set the priority of the task. + * @details The new priority is always stored in the task's configuration, so + * it will be used the next time the task is started. If the task is + * currently running (ESP only), the change is also applied to the + * live task immediately via vTaskPrioritySet(). + * @param priority New FreeRTOS priority (0 is lowest priority on ESP / + * FreeRTOS). It is clamped to [0, configMAX_PRIORITIES - 1] on ESP. + * @return true if the change was applied to the currently-running task; false + * if the task is not running (the new value still takes effect the + * next time the task is started) or the platform does not support + * changing a live task's priority. + */ + bool set_priority(size_t priority); + + /** + * @brief Set the core affinity (core ID) of the task. + * @details The new core ID is always stored in the task's configuration, so + * it will be used the next time the task is started. On the default + * ESP-IDF FreeRTOS port a running task's core affinity cannot be + * changed (it is fixed when the task is created), so for an + * already-started task this only takes effect after a stop()/start(). + * If the underlying FreeRTOS build does provide a runtime + * core-affinity API (configUSE_CORE_AFFINITY on a multi-core SMP + * build), the change is applied to the live task immediately. + * @param core_id Core to pin the task to (0 or 1), or -1 to leave the task + * unpinned (able to run on any core). + * @return true if the change was applied to the currently-running task; false + * if the task is not running or the platform cannot change a live + * task's core affinity (in which case the new value still takes + * effect the next time the task is started). + */ + bool set_core_id(int core_id); + #if defined(ESP_PLATFORM) || defined(_DOXYGEN_) /** * @brief Start the task watchdog for this task. diff --git a/components/task/src/task.cpp b/components/task/src/task.cpp index 98b33abea..e85595737 100644 --- a/components/task/src/task.cpp +++ b/components/task/src/task.cpp @@ -219,6 +219,61 @@ bool Task::is_started() const { return started_; } bool Task::is_running() const { return is_started(); } +bool Task::set_priority(size_t priority) { +#if defined(ESP_PLATFORM) + // clamp to the valid FreeRTOS priority range + if (priority >= configMAX_PRIORITIES) { + logger_.warn("Requested priority ({}) >= configMAX_PRIORITIES ({}), clamping", priority, + configMAX_PRIORITIES); + priority = configMAX_PRIORITIES - 1; + } +#endif + // always store the new priority so it is used on the next start() + config_.priority = priority; + logger_.debug("Set priority to {} for task '{}'", priority, name_); +#if defined(ESP_PLATFORM) + // if the task is running, apply the change to the live task as well + auto handle = static_cast(get_id()); + if (started_ && handle != nullptr) { + vTaskPrioritySet(handle, static_cast(priority)); + return true; + } +#endif + return false; +} + +bool Task::set_core_id(int core_id) { + // always store the new core id so it is used on the next start() + config_.core_id = core_id; + logger_.debug("Set core id to {} for task '{}'", core_id, name_); +#if defined(ESP_PLATFORM) && defined(configUSE_CORE_AFFINITY) && (configUSE_CORE_AFFINITY == 1) && \ + (configNUMBER_OF_CORES > 1) + // this FreeRTOS build supports changing a live task's core affinity + auto handle = static_cast(get_id()); + if (started_ && handle != nullptr) { + UBaseType_t affinity_mask; + if (core_id < 0) { + // not pinned: allow the task to run on any core + affinity_mask = (1u << configNUMBER_OF_CORES) - 1u; + } else { + affinity_mask = 1u << core_id; + } + vTaskCoreAffinitySet(handle, affinity_mask); + return true; + } + return false; +#else + // the default ESP-IDF FreeRTOS port fixes core affinity at task creation, so + // the new core id only takes effect the next time the task is started + if (started_) { + logger_.warn("Cannot change core affinity of running task '{}'; new core id ({}) will take " + "effect on next start()", + name_, core_id); + } + return false; +#endif +} + #if defined(ESP_PLATFORM) || defined(_DOXYGEN_) std::string Task::get_info() { return fmt::format("[T] '{}',{},{},{}", pcTaskGetName(nullptr), xPortGetCoreID(),