diff --git a/build_mp.py b/build_mp.py index be0d87cc..464fac7f 100755 --- a/build_mp.py +++ b/build_mp.py @@ -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") diff --git a/docs/guides/add-a-board-variant.md b/docs/guides/add-a-board-variant.md index b23eca35..7e45ec20 100644 --- a/docs/guides/add-a-board-variant.md +++ b/docs/guides/add-a-board-variant.md @@ -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 diff --git a/docs/reference/boards.md b/docs/reference/boards.md index fedd3ac9..49224bc8 100644 --- a/docs/reference/boards.md +++ b/docs/reference/boards.md @@ -17,7 +17,7 @@ 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` | @@ -25,6 +25,13 @@ top. Add `--modules` to any of these: | 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 @@ -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. | diff --git a/tests/test_build_mp.py b/tests/test_build_mp.py index 63adab0b..45f86575 100644 --- a/tests/test_build_mp.py +++ b/tests/test_build_mp.py @@ -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() @@ -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): diff --git a/variants/esp32/ESP32_GENERIC/QTPY_PICO/sdkconfig b/variants/esp32/ESP32_GENERIC/QTPY_PICO/sdkconfig index 021dd329..51c96e25 100644 --- a/variants/esp32/ESP32_GENERIC/QTPY_PICO/sdkconfig +++ b/variants/esp32/ESP32_GENERIC/QTPY_PICO/sdkconfig @@ -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 diff --git a/variants/esp32/sdkconfig.esp32s3 b/variants/esp32/sdkconfig.esp32s3 index e901fc2b..208e9fd5 100644 --- a/variants/esp32/sdkconfig.esp32s3 +++ b/variants/esp32/sdkconfig.esp32s3 @@ -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