mirror of
https://github.com/esphome/esphome.git
synced 2026-10-19 04:32:50 +00:00
[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:
co-authored by
Claude Opus 5.5
J. Nick Koston
parent
218e696654
commit
603f16623f
@@ -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"));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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({})
|
||||
Reference in New Issue
Block a user