feat(compile): ESP32-P4 and ESP32-C5 targets in the espidf lane

_idf_target mapped only c3/c6/s3 and defaulted everything else to
'esp32', so a P4 fqbn compiled with the XTENSA toolchain and
-DARDUINO_ESP32P4_DEV. Adds p4/c5 predicates, IDF5-target membership,
0x2000 bootloader offsets, the riscv toolchain requirement, PSRAM held
off until the engines validate it, P4 CPU-freq drops (its kconfig
choices do not overlap the classic set), a rev0 sdkconfig pin for the P4
(the in-browser engine runs the only published mask ROM and v3 firmware
reads a flash descriptor rev0 never wrote), and an honest refusal for C5
arduino builds (no core exists). Validated on staging: an esp32p4
arduino sketch now produces a merged image with the riscv32 toolchain
that the esp32p4js engine accepts.
This commit is contained in:
David Montero Crespo 2026-08-19 06:35:33 +02:00
parent c4860d4f23
commit 010a112bc8
1 changed files with 49 additions and 6 deletions

View File

@ -526,7 +526,7 @@ class ESPIDFCompiler:
# Targets that only exist in ESP-IDF v5.x — the pinned v4.4.7 tree
# predates them, so they build against idf5_path instead of idf_path.
_IDF5_TARGETS: frozenset[str] = frozenset({'esp32c6'})
_IDF5_TARGETS: frozenset[str] = frozenset({'esp32c6', 'esp32p4', 'esp32c5'})
# Flash offset where each chip's boot ROM expects the 2nd-stage
# bootloader. ESP32 / ESP32-S2 use 0x1000; every newer chip (S3, C2,
@ -535,6 +535,8 @@ class ESPIDFCompiler:
_BOOTLOADER_OFFSETS: dict[str, int] = {
'esp32': 0x1000,
'esp32s2': 0x1000,
'esp32p4': 0x2000,
'esp32c5': 0x2000,
}
def _is_esp32c3(self, board_fqbn: str) -> bool:
@ -546,6 +548,16 @@ class ESPIDFCompiler:
f = board_fqbn.lower()
return 'esp32c6' in f or 'esp32-c6' in f
def _is_esp32p4(self, board_fqbn: str) -> bool:
"""Return True if FQBN targets ESP32-P4 (RISC-V, IDF v5.x only)."""
f = board_fqbn.lower()
return 'esp32p4' in f or 'esp32-p4' in f
def _is_esp32c5(self, board_fqbn: str) -> bool:
"""Return True if FQBN targets ESP32-C5 (RISC-V, IDF v5.x only)."""
f = board_fqbn.lower()
return 'esp32c5' in f or 'esp32-c5' in f
def _idf_root(self, use_idf5: bool) -> str:
"""The ESP-IDF tree a compile builds against."""
return self.idf5_path if use_idf5 else self.idf_path
@ -599,6 +611,9 @@ class ESPIDFCompiler:
error rather than being pattern-translated into something it
isn't.
"""
if idf_target == 'esp32c5':
# No arduino-esp32 core supports the C5 yet — IDF-native only.
return False
if idf_target in self._IDF5_TARGETS:
return self.has_arduino5
return self.has_arduino or self.has_arduino5
@ -750,6 +765,10 @@ class ESPIDFCompiler:
return 'esp32c6'
if self._is_esp32s3(board_fqbn):
return 'esp32s3'
if self._is_esp32p4(board_fqbn):
return 'esp32p4'
if self._is_esp32c5(board_fqbn):
return 'esp32c5'
# Default to esp32 (Xtensa LX6) for the original ESP32 / ESP32-S2
return 'esp32'
@ -2237,7 +2256,7 @@ class ESPIDFCompiler:
# resolve. The old xtensa-only glob wrongly rejected RISC-V-only
# roots such as the v5.x install used for esp32c6.
xtensa_glob = 'tools/xtensa-esp32-elf/*/xtensa-esp32-elf/bin'
if idf_target in ('esp32c3', 'esp32c6'):
if idf_target in ('esp32c3', 'esp32c6', 'esp32p4', 'esp32c5'):
required_globs: tuple[str, ...] = (
'tools/riscv32-esp-elf/*/riscv32-esp-elf/bin',
)
@ -2553,8 +2572,11 @@ class ESPIDFCompiler:
# ESP32-C3 / ESP32-C6 have no external PSRAM controller — silently
# disable so a stale field from an upgraded project doesn't trip up
# the build.
if idf_target in ('esp32c3', 'esp32c6'):
# the build. P4/C5 modules DO carry PSRAM, but the in-browser engines
# run without it for now (the P4 engine models PSRAM as optional and
# firmware built with SPIRAM would fail honestly on a board that does
# not declare it) — keep it off until the engines validate it.
if idf_target in ('esp32c3', 'esp32c6', 'esp32p4', 'esp32c5'):
normalized['psram'] = 'disabled'
# ESP32-C3 / ESP32-C6 top out at 160 MHz — clamp the historical 240
@ -2615,7 +2637,7 @@ class ESPIDFCompiler:
drops: tuple[str, ...] = ()
if not arduino_mode:
drops += self._IDF5_PUREIDF_DROP_PREFIXES
elif idf_target in ('esp32c3', 'esp32c6'):
elif idf_target in ('esp32c3', 'esp32c6', 'esp32c5'):
# Arduino v5 on a single-core RISC-V chip: the running-core
# options are hidden by `depends on !FREERTOS_UNICORE`, so drop
# just those two (keep the rest of the Arduino/BT symbols).
@ -2623,11 +2645,20 @@ class ESPIDFCompiler:
'CONFIG_ARDUINO_RUNNING_CORE',
'CONFIG_ARDUINO_EVENT_RUNNING_CORE',
)
if idf_target in ('esp32c3', 'esp32c6'):
if idf_target in ('esp32c3', 'esp32c6', 'esp32c5'):
drops += (
'CONFIG_SPIRAM',
'CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240',
)
if idf_target == 'esp32p4':
# The P4's CPU-frequency kconfig choices (360/400) do not overlap
# the classic 240/160/80/40 set the template renders — drop the
# whole group and let IDF's own default stand. SPIRAM stays off
# (see _normalize_options).
drops += (
'CONFIG_SPIRAM',
'CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ',
)
if idf_target != 'esp32':
drops += ('CONFIG_ESPTOOLPY_FLASHFREQ_26M',)
return drops
@ -2738,6 +2769,18 @@ class ESPIDFCompiler:
rendered = self._fixup_sdkconfig_for_idf5(
rendered, idf_target, arduino_mode
)
if idf_target == 'esp32p4':
# The in-browser esp32p4js engine runs the ONLY published P4 mask
# ROM (rev0), and from silicon v3.0 IDF switches ROM symbol tables
# (esp32p4.rom.eco5.ld moves rom_spiflash_legacy_data) — firmware
# built for v3 reads a flash descriptor the rev0 ROM never wrote
# and rejects every partition. Pin the build to rev0 so images
# boot on the engine (project/esp32p4-js-emulator/STATUS.md).
rendered += (
'\n# Velxio: esp32p4js runs the rev0 mask ROM\n'
'CONFIG_ESP32P4_SELECTS_REV_LESS_V3=y\n'
'CONFIG_ESP32P4_REV_MIN_0=y\n'
)
return rendered
def _render_partition_csv(self, scheme: str) -> str: