Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 68 additions & 12 deletions build_mp.py
Original file line number Diff line number Diff line change
Expand Up @@ -865,24 +865,80 @@ def mhz(pattern):
mhz(r"^CONFIG_SPIRAM_SPEED_(\d+)M=y$") if re.search(r"^CONFIG_SPIRAM=y$", sdk, re.M) else None)


# The highest CPU clock each chip is rated for, in MHz: the top of ESP-IDF's
# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ choice for that target, which matches the
# datasheets. esp32_cpu_rated() covers the exceptions.
ESP32_CPU_RATED = {"esp32": 240, "esp32s2": 240, "esp32s3": 240, "esp32c2": 120, "esp32c3": 160,
"esp32c5": 240, "esp32c6": 160, "esp32c61": 160, "esp32h2": 96, "esp32p4": 400}


def esp32_cpu_rated(sdk):
"""The highest CPU clock, in MHz, for the chip an esp32 sdkconfig builds
for, or None when the target isn't one we know.

A P4 image for revisions before 3.0 is held to their 360 MHz (IDF offers
400 there only as an experimental option). A single-core classic ESP32
build is held to 160, the rating of the ESP32-S0WD and ESP32-D2WD it is
usually for. A C5 with flash encryption is held to 160: IDF says encrypted
flash writes aren't stable above it."""
m = re.search(r'^CONFIG_IDF_TARGET="(\w+)"$', sdk, re.M)
target = m.group(1) if m else ""
rated = ESP32_CPU_RATED.get(target)

def on(name):
return re.search(rf"^CONFIG_{name}=y$", sdk, re.M) is not None

if target == "esp32p4" and on("ESP32P4_SELECTS_REV_LESS_V3"):
rated = 360
elif target == "esp32" and on("FREERTOS_UNICORE"):
rated = 160
elif target == "esp32c5" and on("SECURE_FLASH_ENC_ENABLED"):
rated = 160
return rated


def esp32_cpu(sdk):
"""The CPU clock an esp32 sdkconfig sets and the chip's rated one, in MHz
(either None when the sdkconfig doesn't say)."""
m = re.search(r"^CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=(\d+)$", sdk, re.M)
return (int(m.group(1)) if m else None), esp32_cpu_rated(sdk)


def esp32_speed_note(sdk):
"""Flash and PSRAM should run at the fastest clock the chips are rated for.
Below 80 MHz, code and data that miss the cache wait on a slow bus, and on
a classic ESP32 that starves Bluetooth audio: two boards that fell behind in
A2DP music at 40 MHz played clean at 80 MHz. Say so loudly after a build."""
"""The CPU, flash and PSRAM should run at the fastest clock the chips are
rated for. Below 80 MHz, code and data that miss the cache wait on a slow
bus, and on a classic ESP32 that starves Bluetooth audio: two boards that
fell behind in A2DP music at 40 MHz played clean at 80 MHz. A CPU below its
rating runs out of time first: a 48 kHz synth with a reverb clicked on a
classic ESP32 at 160 MHz and played clean at 240. Say so loudly after a
build."""
flash, psram = esp32_clocks(sdk)
slow = [f"flash at {flash} MHz"] if flash and flash < 80 else []
cpu, rated = esp32_cpu(sdk)
# An image that also runs on a revision 0 P4 can't clock its flash above
# 40 MHz: IDF offers no faster choice there.
flash_floor = 40 if re.search(r"^CONFIG_ESP32P4_REV_MIN_0=y$", sdk, re.M) else 80
slow = [f"the CPU at {cpu} MHz (rated for {rated})"] if cpu and rated and cpu < rated else []
slow += [f"flash at {flash} MHz"] if flash and flash < flash_floor else []
slow += [f"PSRAM at {psram} MHz"] if psram and psram < 80 else []
if not slow:
return
runs = slow[0] if len(slow) == 1 else ", ".join(slow[:-1]) + " and " + slow[-1]
say("\n" + "!" * 72)
say(f" This image runs {' and '.join(slow)}. Run flash and PSRAM at the fastest")
say(" clock your board's chips are rated for: most are rated for 80 MHz or more,")
say(" and at 40 MHz a classic ESP32 can fall behind in Bluetooth audio. Check")
say(" the chips' datasheets (esptool's flash-id reads the flash's JEDEC ID), then")
say(" set CONFIG_ESPTOOLPY_FLASHFREQ_80M=y and CONFIG_SPIRAM_SPEED_80M=y in the")
say(" board's variant sdkconfig, as variants/esp32/ESP32_GENERIC/QTPY_PICO does.")
say(" The flash clock lives in the bootloader: flash it with the app.")
say(f" This image runs {runs}.")
say(" Run the CPU, flash and PSRAM at the fastest clock the board's chips are")
say(" rated for, in the board's variant sdkconfig.")
if cpu and rated and cpu < rated:
say(f" The CPU: set CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_{rated}=y,")
say(f" CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_{cpu}=n and CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ={rated},")
say(" as variants/esp32/ESP32_GENERIC/AUDIO_KIT does. Below its rating, audio")
say(" work runs out of time: a synth with a reverb clicks at 160 MHz on an ESP32.")
if (flash and flash < flash_floor) or (psram and psram < 80):
say(" Flash and PSRAM: most are rated for 80 MHz or more, and at 40 MHz a")
say(" classic ESP32 can fall behind in Bluetooth audio. Check the chips'")
say(" datasheets (esptool's flash-id reads the flash's JEDEC ID), then set")
say(" CONFIG_ESPTOOLPY_FLASHFREQ_80M=y and CONFIG_SPIRAM_SPEED_80M=y, as")
say(" variants/esp32/ESP32_GENERIC/QTPY_PICO does. The flash clock lives in")
say(" the bootloader: flash it with the app.")
say("!" * 72 + "\n")


