From 83252df908d66e3f62a83af1ed96232127f12b9e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Apr 2026 21:19:21 -0500 Subject: [PATCH 1/3] [core] Split hal.h into per-platform headers under core/hal/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror the wake.{h,cpp} → wake/wake_.{h,cpp} decomposition that PR #15978 did. After this change esphome/core/hal.h is a thin dispatcher and each platform's HAL bits (IRAM_ATTR / PROGMEM macros, in_isr_context(), the inline yield/delay/micros/millis/millis_64 wrappers, plus ESP8266's progmem_read_*) live in their own header under esphome/core/hal/. Scope is headers only — there is no esphome/core/hal.cpp today (every out-of-line implementation lives in esphome/components//core.cpp alongside platform-specific concerns) so no new .cpp files are added and no FILTER_SOURCE_FILES entries are needed in core/config.py. recursive_sources=True on the core manifest already picks up the new .h files automatically. No public API moves, no symbol renames, no behavior change. Pure code motion. The only observable difference is the dispatcher #errors when no USE_* is set (today an unknown platform silently fell through to the else branch with empty IRAM_ATTR/PROGMEM); this matches wake.h's behavior. --- esphome/core/hal.h | 187 ++++--------------------------- esphome/core/hal/hal_esp32.h | 35 ++++++ esphome/core/hal/hal_esp8266.h | 44 ++++++++ esphome/core/hal/hal_host.h | 24 ++++ esphome/core/hal/hal_libretiny.h | 93 +++++++++++++++ esphome/core/hal/hal_rp2040.h | 40 +++++++ esphome/core/hal/hal_zephyr.h | 24 ++++ 7 files changed, 279 insertions(+), 168 deletions(-) create mode 100644 esphome/core/hal/hal_esp32.h create mode 100644 esphome/core/hal/hal_esp8266.h create mode 100644 esphome/core/hal/hal_host.h create mode 100644 esphome/core/hal/hal_libretiny.h create mode 100644 esphome/core/hal/hal_rp2040.h create mode 100644 esphome/core/hal/hal_zephyr.h diff --git a/esphome/core/hal.h b/esphome/core/hal.h index 65e0053236..b8bdc783ab 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -6,175 +6,30 @@ #include "esphome/core/time_64.h" #include "esphome/core/time_conversion.h" +// Per-platform HAL bits (IRAM_ATTR / PROGMEM macros, in_isr_context(), +// inline yield/delay/micros/millis/millis_64 wrappers, ESP8266 progmem +// helpers) live under esphome/core/hal/ and are dispatched here based on +// the active USE_* platform define. Each header guards its body with the +// matching #ifdef USE_ and re-enters namespace esphome {} so it +// is safe to be re-included. #if defined(USE_ESP32) -#include -#ifndef PROGMEM -#define PROGMEM -#endif - +#include "esphome/core/hal/hal_esp32.h" #elif defined(USE_ESP8266) - -#include -#ifndef PROGMEM -#define PROGMEM ICACHE_RODATA_ATTR -#endif - -#elif defined(USE_RP2040) - -#define IRAM_ATTR __attribute__((noinline, long_call, section(".time_critical"))) -#define PROGMEM - +#include "esphome/core/hal/hal_esp8266.h" #elif defined(USE_LIBRETINY) - -// IRAM_ATTR places a function in executable RAM so it is callable from an -// ISR even while flash is busy (XIP stall, OTA, logger flash write). -// Each family uses a section its stock linker already routes to RAM: -// RTL8710B → .image2.ram.text, RTL8720C → .sram.text. LN882H is the -// exception: its stock linker has no matching glob, so patch_linker.py -// injects KEEP(*(.sram.text*)) into .flash_copysection at pre-link. -// -// BK72xx (all variants) are left as a no-op: their SDK wraps flash -// operations in GLOBAL_INT_DISABLE() which masks FIQ + IRQ at the CPU for -// the duration of every write, so no ISR fires while flash is stalled and -// the race IRAM_ATTR guards against cannot occur. The trade-off is that -// interrupts are delayed (not dropped) by up to ~20 ms during a sector -// erase, but that is an SDK-level choice and cannot be changed from this -// layer. -#if defined(USE_BK72XX) -#define IRAM_ATTR -#elif defined(USE_LIBRETINY_VARIANT_RTL8710B) -// Stock linker consumes *(.image2.ram.text*) into .ram_image2.text (> BD_RAM). -#define IRAM_ATTR __attribute__((noinline, section(".image2.ram.text"))) +#include "esphome/core/hal/hal_libretiny.h" +#elif defined(USE_RP2040) +#include "esphome/core/hal/hal_rp2040.h" +#elif defined(USE_HOST) +#include "esphome/core/hal/hal_host.h" +#elif defined(USE_ZEPHYR) +#include "esphome/core/hal/hal_zephyr.h" #else -// RTL8720C: stock linker consumes *(.sram.text*) into .ram.code_text. -// LN882H: patch_linker.py.script injects *(.sram.text*) into -// .flash_copysection (> RAM0 AT> FLASH). -#define IRAM_ATTR __attribute__((noinline, section(".sram.text"))) -#endif -#define PROGMEM - -#else - -#define IRAM_ATTR -#define PROGMEM - -#endif - -#ifdef USE_ESP32 -#include -#include -#endif - -#ifdef USE_LIBRETINY -// For the inline millis() fast paths (xTaskGetTickCount, portTICK_PERIOD_MS). -#include -#include -#endif - -#ifdef USE_BK72XX -// Declared in the Beken FreeRTOS port (portmacro.h) and built in ARM mode so -// it is callable from Thumb code via interworking. The MRS CPSR instruction -// is ARM-only and user code here may be built in Thumb, so in_isr_context() -// defers to this port helper on BK72xx instead of reading CPSR inline. -extern "C" uint32_t platform_is_in_interrupt_context(void); -#endif - -// Forward decls from Arduino's for the inline wrappers below. -// NOLINT covers TUs that also include Arduino.h. -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) -// NOLINTBEGIN(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) -extern "C" void yield(void); -extern "C" void delay(unsigned long ms); -extern "C" unsigned long micros(void); -extern "C" unsigned long millis(void); -// NOLINTEND(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) -#endif - -#ifdef USE_RP2040 -// Forward decl from . -extern "C" uint64_t time_us_64(void); +#error "hal.h: not implemented for this platform" #endif namespace esphome { -/// Returns true when executing inside an interrupt handler. -/// always_inline so callers placed in IRAM keep the detection in IRAM. -__attribute__((always_inline)) inline bool in_isr_context() { -#if defined(USE_ESP32) - return xPortInIsrContext() != 0; -#elif defined(USE_ESP8266) - // ESP8266 has no reliable single-register ISR detection: PS.INTLEVEL is - // non-zero both in a real ISR and when user code masks interrupts. The - // ESP8266 wake path is context-agnostic (wake_loop_impl uses esp_schedule - // which is ISR-safe) so this helper is unused on this platform. - return false; -#elif defined(USE_RP2040) - uint32_t ipsr; - __asm__ volatile("mrs %0, ipsr" : "=r"(ipsr)); - return ipsr != 0; -#elif defined(USE_BK72XX) - // BK72xx is ARM968E-S (ARM9); see extern declaration above. - return platform_is_in_interrupt_context() != 0; -#elif defined(USE_LIBRETINY) - // Cortex-M (AmebaZ, AmebaZ2, LN882H). IPSR is the active exception number; - // non-zero means we're in a handler. - uint32_t ipsr; - __asm__ volatile("mrs %0, ipsr" : "=r"(ipsr)); - return ipsr != 0; -#else - // Host and any future platform without an ISR concept. - return false; -#endif -} - -// yield()/delay()/micros()/millis()/millis_64() are inlined per platform to -// drop the wrapper call/return — most relevant to runtime_stats and the main -// loop. ESP8266/LibreTiny/RP2040 share Arduino's ::yield/::delay/::micros. -#if defined(USE_ESP32) -// Forward decl from . -// NOLINTNEXTLINE(readability-redundant-declaration) -extern "C" int64_t esp_timer_get_time(void); -__attribute__((always_inline)) inline void yield() { vPortYield(); } -__attribute__((always_inline)) inline void delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } -__attribute__((always_inline)) inline uint32_t micros() { return static_cast(esp_timer_get_time()); } -uint32_t millis(); -__attribute__((always_inline)) inline uint64_t millis_64() { - return micros_to_millis(static_cast(esp_timer_get_time())); -} -#elif defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) -__attribute__((always_inline)) inline void yield() { ::yield(); } -__attribute__((always_inline)) inline uint32_t micros() { return static_cast(::micros()); } -#if defined(USE_ESP8266) -void delay(uint32_t ms); -uint32_t millis(); -__attribute__((always_inline)) inline uint64_t millis_64() { return Millis64Impl::compute(millis()); } -#elif defined(USE_LIBRETINY) -__attribute__((always_inline)) inline void delay(uint32_t ms) { ::delay(ms); } -// Per-variant millis() fast path — matches MillisInternal::get(). -#if defined(USE_RTL87XX) || defined(USE_LN882X) -static_assert(configTICK_RATE_HZ == 1000, "millis() fast path requires 1 kHz FreeRTOS tick"); -__attribute__((always_inline)) inline uint32_t millis() { - // xTaskGetTickCountFromISR is mandatory in interrupt context per the FreeRTOS API contract. - return in_isr_context() ? xTaskGetTickCountFromISR() : xTaskGetTickCount(); -} -#elif defined(USE_BK72XX) -static_assert(configTICK_RATE_HZ == 500, "BK72xx millis() fast path assumes 500 Hz FreeRTOS tick"); -__attribute__((always_inline)) inline uint32_t millis() { return xTaskGetTickCount() * portTICK_PERIOD_MS; } -#else -__attribute__((always_inline)) inline uint32_t millis() { return static_cast(::millis()); } -#endif -__attribute__((always_inline)) inline uint64_t millis_64() { return Millis64Impl::compute(millis()); } -#else // USE_RP2040 -__attribute__((always_inline)) inline uint32_t millis() { return micros_to_millis(::time_us_64()); } -__attribute__((always_inline)) inline uint64_t millis_64() { return micros_to_millis(::time_us_64()); } -#endif -#else -void yield(); -void delay(uint32_t ms); -uint32_t micros(); -uint32_t millis(); -uint64_t millis_64(); -#endif void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming) void __attribute__((noreturn)) arch_restart(); void arch_init(); @@ -182,13 +37,9 @@ void arch_feed_wdt(); uint32_t arch_get_cpu_cycle_count(); uint32_t arch_get_cpu_freq_hz(); -#ifdef USE_ESP8266 -// ESP8266: pgm_read_* does real flash reads on Harvard architecture -uint8_t progmem_read_byte(const uint8_t *addr); -const char *progmem_read_ptr(const char *const *addr); -uint16_t progmem_read_uint16(const uint16_t *addr); -#else -// All other platforms: PROGMEM is a no-op, so these are direct dereferences +#ifndef USE_ESP8266 +// All non-ESP8266 platforms: PROGMEM is a no-op, so these are direct dereferences. +// ESP8266's out-of-line declarations live in hal/hal_esp8266.h. inline uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } inline const char *progmem_read_ptr(const char *const *addr) { return *addr; } inline uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } diff --git a/esphome/core/hal/hal_esp32.h b/esphome/core/hal/hal_esp32.h new file mode 100644 index 0000000000..e755337540 --- /dev/null +++ b/esphome/core/hal/hal_esp32.h @@ -0,0 +1,35 @@ +#pragma once + +#ifdef USE_ESP32 + +#include +#include +#include +#include + +#include "esphome/core/time_conversion.h" + +#ifndef PROGMEM +#define PROGMEM +#endif + +namespace esphome { + +/// Returns true when executing inside an interrupt handler. +__attribute__((always_inline)) inline bool in_isr_context() { return xPortInIsrContext() != 0; } + +// Forward decl from . +// NOLINTNEXTLINE(readability-redundant-declaration) +extern "C" int64_t esp_timer_get_time(void); + +__attribute__((always_inline)) inline void yield() { vPortYield(); } +__attribute__((always_inline)) inline void delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } +__attribute__((always_inline)) inline uint32_t micros() { return static_cast(esp_timer_get_time()); } +uint32_t millis(); +__attribute__((always_inline)) inline uint64_t millis_64() { + return micros_to_millis(static_cast(esp_timer_get_time())); +} + +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/core/hal/hal_esp8266.h b/esphome/core/hal/hal_esp8266.h new file mode 100644 index 0000000000..b4476f1ab3 --- /dev/null +++ b/esphome/core/hal/hal_esp8266.h @@ -0,0 +1,44 @@ +#pragma once + +#ifdef USE_ESP8266 + +#include +#include + +#include "esphome/core/time_64.h" + +#ifndef PROGMEM +#define PROGMEM ICACHE_RODATA_ATTR +#endif + +// Forward decls from Arduino's for the inline wrappers below. +// NOLINTBEGIN(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) +extern "C" void yield(void); +extern "C" void delay(unsigned long ms); +extern "C" unsigned long micros(void); +extern "C" unsigned long millis(void); +// NOLINTEND(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) + +namespace esphome { + +/// Returns true when executing inside an interrupt handler. +/// ESP8266 has no reliable single-register ISR detection: PS.INTLEVEL is +/// non-zero both in a real ISR and when user code masks interrupts. The +/// ESP8266 wake path is context-agnostic (wake_loop_impl uses esp_schedule +/// which is ISR-safe) so this helper is unused on this platform. +__attribute__((always_inline)) inline bool in_isr_context() { return false; } + +__attribute__((always_inline)) inline void yield() { ::yield(); } +__attribute__((always_inline)) inline uint32_t micros() { return static_cast(::micros()); } +void delay(uint32_t ms); +uint32_t millis(); +__attribute__((always_inline)) inline uint64_t millis_64() { return Millis64Impl::compute(millis()); } + +// ESP8266: pgm_read_* does real flash reads on Harvard architecture +uint8_t progmem_read_byte(const uint8_t *addr); +const char *progmem_read_ptr(const char *const *addr); +uint16_t progmem_read_uint16(const uint16_t *addr); + +} // namespace esphome + +#endif // USE_ESP8266 diff --git a/esphome/core/hal/hal_host.h b/esphome/core/hal/hal_host.h new file mode 100644 index 0000000000..145fe4ea9c --- /dev/null +++ b/esphome/core/hal/hal_host.h @@ -0,0 +1,24 @@ +#pragma once + +#ifdef USE_HOST + +#include + +#define IRAM_ATTR +#define PROGMEM + +namespace esphome { + +/// Returns true when executing inside an interrupt handler. +/// Host has no ISR concept. +__attribute__((always_inline)) inline bool in_isr_context() { return false; } + +void yield(); +void delay(uint32_t ms); +uint32_t micros(); +uint32_t millis(); +uint64_t millis_64(); + +} // namespace esphome + +#endif // USE_HOST diff --git a/esphome/core/hal/hal_libretiny.h b/esphome/core/hal/hal_libretiny.h new file mode 100644 index 0000000000..e0d92735bb --- /dev/null +++ b/esphome/core/hal/hal_libretiny.h @@ -0,0 +1,93 @@ +#pragma once + +#ifdef USE_LIBRETINY + +#include + +// For the inline millis() fast paths (xTaskGetTickCount, portTICK_PERIOD_MS). +#include +#include + +#include "esphome/core/time_64.h" + +// IRAM_ATTR places a function in executable RAM so it is callable from an +// ISR even while flash is busy (XIP stall, OTA, logger flash write). +// Each family uses a section its stock linker already routes to RAM: +// RTL8710B → .image2.ram.text, RTL8720C → .sram.text. LN882H is the +// exception: its stock linker has no matching glob, so patch_linker.py +// injects KEEP(*(.sram.text*)) into .flash_copysection at pre-link. +// +// BK72xx (all variants) are left as a no-op: their SDK wraps flash +// operations in GLOBAL_INT_DISABLE() which masks FIQ + IRQ at the CPU for +// the duration of every write, so no ISR fires while flash is stalled and +// the race IRAM_ATTR guards against cannot occur. The trade-off is that +// interrupts are delayed (not dropped) by up to ~20 ms during a sector +// erase, but that is an SDK-level choice and cannot be changed from this +// layer. +#if defined(USE_BK72XX) +#define IRAM_ATTR +#elif defined(USE_LIBRETINY_VARIANT_RTL8710B) +// Stock linker consumes *(.image2.ram.text*) into .ram_image2.text (> BD_RAM). +#define IRAM_ATTR __attribute__((noinline, section(".image2.ram.text"))) +#else +// RTL8720C: stock linker consumes *(.sram.text*) into .ram.code_text. +// LN882H: patch_linker.py.script injects *(.sram.text*) into +// .flash_copysection (> RAM0 AT> FLASH). +#define IRAM_ATTR __attribute__((noinline, section(".sram.text"))) +#endif +#define PROGMEM + +#ifdef USE_BK72XX +// Declared in the Beken FreeRTOS port (portmacro.h) and built in ARM mode so +// it is callable from Thumb code via interworking. The MRS CPSR instruction +// is ARM-only and user code here may be built in Thumb, so in_isr_context() +// defers to this port helper on BK72xx instead of reading CPSR inline. +extern "C" uint32_t platform_is_in_interrupt_context(void); +#endif + +// Forward decls from Arduino's for the inline wrappers below. +// NOLINTBEGIN(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) +extern "C" void yield(void); +extern "C" void delay(unsigned long ms); +extern "C" unsigned long micros(void); +extern "C" unsigned long millis(void); +// NOLINTEND(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) + +namespace esphome { + +/// Returns true when executing inside an interrupt handler. +__attribute__((always_inline)) inline bool in_isr_context() { +#if defined(USE_BK72XX) + // BK72xx is ARM968E-S (ARM9); see extern declaration above. + return platform_is_in_interrupt_context() != 0; +#else + // Cortex-M (AmebaZ, AmebaZ2, LN882H). IPSR is the active exception number; + // non-zero means we're in a handler. + uint32_t ipsr; + __asm__ volatile("mrs %0, ipsr" : "=r"(ipsr)); + return ipsr != 0; +#endif +} + +__attribute__((always_inline)) inline void yield() { ::yield(); } +__attribute__((always_inline)) inline void delay(uint32_t ms) { ::delay(ms); } +__attribute__((always_inline)) inline uint32_t micros() { return static_cast(::micros()); } + +// Per-variant millis() fast path — matches MillisInternal::get(). +#if defined(USE_RTL87XX) || defined(USE_LN882X) +static_assert(configTICK_RATE_HZ == 1000, "millis() fast path requires 1 kHz FreeRTOS tick"); +__attribute__((always_inline)) inline uint32_t millis() { + // xTaskGetTickCountFromISR is mandatory in interrupt context per the FreeRTOS API contract. + return in_isr_context() ? xTaskGetTickCountFromISR() : xTaskGetTickCount(); +} +#elif defined(USE_BK72XX) +static_assert(configTICK_RATE_HZ == 500, "BK72xx millis() fast path assumes 500 Hz FreeRTOS tick"); +__attribute__((always_inline)) inline uint32_t millis() { return xTaskGetTickCount() * portTICK_PERIOD_MS; } +#else +__attribute__((always_inline)) inline uint32_t millis() { return static_cast(::millis()); } +#endif +__attribute__((always_inline)) inline uint64_t millis_64() { return Millis64Impl::compute(millis()); } + +} // namespace esphome + +#endif // USE_LIBRETINY diff --git a/esphome/core/hal/hal_rp2040.h b/esphome/core/hal/hal_rp2040.h new file mode 100644 index 0000000000..156ff33b86 --- /dev/null +++ b/esphome/core/hal/hal_rp2040.h @@ -0,0 +1,40 @@ +#pragma once + +#ifdef USE_RP2040 + +#include + +#include "esphome/core/time_conversion.h" + +#define IRAM_ATTR __attribute__((noinline, long_call, section(".time_critical"))) +#define PROGMEM + +// Forward decls from Arduino's for the inline wrappers below. +// NOLINTBEGIN(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) +extern "C" void yield(void); +extern "C" void delay(unsigned long ms); +extern "C" unsigned long micros(void); +extern "C" unsigned long millis(void); +// NOLINTEND(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) + +// Forward decl from . +extern "C" uint64_t time_us_64(void); + +namespace esphome { + +/// Returns true when executing inside an interrupt handler. +__attribute__((always_inline)) inline bool in_isr_context() { + uint32_t ipsr; + __asm__ volatile("mrs %0, ipsr" : "=r"(ipsr)); + return ipsr != 0; +} + +__attribute__((always_inline)) inline void yield() { ::yield(); } +__attribute__((always_inline)) inline void delay(uint32_t ms) { ::delay(ms); } +__attribute__((always_inline)) inline uint32_t micros() { return static_cast(::micros()); } +__attribute__((always_inline)) inline uint32_t millis() { return micros_to_millis(::time_us_64()); } +__attribute__((always_inline)) inline uint64_t millis_64() { return micros_to_millis(::time_us_64()); } + +} // namespace esphome + +#endif // USE_RP2040 diff --git a/esphome/core/hal/hal_zephyr.h b/esphome/core/hal/hal_zephyr.h new file mode 100644 index 0000000000..e28be5c775 --- /dev/null +++ b/esphome/core/hal/hal_zephyr.h @@ -0,0 +1,24 @@ +#pragma once + +#ifdef USE_ZEPHYR + +#include + +#define IRAM_ATTR +#define PROGMEM + +namespace esphome { + +/// Returns true when executing inside an interrupt handler. +/// Zephyr/nRF52: not currently consulted — wake path is platform-specific. +__attribute__((always_inline)) inline bool in_isr_context() { return false; } + +void yield(); +void delay(uint32_t ms); +uint32_t micros(); +uint32_t millis(); +uint64_t millis_64(); + +} // namespace esphome + +#endif // USE_ZEPHYR From 0f128d2f15ed9a1f0f1f2970a421fa0c9b5e47f1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Apr 2026 05:37:48 -0500 Subject: [PATCH 2/3] [esp8266] Inline delayMicroseconds/arch_feed_wdt/progmem_read_* in hal_esp8266.h MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These wrappers were one-line forwarders to platform primitives: delayMicroseconds(us) -> delay_microseconds_safe(us) arch_feed_wdt() -> system_soft_wdt_feed() progmem_read_byte(p) -> pgm_read_byte(p) progmem_read_ptr(p) -> pgm_read_ptr(p) cast progmem_read_uint16(p) -> pgm_read_word(p) Mark them __attribute__((always_inline)) inline in hal_esp8266.h so the wrapper call/return is eliminated at every call site, and remove the out-of-line definitions from components/esp8266/core.cpp. The IRAM_ATTR previously on delayMicroseconds() was decorative — its body calls delay_microseconds_safe() which lives in flash, so an IRAM ISR caller already jumped from SRAM into flash. Inlining is no worse than the prior code (same reasoning as the libretiny IRAM_ATTR note in the parent commit's PR description). The dispatcher hal.h now gates its delayMicroseconds/arch_feed_wdt declarations behind #ifndef USE_ESP8266 so clang-tidy does not flag them as redundant on top of the inline definitions. --- esphome/components/esp8266/core.cpp | 13 +------------ esphome/core/hal.h | 6 +++++- esphome/core/hal/hal_esp8266.h | 28 ++++++++++++++++++++++++---- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index ed28027445..9161ca6aaf 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -84,7 +84,7 @@ void HOT delay(uint32_t ms) { optimistic_yield(1000); } } -void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } +// delayMicroseconds(), arch_feed_wdt(), and progmem_read_*() are inlined in hal/hal_esp8266.h. void arch_restart() { system_restart(); // restart() doesn't always end execution @@ -93,17 +93,6 @@ void arch_restart() { } } void arch_init() {} -void HOT arch_feed_wdt() { system_soft_wdt_feed(); } - -uint8_t progmem_read_byte(const uint8_t *addr) { - return pgm_read_byte(addr); // NOLINT -} -const char *progmem_read_ptr(const char *const *addr) { - return reinterpret_cast(pgm_read_ptr(addr)); // NOLINT -} -uint16_t progmem_read_uint16(const uint16_t *addr) { - return pgm_read_word(addr); // NOLINT -} uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return esp_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { return F_CPU; } diff --git a/esphome/core/hal.h b/esphome/core/hal.h index b8bdc783ab..e20797cf95 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -30,10 +30,14 @@ namespace esphome { +// ESP8266 inlines delayMicroseconds() and arch_feed_wdt() in hal/hal_esp8266.h; +// every other platform keeps them out-of-line in components//core.cpp. +#ifndef USE_ESP8266 void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming) +void arch_feed_wdt(); +#endif void __attribute__((noreturn)) arch_restart(); void arch_init(); -void arch_feed_wdt(); uint32_t arch_get_cpu_cycle_count(); uint32_t arch_get_cpu_freq_hz(); diff --git a/esphome/core/hal/hal_esp8266.h b/esphome/core/hal/hal_esp8266.h index b4476f1ab3..d947612bcc 100644 --- a/esphome/core/hal/hal_esp8266.h +++ b/esphome/core/hal/hal_esp8266.h @@ -4,6 +4,7 @@ #include #include +#include #include "esphome/core/time_64.h" @@ -19,8 +20,15 @@ extern "C" unsigned long micros(void); extern "C" unsigned long millis(void); // NOLINTEND(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) +// Forward decl from for arch_feed_wdt() inline below. +extern "C" void system_soft_wdt_feed(void); + namespace esphome { +// Forward decl from helpers.h so this header stays cheap. +// NOLINTNEXTLINE(readability-redundant-declaration) +void delay_microseconds_safe(uint32_t us); + /// Returns true when executing inside an interrupt handler. /// ESP8266 has no reliable single-register ISR detection: PS.INTLEVEL is /// non-zero both in a real ISR and when user code masks interrupts. The @@ -34,10 +42,22 @@ void delay(uint32_t ms); uint32_t millis(); __attribute__((always_inline)) inline uint64_t millis_64() { return Millis64Impl::compute(millis()); } -// ESP8266: pgm_read_* does real flash reads on Harvard architecture -uint8_t progmem_read_byte(const uint8_t *addr); -const char *progmem_read_ptr(const char *const *addr); -uint16_t progmem_read_uint16(const uint16_t *addr); +// ESP8266: pgm_read_* does aligned 32-bit flash reads on Harvard architecture. +// Inline-forward to the platform macros so the wrappers themselves don't +// occupy IRAM/flash on every call site. +__attribute__((always_inline)) inline uint8_t progmem_read_byte(const uint8_t *addr) { + return pgm_read_byte(addr); // NOLINT +} +__attribute__((always_inline)) inline const char *progmem_read_ptr(const char *const *addr) { + return reinterpret_cast(pgm_read_ptr(addr)); // NOLINT +} +__attribute__((always_inline)) inline uint16_t progmem_read_uint16(const uint16_t *addr) { + return pgm_read_word(addr); // NOLINT +} + +// NOLINTNEXTLINE(readability-identifier-naming) +__attribute__((always_inline)) inline void delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } +__attribute__((always_inline)) inline void arch_feed_wdt() { system_soft_wdt_feed(); } } // namespace esphome From 4e517b422df0e6d690118429140ae5a1955b39c9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Apr 2026 06:08:04 -0500 Subject: [PATCH 3/3] [esp8266] NOLINT redundant-declaration on system_soft_wdt_feed forward decl clang-tidy flagged the forward decl in hal_esp8266.h because also declares the function (when included via SDK headers). Both decls are identical `extern "C"` so the redundancy is harmless; suppress the warning on the hal_esp8266.h side. --- esphome/core/hal/hal_esp8266.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/core/hal/hal_esp8266.h b/esphome/core/hal/hal_esp8266.h index d947612bcc..04326a3579 100644 --- a/esphome/core/hal/hal_esp8266.h +++ b/esphome/core/hal/hal_esp8266.h @@ -21,6 +21,7 @@ extern "C" unsigned long millis(void); // NOLINTEND(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) // Forward decl from for arch_feed_wdt() inline below. +// NOLINTNEXTLINE(readability-redundant-declaration) extern "C" void system_soft_wdt_feed(void); namespace esphome {