mirror of
https://github.com/esphome/esphome.git
synced 2026-09-28 07:20:24 +00:00
[uart] clock_source option (#16486)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
co-authored by
J. Nick Koston
parent
071fc08e32
commit
7dc98c683b
@@ -5,6 +5,22 @@ import re
|
||||
from esphome import automation, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS
|
||||
from esphome.components.esp32 import (
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32C2,
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32C5,
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32C61,
|
||||
VARIANT_ESP32H2,
|
||||
VARIANT_ESP32H4,
|
||||
VARIANT_ESP32H21,
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
VARIANT_ESP32S31,
|
||||
variant_filtered_enum,
|
||||
)
|
||||
from esphome.config_helpers import (
|
||||
filter_source_files_from_defines,
|
||||
filter_source_files_from_platform,
|
||||
@@ -179,6 +195,33 @@ UART_PARITY_OPTIONS = {
|
||||
CONF_FLUSH_TIMEOUT = "flush_timeout"
|
||||
CONF_RX_FULL_THRESHOLD = "rx_full_threshold"
|
||||
CONF_RX_TIMEOUT = "rx_timeout"
|
||||
CONF_CLOCK_SOURCE = "clock_source"
|
||||
|
||||
UARTClockSource = cg.global_ns.enum("uart_sclk_t")
|
||||
UART_CLOCK_SOURCES = {
|
||||
"DEFAULT": UARTClockSource.UART_SCLK_DEFAULT,
|
||||
"APB": UARTClockSource.UART_SCLK_APB,
|
||||
"XTAL": UARTClockSource.UART_SCLK_XTAL,
|
||||
"RTC": UARTClockSource.UART_SCLK_RTC,
|
||||
"REF_TICK": UARTClockSource.UART_SCLK_REF_TICK,
|
||||
}
|
||||
|
||||
# Keep in sync with SOC_UART_SUPPORT_* in ESP-IDF's per-variant soc_caps.h.
|
||||
UART_CLOCK_SOURCES_BY_VARIANT = {
|
||||
VARIANT_ESP32: ["DEFAULT", "APB", "REF_TICK"],
|
||||
VARIANT_ESP32C2: ["DEFAULT", "XTAL", "RTC"],
|
||||
VARIANT_ESP32C3: ["DEFAULT", "APB", "XTAL", "RTC"],
|
||||
VARIANT_ESP32C5: ["DEFAULT", "XTAL", "RTC"],
|
||||
VARIANT_ESP32C6: ["DEFAULT", "XTAL", "RTC"],
|
||||
VARIANT_ESP32C61: ["DEFAULT", "XTAL", "RTC"],
|
||||
VARIANT_ESP32H2: ["DEFAULT", "XTAL", "RTC"],
|
||||
VARIANT_ESP32H4: ["DEFAULT", "XTAL", "RTC"],
|
||||
VARIANT_ESP32H21: ["DEFAULT", "XTAL", "RTC"],
|
||||
VARIANT_ESP32P4: ["DEFAULT", "XTAL", "RTC"],
|
||||
VARIANT_ESP32S2: ["DEFAULT", "APB", "REF_TICK"],
|
||||
VARIANT_ESP32S3: ["DEFAULT", "APB", "XTAL", "RTC"],
|
||||
VARIANT_ESP32S31: ["DEFAULT", "XTAL", "RTC"],
|
||||
}
|
||||
|
||||
UARTDirection = uart_ns.enum("UARTDirection")
|
||||
UART_DIRECTIONS = {
|
||||
@@ -263,6 +306,10 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_FLUSH_TIMEOUT): cv.All(
|
||||
cv.only_on_esp32, cv.positive_time_period_milliseconds
|
||||
),
|
||||
cv.Optional(CONF_CLOCK_SOURCE): cv.All(
|
||||
cv.only_on_esp32,
|
||||
variant_filtered_enum(UART_CLOCK_SOURCES_BY_VARIANT, upper=True),
|
||||
),
|
||||
cv.Optional(CONF_STOP_BITS, default=1): cv.one_of(1, 2, int=True),
|
||||
cv.Optional(CONF_DATA_BITS, default=8): cv.int_range(min=5, max=8),
|
||||
cv.Optional(CONF_PARITY, default="NONE"): cv.enum(
|
||||
@@ -344,6 +391,9 @@ async def to_code(config):
|
||||
cg.add(var.set_rx_timeout(config[CONF_RX_TIMEOUT]))
|
||||
if CONF_FLUSH_TIMEOUT in config:
|
||||
cg.add(var.set_flush_timeout(config[CONF_FLUSH_TIMEOUT]))
|
||||
# The member already defaults to UART_SCLK_DEFAULT, so only emit a real choice
|
||||
if (clock_source := config.get(CONF_CLOCK_SOURCE, "DEFAULT")) != "DEFAULT":
|
||||
cg.add(var.set_clock_source(UART_CLOCK_SOURCES[clock_source]))
|
||||
cg.add(var.set_stop_bits(config[CONF_STOP_BITS]))
|
||||
cg.add(var.set_data_bits(config[CONF_DATA_BITS]))
|
||||
cg.add(var.set_parity(config[CONF_PARITY]))
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_private/gpio.h"
|
||||
#include "soc/gpio_num.h"
|
||||
#include "soc/soc_caps.h"
|
||||
#include "soc/uart_pins.h"
|
||||
|
||||
#ifdef USE_UART_WAKE_LOOP_ON_RX
|
||||
@@ -37,6 +38,32 @@ static constexpr bool is_default_uart0_pin(int8_t pin_num) {
|
||||
return pin_num == U0TXD_GPIO_NUM || pin_num == U0RXD_GPIO_NUM;
|
||||
}
|
||||
|
||||
// clock_source_ is stored in a byte; every uart_sclk_t value is a soc_module_clk_t below SOC_MOD_CLK_INVALID
|
||||
static_assert(SOC_MOD_CLK_INVALID <= UINT8_MAX, "uart_sclk_t no longer fits in uint8_t clock_source_");
|
||||
|
||||
static const LogString *clock_source_to_str(uart_sclk_t clock_source) {
|
||||
switch (clock_source) {
|
||||
#if SOC_UART_SUPPORT_APB_CLK
|
||||
case UART_SCLK_APB:
|
||||
return LOG_STR("APB");
|
||||
#endif
|
||||
#if SOC_UART_SUPPORT_XTAL_CLK
|
||||
case UART_SCLK_XTAL:
|
||||
return LOG_STR("XTAL");
|
||||
#endif
|
||||
#if SOC_UART_SUPPORT_RTC_CLK
|
||||
case UART_SCLK_RTC:
|
||||
return LOG_STR("RTC");
|
||||
#endif
|
||||
#if SOC_UART_SUPPORT_REF_TICK
|
||||
case UART_SCLK_REF_TICK:
|
||||
return LOG_STR("REF_TICK");
|
||||
#endif
|
||||
default:
|
||||
return clock_source == UART_SCLK_DEFAULT ? LOG_STR("DEFAULT") : LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
uart_config_t IDFUARTComponent::get_config_() {
|
||||
uart_parity_t parity = UART_PARITY_DISABLE;
|
||||
if (this->parity_ == UART_CONFIG_PARITY_EVEN) {
|
||||
@@ -70,7 +97,7 @@ uart_config_t IDFUARTComponent::get_config_() {
|
||||
uart_config.parity = parity;
|
||||
uart_config.stop_bits = this->stop_bits_ == 1 ? UART_STOP_BITS_1 : UART_STOP_BITS_2;
|
||||
uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
|
||||
uart_config.source_clk = UART_SCLK_DEFAULT;
|
||||
uart_config.source_clk = static_cast<uart_sclk_t>(this->clock_source_);
|
||||
uart_config.rx_flow_ctrl_thresh = 122;
|
||||
|
||||
return uart_config;
|
||||
@@ -336,12 +363,14 @@ void IDFUARTComponent::dump_config() {
|
||||
" Baud Rate: %" PRIu32 " baud\n"
|
||||
" Data Bits: %u\n"
|
||||
" Parity: %s\n"
|
||||
" Stop bits: %u"
|
||||
" Stop bits: %u\n"
|
||||
" Clock Source: %s"
|
||||
#ifdef USE_UART_WAKE_LOOP_ON_RX
|
||||
"\n Wake on data RX: ENABLED"
|
||||
#endif
|
||||
,
|
||||
this->baud_rate_, this->data_bits_, LOG_STR_ARG(parity_to_str(this->parity_)), this->stop_bits_);
|
||||
this->baud_rate_, this->data_bits_, LOG_STR_ARG(parity_to_str(this->parity_)), this->stop_bits_,
|
||||
LOG_STR_ARG(clock_source_to_str(static_cast<uart_sclk_t>(this->clock_source_))));
|
||||
this->check_logger_conflict();
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,8 @@ class IDFUARTComponent final : public UARTComponent, public Component {
|
||||
|
||||
void set_flush_timeout(uint32_t flush_timeout_ms) override { this->flush_timeout_ms_ = flush_timeout_ms; }
|
||||
|
||||
void set_clock_source(uart_sclk_t clock_source) { this->clock_source_ = static_cast<uint8_t>(clock_source); }
|
||||
|
||||
uint8_t get_hw_serial_number() { return this->uart_num_; }
|
||||
|
||||
/// Discard everything received so far: the peek cache and the driver's RX buffer.
|
||||
@@ -105,7 +107,8 @@ class IDFUARTComponent final : public UARTComponent, public Component {
|
||||
|
||||
bool has_peek_{false};
|
||||
uint8_t peek_byte_{0};
|
||||
uint32_t flush_timeout_ms_{0}; ///< 0 means wait indefinitely (portMAX_DELAY).
|
||||
uint8_t clock_source_{UART_SCLK_DEFAULT}; ///< uart_sclk_t stored in a byte; the IDF values are all small.
|
||||
uint32_t flush_timeout_ms_{0}; ///< 0 means wait indefinitely (portMAX_DELAY).
|
||||
|
||||
#ifdef USE_UART_WAKE_LOOP_ON_RX
|
||||
// ISR callback for UART RX data notification — wakes the main loop directly.
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
"""Tests for UART clock source validation and code generation."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.esp32 import KEY_VARIANT, VARIANTS
|
||||
from esphome.components.uart import (
|
||||
CONF_CLOCK_SOURCE,
|
||||
CONFIG_SCHEMA,
|
||||
UART_CLOCK_SOURCES,
|
||||
UART_CLOCK_SOURCES_BY_VARIANT,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import PlatformFramework
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def clock_source_validator() -> cv.All:
|
||||
"""Find the clock validator without depending on pre-validator ordering."""
|
||||
schema = next(
|
||||
validator
|
||||
for validator in CONFIG_SCHEMA.validators
|
||||
if isinstance(validator, cv.Schema)
|
||||
)
|
||||
return schema.schema[cv.Optional(CONF_CLOCK_SOURCE)]
|
||||
|
||||
|
||||
def test_clock_source_table_covers_all_variants() -> None:
|
||||
"""New ESP32 variants must declare their supported UART clocks."""
|
||||
assert set(UART_CLOCK_SOURCES_BY_VARIANT) == set(VARIANTS)
|
||||
for sources in UART_CLOCK_SOURCES_BY_VARIANT.values():
|
||||
assert "DEFAULT" in sources
|
||||
assert set(sources) <= UART_CLOCK_SOURCES.keys()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("variant", "source", "supported"),
|
||||
[
|
||||
("ESP32", "REF_TICK", True),
|
||||
("ESP32", "XTAL", False),
|
||||
("ESP32S2", "REF_TICK", True),
|
||||
("ESP32S2", "RTC", False),
|
||||
("ESP32C3", "APB", True),
|
||||
("ESP32C3", "xtal", True),
|
||||
("ESP32C3", "REF_TICK", False),
|
||||
("ESP32S3", "RTC", True),
|
||||
("ESP32S3", "REF_TICK", False),
|
||||
("ESP32C2", "APB", False),
|
||||
("ESP32C6", "APB", False),
|
||||
("ESP32C6", "DEFAULT", True),
|
||||
("ESP32C5", "RTC", True),
|
||||
("ESP32C61", "XTAL", True),
|
||||
("ESP32H4", "RTC", True),
|
||||
("ESP32H21", "XTAL", True),
|
||||
("ESP32S31", "RTC", True),
|
||||
("ESP32H2", "XTAL", True),
|
||||
("ESP32P4", "RTC", True),
|
||||
("ESP32C3", "INVALID", False),
|
||||
],
|
||||
)
|
||||
def test_clock_source_validation(
|
||||
variant: str,
|
||||
source: str,
|
||||
supported: bool,
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
clock_source_validator: cv.All,
|
||||
) -> None:
|
||||
"""Reject unsupported clocks and normalize accepted names."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_VARIANT: variant},
|
||||
)
|
||||
if supported:
|
||||
assert clock_source_validator(source) == source.upper()
|
||||
else:
|
||||
with pytest.raises(cv.Invalid):
|
||||
clock_source_validator(source)
|
||||
|
||||
|
||||
def test_clock_source_requires_esp32(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
clock_source_validator: cv.All,
|
||||
) -> None:
|
||||
"""Reject the ESP32-only option before accessing the chip variant."""
|
||||
set_core_config(PlatformFramework.ESP8266_ARDUINO)
|
||||
with pytest.raises(cv.Invalid, match="ESP32"):
|
||||
clock_source_validator("DEFAULT")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("source", [None, "default", "xtal"])
|
||||
def test_clock_source_codegen(
|
||||
source: str | None,
|
||||
tmp_path: Path,
|
||||
generate_main: Callable[[str | Path], str],
|
||||
) -> None:
|
||||
"""Emit IDF constants directly and skip the setter when nothing changes."""
|
||||
config = tmp_path / "uart.yaml"
|
||||
config.write_text(
|
||||
"esphome:\n name: uart-clock-test\n"
|
||||
"esp32:\n variant: esp32c3\n framework:\n type: esp-idf\n"
|
||||
"uart:\n id: test_uart\n tx_pin: GPIO4\n baud_rate: 9600\n"
|
||||
+ (f" clock_source: {source}\n" if source else ""),
|
||||
encoding="utf-8",
|
||||
)
|
||||
main_cpp = generate_main(config)
|
||||
if source in (None, "default"):
|
||||
assert "set_clock_source(" not in main_cpp
|
||||
else:
|
||||
assert f"test_uart->set_clock_source(::UART_SCLK_{source.upper()});" in main_cpp
|
||||
@@ -24,5 +24,6 @@ uart:
|
||||
tx_pin: 18
|
||||
rx_pin: 19
|
||||
baud_rate: 115200
|
||||
clock_source: XTAL
|
||||
debug:
|
||||
debug_prefix: "[UART1] "
|
||||
|
||||
@@ -28,6 +28,7 @@ uart:
|
||||
tx_pin: 21
|
||||
rx_pin: 22
|
||||
baud_rate: 115200
|
||||
clock_source: REF_TICK
|
||||
debug:
|
||||
debug_prefix: "[UART1] "
|
||||
- id: uart_debug_custom
|
||||
|
||||
@@ -248,6 +248,41 @@ def test_convert_keys_bare_callable_dotted_qualname() -> None:
|
||||
assert set(converted["schema"]["config_vars"]) == {"string"}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def language_schema_dir(tmp_path_factory: pytest.TempPathFactory) -> Path:
|
||||
"""Run the full language-schema build once and return the output directory.
|
||||
|
||||
The build must run in a fresh interpreter: ``build_language_schema.py``
|
||||
enables schema extraction *before* importing any esphome component, and the
|
||||
extraction hooks are no-ops if the components were already imported (as they
|
||||
are inside the pytest session). Running it as a subprocess mirrors how CI
|
||||
generates the schema and keeps these tests isolated from import order.
|
||||
"""
|
||||
out_dir = tmp_path_factory.mktemp("language_schema")
|
||||
subprocess.run(
|
||||
[sys.executable, str(SCRIPT_PATH), "--output-path", str(out_dir)],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return out_dir
|
||||
|
||||
|
||||
def test_uart_clock_source_preserves_variant_metadata(
|
||||
language_schema_dir: Path,
|
||||
) -> None:
|
||||
"""UART clock choices retain chip restrictions in the editor schema."""
|
||||
uart_schema = json.loads((language_schema_dir / "uart.json").read_text())
|
||||
clock_source = uart_schema["uart"]["schemas"]["CONFIG_SCHEMA"]["schema"][
|
||||
"config_vars"
|
||||
]["clock_source"]
|
||||
|
||||
assert clock_source["type"] == "enum"
|
||||
assert clock_source["values"]["REF_TICK"] == {"variants": ["ESP32", "ESP32S2"]}
|
||||
assert "ESP32C6" in clock_source["values"]["XTAL"]["variants"]
|
||||
assert "ESP32C6" not in clock_source["values"]["APB"]["variants"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Regression tests for the lvgl schema dump.
|
||||
#
|
||||
@@ -261,23 +296,8 @@ def test_convert_keys_bare_callable_dotted_qualname() -> None:
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def lvgl_schema(tmp_path_factory: pytest.TempPathFactory) -> dict:
|
||||
"""Run the full language-schema build once and return parsed lvgl.json.
|
||||
|
||||
The build must run in a fresh interpreter: ``build_language_schema.py``
|
||||
enables schema extraction *before* importing any esphome component, and the
|
||||
extraction hooks are no-ops if the components were already imported (as they
|
||||
are inside the pytest session). Running it as a subprocess mirrors how CI
|
||||
generates the schema and keeps this test isolated from import order.
|
||||
"""
|
||||
out_dir = tmp_path_factory.mktemp("language_schema")
|
||||
subprocess.run(
|
||||
[sys.executable, str(SCRIPT_PATH), "--output-path", str(out_dir)],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return json.loads((out_dir / "lvgl.json").read_text())
|
||||
def lvgl_schema(language_schema_dir: Path) -> dict:
|
||||
return json.loads((language_schema_dir / "lvgl.json").read_text())
|
||||
|
||||
|
||||
def _lvgl_config_vars(lvgl_schema: dict) -> dict:
|
||||
|
||||
Reference in New Issue
Block a user