Skip to content

ESP32 on the machine framework: LX6 cores, SoC models, DevKitC V4, ESP-IDF boots - #116

Merged
teo1747 merged 16 commits into
feat/esp32-dual-core-targetfrom
claude/dreamy-archimedes-v28ly5
Nov 8, 2026
Merged

teo1747 merged 16 commits into
feat/esp32-dual-core-targetfrom
claude/dreamy-archimedes-v28ly5

Conversation

@teo1747

@teo1747 teo1747 commented Nov 8, 2026 •

Copy link
Copy Markdown
Member

Finishes the ESP32 work started in #115 by bringing the chip into the catalog and the machine framework, so the ESP32 runs like the other boards.

What it adds

  • Two Xtensa LX6 cores (core/mcu/xtensa/lx6.c, lx6fp.c, backend xtensa), configured as Espressif's core-isa.h describes:
    • windowed registers (overflow checks, CALL4/8/12, ENTRY, RETW and underflow, MOVSP, ROTW), zero-overhead loops
    • XEA2 exceptions, 32 interrupts at their levels and types, the CCOMPARE timers against CCOUNT
    • MAC16, booleans, the single-precision FPU, region protection, the debug option (IBREAK, DBREAK watchpoints as ESP-IDF sets them)
    • the double-precision accelerator's user registers
    • APP_CPU is held until DPORT releases it. It sees the ROM with its ROM-data pointers moved +0x3F10, which equals Espressif QEMU's esp32-v3-rom-app.bin byte for byte.
  • espressif/esp32:
    • memory map
    • ROM image built from esp32_rev300_rom.elf (equal to QEMU's esp32-v3-rom.bin)
    • register file from esp32.svd, with the SVD's overlapping fields resolved (svd2regs.py --resolve-overlaps, new) and the RMT RAM added
    • models: DPORT (APP_CPU, caches and flash MMU, interrupt matrix, PERIP_RST_EN for I2C0/1 and LEDC), RTC_CNTL, eFuse (ESP32-D0WD-V3 v3.1 factory bits), APB_CTRL clocks, GPIO/IO_MUX with strapping, UART0-2 (with MEM_RX_STATUS), TIMG0/1, SPI0/1 with the GT25Q32A flash, SHA, LEDC, I2C0/1, RMT, the SAR ADCs through SENS, the DACs through RTC_IO, the internal analog I2C, RNG, and the AHB view
    • supply currents from datasheet v5.3's power modes; French text
  • emblink/esp32_devkitc (ESP32-DevKitC V4, WROOM-32E), built from Espressif's schematic: power path, LED, Boot/EN buttons, headers, and three fixtures.
  • Schema: the xtensa backend, ports up to 64 pins (the ESP32's 40-GPIO bank); the gcc-xtensa toolchain profile.
  • Runtime: an ADC reads a pin's own DAC on a net with no analog part (pin_volts gave the digital level).

Evidence

  • ESP-IDF v5.4's examples run unmodified on the DevKitC (tests/vendor/esp-idf, esp32_*):
    • esp32_hello_world 9/9: ROM banner, chip revision: v3.1, Multicore app, 160 MHz, Hello world!, esp_restart → rst:0xc (SW_CPU_RESET)
    • esp32_blink 6/6, esp32_ledc_basic 2/2 (4 kHz on GPIO5)
    • esp32_i2c_eeprom 3/3: a 24LC256 through I2C0 at 400 kHz
    • esp32_gptimer 17/17, esp32_uart_echo 6/6 (UART2), esp32_real_time_stats 10/10 (both cores)
    • esp32_blink_strip 7/7: a WS2812B through the RMT
    • esp32_adc_oneshot_read 6/6: ADC1 and ADC2 with the eFuse Vref calibration (1.1 V → 1118 → 1100 mV)
    • esp32_dac_oneshot 4/4: both DACs read back by ADC2
  • Arduino-ESP32 3.2.0's sketches for "ESP32 Dev Module" (tests/vendor/arduino-esp32, ESP32_*): AnalogReadSerial, ReadAnalogVoltage, DigitalReadSerial, GetChipID, RepeatTimer.
  • test_lx6: assembled by Espressif's assembler (gen_lx6_programs.py), 0 failures; covers recursion through the ROM's own window vectors, APP_CPU's separate ROM data and a DBREAK watchpoint.
  • dev, ci-clang (-Wconversion -Wsign-conversion -Werror), ci-tcc and MSVC (/W4 /WX) build clean.
  • check_docs, check_banned, validate.py --all and catalog_text.py check pass, and coverage is regenerated.
  • Gallery: docs/gallery/esp32-devkitc.

Not yet

These are declared in the description's fidelity:

  • Wi-Fi and Bluetooth
  • the double-precision accelerator's instructions (their encodings are public, their semantics are not; the run stops with a diagnostic)
  • I2S, SPI2/3, MCPWM, PCNT, TWAI, touch, ULP, AES, RSA
  • the DACs' cosine generator and DMA path; the ADC's nonlinearity above about 2.45 V at 11 dB; I2C's non-FIFO mode
  • PSRAM, sleep

…ots ESP-IDF

Two LX6 cores (core/mcu/xtensa/lx6.c, lx6fp.c) as core-isa.h configures
the ESP32: windowed registers, loops, XEA2 exceptions, seven interrupt
levels, CCOUNT/CCOMPARE timers, MAC16, booleans, single-precision FPU,
region protection, the debug option; backend "xtensa" in build.c.

The ESP32 description (catalog/mcus/espressif/esp32): the memory map,
the ROM image built from Espressif's esp32_rev300_rom.elf (equal to
QEMU's esp32-v3-rom.bin), the register file from esp32.svd, and models
for DPORT (APP_CPU control, caches and flash MMU, interrupt matrix, the
APP_CPU's own ROM view), RTC_CNTL, eFuse, APB_CTRL clocks, GPIO/IO_MUX
with strapping, UART0-2, TIMG0/1, SPI0/1 and the GT25Q32A flash, SHA,
the internal analog I2C, RNG and the AHB view.

Espressif's ROM, ESP-IDF v5.4's second-stage bootloader and hello_world
run unmodified to Hello world!
- emblink/esp32_devkitc: the DevKitC V4's power path (BAT760, AMS1117-3.3),
  power LED, Boot and EN buttons with EN's RC, both 19-pin headers, from
  Espressif's schematic; three fixtures
- test_lx6: programs assembled by Espressif's assembler
  (gen_lx6_programs.py -> lx6_programs.h) check the core's instructions,
  windowed recursion through the ROM's window vectors, exceptions, the
  timer interrupt, FPU, MAC16, and the APP_CPU's ROM data
- tests/vendor/esp-idf: esp32_hello_world built for the esp32 target
- the description's fidelity and provenance, CHANGELOG, STATUS, coverage
- a zero-overhead loop is not taken back when its last instruction
  branches to LEND (the ROM's strcmp); the DFP accelerator's user
  registers; no debug hooks in the core
The GPIO LED on GPIO2 (sdkconfig.defaults), an LED and 330 ohm on IO2:
gpio_set_level through the matrix every second, 6/6.
run.py skips an example whose toolchain is missing rather than every
example, and CI installs xtensa-esp32-elf with riscv32-esp-elf so the
esp32_* examples run on the ESP32-DevKitC.
Eight high-speed and eight low-speed channels on four timers each
(core/mcu/esp32/soc/ledc.c, the ESP32-C3's model on the ESP32's
registers): REF_TICK, APB_CLK or SLOW_CLK over the 10.8 divider, up to 20
bits of duty, high-speed settings at the next overflow and low-speed ones
after PARA_UP, fading, the overflow and fade interrupts, the matrix's
ledc_hs/ls_sig_out. ledc_basic's 4 kHz on GPIO5: 2/2.
Datasheet v5.3 table 4-2: Modem-sleep of the dual-core chips at 80, 160
and 240 MHz (the range's low end waiting, high end running), 1 uA held;
table 5-4's radio currents for when the radios are modelled.
The ESP32's I2C controllers from the C3's bit-level model on the ESP32's
registers: 16 commands with its op codes (RSTART 0, WRITE 1, READ 2,
STOP 3, END 4), the bus timing in APB_CLK cycles as i2c_ll counts it
(SCL high + 7, or the filter's share), ACK_ERR, arbitration, the SCL
timeout, the FIFOs through DATA and its AHB view, master and slave.
The GPIO model gains the signal watch the controller follows the lines
with, and DPORT's PERIP_RST_EN resets I2C0, I2C1 and LEDC.

ESP-IDF v5.4's i2c_eeprom passes unmodified on the DevKitC with a
24LC256 (esp32_i2c_eeprom 3/3).
tools/validate.py rejected the ESP32's description: the schema had no
xtensa backend and capped a port at 32 pins (the ESP32's GPIO bank has
40), Espressif's SVD lists fields that overlap (a whole-register STATUS
beside its parts, two names for one bit), there was no gcc-xtensa
toolchain profile, and the fidelity's validation entries were prose.

svd2regs.py gains --resolve-overlaps (a field containing another taken
out, of fields over the same bits the first kept, recorded in source);
the ESP32's register file is regenerated with it, WIFI_CLK_EN kept as
one field and the SVD's stray fields in CPUSDIO_INT1 and SAR_MEAS_WAIT2
reserved. The board and the MCU have their French text.
Three more of ESP-IDF's examples run unmodified on the DevKitC:
gptimer (TIMG0, 17/17), uart_echo on UART2 (6/6) and real_time_stats
across both cores (10/10, with IDLE0 and IDLE1).

uart_echo echoed 128 bytes for each line: on the ESP32, ESP-IDF's
uart_ll_get_rxfifo_len counts the bytes waiting from MEM_RX_STATUS's
read and write addresses (the DPORT read's FIFO count is not to be
trusted), and with both at 0 takes a nonzero count for a full FIFO. The
UART model now moves the two addresses with each byte.
The ESP32's remote control peripheral from the C3's model on the
ESP32's registers: eight channels each sending or receiving through
the matrix (rmt_sig_out0-7, rmt_sig_in0-7), 64-word blocks of the
512-word RAM (added to the register file with svd2regs' --memory: the
SVD leaves it out), the clock from APB_CLK or REF_TICK, end markers,
continuous mode, wrap, TX_LIM, the carrier, the receiver's idle
threshold and filter, CHnDATA, three interrupt bits a channel.

ESP-IDF's get-started/blink with its LED-strip option runs unmodified
on the DevKitC with a WS2812B on IO18 (esp32_blink_strip 7/7).
SENS's RTC controller's one-shot conversions on ADC1 (GPIO36-39, 32-35)
and ADC2 (GPIO4, 0, 2, 15, 13, 12, 14, 27, 25, 26): MEAS1/2_START_SAR,
the attenuation per channel, 9 to 12 bits, the converter's inverted
output and DATA_INV, DONE after the conversion's SAR clocks. The
transfer is the line ESP-IDF's Vref calibration fits for the chip, with
the chip's eFuse Vref, so the calibrated voltage matches the pin's.

ESP-IDF's adc/oneshot_read runs unmodified on the DevKitC with three
sources (esp32_adc_oneshot_read 6/6: 1118 for 1.1 V, 2402 for 2.2 V,
433 for 0.5 V on ADC2).
Five sketches compiled for "ESP32 Dev Module" with Arduino-ESP32 3.2.0
run unmodified (tests/vendor/arduino-esp32, ESP32_*): AnalogReadSerial
and ReadAnalogVoltage on VP, DigitalReadSerial, GetChipID (ESP32-D0WD-V3
Rev 301, two cores) and RepeatTimer stopped by the board's Boot button.

Arduino turns on FreeRTOS's end-of-stack watchpoint, and its loopTask
stopped at once with "Stack canary watchpoint triggered": the core took
DBREAKC's MASK as the address bits ignored, where its set bits are the
low bits compared (ESP-IDF's 32-byte watchpoint writes 0x20). test_lx6
gains a store watchpoint over 32 bytes, missed just past it and taken
inside it.
(u32)PADS cut a 64-bit constant, which MSVC's /W4 /WX rejects; the low
word is taken with a mask instead.
The branch register's bit is unsigned, the comparison's result an int:
MSVC's /W4 /WX rejects the mixed ==.
RTC_IO's PAD_DAC1 and PAD_DAC2: with the pad the RTC IO MUX's and the
DAC powered, GPIO25 and GPIO26 carry VDD3P3_RTC x code / 255 (the GPIO
model hands the pad to the DAC as an analog output).

ESP-IDF's dac/dac_oneshot runs unmodified on the DevKitC, its ADC2
reading both DACs back on their pads (esp32_dac_oneshot 4/4). For that
the runtime's pin_volts gives a pin's own DAC voltage when its net has
no analog part (it gave the digital level, so an ADC read 0).
@teo1747
teo1747 merged commit d512717 into feat/esp32-dual-core-target Nov 8, 2026
19 checks passed
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.

1 participant