[esp32] Use xTaskGetTickCount() for millis() when tick rate is 1kHz (#15661)

This commit is contained in:
J. Nick Koston
2026-04-22 06:44:53 +02:00
committed by GitHub
parent a3b49d1ed9
commit 23ad30cb4c
6 changed files with 80 additions and 14 deletions
+20 -1
View File
@@ -23,7 +23,26 @@ extern "C" __attribute__((weak)) void initArduino() {}
namespace esphome { namespace esphome {
void HOT yield() { vPortYield(); } void HOT yield() { vPortYield(); }
uint32_t IRAM_ATTR HOT millis() { return micros_to_millis(static_cast<uint64_t>(esp_timer_get_time())); } // Use xTaskGetTickCount() when tick rate is 1 kHz (ESPHome's default via sdkconfig),
// falling back to esp_timer for non-standard rates. IRAM_ATTR is required because
// Wiegand and ZyAura call millis() from IRAM_ATTR ISR handlers on ESP32.
// xTaskGetTickCountFromISR() is used in ISR context to satisfy the FreeRTOS API contract.
uint32_t IRAM_ATTR HOT millis() {
#if CONFIG_FREERTOS_HZ == 1000
if (xPortInIsrContext()) [[unlikely]] {
return xTaskGetTickCountFromISR();
}
return xTaskGetTickCount();
#else
return micros_to_millis(static_cast<uint64_t>(esp_timer_get_time()));
#endif
}
// millis_64() stays on esp_timer — a different clock from xTaskGetTickCount(). This is
// safe because the two are never cross-compared: millis() values are only used for
// millis()-vs-millis() deltas (feed_wdt, warn_blocking, component start time), while
// millis_64() is used by the Scheduler and uptime sensors. On ESP32 (USE_NATIVE_64BIT_TIME),
// Scheduler::millis_64_from_(now) discards the 32-bit now and calls millis_64() directly,
// so the Scheduler is internally consistent on the esp_timer clock.
uint64_t HOT millis_64() { return micros_to_millis<uint64_t>(static_cast<uint64_t>(esp_timer_get_time())); } uint64_t HOT millis_64() { return micros_to_millis<uint64_t>(static_cast<uint64_t>(esp_timer_get_time())); }
void HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } void HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); }
uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); } uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); }
+7 -9
View File
@@ -78,7 +78,7 @@ void Application::setup() {
Component *component = this->components_[i]; Component *component = this->components_[i];
// Update loop_component_start_time_ before calling each component during setup // Update loop_component_start_time_ before calling each component during setup
this->loop_component_start_time_ = millis(); this->loop_component_start_time_ = MillisInternal::get();
component->call(); component->call();
this->scheduler.process_to_add(); this->scheduler.process_to_add();
this->feed_wdt(); this->feed_wdt();
@@ -91,17 +91,15 @@ void Application::setup() {
this->app_state_ |= STATUS_LED_WARNING; this->app_state_ |= STATUS_LED_WARNING;
do { do {
uint32_t now = millis();
// Service scheduler and process pending loop enables to handle GPIO // Service scheduler and process pending loop enables to handle GPIO
// interrupts during setup. During setup we always run the component // interrupts during setup. During setup we always run the component
// phase (no loop_interval_ gate), so call both helpers unconditionally. // phase (no loop_interval_ gate), so call both helpers unconditionally.
this->scheduler_tick_(now); this->scheduler_tick_(MillisInternal::get());
this->before_component_phase_(); this->before_component_phase_();
for (uint32_t j = 0; j <= i; j++) { for (uint32_t j = 0; j <= i; j++) {
// Update loop_component_start_time_ right before calling each component // Update loop_component_start_time_ right before calling each component
this->loop_component_start_time_ = millis(); this->loop_component_start_time_ = MillisInternal::get();
this->components_[j]->call(); this->components_[j]->call();
this->feed_wdt(); this->feed_wdt();
} }
@@ -215,7 +213,7 @@ void Application::process_dump_config_() {
void Application::feed_wdt() { void Application::feed_wdt() {
// Cold entry: callers without a millis() timestamp in hand. Fetches the // Cold entry: callers without a millis() timestamp in hand. Fetches the
// time and takes the same rate-limit paths as feed_wdt_with_time(). // time and takes the same rate-limit paths as feed_wdt_with_time().
uint32_t now = millis(); uint32_t now = MillisInternal::get();
if (now - this->last_wdt_feed_ > WDT_FEED_INTERVAL_MS) { if (now - this->last_wdt_feed_ > WDT_FEED_INTERVAL_MS) {
this->feed_wdt_slow_(now); this->feed_wdt_slow_(now);
} }
@@ -305,7 +303,7 @@ void Application::run_powerdown_hooks() {
} }
void Application::teardown_components(uint32_t timeout_ms) { void Application::teardown_components(uint32_t timeout_ms) {
uint32_t start_time = millis(); uint32_t start_time = MillisInternal::get();
// Use a StaticVector instead of std::vector to avoid heap allocation // Use a StaticVector instead of std::vector to avoid heap allocation
// since we know the actual size at compile time // since we know the actual size at compile time
@@ -384,7 +382,7 @@ void Application::teardown_components(uint32_t timeout_ms) {
} }
// Update time for next iteration // Update time for next iteration
now = millis(); now = MillisInternal::get();
} }
if (pending_count > 0) { if (pending_count > 0) {
@@ -427,7 +425,7 @@ void Application::disable_component_loop_(Component *component) {
// This prevents integer underflow in timing calculations by ensuring // This prevents integer underflow in timing calculations by ensuring
// the swapped component starts with a fresh timing reference, avoiding // the swapped component starts with a fresh timing reference, avoiding
// errors caused by stale or wrapped timing values. // errors caused by stale or wrapped timing values.
this->loop_component_start_time_ = millis(); this->loop_component_start_time_ = MillisInternal::get();
} }
} }
return; return;
+1 -1
View File
@@ -637,7 +637,7 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() {
// (advanced by its per-item feeds) or `now` unchanged. We adopt it as `now` // (advanced by its per-item feeds) or `now` unchanged. We adopt it as `now`
// so the gate check and WDT feed both reflect actual elapsed time after // so the gate check and WDT feed both reflect actual elapsed time after
// scheduler dispatch, without an extra millis() call. // scheduler dispatch, without an extra millis() call.
uint32_t now = this->scheduler_tick_(millis()); uint32_t now = this->scheduler_tick_(MillisInternal::get());
// Guarantee one WDT feed per tick even when the scheduler had nothing to // 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 // dispatch and the component phase is gated out — covers configs with no
// looping components and no scheduler work (setup() has its own // looping components and no scheduler work (setup() has its own
+2 -1
View File
@@ -9,6 +9,7 @@
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/millis_internal.h"
#include "esphome/core/optional.h" #include "esphome/core/optional.h"
// Forward declarations for friend access from codegen-generated setup() // Forward declarations for friend access from codegen-generated setup()
@@ -656,7 +657,7 @@ class WarnIfComponentBlockingGuard {
#ifdef USE_RUNTIME_STATS #ifdef USE_RUNTIME_STATS
this->component_->runtime_stats_.record_time(micros() - this->started_us_); this->component_->runtime_stats_.record_time(micros() - this->started_us_);
#endif #endif
uint32_t curr_time = millis(); uint32_t curr_time = MillisInternal::get();
#ifndef USE_BENCHMARK #ifndef USE_BENCHMARK
// Fast path: compare against constant threshold in ms (computed at compile time from centiseconds) // Fast path: compare against constant threshold in ms (computed at compile time from centiseconds)
static constexpr uint32_t WARN_IF_BLOCKING_OVER_MS = static_cast<uint32_t>(WARN_IF_BLOCKING_OVER_CS) * 10U; static constexpr uint32_t WARN_IF_BLOCKING_OVER_MS = static_cast<uint32_t>(WARN_IF_BLOCKING_OVER_CS) * 10U;
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#if defined(USE_ESP32)
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <sdkconfig.h>
#endif
namespace esphome {
// Friend-gated accessor for a fast millis() variant intended only for
// known task-context callers on the main loop hot path (Application::loop()
// and WarnIfComponentBlockingGuard::finish()). It skips the ISR-context
// dispatch that the public esphome::millis() pays on ESP32.
//
// MUST NOT be called from ISR context: on ESP32 it calls the non-FromISR
// FreeRTOS API directly, which is undefined behavior in ISR context.
//
// Adding new callers requires adding a friend declaration here — that
// is the review point. Do not relax the access (e.g. by making get()
// public) without considering the ISR-safety contract.
//
// Other platforms currently delegate to the public millis(); the friend
// gate still enforces the intent so platform-specific fast paths can be
// added later without changing call sites.
class MillisInternal {
private:
static ESPHOME_ALWAYS_INLINE uint32_t get() {
#if defined(USE_ESP32) && CONFIG_FREERTOS_HZ == 1000
return xTaskGetTickCount();
#else
return millis();
#endif
}
friend class Application;
friend class WarnIfComponentBlockingGuard;
};
} // namespace esphome
+8 -2
View File
@@ -285,8 +285,14 @@ class Scheduler {
bool cancel_retry_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id); bool cancel_retry_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id);
// Extend a 32-bit millis() value to 64-bit. Use when the caller already has a fresh now. // Extend a 32-bit millis() value to 64-bit. Use when the caller already has a fresh now.
// On platforms with native 64-bit time, ignores now and uses millis_64() directly. // On platforms with native 64-bit time (ESP32, Host, Zephyr, RP2040 — see
// On other platforms, extends now to 64-bit using rollover tracking. // USE_NATIVE_64BIT_TIME in defines.h), ignores now and uses millis_64() directly, so the
// Scheduler always works in 64-bit time regardless of what the caller's 32-bit now came
// from. On ESP32 specifically, millis() comes from xTaskGetTickCount while millis_64()
// comes from esp_timer — two different clocks — but that is safe because scheduling
// compares millis_64 values against millis_64 only, never against millis().
// On platforms without native 64-bit time (e.g. ESP8266), extends now to 64-bit using
// rollover tracking, so both millis() and scheduling use the same underlying clock.
uint64_t ESPHOME_ALWAYS_INLINE millis_64_from_(uint32_t now) { uint64_t ESPHOME_ALWAYS_INLINE millis_64_from_(uint32_t now) {
#ifdef USE_NATIVE_64BIT_TIME #ifdef USE_NATIVE_64BIT_TIME
(void) now; (void) now;