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 000000000..529c237f0 Binary files /dev/null and b/components/interrupt/example/.cache/clangd/index/interrupt_example.cpp.CC43D66CE96D9977.idx differ 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(),