From bef4c8a86c275c0c64bfe230c96a77fe5dd87835 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sat, 11 Apr 2026 17:36:27 -0400 Subject: [PATCH 1/5] [cc1101] Extract chip configuration into configure() method (#15635) --- esphome/components/cc1101/cc1101.cpp | 43 +++++++++++++++++----------- esphome/components/cc1101/cc1101.h | 1 + 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/esphome/components/cc1101/cc1101.cpp b/esphome/components/cc1101/cc1101.cpp index c231f314cc..ea0138e1dd 100644 --- a/esphome/components/cc1101/cc1101.cpp +++ b/esphome/components/cc1101/cc1101.cpp @@ -106,6 +106,30 @@ void IRAM_ATTR CC1101Component::gpio_intr(CC1101Component *arg) { arg->enable_lo void CC1101Component::setup() { this->spi_setup(); + + if (this->gdo0_pin_ != nullptr) { + this->gdo0_pin_->setup(); + } + + this->configure(); + if (this->is_failed()) { + return; + } + + // Defer pin mode setup until after all components have completed setup() + // This handles the case where remote_transmitter runs after CC1101 and changes pin mode + if (this->gdo0_pin_ != nullptr) { + this->defer([this]() { + this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT); + if (this->state_.PKT_FORMAT == static_cast(PacketFormat::PACKET_FORMAT_FIFO)) { + this->gdo0_pin_->attach_interrupt(&CC1101Component::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE); + } + }); + } +} + +void CC1101Component::configure() { + // Manual reset sequence per CC1101 datasheet section 19.1.2 this->cs_->digital_write(true); delayMicroseconds(1); this->cs_->digital_write(false); @@ -128,11 +152,6 @@ void CC1101Component::setup() { return; } - // Setup GDO0 pin if configured - if (this->gdo0_pin_ != nullptr) { - this->gdo0_pin_->setup(); - } - this->initialized_ = true; for (uint8_t i = 0; i <= static_cast(Register::TEST0); i++) { @@ -142,21 +161,11 @@ void CC1101Component::setup() { this->write_(static_cast(i)); } this->set_output_power(this->output_power_requested_); + if (!this->enter_rx_()) { this->mark_failed(); return; } - - // Defer pin mode setup until after all components have completed setup() - // This handles the case where remote_transmitter runs after CC1101 and changes pin mode - if (this->gdo0_pin_ != nullptr) { - this->defer([this]() { - this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT); - if (this->state_.PKT_FORMAT == static_cast(PacketFormat::PACKET_FORMAT_FIFO)) { - this->gdo0_pin_->attach_interrupt(&CC1101Component::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE); - } - }); - } } void CC1101Component::call_listeners_(const std::vector &packet, float freq_offset, float rssi, uint8_t lqi) { @@ -273,7 +282,7 @@ void CC1101Component::begin_rx() { void CC1101Component::reset() { this->strobe_(Command::RES); - this->setup(); + this->configure(); } void CC1101Component::set_idle() { diff --git a/esphome/components/cc1101/cc1101.h b/esphome/components/cc1101/cc1101.h index 68d81ac8f3..000a13d586 100644 --- a/esphome/components/cc1101/cc1101.h +++ b/esphome/components/cc1101/cc1101.h @@ -25,6 +25,7 @@ class CC1101Component : public Component, void setup() override; void loop() override; void dump_config() override; + void configure(); // Actions void begin_tx(); From ddbf6f2347119006963a261f0d307f45ac0df658 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Apr 2026 14:32:49 -1000 Subject: [PATCH 2/5] [core] Inline feed_wdt hot path with out-of-line slow path Split Application::feed_wdt() into an ALWAYS_INLINE wrapper that checks the 3ms rate limit against last_wdt_feed_ and a feed_wdt_slow_() callee that performs the actual arch_feed_wdt() + status LED re-dispatch. Callers on the hot path (loop_task before/after each component) that already have a millis() timestamp in hand now pay only a load + sub + branch on the no-op path instead of a full call8 / entry / retw. Moves the rate-limit state from a function-local static to a class member (last_wdt_feed_) so the inline can access it. --- esphome/core/application.cpp | 11 ++++++----- esphome/core/application.h | 20 ++++++++++++++++++-- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index cd75859880..d1aa461d0e 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -196,14 +196,15 @@ void Application::process_dump_config_() { this->dump_config_at_++; } -void HOT Application::feed_wdt(uint32_t time) { - static uint32_t last_feed = 0; +void HOT Application::feed_wdt_slow_(uint32_t time) { // Use provided time if available, otherwise get current time uint32_t now = time ? time : millis(); - // Compare in milliseconds (3ms threshold) - if (now - last_feed > 3) { + // Compare in milliseconds (3ms threshold). The inline wrapper already + // performs this check when time != 0; repeat it here for the time == 0 + // entry and as a safety net. + if (now - this->last_wdt_feed_ > 3) { arch_feed_wdt(); - last_feed = now; + this->last_wdt_feed_ = now; #ifdef USE_STATUS_LED if (status_led::global_status_led != nullptr) { status_led::global_status_led->call(); diff --git a/esphome/core/application.h b/esphome/core/application.h index 6b2969b490..923cbfdb03 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -385,7 +385,19 @@ class Application { void schedule_dump_config() { this->dump_config_at_ = 0; } - void feed_wdt(uint32_t time = 0); + /// Feed the task watchdog. Hot-path inline rate-limit check: callers that + /// already have a timestamp in hand pay only a load + sub + branch on the + /// common (no-op) path. The actual arch feed + status LED update live in + /// feed_wdt_slow_ to keep this small enough to inline freely. + /// + /// Pass time==0 to request millis() be read for you (low-frequency callers + /// only — always takes the slow path). + void ESPHOME_ALWAYS_INLINE feed_wdt(uint32_t time = 0) { + if (time != 0 && static_cast(time - this->last_wdt_feed_) <= 3) { + return; + } + this->feed_wdt_slow_(time); + } void reboot(); @@ -615,7 +627,10 @@ class Application { /// Caller must ensure dump_config_at_ < components_.size(). void __attribute__((noinline)) process_dump_config_(); - void feed_wdt_arch_(); + /// Slow path for feed_wdt(): actually calls arch_feed_wdt(), updates + /// last_wdt_feed_, and re-dispatches the status LED. Out of line so the + /// inline wrapper stays tiny. + void feed_wdt_slow_(uint32_t time); /// Perform a delay while also monitoring socket file descriptors for readiness #ifdef USE_HOST @@ -669,6 +684,7 @@ class Application { // 4-byte members uint32_t last_loop_{0}; uint32_t loop_component_start_time_{0}; + uint32_t last_wdt_feed_{0}; // millis() of most recent arch_feed_wdt(); rate-limits feed_wdt() hot path #ifdef USE_HOST int max_fd_{-1}; // Highest file descriptor number for select() From a70ec9ec06931956d8ea8279e8820b147cebfdf8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Apr 2026 14:51:59 -1000 Subject: [PATCH 3/5] [scheduler] Feed watchdog after each scheduled item, drop top-of-loop feed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The main loop used to feed the watchdog unconditionally right after Scheduler::call() returned, regardless of whether the scheduler had any actual work to do. On an idle device this meant every outer loop iteration paid the inline rate-limit check (load + sub + branch) for no benefit. Move the feed into Scheduler::execute_item_() so it fires only after a scheduled callback actually runs, and covers both the main heap path and the defer queue path (both go through execute_item_). This also bounds the max feed gap during a burst of back-to-back scheduled items by max(item_runtime) instead of sum(item_runtime). The top-of-loop feed in Application::before_loop_tasks_() is now unnecessary — when Scheduler::call does no work, the only elapsed time is the sleep wake plus a few instructions, and when it does have work, it fed the wdt as it went. --- esphome/core/application.h | 9 +++++---- esphome/core/scheduler.cpp | 8 +++++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index 923cbfdb03..f8e9b0e360 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -829,12 +829,13 @@ inline void ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_ this->drain_wake_notifications_(); #endif - // Process scheduled tasks + // Process scheduled tasks. Scheduler::call now feeds the watchdog itself + // after each scheduled item that actually runs, so we no longer need an + // unconditional feed here — when Scheduler::call has no work to do, the + // only elapsed time is a sleep wake + a few instructions, and when it does + // have work, it fed the wdt as it went. this->scheduler.call(loop_start_time); - // Feed the watchdog timer - this->feed_wdt(loop_start_time); - // Process any pending enable_loop requests from ISRs // This must be done before marking in_loop_ = true to avoid race conditions if (this->has_pending_enable_loop_requests_) { diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index dff50b03ef..7c9263583d 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -739,7 +739,13 @@ uint32_t HOT Scheduler::execute_item_(SchedulerItem *item, uint32_t now) { App.set_current_component(item->component); WarnIfComponentBlockingGuard guard{item->component, now}; item->callback(); - return guard.finish(); + uint32_t end = guard.finish(); + // Feed the watchdog after each scheduled item (both main heap and defer + // queue paths go through here). A run of back-to-back callbacks cannot + // starve the wdt. The inline fast path is a load + sub + branch — nearly + // free when the 3 ms rate limit hasn't elapsed. + App.feed_wdt(end); + return end; } // Common implementation for cancel operations - handles locking From 715f0ca6f7c7d1dec65d4fd2e2670c4122c8e141 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Apr 2026 14:54:02 -1000 Subject: [PATCH 4/5] [core] Extract WDT_FEED_INTERVAL_MS constexpr Replace the magic 3 in both the inline feed_wdt check and the slow path with a named constexpr so the rate-limit threshold is defined once. --- esphome/core/application.cpp | 7 +++---- esphome/core/application.h | 7 ++++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index d1aa461d0e..84a1fc346f 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -199,10 +199,9 @@ void Application::process_dump_config_() { void HOT Application::feed_wdt_slow_(uint32_t time) { // Use provided time if available, otherwise get current time uint32_t now = time ? time : millis(); - // Compare in milliseconds (3ms threshold). The inline wrapper already - // performs this check when time != 0; repeat it here for the time == 0 - // entry and as a safety net. - if (now - this->last_wdt_feed_ > 3) { + // The inline wrapper already performs this check when time != 0; + // repeat it here for the time == 0 entry and as a safety net. + if (now - this->last_wdt_feed_ > WDT_FEED_INTERVAL_MS) { arch_feed_wdt(); this->last_wdt_feed_ = now; #ifdef USE_STATUS_LED diff --git a/esphome/core/application.h b/esphome/core/application.h index f8e9b0e360..7cde0bf85d 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -385,6 +385,11 @@ class Application { void schedule_dump_config() { this->dump_config_at_ = 0; } + /// Minimum interval between real arch_feed_wdt() calls. Chosen to keep the + /// rate of HAL pokes low while still being small enough that any plausible + /// watchdog timeout (seconds) has orders of magnitude of safety margin. + static constexpr uint32_t WDT_FEED_INTERVAL_MS = 3; + /// Feed the task watchdog. Hot-path inline rate-limit check: callers that /// already have a timestamp in hand pay only a load + sub + branch on the /// common (no-op) path. The actual arch feed + status LED update live in @@ -393,7 +398,7 @@ class Application { /// Pass time==0 to request millis() be read for you (low-frequency callers /// only — always takes the slow path). void ESPHOME_ALWAYS_INLINE feed_wdt(uint32_t time = 0) { - if (time != 0 && static_cast(time - this->last_wdt_feed_) <= 3) { + if (time != 0 && static_cast(time - this->last_wdt_feed_) <= WDT_FEED_INTERVAL_MS) { return; } this->feed_wdt_slow_(time); From dc5626eb855829ad8f54fa9a1e9908831f65eed3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Apr 2026 14:57:01 -1000 Subject: [PATCH 5/5] [core] Invert feed_wdt condition so slow-path call is inside the if MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cleaner than the early-return form — the action (calling feed_wdt_slow_) reads as the body of the conditional instead of falling through past a guard clause. Logically identical and compiles to the same code. --- esphome/core/application.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index 7cde0bf85d..02af5664a4 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -398,10 +398,9 @@ class Application { /// Pass time==0 to request millis() be read for you (low-frequency callers /// only — always takes the slow path). void ESPHOME_ALWAYS_INLINE feed_wdt(uint32_t time = 0) { - if (time != 0 && static_cast(time - this->last_wdt_feed_) <= WDT_FEED_INTERVAL_MS) { - return; + if (time == 0 || static_cast(time - this->last_wdt_feed_) > WDT_FEED_INTERVAL_MS) { + this->feed_wdt_slow_(time); } - this->feed_wdt_slow_(time); } void reboot();