[esp8266] Add enable_serial/enable_serial1 helpers to exclude unused Serial objects (#12736)

This commit is contained in:
J. Nick Koston
2026-01-05 17:26:49 -10:00
committed by GitHub
parent 7ba4dc0f1a
commit 8518424a88
7 changed files with 138 additions and 29 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -75,6 +75,7 @@ void ESP8266UartComponent::setup() {
// is 1 we still want to use Serial.
SerialConfig config = static_cast<SerialConfig>(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_);

View File

@@ -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