Skip to content

busio.SPI: treat baudrate as a ceiling, not nearest#11081

Merged
tannewt merged 6 commits into
adafruit:mainfrom
dhalbert:SPI-clock-ceiling
Jun 30, 2026
Merged

busio.SPI: treat baudrate as a ceiling, not nearest#11081
tannewt merged 6 commits into
adafruit:mainfrom
dhalbert:SPI-clock-ceiling

Conversation

@dhalbert

@dhalbert dhalbert commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

Note: This PR description was mostly written by Claude (Claude Code), and touched up by me (@dhalbert).

Summary

busio.SPI documents baudrate as a desired clock rate, and most ports already pick the requested rate or the nearest lower available rate. But several ports rounded to the nearest available rate (or worse), so the actual SPI clock could come out higher than requested and overclock a device. This PR makes the requested baudrate a ceiling consistently: the resulting clock is the requested rate or the nearest lower available rate, never higher.

Per-port status

Port Before Change
atmel-samd rounded to nearest ((f_ref/(2·baud)) − 0.5) ceiling divisor ceil(f_ref/(2·baud)) − 1 (in the samd-peripherals submodule)
espressif nearest (ESP-IDF spi_get_actual_clock, abs-error min) probe via spi_bus_add_devicespi_device_get_actual_freqspi_bus_remove_device, bisecting downward
cxd56 nearest (NuttX SPI_SETFREQUENCY overshoots at low freqs) bisect downward using the actual frequency it returns
analog overshoots (MXC_SPI_SetFrequency floors the divisor) bisect downward using MXC_SPI_GetFrequency readback
broadcom, mimxrt10xx, nordic, raspberrypi, silabs, stm already floor no change

None of the fixes depend on chip-specific divisor internals; each uses the port's public "set then read back the actual rate" path (or, for atmel-samd, the documented BAUD formula).

Submodule

The atmel-samd fix lives in samd-peripherals (adafruit/samd-peripherals#49, merged); this PR bumps the submodule pointer to pick it up.

Docs

shared-bindings/busio/SPI.c now documents the ceiling behavior on both the configure() baudrate parameter and the frequency attribute. [I reworked some of this to be less verbose and clearer. --@dhalbert]

Testing

  • atmel-samd — tested on Metro M4.
  • espressif — tested on ESP32-S3, ESP32-C6, and ESP32: no frequency exceeds the request.
  • cxd56 — tested on SPRESENSE (CXD5602): no frequency exceeds the request.
  • analognot tested on hardware (no MAX32xxx board available); the logic mirrors the cxd56/espressif fixes.

Known limitation: Zephyr

The zephyr-cp port is not addressed. Zephyr's struct spi_config only takes a requested frequency and provides no API to read back the achieved rate (zephyrproject-rtos/zephyr#77922), so the read-back-and-bisect approach used by the other ports is not available. Behavior is per-driver: nRF, STM32, Renesas RA8, and Smartbond already floor, but the RP2040/RP2350 PIO, NXP flexcomm (MCXN947/RW612), and NXP ecspi (MIMXRT1170) drivers round to nearest and can exceed the request. This is now documented as a limitation in the busio.SPI.configure() documentation.

dhalbert and others added 6 commits June 29, 2026 16:10
Bump the samd-peripherals submodule to pick up adafruit/samd-peripherals#49,
which makes busio.SPI treat the requested baudrate as a ceiling (the
resulting frequency is the requested value or the nearest lower available
value, never higher) instead of rounding to the nearest available baudrate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ESP-IDF driver rounds clock_speed_hz to the nearest achievable
frequency, which can be higher than requested and overclock a device.
Probe candidate targets through the public API (spi_bus_add_device ->
spi_device_get_actual_freq -> spi_bus_remove_device), bisecting downward
to find the highest achievable frequency that does not exceed the
requested baudrate, without depending on divisor internals that vary
between Espressif chips. The reported frequency is read back from the
configured device.

Tested on ESP32-S3, ESP32-C6, and ESP32.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The actual SPI clock is now set to the requested rate or the nearest
lower available rate, never higher. Update the configure() baudrate
parameter and the frequency attribute docstrings to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SPI_SETFREQUENCY() picks the nearest available frequency, which overshoots
the request at low frequencies (e.g. 100000 -> 102362). Treat baudrate as a
ceiling: when the driver overshoots, bisect downward on the requested target
for the highest available frequency that does not exceed baudrate. The driver
returns the actual frequency each call, so this does not depend on its divisor
internals.

Tested on SPRESENSE (CXD5602): no frequency exceeds the requested baudrate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MXC_SPI_SetFrequency() floors the clock divisor, so the actual SPI clock can
be higher than requested (e.g. a 30 MHz request on a 50 MHz peripheral clock
runs at the full 50 MHz). Treat baudrate as a ceiling: when the hardware
overshoots, bisect downward on the requested target for the highest rate that
does not exceed baudrate. MXC_SPI_GetFrequency() reports the actual configured
rate, so this does not depend on the divisor internals. Also store the actual
rate so the frequency property reports it instead of the requested value.

Not tested on hardware (no MAX32xxx board available); logic mirrors the
cxd56 and espressif ceiling fixes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tighten the configure() baudrate wording and document that on Zephyr the
ceiling behavior may be violated and the frequency attribute may not reflect
the actual baudrate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@tannewt tannewt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! I'm surprised so many ports simply rounded.

@tannewt tannewt merged commit e12cabb into adafruit:main Jun 30, 2026
670 checks passed
@dhalbert dhalbert deleted the SPI-clock-ceiling branch July 1, 2026 02:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make the specified frequency for busio.I2C be a ceiling, not an approximation

2 participants