Decide: how does the compiler learn WHICH ESP chip, and what does it derive?
- Type: decision (Track U) — naming + architecture, affects every ESP codegen decision from here on.
- Raised: 2026-08-11, scoping [[bug-a-riscv32-and-xtensa-have-no-atomic-codegen]]. That ticket asks for atomics on riscv32/xtensa and cannot be answered per-ISA, because the answer differs within riscv32 — which is what surfaced this.
The problem: the chip is currently IMPLIED by the ISA
There is no chip axis. --target=riscv32 means "ESP32-C3" by convention, and
the convention is written down in three unrelated places:
- the validation message says it outright —
--esp-profile=bare requires --target=riscv32 (esp32c3) or --target=xtensa (esp32s3)(compiler/compiler.pas); ESP_BARE_IRAM_BASEis the C3's SRAM map andESP_BARE_IRAM_BASE_XTis the S3's — two constants picked by ISA (compiler/defs.inc);compiler/rv32enc.incis headed "RV32IMC codegen" — the C3's exact ISA, with no AMO/LR-SC encoders.
That works while there is one chip per ISA. It breaks the moment a second riscv32 part appears, and it breaks in three independent ways at once: ISA extensions, core count, and memory map.
Today's axes
| axis | flag | values |
|---|---|---|
| ISA / backend | --target= |
riscv32, xtensa, x86_64, … |
| platform | --platform= |
esp | posix (derived from target, overridable) |
| profile | --esp-profile=bare |
bare metal vs IDF |
| xtensa ABI | --xtensa-abi= |
windowed | call0 |
What IDF calls things (borrow, do not invent)
idf.py set-target <x> and CONFIG_IDF_TARGET_<X> use one lowercase token, no
separator: esp32, esp32s2, esp32s3, esp32c2, esp32c3, esp32c6,
esp32h2, esp32p4. Every ESP developer already types these, and an IDF
wrapper can pass $IDF_TARGET through untranslated.
Borrowing verbatim also survives Espressif renaming their own scheme — an
invented short form (--esp=c3) assumes the esp32 prefix is a constant, and
esp32p4 already shows the prefix is a brand rather than a family descriptor.
A future 64-bit part need not be esp32* at all.
The fork: WHERE does the chip live?
(A) A separate axis — --esp-chip=esp32c3, alongside platform/profile,
defaulting per ISA to today's assumption.
- keeps
--targetmeaning "backend", which is what it means everywhere else and what CLAUDE.md's track model assumes; - but
--esp-chip=esp32c3says "esp" twice, and the ISA and the chip can then be set to contradict each other (--target=xtensa --esp-chip=esp32c3), so it needs a validation rule that (B) makes structurally impossible.
(B) SoC targets in the existing namespace — --target=esp32c6 (recommended).
- one namespace, no redundancy, reads like
idf.py set-targetand like every other toolchain's-mcpu; - the SoC target IMPLIES arch +
platform=esp+ the capability row, so the contradiction in (A) cannot be expressed; - keeps
--target=riscv32and--target=xtensaas the generic forms: riscv32 is genuinely dual-role today (bare C3 or hosted linux under qemu-user, which the cross-test infrastructure uses), so the generic ISA targets must survive. They keep meaning exactly what they mean now — riscv32 → C3 caps, xtensa → S3 caps — so no existing command line changes behaviour.
The half that matters more than the name: a CAPABILITY TABLE
Whichever spelling wins, codegen must consult capabilities, not chip names. One table, and every decision site asks it:
| chip | ISA | cores | atomic primitive |
|---|---|---|---|
| esp32c3 | RV32IMC | 1 | interrupt mask (no A) |
| esp32c2 | RV32IMC | 1 | interrupt mask (no A) |
| esp32c6 / esp32h2 | RV32IMAC | 1 | AMO / LR-SC |
| esp32p4 | RV32IMAFC | 2 | AMO / LR-SC |
| esp32s2 | Xtensa LX7 | 1 | S32C1I |
| esp32 / esp32s3 | Xtensa LX6/LX7 | 2 | S32C1I |
The alternative is if chip = esp32c3 … else if esp32c6 … spreading across the
backends, the memory map and the peripheral bases — N mechanisms for one
concept, and far harder to unpick later than to set up now
(devdocs/dev/normalise-dont-special-case.md).
Capabilities the table owes its callers on day one: SocCoreCount,
SocHasAtomicISA, SocIramBase / SocStackTop (already hardcoded per ISA
today), SocUartBase (already assumed identical "on both").
What it immediately unblocks
Atomics stops being a special case and falls out of the table:
- has atomic ISA → emit it (
S32C1Iretry loop on xtensa; AMO / LR-SC on riscv-with-A); - no atomic ISA, 1 core → interrupt-masked critical section (what ESP-IDF does on the C3);
- no atomic ISA, 2 cores → refuse honestly — and no ESP part is in that box, so the table proves that gap cannot be silently wrong.
Note the single-core row is not "atomics unnecessary": FreeRTOS preempts tasks
on one core, so a bare n := n + 1 still tears. Single-core means the CHEAP
primitive suffices, not that none is needed.
Recommendation
(B), borrowing IDF's spelling verbatim, plus the capability table — with the
generic riscv32 / xtensa targets kept as today's defaults so nothing
existing moves. The table is the part worth insisting on; the flag spelling is
reversible, a chip-name conditional sprayed through five files is not.
Gate (for whoever implements the decision)
--target=esp32c3 and --target=riscv32 producing byte-identical output;
likewise esp32s3 vs xtensa; the capability table consulted (not chip names)
at every site that currently hardcodes a C3/S3 fact; self-host byte-identical.
DECIDED 2026-08-11 (user): option (B) — IDF spelling, SoC targets, capability table
"good. let's file that. … maybe IDF has a naming scheme that we can borrow here. but then it's by like
--target=esp32c6etc." … "yes, so this is decided right" — user
So, confirmed:
- Chip names are IDF's, verbatim —
esp32,esp32s2,esp32s3,esp32c2,esp32c3,esp32c6,esp32h2,esp32p4. We do not invent a short form;idf.py set-targetand$IDF_TARGETpass through untranslated, and nothing assumes theesp32prefix is permanent. - They live in the
--target=namespace, not a second--esp-chip=axis. A SoC target implies arch +platform=esp+ its capability row, so an ISA/chip contradiction cannot be expressed. --target=riscv32and--target=xtensastay, meaning exactly what they mean today (C3 caps and S3 caps respectively) — riscv32 is dual-role with hosted linux under qemu-user, which the cross-test infrastructure depends on. No existing command line changes behaviour.- Codegen consults CAPABILITIES, never chip names. This is the load-bearing half: the flag spelling is reversible, a chip-name conditional spread across the backends, the memory map and the peripheral bases is not.
Implementation order
The capability table first, as the single home for facts that are currently
hardcoded per ISA (ESP_BARE_IRAM_BASE / _XT, the UART base, the implied core
count), then the SoC targets that select a row, then the consumers.
bug-a-riscv32-and-xtensa-have-no-atomic-codegen is the first consumer and is
partly independent: the xtensa half needs no chip gate (S32C1I is on every
LX6/LX7 part) and can land before or after this. The riscv32 half is the one
that needs the table, because the answer differs within the ISA.
Implemented 2026-08-11 — the table and the targets (first slice)
Landed, in the decided order:
- The capability table (
compiler/defs.inc): SoC ids with IDF's spellings,SocFromName/SocName, and the accessorsSocIsXtensa,SocCoreCount,SocHasAtomicISA,SocIramBase. Each row carries its hardware fact in a comment, and the table makes explicit that no ESP part is in the "no atomic ISA AND 2 cores" box — the combination with no correct primitive. - SoC targets —
--target=esp32c3…--target=esp32p4, parsed in the existing namespace, implying the ISA.--target=riscv32/--target=xtensadefault toesp32c3/esp32s3, which is exactly what they have always meant; that default is now stated ONCE instead of being implied in three unrelated places. - First consumer converted: the ELF writer asked
TargetArch = TARGET_XTENSAto choose between two IRAM constants; it asksSocIramBase(TargetSoc)now. - The
--esp-profile=barevalidation message no longer hardcodes "riscv32 (esp32c3) or xtensa (esp32s3)".
Equivalence is asserted in the Makefile, not just measured once: the same
source built as riscv32 and as esp32c3 must cmp equal, likewise
xtensa/esp32s3 under --esp-profile=bare. If those ever diverge, the table
has started disagreeing with the constants it replaced.
One measurement correction worth recording: a first pass "verified" the xtensa
equivalence with a loop that left STALE outputs in place when a compile failed,
so cmp compared the previous iteration's riscv files and reported success.
Hosted xtensa does not compile this program at all — pre-existing, "external
(dynamic) symbols not yet supported", and it fails identically for both
spellings. The real check requires both outputs to exist.
Still to do
SocCoreCount and SocHasAtomicISA have no consumer yet — they exist for
[[bug-a-riscv32-and-xtensa-have-no-atomic-codegen]], which is the next slice and
whose xtensa half needs no chip gate. Facts still hardcoded per ISA elsewhere
(the shared ESP_BARE_STACK_TOP, the UART base named only in a comment) should
migrate to the table as they acquire a second value.