Expand Down
31 changes: 23 additions & 8 deletions docs/guides/add-a-board-variant.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,29 @@ adds a coredump partition on the S3, which a standard build leaves unused).
Without one, the board's own is used. Either way, autosize grows the app
partition when the image doesn't fit.

Set flash and PSRAM to the fastest clock the board's chips are rated for,
usually 80 MHz, in the variant's `sdkconfig`
([`QTPY_PICO`'s](https://github.com/PyDevices/micropython-pydevices/blob/main/variants/esp32/ESP32_GENERIC/QTPY_PICO/sdkconfig)
is two lines). Upstream's generic boards run them at 40 MHz, and at that
clock a classic ESP32 can fall behind in Bluetooth audio. Read the flash's
JEDEC ID with `esptool.py flash-id` and check its datasheet first. The flash
clock lives in the bootloader, so write the bootloader with the app. A build
below 80 MHz says so when it finishes.
Run the CPU, flash and PSRAM at the fastest clocks the board's chips are
rated for. The CPU's rating is the chip's: 240 MHz for an ESP32, S2, S3 or C5,
160 for a C3 or C6, 400 for a P4 of revision 3 or later and 360 before that.
Every S3 build already runs at 240
([`variants/esp32/sdkconfig.esp32s3`](https://github.com/PyDevices/micropython-pydevices/blob/main/variants/esp32/sdkconfig.esp32s3)),
and a P4 build runs at its revision's rating by default. A classic ESP32
board sets 240 in the variant's `sdkconfig`, as
[`AUDIO_KIT`'s](https://github.com/PyDevices/micropython-pydevices/blob/main/variants/esp32/ESP32_GENERIC/AUDIO_KIT/sdkconfig)
does: upstream's default is 160 MHz, and at 160 a 48 kHz synth with a reverb
runs out of time and clicks. The ESP32-S0WD and ESP32-D2WD are the exceptions,
rated for 160.

Flash and PSRAM are usually rated for 80 MHz, which the variant's `sdkconfig`
sets in two lines
([`QTPY_PICO`'s](https://github.com/PyDevices/micropython-pydevices/blob/main/variants/esp32/ESP32_GENERIC/QTPY_PICO/sdkconfig)).
Upstream's generic boards run them at 40 MHz, and at that clock a classic
ESP32 can fall behind in Bluetooth audio. Read the flash's JEDEC ID with
`esptool.py flash-id` and check its datasheet first. The flash clock lives in
the bootloader, so write the bootloader with the app. A P4 image for chips
before revision 3 keeps its flash at 40 MHz, the most ESP-IDF offers there.

A build with the CPU below its chip's rating, or flash or PSRAM below 80 MHz,
says so when it finishes, with the lines to set.

## An rp2 board

Expand Down
11 changes: 9 additions & 2 deletions docs/reference/boards.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,21 @@ top. Add `--modules` to any of these:
| LilyGO T-Embed S3 | `--port esp32 --board ESP32_GENERIC_S3 --variant T_EMBED --flash 16MB` |
| Ai-Thinker ESP32-Audio-Kit (ESP32-A1S), CPU at 240 MHz, flash and PSRAM at 80 MHz | `--port esp32 --board ESP32_GENERIC --variant AUDIO_KIT --flash 4MB` (write the bootloader with the app) |
| WT32-SC01 Plus | `--port esp32 --board ESP32_GENERIC_S3 --flash 16MB` |
| Adafruit QT Py ESP32 Pico (8 MB flash, 2 MB PSRAM), flash and PSRAM at 80 MHz | `--port esp32 --board ESP32_GENERIC --variant QTPY_PICO --flash 8MB` (write the bootloader with the app) |
| Adafruit QT Py ESP32 Pico (8 MB flash, 2 MB PSRAM), CPU at 240 MHz, flash and PSRAM at 80 MHz | `--port esp32 --board ESP32_GENERIC --variant QTPY_PICO --flash 8MB` (write the bootloader with the app) |
| Raspberry Pi Pico (RP2040) | `--port rp2 --board RPI_PICO` (not `--modules all`: [it doesn't fit](../guides/fit-firmware-on-a-small-flash.md)) |
| Raspberry Pi Pico 2 (RP2350) | `--port rp2 --board RPI_PICO2` |
| Pimoroni Pico Plus 2 W (16 MB flash, 8 MB PSRAM, 48 GPIOs) | `--port rp2 --board RPI_PICO2_W --variant PIMORONI_PICO_PLUS2_W` |
| Arduino GIGA R1 WiFi | `--port stm32 --board ARDUINO_GIGA` (past 1,280 KB it [takes the M4 core's flash](../guides/fit-firmware-on-a-small-flash.md)) |
| Arduino GIGA R1 WiFi, playing through its headphone jack ([`audioio.AudioOut`](https://github.com/PyDevices/micropython-pydevices/blob/main/modules/audioif/README.md#audioioaudioout--the-same-output-on-a-dac)) | `--port stm32 --board ARDUINO_GIGA --modules audiodsp,audioif` |
| Arduino GIGA R1 WiFi with the GIGA Display Shield | `--port stm32 --board ARDUINO_GIGA --variant DISPLAY_SHIELD --modules displayif,lvgl` ([two images to flash](../guides/flash-a-board.md#stm32-arduino-giga-r1-wifi)) |

Every ESP32 board here runs its CPU at the chip's rated clock (240 MHz on an
ESP32 or S3; 400 on a P4 of revision 3 or later, 360 before that), and its
flash and PSRAM at the fastest clock its chips are rated for: 80 MHz or more,
except the flash of a P4 image for revisions before 3, which ESP-IDF holds to
40 MHz. [Add a board variant](../guides/add-a-board-variant.md#an-esp32-board)
says how a new board does the same.

Our ESP32-S3 builds need an S3 with PSRAM: they keep the TLS library in PSRAM,
and on an S3 without it, Wi-Fi can't join a WPA2 network. Most S3 boards have
PSRAM. For one that doesn't, such as Adafruit's QT Py ESP32-S3 with 8 MB flash
Expand All @@ -47,7 +54,7 @@ Our variants, in [`variants/`](https://github.com/PyDevices/micropython-pydevice
| esp32 | `ESP32_GENERIC_S3` `LCD_4_3`, `LCD_7`, `T_EMBED` | The S3 boards in the table above. |
| esp32 | `ESP32_GENERIC_P4` `WIFI6_DEV_KIT`, `WIFI6_DEV_KIT_H2` | The P4 DEV-KIT, with the USB device on the two different connectors in the table above. |
| esp32 | `ESP32_GENERIC_P4` `P4X_EYE` | The ESP32-P4X-EYE: `C6_WIFI` with ESP-Hosted on the pins its C6 uses, the OV2710 sensor driver, the USB device on the port marked "USB 2.0" and the REPL on "Debug", and a 9 MB app slot for the camera and vision modules. |
| esp32 | `ESP32_GENERIC` `QTPY_PICO` | The QT Py ESP32 Pico: upstream's PSRAM variant with flash and PSRAM at 80 MHz, which keeps Bluetooth audio from falling behind. |
| esp32 | `ESP32_GENERIC` `QTPY_PICO` | The QT Py ESP32 Pico: upstream's PSRAM variant with flash and PSRAM at 80 MHz, which keeps Bluetooth audio from falling behind, and the CPU at 240 MHz. |
| rp2 | `RPI_PICO2_W` `PIMORONI_PICO_PLUS2_W` | The Pimoroni Pico Plus 2 W. |
| stm32 | `ARDUINO_GIGA` `DISPLAY_SHIELD` | The GIGA R1 WiFi with room for LVGL and the shield's MIPI DSI panel: the Wi-Fi and Bluetooth firmware moved to QSPI flash as a second image, Bluetooth LE through NimBLE, a one-dimensional ulab with numpy's array core and elementwise maths only ([what's out](https://github.com/PyDevices/micropython-pydevices/blob/main/variants/stm32/ARDUINO_GIGA/DISPLAY_SHIELD/ulab_config.h)), LVGL's 40 px font left out, and USB through TinyUSB: `machine.USBDevice` on the USB-C port and a USB host on the USB-A port, where bus-powered devices that draw more than about 500 mA may not run; use a powered hub. |

Expand Down
37 changes: 36 additions & 1 deletion tests/test_build_mp.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ def test_module_without_a_lock_row(self):


class Esp32Speed(unittest.TestCase):
"""A build that leaves flash or PSRAM below 80 MHz says so after it."""
"""A build that leaves the CPU below its chip's rating, or flash or PSRAM
below 80 MHz, says so after it."""

def note(self, sdk):
out = io.StringIO()
Expand All @@ -105,6 +106,40 @@ def test_slow_flash_alone(self):
self.assertIn("flash at 40 MHz.", out)
self.assertNotIn("PSRAM at 40", out)

def test_cpu_below_rating_is_named_with_the_lines_to_set(self):
sdk = 'CONFIG_IDF_TARGET="esp32s3"\nCONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=160\nCONFIG_ESPTOOLPY_FLASHFREQ_80M=y\n'
out = self.note(sdk)
self.assertIn("the CPU at 160 MHz (rated for 240).", out)
self.assertIn("CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y", out)
self.assertIn("CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=240", out)
self.assertNotIn("FLASHFREQ_80M", out)

def test_cpu_with_slow_flash_and_psram(self):
sdk = ('CONFIG_IDF_TARGET="esp32"\nCONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=160\n'
"CONFIG_ESPTOOLPY_FLASHFREQ_40M=y\nCONFIG_SPIRAM=y\nCONFIG_SPIRAM_SPEED_40M=y\n")
self.assertIn("the CPU at 160 MHz (rated for 240), flash at 40 MHz and PSRAM at 40 MHz.", self.note(sdk))

def test_cpu_at_rating_is_quiet(self):
for target, mhz in (("esp32", 240), ("esp32s3", 240), ("esp32c3", 160), ("esp32c6", 160),
("esp32c2", 120), ("esp32h2", 96), ("esp32p4", 400)):
sdk = f'CONFIG_IDF_TARGET="{target}"\nCONFIG_ESP_DEFAULT_CPU_FREQ_MHZ={mhz}\nCONFIG_ESPTOOLPY_FLASHFREQ_80M=y\n'
self.assertEqual(self.note(sdk), "", target)

def test_p4_before_revision_3_is_rated_360(self):
sdk = 'CONFIG_IDF_TARGET="esp32p4"\nCONFIG_ESP32P4_SELECTS_REV_LESS_V3=y\nCONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=360\n'
self.assertEqual(self.note(sdk), "")
sdk = 'CONFIG_IDF_TARGET="esp32p4"\nCONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=360\n'
self.assertIn("the CPU at 360 MHz (rated for 400)", self.note(sdk))

def test_p4_image_for_revision_0_keeps_flash_at_40(self):
sdk = ('CONFIG_IDF_TARGET="esp32p4"\nCONFIG_ESP32P4_REV_MIN_0=y\nCONFIG_ESP32P4_SELECTS_REV_LESS_V3=y\n'
"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=360\nCONFIG_ESPTOOLPY_FLASHFREQ_40M=y\nCONFIG_SPIRAM=y\nCONFIG_SPIRAM_SPEED_200M=y\n")
self.assertEqual(self.note(sdk), "")

def test_single_core_esp32_is_rated_160(self):
sdk = 'CONFIG_IDF_TARGET="esp32"\nCONFIG_FREERTOS_UNICORE=y\nCONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=160\n'
self.assertEqual(self.note(sdk), "")


class ModulesMoved(unittest.TestCase):
def test_nothing_moved(self):
Expand Down
7 changes: 7 additions & 0 deletions variants/esp32/ESP32_GENERIC/QTPY_PICO/sdkconfig
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,10 @@ CONFIG_ESPTOOLPY_FLASHFREQ_80M=y
CONFIG_ESPTOOLPY_FLASHFREQ_40M=n
CONFIG_SPIRAM_SPEED_80M=y
CONFIG_SPIRAM_SPEED_40M=n

# The CPU at 240 MHz, the ESP32-PICO-V3-02's rating, where upstream's default
# is 160. A 48 kHz synth with a reverb on a classic ESP32 needs about 5 ms of
# each 5.3 ms block at 160 MHz and clicks; at 240 it needs about 3.5 ms.
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160=n
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=240
9 changes: 9 additions & 0 deletions variants/esp32/sdkconfig.esp32s3
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC=y
# No core dump to flash in a standard build; ./build_mp.py --coredump makes a
# troubleshooting build that has one (sdkconfig.coredump beside this file).

# The CPU at 240 MHz, the rating of every ESP32-S3. Upstream's octal-PSRAM
# variant already sets it, but its generic S3 build (quad PSRAM) runs at 160.
# Audio runs out of time first: a 48 kHz synth with a reverb needs about 5 ms
# of each 5.3 ms block on a classic ESP32 at 160 MHz and clicks, and about
# 3.5 ms at 240.
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160=n
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=240

# More lwIP sockets. spremote alone holds the AP session, the dealer's TLS
# websocket, mDNS and the pairing listener, and every pairing-HTTP client, DNS
# lookup and phone poll takes one more. With the default 10, the pairing server
Expand Down
Loading