[usb_host] Size the FIFO split on ESP32-S2/S3 from periodic packet requests (#21081)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
ab-tools
2026-10-18 20:44:05 +00:00
committed by GitHub
co-authored by Claude Opus 5.5 J. Nick Koston
parent 218e696654
commit 603f16623f
5 changed files with 273 additions and 1 deletions
+91 -1
View File
@@ -11,11 +11,12 @@ from esphome.components.esp32 import (
VARIANT_ESP32S31,
add_idf_component,
add_idf_sdkconfig_option,
get_esp32_variant,
idf_version,
only_on_variant,
)
import esphome.config_validation as cv
from esphome.const import CONF_DEVICES, CONF_ID
from esphome.const import CONF_DEVICES, CONF_FRAMEWORK, CONF_ID
from esphome.core import CORE
from esphome.cpp_generator import MockObj
from esphome.cpp_types import Component
@@ -117,12 +118,101 @@ def _final_validate(config: ConfigType) -> ConfigType:
if redundant
else "narrow its filter so it no longer matches that device",
)
_reconcile_fifo()
return config
FINAL_VALIDATE_SCHEMA = _final_validate
# S2/S3 full-speed controller: 200 usable 4-byte FIFO lines (EPINFO takes the rest of 256) shared by RX,
# non-periodic TX and periodic TX
FIFO_VARIANTS = (VARIANT_ESP32S2, VARIANT_ESP32S3)
FIFO_LINES = 200
FIFO_LINE_BYTES = 4
FIFO_RX_STATUS_LINES = 2
# (RX, NPTX, PTX) lines ESP-IDF picks for each CONFIG_USB_HOST_HW_BUFFER_BIAS_* option
FIFO_BIAS_SPLITS = {
"CONFIG_USB_HOST_HW_BUFFER_BIAS_IN": (152, 16, 32),
"CONFIG_USB_HOST_HW_BUFFER_BIAS_PERIODIC_OUT": (34, 16, 150),
}
FIFO_BALANCED_SPLIT = (104, 64, 32)
# Custom split: 64-byte control/bulk, at least 128 bytes each way
FIFO_NPTX_LINES = 16
FIFO_PTX_MIN_LINES = 32
FIFO_RX_MIN_LINES = 32 + FIFO_RX_STATUS_LINES
# usb_host_config_t::fifo_settings_custom; USB host is not reliable on older releases
FIFO_CUSTOM_MIN_IDF = cv.Version(5, 5, 0)
KEY_PERIODIC_FIFO = "periodic_fifo"
def request_periodic_fifo(in_bytes: int = 0, out_bytes: int = 0) -> None:
"""Request FIFO space for the largest periodic IN/OUT packet. Call from CONFIG_SCHEMA validation."""
requested = CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_PERIODIC_FIFO, [0, 0])
requested[0] = max(requested[0], in_bytes)
requested[1] = max(requested[1], out_bytes)
def _fifo_lines(size: int) -> int:
return -(-size // FIFO_LINE_BYTES)
def _bias_option() -> str | None:
framework = fv.full_config.get().get("esp32", {}).get(CONF_FRAMEWORK, {})
options = framework.get("sdkconfig_options", {})
return next((o for o in FIFO_BIAS_SPLITS if options.get(o) in ("y", True)), None)
def fifo_split() -> tuple[int, int, int] | None:
"""(RX, NPTX, PTX) lines for the requests, or None when the default split fits them."""
requested = CORE.data.get(DOMAIN, {}).get(KEY_PERIODIC_FIFO)
if requested is None or get_esp32_variant() not in FIFO_VARIANTS:
return None
in_bytes, out_bytes = requested
rx = _fifo_lines(in_bytes) + FIFO_RX_STATUS_LINES
ptx = _fifo_lines(out_bytes)
bias = _bias_option()
default_rx, _, default_ptx = FIFO_BIAS_SPLITS[bias] if bias else FIFO_BALANCED_SPLIT
if rx <= default_rx and ptx <= default_ptx:
return None
if bias:
raise cv.Invalid(
f"The requested USB packets don't fit the FIFO split {bias} selects; remove it from "
"sdkconfig_options so usb_host can size the FIFO"
)
rx = max(FIFO_RX_MIN_LINES, rx)
ptx = max(FIFO_PTX_MIN_LINES, ptx)
spare = FIFO_LINES - FIFO_NPTX_LINES - rx - ptx
if spare < 0:
raise cv.Invalid(
f"The requested USB packets need {(rx + ptx + FIFO_NPTX_LINES) * FIFO_LINE_BYTES} bytes of USB FIFO, "
f"but the {get_esp32_variant()} has {FIFO_LINES * FIFO_LINE_BYTES} usable"
)
# Devices may declare a larger max packet size, so the requested directions share the rest
if in_bytes and out_bytes:
rx += spare // 2
ptx += spare - spare // 2
elif out_bytes:
ptx += spare
else:
rx += spare
return rx, FIFO_NPTX_LINES, ptx
def _reconcile_fifo() -> None:
if (split := fifo_split()) is None:
return
if idf_version() < FIFO_CUSTOM_MIN_IDF:
raise cv.Invalid(
f"Sizing the USB FIFO on the {get_esp32_variant()} needs ESP-IDF {FIFO_CUSTOM_MIN_IDF} or later"
)
rx, nptx, ptx = split
cg.add_define("USE_USB_HOST_FIFO_CONFIG")
cg.add_define("USB_HOST_FIFO_RX_LINES", rx)
cg.add_define("USB_HOST_FIFO_NPTX_LINES", nptx)
cg.add_define("USB_HOST_FIFO_PTX_LINES", ptx)
def _set_max_packet_size(config: dict) -> dict:
CORE.data.setdefault(DOMAIN, {})[CONF_MAX_PACKET_SIZE] = config[
CONF_MAX_PACKET_SIZE
@@ -9,6 +9,12 @@ namespace esphome::usb_host {
void USBHost::setup() {
usb_host_config_t config{};
#ifdef USE_USB_HOST_FIFO_CONFIG
// Sized in Python from component requests
config.fifo_settings_custom.rx_fifo_lines = USB_HOST_FIFO_RX_LINES;
config.fifo_settings_custom.nptx_fifo_lines = USB_HOST_FIFO_NPTX_LINES;
config.fifo_settings_custom.ptx_fifo_lines = USB_HOST_FIFO_PTX_LINES;
#endif
if (usb_host_install(&config) != ESP_OK) {
this->status_set_error(LOG_STR("usb_host_install failed"));
+4
View File
@@ -542,6 +542,10 @@
#define USE_WIFI_RUNTIME_ROAMING_SUPPRESSION
#define USB_HOST_MAX_REQUESTS 16
#define USB_HOST_MAX_PACKET_SIZE 64
#define USE_USB_HOST_FIFO_CONFIG
#define USB_HOST_FIFO_RX_LINES 93
#define USB_HOST_FIFO_NPTX_LINES 16
#define USB_HOST_FIFO_PTX_LINES 91
#define USB_UART_OUTPUT_CHUNK_COUNT 5
#define USE_USB_UART_CH34X
#define USE_USB_UART_CP210X
+172
View File
@@ -0,0 +1,172 @@
"""Tests for the usb_host FIFO split sized from periodic packet requests."""
import pytest
from esphome import config_validation as cv
from esphome.components import usb_host
from esphome.components.esp32 import VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3
from esphome.config import Config
from esphome.const import (
CONF_FRAMEWORK,
KEY_IDF_VERSION,
KEY_VARIANT,
PlatformFramework,
)
from esphome.core import CORE
from tests.component_tests.types import SetCoreConfigCallable
IDF_5_5 = cv.Version(5, 5, 6)
def _set_variant(
set_core_config: SetCoreConfigCallable, variant: str, idf: cv.Version = IDF_5_5
) -> None:
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_VARIANT: variant, KEY_IDF_VERSION: idf},
)
def test_no_request_keeps_the_default_split(
set_core_config: SetCoreConfigCallable,
) -> None:
_set_variant(set_core_config, VARIANT_ESP32S3)
assert usb_host.fifo_split() is None
def test_in_and_out_requests_share_the_spare_lines(
set_core_config: SetCoreConfigCallable,
) -> None:
# 196-byte packets each way, such as 48 kHz 16-bit stereo audio: 49 + 51 lines, plus 84 spare.
_set_variant(set_core_config, VARIANT_ESP32S3)
usb_host.request_periodic_fifo(in_bytes=196, out_bytes=196)
rx, nptx, ptx = usb_host.fifo_split()
assert (rx, nptx, ptx) == (93, 16, 91)
assert rx + nptx + ptx == 200
assert ptx * 4 >= 196
assert (rx - 2) * 4 >= 196
def test_out_request_gets_the_rest(set_core_config: SetCoreConfigCallable) -> None:
_set_variant(set_core_config, VARIANT_ESP32S2)
usb_host.request_periodic_fifo(out_bytes=196)
assert usb_host.fifo_split() == (34, 16, 150)
def test_in_request_gets_the_rest(set_core_config: SetCoreConfigCallable) -> None:
# 512 bytes is more than the default 408 bytes of RX.
_set_variant(set_core_config, VARIANT_ESP32S3)
usb_host.request_periodic_fifo(in_bytes=512)
assert usb_host.fifo_split() == (152, 16, 32)
@pytest.mark.parametrize(
("in_bytes", "out_bytes"), [(0, 0), (196, 0), (408, 128), (0, 128)]
)
def test_requests_that_fit_the_default_keep_it(
set_core_config: SetCoreConfigCallable, in_bytes: int, out_bytes: int
) -> None:
_set_variant(set_core_config, VARIANT_ESP32S3)
usb_host.request_periodic_fifo(in_bytes=in_bytes, out_bytes=out_bytes)
assert usb_host.fifo_split() is None
def test_requests_that_fit_the_default_need_no_newer_idf(
set_core_config: SetCoreConfigCallable,
) -> None:
_set_variant(set_core_config, VARIANT_ESP32S3, idf=cv.Version(5, 4, 2))
usb_host.request_periodic_fifo(in_bytes=34, out_bytes=128)
usb_host.FINAL_VALIDATE_SCHEMA({})
assert "USE_USB_HOST_FIFO_CONFIG" not in _defines()
def test_requests_keep_the_largest_packet(
set_core_config: SetCoreConfigCallable,
) -> None:
_set_variant(set_core_config, VARIANT_ESP32S3)
usb_host.request_periodic_fifo(out_bytes=400)
usb_host.request_periodic_fifo(in_bytes=96, out_bytes=196)
assert CORE.data[usb_host.DOMAIN][usb_host.KEY_PERIODIC_FIFO] == [96, 400]
def test_only_full_speed_variants_are_sized(
set_core_config: SetCoreConfigCallable,
) -> None:
_set_variant(set_core_config, VARIANT_ESP32P4)
usb_host.request_periodic_fifo(out_bytes=196)
assert usb_host.fifo_split() is None
def test_requests_that_do_not_fit_are_rejected(
set_core_config: SetCoreConfigCallable,
) -> None:
_set_variant(set_core_config, VARIANT_ESP32S3)
usb_host.request_periodic_fifo(in_bytes=776, out_bytes=776)
with pytest.raises(cv.Invalid, match="bytes of USB FIFO"):
usb_host.fifo_split()
def _defines() -> dict[str, str]:
return {define.name: str(define.value) for define in CORE.defines}
def test_final_validate_emits_the_split(
set_core_config: SetCoreConfigCallable,
) -> None:
_set_variant(set_core_config, VARIANT_ESP32S3)
usb_host.request_periodic_fifo(out_bytes=196)
usb_host.FINAL_VALIDATE_SCHEMA({})
defines = _defines()
assert "USE_USB_HOST_FIFO_CONFIG" in defines
assert (
defines["USB_HOST_FIFO_RX_LINES"],
defines["USB_HOST_FIFO_NPTX_LINES"],
defines["USB_HOST_FIFO_PTX_LINES"],
) == ("34", "16", "150")
def _set_bias(set_core_config: SetCoreConfigCallable, option: str) -> None:
full = Config()
full["esp32"] = {CONF_FRAMEWORK: {"sdkconfig_options": {option: "y"}}}
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_VARIANT: VARIANT_ESP32S3, KEY_IDF_VERSION: IDF_5_5},
full_config=full,
)
def test_request_that_fits_the_selected_bias_keeps_it(
set_core_config: SetCoreConfigCallable,
) -> None:
# The IN bias leaves RX 152 lines, enough for 512-byte IN packets.
_set_bias(set_core_config, "CONFIG_USB_HOST_HW_BUFFER_BIAS_IN")
usb_host.request_periodic_fifo(in_bytes=512)
assert usb_host.fifo_split() is None
@pytest.mark.parametrize(
("option", "in_bytes", "out_bytes"),
[
# The IN bias reserves 152 RX lines, leaving no room for more than 128-byte OUT packets.
("CONFIG_USB_HOST_HW_BUFFER_BIAS_IN", 0, 196),
# The periodic OUT bias reserves 150 PTX lines, leaving no room for more than 128-byte IN packets.
("CONFIG_USB_HOST_HW_BUFFER_BIAS_PERIODIC_OUT", 196, 0),
],
)
def test_request_conflicting_with_the_selected_bias_is_rejected(
set_core_config: SetCoreConfigCallable, option: str, in_bytes: int, out_bytes: int
) -> None:
_set_bias(set_core_config, option)
usb_host.request_periodic_fifo(in_bytes=in_bytes, out_bytes=out_bytes)
with pytest.raises(cv.Invalid, match=f"FIFO split {option} selects"):
usb_host.fifo_split()
@pytest.mark.parametrize("idf", [cv.Version(5, 3, 4), cv.Version(5, 4, 3)])
def test_final_validate_needs_idf_5_5(
set_core_config: SetCoreConfigCallable, idf: cv.Version
) -> None:
_set_variant(set_core_config, VARIANT_ESP32S3, idf=idf)
usb_host.request_periodic_fifo(out_bytes=196)
with pytest.raises(cv.Invalid, match="needs ESP-IDF 5.5.0 or later"):
usb_host.FINAL_VALIDATE_SCHEMA({})