Compare commits

...
8 changed files with 257 additions and 25 deletions
+11
View File
@@ -53,10 +53,14 @@ from .const import (
KEY_SERIAL1_REQUIRED,
KEY_SERIAL_REQUIRED,
KEY_WAVEFORM_REQUIRED,
PREFERENCES_RTC_EBOOT_WORDS,
PREFERENCES_RTC_NORMAL_WORDS,
THROW_STUBS_HEADER,
check_preferences_fit,
enable_serial,
enable_serial1,
esp8266_ns,
preferences_flash_words,
)
from .gpio import PinInitialState, add_pin_initial_states_array
@@ -467,6 +471,12 @@ async def to_code(config: ConfigType) -> None:
# Other projects like Tasmota & ESPEasy also use this
cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH")
cg.add_define("ESP8266_PREFERENCES_RTC_NORMAL_WORDS", PREFERENCES_RTC_NORMAL_WORDS)
cg.add_define("ESP8266_PREFERENCES_RTC_EBOOT_WORDS", PREFERENCES_RTC_EBOOT_WORDS)
cg.add_define(
"ESP8266_PREFERENCES_FLASH_WORDS",
preferences_flash_words(config[CONF_RESTORE_FROM_FLASH]),
)
if config[CONF_RESTORE_FROM_FLASH]:
cg.add_define("USE_ESP8266_PREFERENCES_FLASH")
@@ -528,6 +538,7 @@ async def to_code(config: ConfigType) -> None:
CORE.add_job(add_pin_initial_states_array)
CORE.add_job(finalize_waveform_config)
CORE.add_job(finalize_serial_config)
CORE.add_job(check_preferences_fit)
@coroutine_with_priority(CoroPriority.WORKAROUNDS)
+86 -1
View File
@@ -1,9 +1,13 @@
import logging
import esphome.codegen as cg
# Re-exported from the shared definition; here it indexes the BOARDS
# metadata dicts, whose entries in boards.py spell the literal.
from esphome.const import KEY_FLASH_SIZE # noqa: F401 # pylint: disable=unused-import
from esphome.core import CORE
from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority
_LOGGER = logging.getLogger(__name__)
KEY_ESP8266 = "esp8266"
KEY_BOARD = "board"
@@ -24,6 +28,87 @@ KEY_LDSCRIPT = "ldscript"
# Crash-decode tools, resolved once per run by __init__
KEY_DECODE_TOOLS = "decode_tools"
# Saved-settings space in 4-byte words; preferences.cpp gets these as defines. A value cannot span the two
# RTC regions, and the flash store is smaller when only explicit flash preferences use it.
PREFERENCES_RTC_NORMAL_WORDS = 78
PREFERENCES_RTC_EBOOT_WORDS = 32
PREFERENCES_RTC_WORDS = PREFERENCES_RTC_NORMAL_WORDS + PREFERENCES_RTC_EBOOT_WORDS
PREFERENCES_FLASH_WORDS = 128
PREFERENCES_FLASH_WORDS_WITHOUT_RESTORE = 64
KEY_PREFERENCES = "preferences"
def preferences_flash_words(restore_from_flash: bool) -> int:
"""Size of the flash store for this configuration."""
return (
PREFERENCES_FLASH_WORDS
if restore_from_flash
else PREFERENCES_FLASH_WORDS_WITHOUT_RESTORE
)
def preference_words(value_bytes: int) -> int:
"""Words a saved value takes, checksum word included (rtc_pref_bytes_to_words plus one)."""
return (value_bytes + 3) // 4 + 1
def reserve_preferences(
count: int, value_bytes: int, in_flash: bool | None = None
) -> None:
"""Record saved values a component creates; check_preferences_fit() adds them all up.
in_flash is the value's own storage, as passed to make_preference(); None is the two-argument
form, which follows restore_from_flash.
"""
if in_flash is None:
in_flash = CORE.config[KEY_ESP8266][CONF_RESTORE_FROM_FLASH]
reservations = CORE.data.setdefault(KEY_ESP8266, {}).setdefault(KEY_PREFERENCES, [])
reservations.extend([(preference_words(value_bytes), in_flash)] * count)
def _rtc_values_can_split(values: list[int]) -> bool:
"""Whether some order fills the two RTC regions without a value spanning them."""
total = sum(values)
if total <= PREFERENCES_RTC_NORMAL_WORDS:
return True
# Bit i of reachable is set when some subset of the values sums to i
reachable = 1
for words in values:
reachable |= reachable << words
lowest = max(0, total - PREFERENCES_RTC_EBOOT_WORDS)
highest = min(total, PREFERENCES_RTC_NORMAL_WORDS)
return any(reachable >> normal & 1 for normal in range(lowest, highest + 1))
@coroutine_with_priority(CoroPriority.FINAL)
async def check_preferences_fit() -> None:
"""Fail the build when the reserved saved values cannot fit the space preferences.cpp has."""
flash_store = preferences_flash_words(
CORE.config[KEY_ESP8266][CONF_RESTORE_FROM_FLASH]
)
reservations = CORE.data[KEY_ESP8266].get(KEY_PREFERENCES, [])
flash_words = sum(w for w, in_flash in reservations if in_flash)
rtc_values = [w for w, in_flash in reservations if not in_flash]
if flash_words > flash_store:
raise EsphomeError(
f"Saved values need {flash_words} words but the ESP8266 flash store has {flash_store}; "
"use fewer or smaller saved values"
)
if not _rtc_values_can_split(rtc_values):
raise EsphomeError(
f"Saved values need {sum(rtc_values)} words but ESP8266 RTC memory has {PREFERENCES_RTC_WORDS} in "
f"regions of {PREFERENCES_RTC_NORMAL_WORDS} and {PREFERENCES_RTC_EBOOT_WORDS} that a value cannot "
"span; use fewer or smaller saved values, or store them in flash"
)
if sum(rtc_values) > PREFERENCES_RTC_NORMAL_WORDS:
_LOGGER.warning(
"Saved values reserve %d of %d words of ESP8266 RTC memory; the fit depends on setup order, and "
"saved settings that do not reserve are not counted",
sum(rtc_values),
PREFERENCES_RTC_WORDS,
)
# esp8266 namespace is already defined by arduino, manually prefix esphome
esp8266_ns = cg.global_ns.namespace("esphome").namespace("esp8266")
+5 -7
View File
@@ -4,6 +4,7 @@
#ifdef USE_ESP8266_CRASH_HANDLER
#include "crash_handler.h"
#include "rtc_layout.h"
#include "esphome/core/log.h"
#include <cinttypes>
@@ -46,11 +47,8 @@ static inline bool IRAM_ATTR is_code_addr(uint32_t val) {
// Recover the actual code address from a windowed-ABI return address on the stack.
static inline uint32_t IRAM_ATTR recover_code_addr(uint32_t val) { return (val & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE; }
// RTC user memory layout for crash backtrace data.
// User-accessible RTC memory: blocks 64-191 (each block = 4 bytes).
// We use blocks 174-191 (last 18 blocks, 72 bytes) to minimize conflicts.
// Crash data lives in the last esphome::esp8266::RTC_CRASH_WORDS of user RTC memory (rtc_layout.h).
// Store 16 raw candidates, filter to real return addresses at log time.
static constexpr uint8_t RTC_CRASH_BASE = 174;
static constexpr size_t MAX_BACKTRACE = 16;
// Magic word packs sentinel, version, and count into one uint32_t:
@@ -78,7 +76,7 @@ struct RtcCrashData {
uint32_t backtrace[MAX_BACKTRACE];
uint32_t epc1; // Fault PC, used to filter duplicates
};
static_assert(sizeof(RtcCrashData) == 72, "RtcCrashData must fit in 18 RTC blocks");
static_assert(sizeof(RtcCrashData) == esphome::esp8266::RTC_CRASH_WORDS * 4, "RtcCrashData must fill its RTC words");
namespace esphome::esp8266 {
@@ -129,7 +127,7 @@ static const LogString *get_reset_reason(uint32_t reason) {
// Returns the number of valid backtrace entries (0 if no data found).
static uint8_t read_rtc_backtrace(uint32_t *backtrace, size_t max_entries) {
RtcCrashData rtc_data;
if (!system_rtc_mem_read(RTC_CRASH_BASE, &rtc_data, sizeof(rtc_data)))
if (!system_rtc_mem_read(esphome::esp8266::RTC_CRASH_BASE, &rtc_data, sizeof(rtc_data)))
return 0;
uint32_t magic = rtc_data.magic;
if ((magic & CRASH_SENTINEL_MASK) != CRASH_SENTINEL || (magic & CRASH_VERSION_MASK) != CRASH_VERSION)
@@ -233,7 +231,7 @@ extern "C" void IRAM_ATTR custom_crash_callback(struct rst_info *rst_info, uint3
data.epc1 = epc1;
data.magic = CRASH_SENTINEL | CRASH_VERSION | count;
system_rtc_mem_write(RTC_CRASH_BASE, &data, sizeof(data));
system_rtc_mem_write(esphome::esp8266::RTC_CRASH_BASE, &data, sizeof(data));
}
#endif // USE_ESP8266_CRASH_HANDLER
+13 -17
View File
@@ -6,6 +6,7 @@ extern "C" {
}
#include "preferences.h"
#include "rtc_layout.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences_rtc.h"
@@ -17,31 +18,26 @@ namespace esphome::esp8266 {
ESPHOME_LOG_TAG(TAG, "preferences");
static constexpr uint32_t ESP_RTC_USER_MEM_START = 0x60001200;
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128;
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = RTC_USER_MEM_WORDS;
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4;
// RTC memory layout:
// RTC memory layout (sizes come from const.py through codegen):
// - Eboot region: RTC words 0-31 (reserved, mapped from preference offset 78-109)
// - Normal region: RTC words 32-109 (mapped from preference offset 0-77)
// - Crash handler: RTC words 110-127 (reserved for crash_handler.cpp backtrace data)
static constexpr uint32_t RTC_EBOOT_REGION_WORDS = 32; // Words 0-31 reserved for eboot
static constexpr uint32_t RTC_NORMAL_REGION_WORDS = 78; // Words 32-109 for normal prefs
static constexpr uint32_t RTC_EBOOT_REGION_WORDS = ESP8266_PREFERENCES_RTC_EBOOT_WORDS;
static constexpr uint32_t RTC_NORMAL_REGION_WORDS = ESP8266_PREFERENCES_RTC_NORMAL_WORDS;
static constexpr uint32_t PREF_TOTAL_WORDS = RTC_EBOOT_REGION_WORDS + RTC_NORMAL_REGION_WORDS; // 110
static_assert(PREF_TOTAL_WORDS <= RTC_CRASH_BASE - RTC_SYSTEM_USER_BASE,
"preference regions overlap the crash handler's RTC words");
// Maximum preference size in words (limited by uint8_t length_words field)
static constexpr uint32_t MAX_PREFERENCE_WORDS = 255;
#define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START)
// Flash storage size depends on esp8266 -> restore_from_flash YAML option (default: false).
// When enabled (USE_ESP8266_PREFERENCES_FLASH), all preferences default to flash and need
// 128 words (512 bytes). When disabled, only explicit flash prefs use this storage so
// 64 words (256 bytes) suffices since most preferences go to RTC memory instead.
#ifdef USE_ESP8266_PREFERENCES_FLASH
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 128;
#else
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 64;
#endif
// Smaller without restore_from_flash, when only explicit flash preferences live here
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = ESP8266_PREFERENCES_FLASH_WORDS;
static uint32_t
s_flash_storage[ESP8266_FLASH_STORAGE_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -60,7 +56,7 @@ static inline bool esp_rtc_user_mem_write(uint32_t index, uint32_t value) {
if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) {
return false;
}
if (index < 32 && s_prevent_write) {
if (index < RTC_EBOOT_REGION_WORDS && s_prevent_write) {
return false;
}
@@ -123,7 +119,7 @@ static bool load_from_rtc(size_t offset, uint32_t *data, size_t len) {
// Maximum buffer for any single preference - bounded by storage sizes.
// Flash prefs: bounded by ESP8266_FLASH_STORAGE_SIZE (128 or 64 words).
// RTC prefs: bounded by RTC_NORMAL_REGION_WORDS (96) - a single pref can't span both RTC regions.
// RTC prefs: bounded by RTC_NORMAL_REGION_WORDS (78) - a single pref can't span both RTC regions.
static constexpr size_t PREF_MAX_BUFFER_WORDS =
ESP8266_FLASH_STORAGE_SIZE > RTC_NORMAL_REGION_WORDS ? ESP8266_FLASH_STORAGE_SIZE : RTC_NORMAL_REGION_WORDS;
@@ -180,8 +176,8 @@ ESPPreferenceObject ESP8266Preferences::make_preference(size_t length, uint32_t
} else {
uint32_t start = this->current_offset;
bool in_normal = start < RTC_NORMAL_REGION_WORDS;
// Normal: offset 0-95 maps to RTC offset 32-127
// Eboot: offset 96-127 maps to RTC offset 0-31
// Normal: offset 0-77 maps to RTC offset 32-109
// Eboot: offset 78-109 maps to RTC offset 0-31
if (in_normal && start + total_words > RTC_NORMAL_REGION_WORDS) {
// start is in normal but end is not -> switch to Eboot
this->current_offset = start = RTC_NORMAL_REGION_WORDS;
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#ifdef USE_ESP8266
#include <cstdint>
namespace esphome::esp8266 {
// User RTC memory is system blocks 64-191, one 4-byte word each. preferences.cpp owns the first
// words, crash_handler.cpp the last; const.py hands preferences.cpp its region sizes.
static constexpr uint32_t RTC_SYSTEM_USER_BASE = 64;
static constexpr uint32_t RTC_USER_MEM_WORDS = 128;
static constexpr uint32_t RTC_CRASH_WORDS = 18;
static constexpr uint32_t RTC_CRASH_BASE = RTC_SYSTEM_USER_BASE + RTC_USER_MEM_WORDS - RTC_CRASH_WORDS;
} // namespace esphome::esp8266
#endif // USE_ESP8266
+3
View File
@@ -597,6 +597,9 @@
#define USE_ESP8266_LOGGER_SERIAL
#define USE_ESP8266_LOGGER_SERIAL1
#define USE_ESP8266_PREFERENCES_FLASH
#define ESP8266_PREFERENCES_FLASH_WORDS 128
#define ESP8266_PREFERENCES_RTC_EBOOT_WORDS 32
#define ESP8266_PREFERENCES_RTC_NORMAL_WORDS 78
#define USE_ESP8266_UART_SERIAL
#define USE_ESP8266_UART_SERIAL1
#define USE_HTTP_REQUEST_ESP8266_HTTPS
+14
View File
@@ -102,3 +102,17 @@ def is_in_flash(value: str) -> bool:
if not in_flash:
request_rtc_storage()
return in_flash
def reserve_preferences(
count: int, value_bytes: int, in_flash: bool | None = None
) -> None:
"""Reserve saved values from to_code so a platform with a small store can check they all fit.
in_flash is the value's own storage (is_in_flash(config[CONF_STORAGE])); leave it None for a
component that calls the two-argument make_preference() and takes the platform default.
"""
if CORE.is_esp8266:
from esphome.components.esp8266.const import reserve_preferences as reserve
reserve(count, value_bytes, in_flash)
@@ -0,0 +1,107 @@
"""The config-time fit check must agree with make_preference() in preferences.cpp."""
import asyncio
import pytest
from esphome import preferences
from esphome.components.esp8266.const import (
KEY_ESP8266,
KEY_PREFERENCES,
check_preferences_fit,
preference_words,
reserve_preferences,
)
from esphome.const import KEY_CORE, KEY_TARGET_PLATFORM
from esphome.core import CORE, EsphomeError
@pytest.fixture(autouse=True)
def esp8266_core() -> None:
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: "esp8266"}
CORE.data[KEY_ESP8266] = {}
CORE.config = {KEY_ESP8266: {"restore_from_flash": False}}
def _check() -> None:
asyncio.run(check_preferences_fit())
@pytest.mark.parametrize(
("value_bytes", "words"), [(0, 1), (1, 2), (4, 2), (5, 3), (94, 25)]
)
def test_preference_words_rounds_up_and_adds_the_checksum(
value_bytes: int, words: int
) -> None:
assert preference_words(value_bytes) == words
@pytest.mark.parametrize(
("restore_from_flash", "in_flash", "fitting", "match"),
[
# 25 words each: 3 in the 78-word region and 1 in the 32-word one
(False, False, 4, "RTC memory has 110"),
(False, None, 4, "RTC memory has 110"),
# an explicit RTC value stays in RTC whatever restore_from_flash says
(True, False, 4, "RTC memory has 110"),
# 125 of 128 flash words, by the two-argument default or explicitly
(True, None, 5, "flash store has 128"),
(True, True, 5, "flash store has 128"),
# explicit flash values without restore_from_flash use the 64-word store
(False, True, 2, "flash store has 64"),
],
)
def test_reservations_add_up_against_the_store_they_land_in(
restore_from_flash: bool, in_flash: bool | None, fitting: int, match: str
) -> None:
CORE.config[KEY_ESP8266]["restore_from_flash"] = restore_from_flash
reserve_preferences(fitting - 1, 94, in_flash) # one component
reserve_preferences(1, 94, in_flash) # another component
_check()
reserve_preferences(1, 94, in_flash)
with pytest.raises(EsphomeError, match=match):
_check()
def test_values_that_cannot_split_between_the_regions_fail() -> None:
# 5 x 20 words is 100 of 110, but the normal region takes 3 and the eboot region 1
reserve_preferences(5, 76)
with pytest.raises(EsphomeError, match="cannot span"):
_check()
def test_a_value_larger_than_either_region_fails() -> None:
reserve_preferences(1, 400)
with pytest.raises(EsphomeError, match="cannot span"):
_check()
def test_a_split_that_works_in_some_order_passes_with_a_warning(
caplog: pytest.LogCaptureFixture,
) -> None:
# 78 + 32: one value fills each region exactly
reserve_preferences(1, 308)
reserve_preferences(1, 124)
_check()
assert "depends on setup order" in caplog.text
def test_nothing_reserved_passes_quietly(caplog: pytest.LogCaptureFixture) -> None:
_check()
assert caplog.text == ""
def test_the_check_runs_as_a_scheduled_job() -> None:
reserve_preferences(3, 400)
CORE.add_job(check_preferences_fit)
with pytest.raises(EsphomeError, match="cannot span"):
CORE.flush_tasks()
def test_components_reserve_through_esphome_preferences() -> None:
preferences.reserve_preferences(2, 94, in_flash=True)
assert CORE.data[KEY_ESP8266][KEY_PREFERENCES] == [(25, True), (25, True)]
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: "esp32"}
CORE.data[KEY_ESP8266] = {}
preferences.reserve_preferences(2, 94)
assert KEY_PREFERENCES not in CORE.data[KEY_ESP8266]