mirror of
https://github.com/esphome/esphome.git
synced 2026-09-30 16:30:22 +00:00
[network] Keep lwip off the PSRAM tier when Ethernet is configured (#19367)
This commit is contained in:
@@ -9,9 +9,11 @@ from esphome.components.zephyr import zephyr_add_prj_conf
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ENABLE_IPV6,
|
||||
CONF_ETHERNET,
|
||||
CONF_ID,
|
||||
CONF_MIN_IPV6_ADDR_COUNT,
|
||||
CONF_PRIORITY,
|
||||
CONF_WIFI,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
import esphome.final_validate as fv
|
||||
@@ -28,6 +30,16 @@ KEY_HIGH_PERFORMANCE_NETWORKING = "high_performance_networking"
|
||||
CONF_ENABLE_HIGH_PERFORMANCE = "enable_high_performance"
|
||||
CONF_TCP_SEND_BUFFER = "tcp_send_buffer"
|
||||
|
||||
# TCP receive window of the optimized lwip tier (PSRAM not guaranteed)
|
||||
TCP_WND_OPTIMIZED = 65534
|
||||
# Ethernet drivers keep every queued frame in internal RAM, so ethernet-only builds
|
||||
# get a window sized for a LAN round trip and the stock lwip input mailbox. Dual
|
||||
# wifi + ethernet builds keep wifi's sizes; the ethernet component moves their
|
||||
# received frames to PSRAM instead.
|
||||
TCP_WND_ETHERNET = 16384
|
||||
TCPIP_RECVMBOX_ETHERNET = 32
|
||||
TCPIP_RECVMBOX_OPTIMIZED = 64
|
||||
|
||||
# lwIP queues at most this many unsent/unacked bytes per TCP socket; the
|
||||
# stock ESP-IDF default (5744 bytes) stalls bursty senders like a Bluetooth
|
||||
# proxy streaming GATT notifications. Bounds follow the lwIP guidance for the
|
||||
@@ -166,6 +178,8 @@ def require_high_performance_networking() -> None:
|
||||
Configuration is PSRAM-aware:
|
||||
- With PSRAM guaranteed: Aggressive settings (512 RX buffers, 512KB TCP windows)
|
||||
- Without PSRAM: Conservative optimized settings (64 buffers, 65KB TCP windows)
|
||||
- Ethernet only: 16KB TCP windows regardless of PSRAM, because ESP-IDF
|
||||
ethernet drivers keep received frames in internal RAM
|
||||
|
||||
Example:
|
||||
from esphome.components import network
|
||||
@@ -417,8 +431,11 @@ async def to_code(config: ConfigType) -> None:
|
||||
if CORE.is_esp32 and should_enable:
|
||||
# Check if PSRAM is guaranteed (set by psram component during final validation)
|
||||
psram_guaranteed = psram_is_guaranteed()
|
||||
# ESP-IDF ethernet drivers malloc() received frames into internal RAM, so
|
||||
# lwip is never sized for PSRAM on an ethernet-only build.
|
||||
ethernet_only = CONF_ETHERNET in CORE.config and CONF_WIFI not in CORE.config
|
||||
|
||||
if psram_guaranteed:
|
||||
if psram_guaranteed and not ethernet_only:
|
||||
_LOGGER.info(
|
||||
"Applying high-performance lwip settings (PSRAM guaranteed): 512KB TCP windows, 512 mailbox sizes"
|
||||
)
|
||||
@@ -451,15 +468,25 @@ async def to_code(config: ConfigType) -> None:
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_OVERSIZE_MSS", True)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_QUEUE_OOSEQ", True)
|
||||
else:
|
||||
# Every queued byte is internal RAM on ethernet and a LAN round trip needs
|
||||
# little window; wifi keeps the larger sizes.
|
||||
if ethernet_only:
|
||||
tcp_window, tcpip_mailbox = TCP_WND_ETHERNET, TCPIP_RECVMBOX_ETHERNET
|
||||
else:
|
||||
tcp_window, tcpip_mailbox = TCP_WND_OPTIMIZED, TCPIP_RECVMBOX_OPTIMIZED
|
||||
_LOGGER.info(
|
||||
"Applying optimized lwip settings: 65KB TCP windows, 64 mailbox sizes"
|
||||
"Applying optimized lwip settings: %dKB TCP windows, %d entry input mailbox",
|
||||
tcp_window // 1000,
|
||||
tcpip_mailbox,
|
||||
)
|
||||
# PSRAM not guaranteed - use more conservative, but still optimized settings
|
||||
# Based on https://github.com/espressif/esp-idf/blob/release/v5.4/examples/wifi/iperf/sdkconfig.defaults.esp32
|
||||
# The send buffer stays at 65534 on ethernet too: it only fills under outbound
|
||||
# load, and bursty senders such as a Bluetooth proxy stall on the IDF default.
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT", 65534)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT", 65534)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT", tcp_window)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE", 64)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 64)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", tcpip_mailbox)
|
||||
|
||||
# After the high performance block so an explicit size wins over the
|
||||
# bundle's 65534 (last write wins in the sdkconfig store).
|
||||
|
||||
@@ -771,7 +771,7 @@ async def to_code(config):
|
||||
"Applying high-performance WiFi settings (PSRAM guaranteed): 512 RX buffers, 32 TX buffers"
|
||||
)
|
||||
# PSRAM is guaranteed - use aggressive settings
|
||||
# Higher maximum values are allowed because CONFIG_LWIP_WND_SCALE is set to true in networking component
|
||||
# Higher maximum values are allowed because CONFIG_LWIP_WND_SCALE may be set to true in networking component
|
||||
# Based on https://github.com/espressif/esp-adf/issues/297#issuecomment-783811702
|
||||
|
||||
# Large dynamic RX buffers (requires PSRAM)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
"""Shared helpers for the network component tests."""
|
||||
|
||||
from esphome.components.esp32.const import KEY_ESP32, KEY_SDKCONFIG_OPTIONS
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
def sdkconfig_option(name: str) -> int | bool | None:
|
||||
"""Return a generated sdkconfig value, or None when unset or not ESP32."""
|
||||
if KEY_ESP32 not in CORE.data:
|
||||
return None
|
||||
return CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS].get(name)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
ethernet:
|
||||
type: LAN8720
|
||||
mdc_pin: 23
|
||||
mdio_pin: 32
|
||||
clk:
|
||||
pin: 0
|
||||
mode: CLK_EXT_IN
|
||||
phy_addr: 0
|
||||
power_pin: 33
|
||||
|
||||
network:
|
||||
enable_high_performance: true
|
||||
@@ -0,0 +1,23 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
ignore_not_found: false
|
||||
|
||||
ethernet:
|
||||
type: W5500
|
||||
clk_pin: GPIO12
|
||||
mosi_pin: GPIO11
|
||||
miso_pin: GPIO13
|
||||
cs_pin: GPIO10
|
||||
interrupt_pin: GPIO14
|
||||
reset_pin: GPIO9
|
||||
|
||||
network:
|
||||
enable_high_performance: true
|
||||
@@ -0,0 +1,30 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
ignore_not_found: false
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
ethernet:
|
||||
type: W5500
|
||||
clk_pin: GPIO12
|
||||
mosi_pin: GPIO11
|
||||
miso_pin: GPIO13
|
||||
cs_pin: GPIO10
|
||||
interrupt_pin: GPIO14
|
||||
reset_pin: GPIO9
|
||||
|
||||
network:
|
||||
priority:
|
||||
- wifi
|
||||
- ethernet
|
||||
enable_high_performance: true
|
||||
@@ -0,0 +1,14 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
network:
|
||||
enable_high_performance: true
|
||||
@@ -0,0 +1,18 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
ignore_not_found: false
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
network:
|
||||
enable_high_performance: true
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Tests for the ``network: enable_high_performance:`` lwip tier selection.
|
||||
|
||||
Ethernet drivers keep received frames in internal RAM, so an ethernet-only
|
||||
build must keep lwip off the PSRAM tier even when PSRAM is guaranteed.
|
||||
"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.network import require_high_performance_networking
|
||||
from tests.component_tests.network import sdkconfig_option
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"fixture",
|
||||
"window",
|
||||
"tcp_mailbox",
|
||||
"tcpip_mailbox",
|
||||
"window_scale",
|
||||
"wifi_rx_buffers",
|
||||
),
|
||||
[
|
||||
("high_perf_wifi_psram.yaml", 512000, 512, 512, True, 512),
|
||||
("high_perf_wifi_no_psram.yaml", 65534, 64, 64, None, 64),
|
||||
# dual builds keep wifi's sizes; the ethernet component moves frames to PSRAM
|
||||
("high_perf_wifi_ethernet_psram.yaml", 512000, 512, 512, True, 512),
|
||||
("high_perf_ethernet_no_psram.yaml", 16384, 64, 32, None, None),
|
||||
# ethernet only with PSRAM guaranteed: the cell the guard exists for
|
||||
("high_perf_ethernet_psram.yaml", 16384, 64, 32, None, None),
|
||||
],
|
||||
)
|
||||
def test_lwip_tier(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
fixture: str,
|
||||
window: int,
|
||||
tcp_mailbox: int,
|
||||
tcpip_mailbox: int,
|
||||
window_scale: bool | None,
|
||||
wifi_rx_buffers: int | None,
|
||||
) -> None:
|
||||
# The wifi component only reacts to a component request, so request it the
|
||||
# way sendspin does; the config key alone decides the lwip tier.
|
||||
require_high_performance_networking()
|
||||
generate_main(component_config_path(fixture))
|
||||
assert sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT") == window
|
||||
assert sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE") == tcp_mailbox
|
||||
assert sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE") == tcpip_mailbox
|
||||
assert sdkconfig_option("CONFIG_LWIP_WND_SCALE") is window_scale
|
||||
# Wifi RX buffers really go to PSRAM, so the wifi tier is never downgraded
|
||||
assert sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM") == wifi_rx_buffers
|
||||
@@ -19,6 +19,7 @@ from esphome.components.network import (
|
||||
from esphome.const import CONF_PRIORITY, PlatformFramework
|
||||
from esphome.core import CORE
|
||||
import esphome.final_validate as fv
|
||||
from tests.component_tests.network import sdkconfig_option
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
|
||||
@@ -217,13 +218,7 @@ def test_no_primary_interface_define_without_priority(
|
||||
|
||||
|
||||
def _dns_per_default_netif_option() -> bool | None:
|
||||
from esphome.components.esp32.const import KEY_ESP32, KEY_SDKCONFIG_OPTIONS
|
||||
|
||||
if KEY_ESP32 not in CORE.data: # non-ESP32 configs have no sdkconfig at all
|
||||
return None
|
||||
return CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS].get(
|
||||
"CONFIG_ESP_NETIF_SET_DNS_PER_DEFAULT_NETIF"
|
||||
)
|
||||
return sdkconfig_option("CONFIG_ESP_NETIF_SET_DNS_PER_DEFAULT_NETIF")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -14,32 +14,24 @@ import pytest
|
||||
from voluptuous import Invalid
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.esp32.const import (
|
||||
KEY_SDKCONFIG_OPTIONS,
|
||||
KEY_VARIANT,
|
||||
VARIANT_ESP32,
|
||||
)
|
||||
from esphome.components.esp32.const import KEY_VARIANT, VARIANT_ESP32
|
||||
from esphome.components.network import (
|
||||
CONF_TCP_SEND_BUFFER,
|
||||
CONFIG_SCHEMA,
|
||||
TCP_SEND_BUFFER_MAX,
|
||||
TCP_SEND_BUFFER_MIN,
|
||||
)
|
||||
from esphome.const import KEY_ESP32, KEY_FRAMEWORK_VERSION, PlatformFramework
|
||||
from esphome.core import CORE
|
||||
from esphome.const import KEY_FRAMEWORK_VERSION, PlatformFramework
|
||||
from tests.component_tests.network import sdkconfig_option
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
|
||||
def _sdkconfig_option(name: str) -> int | None:
|
||||
return CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS].get(name)
|
||||
|
||||
|
||||
def test_tcp_send_buffer_sets_sdkconfig(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
generate_main(component_config_path("tcp_send_buffer.yaml"))
|
||||
assert _sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT") == 32000
|
||||
assert sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT") == 32000
|
||||
|
||||
|
||||
def test_tcp_send_buffer_overrides_high_performance(
|
||||
@@ -48,7 +40,7 @@ def test_tcp_send_buffer_overrides_high_performance(
|
||||
) -> None:
|
||||
"""An explicit size wins over the high performance bundle's 65534."""
|
||||
generate_main(component_config_path("tcp_send_buffer_high_perf.yaml"))
|
||||
assert _sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT") == 16384
|
||||
assert sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT") == 16384
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [TCP_SEND_BUFFER_MIN, TCP_SEND_BUFFER_MAX])
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
# it must actually compile (not just validate) to guard the reconciler wiring.
|
||||
# WiFi is listed first so the build also exercises the wifi-primary branch in
|
||||
# network/util.cpp (the ethernet-primary branch matches the legacy order).
|
||||
# PSRAM guaranteed plus high performance networking: a dual build keeps wifi's
|
||||
# PSRAM tier, so this compiles the sdkconfig a wifi + ethernet audio device gets.
|
||||
psram:
|
||||
ignore_not_found: false
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
@@ -21,3 +26,4 @@ network:
|
||||
priority:
|
||||
- wifi
|
||||
- ethernet
|
||||
enable_high_performance: true
|
||||
|
||||
Reference in New Issue
Block a user