From 8518424a88a303ba6fa49cd7a511f47a920673fc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Jan 2026 17:26:49 -1000 Subject: [PATCH] [esp8266] Add enable_serial/enable_serial1 helpers to exclude unused Serial objects (#12736) --- esphome/components/esp8266/__init__.py | 33 ++++++++++++ esphome/components/esp8266/const.py | 36 +++++++++++++ esphome/components/logger/__init__.py | 12 +++++ esphome/components/logger/logger_esp8266.cpp | 50 +++++++++---------- esphome/components/uart/__init__.py | 22 ++++++++ .../uart/uart_component_esp8266.cpp | 10 +++- esphome/core/defines.h | 4 ++ 7 files changed, 138 insertions(+), 29 deletions(-) diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 77ccaf52c1..c7b5d5c130 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -23,12 +23,18 @@ from esphome.helpers import copy_file_if_changed from .boards import BOARDS, ESP8266_LD_SCRIPTS from .const import ( CONF_EARLY_PIN_INIT, + CONF_ENABLE_SERIAL, + CONF_ENABLE_SERIAL1, CONF_RESTORE_FROM_FLASH, KEY_BOARD, KEY_ESP8266, KEY_FLASH_SIZE, KEY_PIN_INITIAL_STATES, + KEY_SERIAL1_REQUIRED, + KEY_SERIAL_REQUIRED, KEY_WAVEFORM_REQUIRED, + enable_serial, + enable_serial1, esp8266_ns, ) from .gpio import PinInitialState, add_pin_initial_states_array @@ -171,6 +177,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_BOARD_FLASH_MODE, default="dout"): cv.one_of( *BUILD_FLASH_MODES, lower=True ), + cv.Optional(CONF_ENABLE_SERIAL): cv.boolean, + cv.Optional(CONF_ENABLE_SERIAL1): cv.boolean, } ), set_core_data, @@ -231,6 +239,12 @@ async def to_code(config): if config[CONF_EARLY_PIN_INIT]: cg.add_define("USE_ESP8266_EARLY_PIN_INIT") + # Allow users to force-enable Serial objects for use in lambdas or external libraries + if config.get(CONF_ENABLE_SERIAL): + enable_serial() + if config.get(CONF_ENABLE_SERIAL1): + enable_serial1() + # Arduino 2 has a non-standards conformant new that returns a nullptr instead of failing when # out of memory and exceptions are disabled. Since Arduino 2.6.0, this flag can be used to make # new abort instead. Use it so that OOM fails early (on allocation) instead of on dereference of @@ -271,6 +285,7 @@ async def to_code(config): CORE.add_job(add_pin_initial_states_array) CORE.add_job(finalize_waveform_config) + CORE.add_job(finalize_serial_config) @coroutine_with_priority(CoroPriority.WORKAROUNDS) @@ -286,6 +301,24 @@ async def finalize_waveform_config() -> None: cg.add_build_flag("-DUSE_ESP8266_WAVEFORM_STUBS") +@coroutine_with_priority(CoroPriority.WORKAROUNDS) +async def finalize_serial_config() -> None: + """Exclude unused Arduino Serial objects from the build. + + This runs at WORKAROUNDS priority (-999) to ensure all components + have had a chance to call enable_serial() or enable_serial1() first. + + The Arduino ESP8266 core defines two global Serial objects (32 bytes each). + By adding NO_GLOBAL_SERIAL or NO_GLOBAL_SERIAL1 build flags, we prevent + unused Serial objects from being linked, saving 32 bytes each. + """ + esp8266_data = CORE.data.get(KEY_ESP8266, {}) + if not esp8266_data.get(KEY_SERIAL_REQUIRED, False): + cg.add_build_flag("-DNO_GLOBAL_SERIAL") + if not esp8266_data.get(KEY_SERIAL1_REQUIRED, False): + cg.add_build_flag("-DNO_GLOBAL_SERIAL1") + + # Called by writer.py def copy_files() -> None: dir = Path(__file__).parent diff --git a/esphome/components/esp8266/const.py b/esphome/components/esp8266/const.py index 14425cde68..229ac61f24 100644 --- a/esphome/components/esp8266/const.py +++ b/esphome/components/esp8266/const.py @@ -6,8 +6,12 @@ KEY_BOARD = "board" KEY_PIN_INITIAL_STATES = "pin_initial_states" CONF_RESTORE_FROM_FLASH = "restore_from_flash" CONF_EARLY_PIN_INIT = "early_pin_init" +CONF_ENABLE_SERIAL = "enable_serial" +CONF_ENABLE_SERIAL1 = "enable_serial1" KEY_FLASH_SIZE = "flash_size" KEY_WAVEFORM_REQUIRED = "waveform_required" +KEY_SERIAL_REQUIRED = "serial_required" +KEY_SERIAL1_REQUIRED = "serial1_required" # esp8266 namespace is already defined by arduino, manually prefix esphome esp8266_ns = cg.global_ns.namespace("esphome").namespace("esp8266") @@ -29,3 +33,35 @@ def require_waveform() -> None: require_waveform() """ CORE.data.setdefault(KEY_ESP8266, {})[KEY_WAVEFORM_REQUIRED] = True + + +def enable_serial() -> None: + """Mark that Arduino Serial (UART0) is required. + + Call this from components that use the global Serial object. + If no component calls this, Serial is excluded from the build + to save 32 bytes of RAM. + + Example: + from esphome.components.esp8266.const import enable_serial + + async def to_code(config): + enable_serial() + """ + CORE.data.setdefault(KEY_ESP8266, {})[KEY_SERIAL_REQUIRED] = True + + +def enable_serial1() -> None: + """Mark that Arduino Serial1 (UART1) is required. + + Call this from components that use the global Serial1 object. + If no component calls this, Serial1 is excluded from the build + to save 32 bytes of RAM. + + Example: + from esphome.components.esp8266.const import enable_serial1 + + async def to_code(config): + enable_serial1() + """ + CORE.data.setdefault(KEY_ESP8266, {})[KEY_SERIAL1_REQUIRED] = True diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 7132cd8956..0a6035f8d1 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -337,6 +337,18 @@ async def to_code(config): is_at_least_very_verbose = this_severity >= very_verbose_severity has_serial_logging = baud_rate != 0 + # Add defines for which Serial object is needed (allows linker to exclude unused) + if CORE.is_esp8266: + from esphome.components.esp8266.const import enable_serial, enable_serial1 + + hw_uart = config.get(CONF_HARDWARE_UART, UART0) + if has_serial_logging and hw_uart in (UART0, UART0_SWAP): + cg.add_define("USE_ESP8266_LOGGER_SERIAL") + enable_serial() + elif has_serial_logging and hw_uart == UART1: + cg.add_define("USE_ESP8266_LOGGER_SERIAL1") + enable_serial1() + if ( (CORE.is_esp8266 or CORE.is_rp2040) and has_serial_logging diff --git a/esphome/components/logger/logger_esp8266.cpp b/esphome/components/logger/logger_esp8266.cpp index 0fc73b747a..6cee1baca5 100644 --- a/esphome/components/logger/logger_esp8266.cpp +++ b/esphome/components/logger/logger_esp8266.cpp @@ -7,26 +7,21 @@ namespace esphome::logger { static const char *const TAG = "logger"; void Logger::pre_setup() { - if (this->baud_rate_ > 0) { - switch (this->uart_) { - case UART_SELECTION_UART0: - case UART_SELECTION_UART0_SWAP: - this->hw_serial_ = &Serial; - Serial.begin(this->baud_rate_); - if (this->uart_ == UART_SELECTION_UART0_SWAP) { - Serial.swap(); - } - Serial.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE); - break; - case UART_SELECTION_UART1: - this->hw_serial_ = &Serial1; - Serial1.begin(this->baud_rate_); - Serial1.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE); - break; - } - } else { - uart_set_debug(UART_NO); +#if defined(USE_ESP8266_LOGGER_SERIAL) + this->hw_serial_ = &Serial; + Serial.begin(this->baud_rate_); + if (this->uart_ == UART_SELECTION_UART0_SWAP) { + Serial.swap(); } + Serial.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE); +#elif defined(USE_ESP8266_LOGGER_SERIAL1) + this->hw_serial_ = &Serial1; + Serial1.begin(this->baud_rate_); + Serial1.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE); +#else + // No serial logging - disable debug output + uart_set_debug(UART_NO); +#endif global_logger = this; @@ -39,15 +34,16 @@ void HOT Logger::write_msg_(const char *msg, size_t len) { } const LogString *Logger::get_uart_selection_() { - switch (this->uart_) { - case UART_SELECTION_UART0: - return LOG_STR("UART0"); - case UART_SELECTION_UART1: - return LOG_STR("UART1"); - case UART_SELECTION_UART0_SWAP: - default: - return LOG_STR("UART0_SWAP"); +#if defined(USE_ESP8266_LOGGER_SERIAL) + if (this->uart_ == UART_SELECTION_UART0_SWAP) { + return LOG_STR("UART0_SWAP"); } + return LOG_STR("UART0"); +#elif defined(USE_ESP8266_LOGGER_SERIAL1) + return LOG_STR("UART1"); +#else + return LOG_STR("NONE"); +#endif } } // namespace esphome::logger diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 9ec95964ec..31e37a06e0 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -378,6 +378,28 @@ async def to_code(config): if CONF_DEBUG in config: await debug_to_code(config[CONF_DEBUG], var) + # ESP8266: Enable the Arduino Serial objects that might be used based on pin config + # The C++ code selects hardware serial at runtime based on these pin combinations: + # - Serial (UART0): TX=1 or null, RX=3 or null + # - Serial (UART0 swap): TX=15 or null, RX=13 or null + # - Serial1: TX=2 or null, RX=8 or null + if CORE.is_esp8266: + from esphome.components.esp8266.const import enable_serial, enable_serial1 + + tx_num = config[CONF_TX_PIN][CONF_NUMBER] if CONF_TX_PIN in config else None + rx_num = config[CONF_RX_PIN][CONF_NUMBER] if CONF_RX_PIN in config else None + + # Check if this config could use Serial (UART0 regular or swap) + if (tx_num is None or tx_num in (1, 15)) and ( + rx_num is None or rx_num in (3, 13) + ): + enable_serial() + cg.add_define("USE_ESP8266_UART_SERIAL") + # Check if this config could use Serial1 + if (tx_num is None or tx_num == 2) and (rx_num is None or rx_num == 8): + enable_serial1() + cg.add_define("USE_ESP8266_UART_SERIAL1") + CORE.add_job(final_step) diff --git a/esphome/components/uart/uart_component_esp8266.cpp b/esphome/components/uart/uart_component_esp8266.cpp index c78daa7462..504d494e2e 100644 --- a/esphome/components/uart/uart_component_esp8266.cpp +++ b/esphome/components/uart/uart_component_esp8266.cpp @@ -75,6 +75,7 @@ void ESP8266UartComponent::setup() { // is 1 we still want to use Serial. SerialConfig config = static_cast(get_config()); +#ifdef USE_ESP8266_UART_SERIAL if (!ESP8266UartComponent::serial0_in_use && (tx_pin_ == nullptr || tx_pin_->get_pin() == 1) && (rx_pin_ == nullptr || rx_pin_->get_pin() == 3) #ifdef USE_LOGGER @@ -100,11 +101,16 @@ void ESP8266UartComponent::setup() { this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); this->hw_serial_->swap(); ESP8266UartComponent::serial0_in_use = true; - } else if ((tx_pin_ == nullptr || tx_pin_->get_pin() == 2) && (rx_pin_ == nullptr || rx_pin_->get_pin() == 8)) { + } else +#endif // USE_ESP8266_UART_SERIAL +#ifdef USE_ESP8266_UART_SERIAL1 + if ((tx_pin_ == nullptr || tx_pin_->get_pin() == 2) && (rx_pin_ == nullptr || rx_pin_->get_pin() == 8)) { this->hw_serial_ = &Serial1; this->hw_serial_->begin(this->baud_rate_, config); this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); - } else { + } else +#endif // USE_ESP8266_UART_SERIAL1 + { this->sw_serial_ = new ESP8266SoftwareSerial(); // NOLINT this->sw_serial_->setup(tx_pin_, rx_pin_, this->baud_rate_, this->stop_bits_, this->data_bits_, this->parity_, this->rx_buffer_size_); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index cee46a2df0..69684fd5c9 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -248,7 +248,11 @@ #define USE_ADC_SENSOR_VCC #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 1, 2) #define USE_CAPTIVE_PORTAL +#define USE_ESP8266_LOGGER_SERIAL +#define USE_ESP8266_LOGGER_SERIAL1 #define USE_ESP8266_PREFERENCES_FLASH +#define USE_ESP8266_UART_SERIAL +#define USE_ESP8266_UART_SERIAL1 #define USE_HTTP_REQUEST_ESP8266_HTTPS #define USE_HTTP_REQUEST_RESPONSE #define USE_I2C