mirror of
https://github.com/esphome/esphome.git
synced 2026-06-01 01:19:45 +08:00
[esp32] Use xTaskGetTickCount() for millis() when tick rate is 1kHz (#15661)
This commit is contained in:
@@ -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(); }
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user