diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index 045cdd09d3..e890567abf 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -49,21 +49,60 @@ SPIDevice = spi_ns.class_("SPIDevice") SPIDataRate = spi_ns.enum("SPIDataRate") SPIMode = spi_ns.enum("SPIMode") -SPI_DATA_RATE_OPTIONS = { - 80e6: SPIDataRate.DATA_RATE_80MHZ, - 40e6: SPIDataRate.DATA_RATE_40MHZ, - 20e6: SPIDataRate.DATA_RATE_20MHZ, - 10e6: SPIDataRate.DATA_RATE_10MHZ, - 8e6: SPIDataRate.DATA_RATE_8MHZ, - 5e6: SPIDataRate.DATA_RATE_5MHZ, - 4e6: SPIDataRate.DATA_RATE_4MHZ, - 2e6: SPIDataRate.DATA_RATE_2MHZ, - 1e6: SPIDataRate.DATA_RATE_1MHZ, - 2e5: SPIDataRate.DATA_RATE_200KHZ, - 75e3: SPIDataRate.DATA_RATE_75KHZ, - 1e3: SPIDataRate.DATA_RATE_1KHZ, +PLATFORM_SPI_CLOCKS = { + PLATFORM_ESP8266: 40e6, + PLATFORM_ESP32: 80e6, + PLATFORM_RP2040: 62.5e6, } -SPI_DATA_RATE_SCHEMA = cv.All(cv.frequency, cv.enum(SPI_DATA_RATE_OPTIONS)) + +MAX_DATA_RATE_ERROR = 0.05 # Max allowable actual data rate difference from requested + + +def _render_hz(value: float) -> str: + """Render a frequency in Hz as a human-readable string using Hz, KHz or MHz. + + Examples: + 500 -> "500 Hz" + 1500 -> "1.5 kHz" + 2000000 -> "2 MHz" + """ + if value >= 1e6: + unit = "MHz" + num = value / 1e6 + elif value >= 1e3: + unit = "kHz" + num = value / 1e3 + else: + unit = "Hz" + num = value + + # Format with up to 2 decimal places, then strip unnecessary trailing zeros and dot + formatted = f"{int(num)}" if unit == "Hz" else f"{num:.2f}".rstrip("0").rstrip(".") + return formatted + unit + + +def _frequency_validator(value): + platform = get_target_platform() + frequency = PLATFORM_SPI_CLOCKS[platform] + value = cv.frequency(value) + if value > frequency: + raise cv.Invalid( + f"The configured SPI data rate ({_render_hz(value)}) exceeds the maximum for this platform ({_render_hz(frequency)})" + ) + if value < 1000: + raise cv.Invalid("The configured SPI data rate must be at least 1000Hz") + divisor = round(frequency / value) + actual = frequency / divisor + error = abs(actual - value) / value + if error > MAX_DATA_RATE_ERROR: + raise cv.Invalid( + f"The configured SPI data rate ({_render_hz(value)}) is not available for this chip - closest is {_render_hz(actual)}" + ) + return value + + +SPI_DATA_RATE_SCHEMA = _frequency_validator + SPI_MODE_OPTIONS = { "MODE0": SPIMode.MODE0, @@ -393,19 +432,20 @@ def spi_device_schema( :param mode Choose single, quad or octal mode. :return: The SPI device schema, `extend` this in your config schema. """ - schema = { - cv.GenerateID(CONF_SPI_ID): cv.use_id(TYPE_CLASS[mode]), - cv.Optional(CONF_DATA_RATE, default=default_data_rate): SPI_DATA_RATE_SCHEMA, - cv.Optional(CONF_SPI_MODE, default=default_mode): cv.enum( - SPI_MODE_OPTIONS, upper=True - ), - cv.Optional(CONF_RELEASE_DEVICE): cv.All(cv.boolean, cv.only_on_esp32), - } - if cs_pin_required: - schema[cv.Required(CONF_CS_PIN)] = pins.gpio_output_pin_schema - else: - schema[cv.Optional(CONF_CS_PIN)] = pins.gpio_output_pin_schema - return cv.Schema(schema) + cs_pin_option = cv.Required if cs_pin_required else cv.Optional + return cv.Schema( + { + cv.GenerateID(CONF_SPI_ID): cv.use_id(TYPE_CLASS[mode]), + cv.Optional( + CONF_DATA_RATE, default=default_data_rate + ): SPI_DATA_RATE_SCHEMA, + cv.Optional(CONF_SPI_MODE, default=default_mode): cv.enum( + SPI_MODE_OPTIONS, upper=True + ), + cv.Optional(CONF_RELEASE_DEVICE): cv.All(cv.boolean, cv.only_on_esp32), + cs_pin_option(CONF_CS_PIN): pins.gpio_output_pin_schema, + } + ) async def register_spi_device(var, config): diff --git a/tests/components/animation/test.rp2040-ard.yaml b/tests/components/animation/test.rp2040-ard.yaml index 32fb4efb04..2c99e937f3 100644 --- a/tests/components/animation/test.rp2040-ard.yaml +++ b/tests/components/animation/test.rp2040-ard.yaml @@ -11,3 +11,4 @@ display: dc_pin: 21 reset_pin: 22 invert_colors: false + data_rate: 10MHz diff --git a/tests/components/chsc6x/test.rp2040-ard.yaml b/tests/components/chsc6x/test.rp2040-ard.yaml index 2e3613a4a3..eb21b8ec4b 100644 --- a/tests/components/chsc6x/test.rp2040-ard.yaml +++ b/tests/components/chsc6x/test.rp2040-ard.yaml @@ -9,6 +9,7 @@ display: invert_colors: True cs_pin: 20 dc_pin: 21 + data_rate: 20MHz pages: - id: page1 lambda: |- diff --git a/tests/components/display/common.yaml b/tests/components/display/common.yaml index 27abb23e03..a722a5f7c2 100644 --- a/tests/components/display/common.yaml +++ b/tests/components/display/common.yaml @@ -6,6 +6,7 @@ display: dc_pin: 13 reset_pin: 21 invert_colors: false + data_rate: 20MHz lambda: |- // Draw an analog clock in the center of the screen int centerX = it.get_width() / 2; diff --git a/tests/components/ili9xxx/common.yaml b/tests/components/ili9xxx/common.yaml index 47384b1398..4665e55e4b 100644 --- a/tests/components/ili9xxx/common.yaml +++ b/tests/components/ili9xxx/common.yaml @@ -11,6 +11,7 @@ display: cs_pin: ${cs_pin1} dc_pin: ${dc_pin1} reset_pin: ${reset_pin1} + data_rate: 20MHz lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ili9xxx @@ -27,5 +28,6 @@ display: reset_pin: ${reset_pin2} auto_clear_enabled: false rotation: 90 + data_rate: 20MHz lambda: |- it.fill(Color::WHITE); diff --git a/tests/components/image/test.rp2040-ard.yaml b/tests/components/image/test.rp2040-ard.yaml index 40f17d57fe..03a9c42a38 100644 --- a/tests/components/image/test.rp2040-ard.yaml +++ b/tests/components/image/test.rp2040-ard.yaml @@ -9,6 +9,7 @@ display: cs_pin: 20 dc_pin: 21 reset_pin: 22 + data_rate: 20MHz invert_colors: true <<: !include common.yaml diff --git a/tests/components/online_image/common-rp2040.yaml b/tests/components/online_image/common-rp2040.yaml index 25891b94bc..bbb514bded 100644 --- a/tests/components/online_image/common-rp2040.yaml +++ b/tests/components/online_image/common-rp2040.yaml @@ -8,6 +8,7 @@ display: spi_id: spi_bus id: main_lcd model: ili9342 + data_rate: 20MHz cs_pin: 20 dc_pin: 17 reset_pin: 21 diff --git a/tests/components/qr_code/common.yaml b/tests/components/qr_code/common.yaml index 15b4e387c6..2ffba67763 100644 --- a/tests/components/qr_code/common.yaml +++ b/tests/components/qr_code/common.yaml @@ -5,6 +5,7 @@ display: cs_pin: ${cs_pin} dc_pin: ${dc_pin} reset_pin: ${reset_pin} + data_rate: 500kHz invert_colors: false lambda: |- // Draw a QR code in the center of the screen diff --git a/tests/components/xpt2046/common.yaml b/tests/components/xpt2046/common.yaml index 3a8e3286a6..eddbd24d6a 100644 --- a/tests/components/xpt2046/common.yaml +++ b/tests/components/xpt2046/common.yaml @@ -6,6 +6,7 @@ display: cs_pin: ${disp_cs_pin} dc_pin: ${dc_pin} reset_pin: ${reset_pin} + data_rate: 20MHz invert_colors: false lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height());