diff --git a/esphome/core/hal.h b/esphome/core/hal.h index 65e0053236b..b8bdc783ab7 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 00000000000..e755337540d --- /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 00000000000..b4476f1ab3b --- /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 00000000000..145fe4ea9c1 --- /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 00000000000..e0d92735bbb --- /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 00000000000..156ff33b863 --- /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 00000000000..e28be5c775d --- /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