From 3b212b9944f4b0d4a8078b6f9e7c1548bcac173e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Apr 2026 04:47:52 -0500 Subject: [PATCH] [core] Add permalink to BDK sctrl_dpll_delay200us in WDT interval comment So future readers can verify the errata claim without chasing the PR. --- esphome/core/application.h | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index 3af02a1861f..77f6b1c74d8 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -226,11 +226,13 @@ class Application { /// - BK72xx HW WDT (10 s): ~5x <-- platform override below #ifdef USE_BK72XX /// BK72xx silicon requires a ~200 µs busy-wait between two watchdog register - /// key writes on every reload (sctrl_dpll_delay200us() in BDK wdt.c), making - /// each arch_feed_wdt() ~300 µs. LibreTiny initialises the HW WDT at 10 s, - /// so 2000 ms keeps a 5x safety margin — matching the ESP8266 ratio that - /// motivated the generic 300 ms value — while cutting feed frequency ~6x - /// and recovering ~50 ms/min of main-loop overhead on typical configs. + /// key writes on every reload, making each arch_feed_wdt() ~300 µs. The + /// sctrl_dpll_delay200us() call lives in BDK's wdt_ctrl (WCMD_RELOAD_PERIOD): + /// https://github.com/libretiny-eu/framework-beken-bdk/blob/44800e7451ea30fbcbd3bb6e905315de59349fee/beken378/driver/wdt/wdt.c#L75-L87 + /// LibreTiny initialises the HW WDT at 10 s, so 2000 ms keeps a 5x safety + /// margin — matching the ESP8266 ratio that motivated the generic 300 ms + /// value — while cutting feed frequency ~6x and recovering ~50 ms/min of + /// main-loop overhead on typical configs. static constexpr uint32_t WDT_FEED_INTERVAL_MS = 2000; #else static constexpr uint32_t WDT_FEED_INTERVAL_MS = 300; @@ -474,6 +476,11 @@ class Application { // millis() of most recent status_led dispatch; rate-limits independently of last_wdt_feed_ uint32_t last_status_led_service_{0}; #endif +#ifdef USE_RUNTIME_STATS + // Count of feed_wdt_slow_() invocations since last runtime_stats log. Lets + // stats output report how often the slow path (arch_feed_wdt) actually runs. + uint32_t wdt_slow_count_{0}; +#endif // 2-byte members (grouped together for alignment) uint16_t dump_config_at_{std::numeric_limits::max()}; // Index into components_ for dump_config progress @@ -572,6 +579,12 @@ inline void ESPHOME_ALWAYS_INLINE __attribute__((optimize("O2"))) Application::l // so the gate check and WDT feed both reflect actual elapsed time after // scheduler dispatch, without an extra millis() call. uint32_t now = this->scheduler_tick_(MillisInternal::get()); +#ifdef USE_RUNTIME_STATS + // Capture immediately after scheduler_tick_ returns so the post-scheduler + // feed_wdt_with_time slice can be separated from the scheduler slice in + // the `before` bucket. + uint32_t loop_after_sched_us = micros(); +#endif // Guarantee one WDT feed per tick even when the scheduler had nothing to // dispatch and the component phase is gated out — covers configs with no // looping components and no scheduler work (setup() has its own @@ -643,10 +656,22 @@ inline void ESPHOME_ALWAYS_INLINE __attribute__((optimize("O2"))) Application::l uint32_t loop_before_overhead_us = loop_before_wall_us > loop_before_scheduled_us ? loop_before_wall_us - static_cast(loop_before_scheduled_us) : 0; + // Sub-split of the `before` bucket. `sched_us` covers MillisInternal::get() + // plus scheduler_tick_ (everything before the post-scheduler feed_wdt_with_time), + // with scheduled-component time subtracted out so it reflects scheduler + // infrastructure overhead only. `wdt_us` covers the post-scheduler + // feed_wdt_with_time() call (including any status_led dispatch when the + // STATUS_LED_DISPATCH_INTERVAL_MS gate also elapses on the same call). + uint32_t loop_before_sched_wall_us = loop_after_sched_us - loop_active_start_us; + uint32_t loop_before_sched_us = loop_before_sched_wall_us > loop_before_scheduled_us + ? loop_before_sched_wall_us - static_cast(loop_before_scheduled_us) + : 0; + uint32_t loop_before_wdt_us = loop_before_end_us - loop_after_sched_us; // tail_us is only defined when Phase B ran; 0 on Phase A-only ticks so the // stats bucket keeps its "component-phase trailing overhead" meaning. uint32_t loop_tail_us = do_component_phase ? (loop_now_us - loop_tail_start_us) : 0; - global_runtime_stats->record_loop_active(loop_now_us - loop_active_start_us, loop_before_overhead_us, loop_tail_us); + global_runtime_stats->record_loop_active(loop_now_us - loop_active_start_us, loop_before_overhead_us, + loop_before_sched_us, loop_before_wdt_us, loop_tail_us); global_runtime_stats->process_pending_stats(now); } #endif