This is the engineering reference for the model: what it implements, register by register, for people writing firmware against it. For using the board in the editor, start with The ESP32-S3-DevKitC-1 and the board page.
ESP32-S3-DevKitC-1 board contract (Boards Wave C)
The ESP32-S3-DevKitC-1 is Espressif's own board for the ESP32-S3: an ESP32-S3-WROOM-1 module, two Xtensa LX7 cores at 240 MHz, two micro-USB sockets and an addressable RGB LED. One kernel component (esp32s3 in crates/parts) wraps an ESP32-S3 SoC model (crates/esp32s3) on the same Xtensa interpreter the classic ESP32 and the ESP8266 use (crates/xtensa-core). This document is the contract, and it wins over any comment in the code.
The catalog type is esp32s3. The catalog program string is elf32 xtensa lx7 esp32s3.
Read section 6 first if you are taking a build from here to real silicon. Unlike the classic ESP32's contract, almost every number here was checked against a page of the ESP32-S3 Technical Reference Manual (version 1.8), the ESP32-S3 Series Datasheet and Espressif's DevKitC-1 user guide and dimension drawing. Section 6 lists the few that were not, and everything that is not modeled at all: the second core, the FPU, the cache, flash, PSRAM and both radios among them.
A sketch for this board compiles in the browser with clang-v3, the wasm build of Espressif's LLVM fork (-mcpu=esp32s3), published 2026-09-28 and checked on this simulator (docs/compile.md). An untouched example still runs its prebuilt ELF from web/public/firmware/esp32s3/, built by Espressif's GCC for the S3. Section 7 has the details.
1. Board pins (catalog order)
Top view, the two micro-USB sockets at the bottom, the WROOM-1 module and its antenna at the top. Espressif's dimension drawing gives the board as 62.74 x 25.40 mm with each header row 1.27 mm in from its edge, so the two 22-pin headers are 22.86 mm (0.9 inch) apart and the board straddles a breadboard with one row of holes left free on each side. The visual draws it at that size.
J1, the left header, pin 1 at the top (22 holes): 3V3, 3V3b, RST, 4, 5, 6, 7, 15, 16, 17, 18, 8, 3, 46, 9, 10, 11, 12, 13, 14, 5V, G.
J3, the right header, pin 1 at the top (22 holes): Gb, TX, RX, 1, 2, 42, 41, 40, 39, 38, 37, 36, 35, 0, 45, 48, 47, 21, 20, 19, Gc, Gd.
44 pins. A number is GPIO n. TX and RX are GPIO43 and GPIO44, UART0. RST is the chip enable. 5V is the USB 5 V rail. G is ground. Duplicated names carry a letter, because catalog pin names are unique; the silkscreen does not print it.
Verified: the order of both headers, pin by pin, against the user guide's J1 and J3 tables (the v1.0 and v1.1 guides give the same order), and the position of the rows against the dimension drawing.
GPIO 26 to 32 are not on either header. They are the SPI flash. GPIO 22 to 25 do not exist on the part at all (the IO MUX function table has no row for them). GPIO 33 and 34 are not on the header either. crates/parts/src/parts/esp32s3.rs has a test that holds all of this.
GPIO 35, 36 and 37 are on the header, and on the variants with octal PSRAM (the DevKitC-1-N8R8 most people buy) they are the PSRAM's and must not be used. This model has no PSRAM, so here they are ordinary pins, and the user guide's note is repeated on the help page.
GPIO 19 and 20 are the chip's USB D- and D+, which the board's "USB" socket is wired to. Here they stay ordinary pads and the USB serial port keeps working even when a sketch drives them, which a real board would not do (section 3.4).
Things on the board:
- The RGB LED. An addressable LED, on GPIO48 on the first revision and on GPIO38 on v1.1. The user guide's "Hardware Revision Details" says this is the one difference between the two. The
revisionproperty picks which,1.0or1.1, and defaults to1.0, which is where the Arduino core's board definition puts it. The LED's data line is the pad; the board part decodes the pad the way the LED's own receiver does (section 5). - BOOT is GPIO0, active low; clicking it on the board grounds the pin. It is on J3 as
0as well. - RST resets the chip.
- A red 3.3 V power LED, drawn lit, because the board is always on USB here.
Electrical: every GPIO drives push-pull at 3.3 V through R_STRONG, sits high-Z, or pulls up or down through 45 k. An input reads high at 1.65 V (half the I/O supply), and a floating or contended net reads 0. 3V3 drives 3.3 V at R_SUPPLY, 5V drives 5 V, G drives 0 V. RST carries a 10 k pull-up to 3.3 V and pulling it below 1.65 V holds the core in reset. The capacitor beside that resistor, the board's power-on reset, is not modeled.
2. Memory map
| SRAM1, instruction bus | 416 KB at 0x4037_8000 to 0x403D_FFFF |
| SRAM1 and SRAM2, data bus | 480 KB at 0x3FC8_8000 to 0x3FCF_FFFF |
| stack top | 0x3FD0_0000 |
| UART0 | 0x6000_0000, section 3.3 |
| GPIO matrix | 0x6000_4000, section 3.1 |
| SENS | 0x6000_8800, section 3.6 |
| IO MUX | 0x6000_9000, section 3.2 |
| RMT | 0x6001_6000, section 3.8 |
| LEDC | 0x6001_9000, section 3.7 |
| SYSTIMER | 0x6002_3000, section 3.5 |
| USB Serial/JTAG | 0x6003_8000, section 3.4 |
| SYSTEM | 0x600C_0000, section 3.9 |
| Interrupt matrix | 0x600C_2000, section 3.9 |
Each peripheral block is 4 KB here, except SENS, which is the upper 2 KB of the low-power management block.
Verified: every address in the table, against the manual's tables 4.3-1 (internal memory) and 4.3-3 (peripherals). SENS is "the Low Power Management base address + 0x800" in the manual's register summary 39.6.2.
One SRAM, two windows, and the model has both. The manual says SRAM1 is "addressed by the CPU through the data bus or instruction bus in the same order": the byte at 0x4037_8000 + n is the byte at 0x3FC8_8000 + n. The model keeps one buffer for the data window and maps the instruction window onto its first 416 KB, so a program can read its own code through the data bus and get the right bytes, which the classic ESP32 model cannot offer. SRAM2 is on the data bus only, straight after SRAM1. firmware/esp32s3/link.ld puts code at the instruction address and starts the data sections at the data-bus address of the first byte after the code, so the two never share RAM, and elfcheck.py refuses an image in which they would.
SRAM0, the 32 KB at 0x4037_0000 that the instruction cache can take, is not modeled; an access to it is a fault.
Every PT_LOAD segment must land in one of the two windows; anything else is refused with the offending address in the message, and the chip is left with no program. An image built for the classic ESP32 is refused this way, since its RAM is at other addresses.
The core resets with pc at the ELF entry point and every address register zero. There is no boot ROM here: the first-stage loader, the flash reads, the clock tree and the cache set-up are all absent, and the runtime makes the state it wants explicit.
3. Peripheral registers
Only the subset below is implemented. Anything else inside a mapped block reads 0 and counts as a stray access; anything outside every block is an access fault.
3.1 GPIO matrix (0x6000_4000)
GPIO numbers run 0 to 48, so every register with one bit per pin comes in two halves: the plain one covers GPIO 0 to 31 and the 1 one GPIO 32 to 48 in its low seventeen bits.
| offset | register |
|---|---|
0x04 |
GPIO_OUT_REG |
0x08 |
GPIO_OUT_W1TS_REG |
0x0C |
GPIO_OUT_W1TC_REG |
0x10 |
GPIO_OUT1_REG |
0x14 |
GPIO_OUT1_W1TS_REG |
0x18 |
GPIO_OUT1_W1TC_REG |
0x20 |
GPIO_ENABLE_REG |
0x24 |
GPIO_ENABLE_W1TS_REG |
0x28 |
GPIO_ENABLE_W1TC_REG |
0x2C |
GPIO_ENABLE1_REG |
0x30 |
GPIO_ENABLE1_W1TS_REG |
0x34 |
GPIO_ENABLE1_W1TC_REG |
0x38 |
GPIO_STRAP_REG |
0x3C |
GPIO_IN_REG |
0x40 |
GPIO_IN1_REG |
0x44 |
GPIO_STATUS_REG |
0x4C |
GPIO_STATUS_W1TC_REG |
0x50 |
GPIO_STATUS1_REG |
0x58 |
GPIO_STATUS1_W1TC_REG |
0x74 + 4n |
GPIO_PINn_REG |
0x154 + 4y |
GPIO_FUNCy_IN_SEL_CFG_REG, stored |
0x554 + 4n |
GPIO_FUNCn_OUT_SEL_CFG_REG |
The W1TS forms of the status registers, GPIO_CPU_INT_REG/GPIO_CPU_INT1_REG (read as the status), GPIO_BT_SELECT_REG, GPIO_SDIO_SELECT_REG and GPIO_CLOCK_GATE_REG are answered as well.
Verified: the base and every offset, against register summary 6.14.
GPIO_PINn_REG (register 6.18): bit 2 PAD_DRIVER makes the pad open drain; bits 7..9 INT_TYPE choose what raises the pin's interrupt (0 off, 1 rising, 2 falling, 3 any edge, 4 low level, 5 high level); bit 13 is the CPU interrupt enable. Verified.
GPIO_FUNCn_OUT_SEL_CFG_REG (register 6.20): bits 0..8 OUT_SEL, bit 9 OUT_INV_SEL, bit 10 OEN_SEL, bit 11 OEN_INV_SEL. OUT_SEL 256 makes the pad follow GPIO_OUT with GPIO_ENABLE as its output enable. 73 to 80 hand the pad to LEDC channels 0 to 7 (ledc_ls_sig_out0 to 7) and 81 to 84 to RMT channels 0 to 3 (rmt_sig_out0 to 3); those numbers are table 6.11-1's, verified. Both peripherals' output enables are tied high in that table ("1'd1"), so with OEN_SEL clear an LEDC or RMT channel drives its pad whatever GPIO_ENABLE says, and with OEN_SEL set GPIO_ENABLE decides. Any other OUT_SEL selects a peripheral that is not modeled, and the model treats it as 256 rather than invent a level.
GPIO_STRAP_REG reads 0x08: bit 3 (GPIO0) high, bits 2, 4 and 5 (GPIO46, GPIO45, GPIO3) low, which is an ordinary boot from 3.3 V flash. The bit assignment is register 6.15's; the value is a constant here, not a latch of what the pins did at reset.
3.2 IO MUX (0x6000_9000)
IO_MUX_PIN_CTRL_REG at 0x00, then one register per GPIO at 0x04 + 4n. Unlike the classic ESP32, the S3's IO MUX is in GPIO order. Bit 7 FUN_WPD, bit 8 FUN_WPU, bit 9 FUN_IE, bits 10..11 FUN_DRV (stored), bits 12..14 MCU_SEL. A pad only reaches GPIO_IN_REG with its input buffer enabled.
Verified: the base, the offsets and the bits, against register 6.36; the function table and reset values against table 6.12-1.
MCU_SEL picks one of up to five functions. Function 1 is the GPIO matrix on every pin. On most pins function 0 is the GPIO matrix too, but on 26 to 32 it is the flash, on 39 to 42 the JTAG pins, on 43 and 44 UART0 and on 47 and 48 the flash clock pair, so a program that wants one of those as a GPIO has to set MCU_SEL to 1. The runtime's pinMode always does. The RGB LED on GPIO48 is one of them: until something sets its MCU_SEL, nothing a program writes to the matrix reaches that pad.
Every pad comes up at the manual's reset value. Table 6.12-1's "RST" column: GPIO0 and the flash pins with their pull-ups, GPIO45 and GPIO46 with their pull-downs, most pins with only the input buffer on, and GPIO 4 to 8, 15, 16 and 19 to 21 with not even that, so digitalRead on one of those reads 0 until the buffer is turned on. MTCK, GPIO39, comes up pulled up, which is what the table says for a part whose JTAG pads have not been disabled by eFuse. GPIO43, U0TXD, comes up with its output enabled on the UART; the UART's own waveform is not modeled (section 3.3), so that pad holds the line's idle high until a program takes it over.
3.3 UART0 (0x6000_0000)
| offset | register |
|---|---|
0x00 |
UART_FIFO_REG |
0x04 |
UART_INT_RAW_REG |
0x08 |
UART_INT_ST_REG |
0x0C |
UART_INT_ENA_REG |
0x10 |
UART_INT_CLR_REG |
0x14 |
UART_CLKDIV_REG |
0x1C |
UART_STATUS_REG |
0x20 |
UART_CONF0_REG |
0x24 |
UART_CONF1_REG |
0x78 |
UART_CLK_CONF_REG |
Verified: the base and every offset (register summary 26.6), and the bits below.
Two 128-byte FIFOs. UART_STATUS_REG bits 0..9 are RXFIFO_CNT and bits 16..25 TXFIFO_CNT. UART_CONF0_REG bit 17 RXFIFO_RST and bit 18 TXFIFO_RST empty them (register 26.9; the classic ESP32 model has these two the other way around, section 6). UART_CONF1_REG holds the two FIFO thresholds, ten bits each, 96 at reset. UART_CLKDIV_REG bits 0..11 are the divisor's integer part and bits 20..23 its sixteenths, 694 at reset.
The divisor divides the UART core clock, which UART_CLK_CONF_REG bits 20..21 SCLK_SEL pick: 1 the 80 MHz APB clock, 3 the 40 MHz crystal, and the crystal is the reset value (register 26.18). 2, the RC_FAST oscillator, is not modeled and is taken as the crystal. SCLK_DIV_NUM (bits 12..19) divides the core clock by SCLK_DIV_NUM + 1, which is believed: the manual calls the field "the integral part of the frequency divisor" and prints the divisor without the one, and at reset the field is 1. The runtime sets SCLK_SEL to 1 and SCLK_DIV_NUM to 0, and then 694 is 115 274 baud. The fractional SCLK_DIV_A/SCLK_DIV_B are not modeled.
Bytes leave the transmit FIFO at the baud rate, not instantly, so a 115 200 baud port carries 11 520 bytes a second here as on a desk; crates/esp32s3/tests/runtime.rs checks that no two bytes of the serial example are closer than a byte time.
UART1 and UART2 are not modeled. U0TXD's pin waveform is not modeled either: the bytes are the host channel, not bits on GPIO43.
3.4 USB Serial/JTAG (0x6003_8000)
The serial half of the controller: the CDC-ACM port the board's "USB" socket carries, which a sketch reaches as USBSerial.
| offset | register |
|---|---|
0x00 |
USB_SERIAL_JTAG_EP1_REG |
0x04 |
USB_SERIAL_JTAG_EP1_CONF_REG |
0x08 |
USB_SERIAL_JTAG_INT_RAW_REG |
0x0C |
USB_SERIAL_JTAG_INT_ST_REG |
0x10 |
USB_SERIAL_JTAG_INT_ENA_REG |
0x14 |
USB_SERIAL_JTAG_INT_CLR_REG |
0x18 |
USB_SERIAL_JTAG_CONF0_REG, stored |
Verified: the base, the offsets (register summary 33.5) and the bits: EP1_CONF bit 0 WR_DONE, bit 1 SERIAL_IN_EP_DATA_FREE, bit 2 SERIAL_OUT_EP_DATA_AVAIL (register 33.2); INT_* bit 2 SERIAL_OUT_RECV_PKT_INT and bit 3 SERIAL_IN_EMPTY_INT, which is set at reset (register 33.6).
The behavior is section 33.3.3's: bytes written to EP1 wait in a 64-byte buffer and leave only when it is flushed, by WR_DONE or by the hardware when the 64th byte goes in; from the flush until the host has the packet, SERIAL_IN_EP_DATA_FREE is clear and a byte written is lost; when the host has it, SERIAL_IN_EMPTY_INT rises. Host bytes arrive a packet (up to 64) at a time, SERIAL_OUT_EP_DATA_AVAIL stays set while any are unread, and the next packet is accepted once the last byte of this one has been read.
The timing is this model's own. The manual says the host "will check periodically". Here the host always has a terminal open and collects a flushed packet as soon as it could cross a 12 Mbit/s bus: the packet plus thirteen bytes of token, handshake and framing, so 51.3 us for 64 bytes. A real host adds up to a frame (1 ms) of scheduling; that is not modeled.
Not modeled: the JTAG half, enumeration, the bus-error interrupts, the RTS/DTR reset and download-mode requests, and suspend. The host is always connected.
3.5 SYSTIMER (0x6002_3000)
Unit 0: the 52-bit counter behind micros() and millis().
| offset | register |
|---|---|
0x00 |
SYSTIMER_CONF_REG |
0x04 |
SYSTIMER_UNIT0_OP_REG |
0x0C |
SYSTIMER_UNIT0_LOAD_HI_REG |
0x10 |
SYSTIMER_UNIT0_LOAD_LO_REG |
0x40 |
SYSTIMER_UNIT0_VALUE_HI_REG |
0x44 |
SYSTIMER_UNIT0_VALUE_LO_REG |
0x5C |
SYSTIMER_UNIT0_LOAD_REG |
Verified: the base, the offsets (register summary 11.5), the bits (CONF bit 30 TIMER_UNIT0_WORK_EN, set at reset; OP bit 30 UPDATE, bit 29 VALUE_VALID) and the 16 MHz count clock ("XTAL_CLK/2.5, which is 16 MHz", section 11.4).
Reading is latch-then-read: write UPDATE, wait for VALUE_VALID, read VALUE_HI and VALUE_LO. The counter is not counted: it is simulated time divided by 62.5 ns, plus whatever a load put there, so there is nothing for it to drift against. Unit 1, the three comparators and their alarms are not modeled; the runtime's tick is the core's own CCOMPARE0 (section 4).
3.6 SENS: ADC1, one shot (0x6000_8800)
The RTC ADC1 controller's software-started conversion.
| offset | register | bits |
|---|---|---|
0x00 |
SENS_SAR_READER1_CTRL_REG |
stored |
0x0C |
SENS_SAR_MEAS1_CTRL2_REG |
bits 0..15 MEAS1_DATA_SAR (read only), bit 16 MEAS1_DONE_SAR, bit 17 MEAS1_START_SAR, bit 18 MEAS1_START_FORCE, bits 19..30 SAR1_EN_PAD (one bit per channel), bit 31 SAR1_EN_PAD_FORCE |
0x10 |
SENS_SAR_MEAS1_MUX_REG |
bit 31 SAR1_DIG_FORCE: set, ADC1 belongs to the digital controller and a software start converts nothing |
0x14 |
SENS_SAR_ATTEN1_REG |
two bits of attenuation per channel, channel n at bits 2n..2n+1; all ones at reset |
Verified: the base, the three offsets and every bit, against registers 39.10 to 39.13.
One conversion: put the attenuation in ATTEN1, set START_FORCE and EN_PAD_FORCE, put the channel's bit in SAR1_EN_PAD, raise MEAS1_START_SAR, wait for MEAS1_DONE_SAR, read the data. A conversion takes a fixed 5 us of simulated time, the model's own figure.
Ten channels, and channel n is GPIO n + 1: ADC1_CH0 is GPIO1 and ADC1_CH9 GPIO10 (the user guide's header tables, pin by pin).
The result is twelve bits, round(4095 x Vin / Vtop), clamped, of the net voltage at the moment the conversion started. Vtop is the top of the datasheet's "effective measurement range": 850 mV at ATTEN0, 1100 mV at ATTEN1, 1600 mV at ATTEN2 and 2900 mV at ATTEN3, which is the reset attenuation. So a pot across the 3.3 V rail reads 4095 over the top eighth of its travel. That is an approximation (section 6): the real converter keeps some headroom above the effective range before it saturates, and its curve bends and is corrected from eFuse calibration.
ADC2, the digital controller, DMA, the calibration eFuses and the temperature sensor are absent.
3.7 LEDC (0x6001_9000)
Eight channels and four timers, all of one speed class: the S3's whole LEDC.
| offset | register | bits |
|---|---|---|
0x00 + 0x14n |
LEDC_CHn_CONF0_REG |
bits 0..1 TIMER_SEL, bit 2 SIG_OUT_EN, bit 3 IDLE_LV, bit 4 PARA_UP (write only) |
0x04 + 0x14n |
LEDC_CHn_HPOINT_REG |
bits 0..13 HPOINT |
0x08 + 0x14n |
LEDC_CHn_DUTY_REG |
bits 0..18 the duty in sixteenths of a count |
0x0C + 0x14n |
LEDC_CHn_CONF1_REG |
the fade hardware: stored, fades nothing |
0x10 + 0x14n |
LEDC_CHn_DUTY_R_REG |
the duty in force, read only |
0xA0 + 8n |
LEDC_TIMERn_CONF_REG |
bits 0..3 DUTY_RES, bits 4..21 CLK_DIV (Q10.8), bit 22 PAUSE, bit 23 RST (set at reset), bit 25 PARA_UP (write only) |
0xA4 + 8n |
LEDC_TIMERn_VALUE_REG |
the counter |
0xC0 |
LEDC_INT_RAW_REG |
bit n = timer n overflowed |
0xC4 |
LEDC_INT_ST_REG |
|
0xC8 |
LEDC_INT_ENA_REG |
stored; no line reaches the core |
0xCC |
LEDC_INT_CLR_REG |
|
0xD0 |
LEDC_CONF_REG |
bits 0..1 APB_CLK_SEL (1, the APB clock, is the only source modeled), bit 31 CLK_EN |
0xFC |
LEDC_DATE_REG |
reads 0x1907_2601 |
Verified: the base, every offset and every bit in the table (register summary 35.4 and registers 35.1 to 35.13). This is a real change from the classic ESP32 model, whose LEDC layout is believed.
A timer counts 2^DUTY_RES counts at 80 MHz divided by CLK_DIV: f = 80e6 / ((CLK_DIV / 256) * 2^DUTY_RES). A channel's output is high from HPOINT for DUTY >> 4 counts.
Nothing takes effect without its update bit. A timer's CLK_DIV and DUTY_RES wait for TIMERn_PARA_UP, and a channel's HPOINT, SIG_OUT_EN, TIMER_SEL and duty wait for PARA_UP_CHn (the lists under registers 35.1 and 35.7). The model keeps what was written apart from what is in force, so firmware that forgets an update bit gets no PWM, as on a desk. PAUSE, RST and IDLE_LV act at once. The duty is listed under DUTY_START; the model loads it on PARA_UP_CHn whether or not DUTY_START is set, which is believed to be what a plain duty change does with the fade hardware idle.
A channel reaches a pad through the GPIO matrix at OUT_SEL 73 + n (section 3.1). The counter is a closed-form function of simulated time, so a core parked in waiti costs nothing while a PWM runs; runtime.rs measures the fade example's period against the 1 ms of 1 kHz within 5 us.
LEDC's clock is off at reset (section 3.9): until SYSTEM_LEDC_CLK_EN is set the block does not answer.
Not modeled: the duty-fade hardware, the overflow counter, clock sources other than APB, and LEDC's line into the interrupt matrix.
3.8 RMT (0x6001_6000)
The remote control peripheral's four transmit channels: what drives the RGB LED.
| offset | register | bits |
|---|---|---|
0x00 + 4n |
RMT_CHnDATA_REG |
write: push a word into channel n's RAM through the APB FIFO |
0x20 + 4n |
RMT_CHnCONF0_REG |
bit 0 TX_START, bit 1 MEM_RD_RST, bit 2 APB_MEM_RST, bit 3 TX_CONTI_MODE, bit 4 MEM_TX_WRAP_EN, bit 5 IDLE_OUT_LV, bit 6 IDLE_OUT_EN, bit 7 TX_STOP, bits 8..15 DIV_CNT, bits 16..19 MEM_SIZE, bit 20 CARRIER_EFF_EN, bit 21 CARRIER_EN, bit 22 CARRIER_OUT_LV, bit 24 CONF_UPDATE |
0x50 + 4n |
RMT_CHnSTATUS_REG |
bits 0..9 the read address, bits 11..20 the APB write address, bits 22..24 the state, bit 25 MEM_EMPTY, bit 26 APB_MEM_WR_ERR |
0x70 |
RMT_INT_RAW_REG |
bit n TX_END, bit 4+n ERR, bit 8+n TX_THR_EVENT, bit 12+n TX_LOOP |
0x74 |
RMT_INT_ST_REG |
|
0x78 |
RMT_INT_ENA_REG |
|
0x7C |
RMT_INT_CLR_REG |
|
0x80 + 4n |
RMT_CHnCARRIER_DUTY_REG |
bits 0..15 low, bits 16..31 high, 0x40 each at reset |
0xA0 + 4n |
RMT_CHn_TX_LIM_REG |
bits 0..8 TX_LIM, bits 9..18 TX_LOOP_NUM, bit 19 TX_LOOP_CNT_EN, bit 20 LOOP_COUNT_RESET, bit 21 LOOP_STOP_EN |
0xC0 |
RMT_SYS_CONF_REG |
bit 0 APB_FIFO_MASK, bits 4..11 SCLK_DIV_NUM, bits 24..25 SCLK_SEL, bit 26 SCLK_ACTIVE, bit 31 CLK_EN |
0xCC |
RMT_DATE_REG |
reads 0x0210_1181 |
0x800 to 0xDFF |
the RAM | 384 words |
The receive channels' registers (0x30 to 0x4C, 0x60 to 0x6C, 0x90 to 0x9C, 0xB0 to 0xBC) are stored and read back and receive nothing.
Verified: the base, every offset and bit in the table (register summary 37.5, registers 37.1 to 37.19), the RAM's size and its NONFIFO start at "RMT base address + 0x800", the pulse code format (period in bits 0..14 and 16..30, level in bits 15 and 31, "the minimum value for the period is zero and is interpreted as a transmission end-marker"), the channel clock (rmt_sclk divided by DIV_CNT, "value 0 represents divider 256", and rmt_sclk the source divided by SCLK_DIV_NUM + 1 + A/B), the reset values (carrier on, MEM_SIZE 1, DIV_CNT 2), and the list of fields that wait for CONF_UPDATE (table 37.3-1).
Believed: the manual gives transmit channel n's NONFIFO start as "RMT base address + 0x800 + n x 48", and the model reads the 48 as words, so channel n's block starts at 0x800 + 192n: the only reading under which the eight 48-word blocks of section 37.3.2 fit the 384 words. The manual's receive-channel start, "0x8c0 + m x 48", does not settle it either way, and the receive channels are not modeled.
What a transmission does, from section 37.3.4: TX_START sends from the start of the channel's block, each code holding its level for period channel-clock ticks; at an end marker the channel stops, raises TX_END, and holds the end marker's level, or IDLE_OUT_LV if IDLE_OUT_EN is set; running off the end of its MEM_SIZE blocks without an end marker stops it with MEM_EMPTY and ERR, unless wrap mode sends it round again, in which case TX_THR_EVENT rises each time TX_LIM more words have gone; continuous mode starts again at every end marker, counting loops when asked to. TX_STOP stops at once.
Configuration update. DIV_CNT, IDLE_OUT_EN, IDLE_OUT_LV, TX_CONTI_MODE, TX_STOP, the carrier fields and TX_LIM's three fields take effect only on CONF_UPDATE (table 37.3-1); the model keeps them apart until then. MEM_SIZE, wrap mode and TX_START act at once.
The carrier is on at reset. A code whose level matches CARRIER_OUT_LV becomes a square wave high for CARRIER_HIGH + 1 and low for CARRIER_LOW + 1 cycles of rmt_sclk. The model starts that wave high at the start of each code, which is believed, and does not modulate the idle level when CARRIER_EFF_EN is clear. A WS2812 needs the carrier off, and the runtime clears CARRIER_EN; the model counts transmissions made with it on, and the runtime tests check the count is zero.
The output is a closed-form function of time between code boundaries, so each edge lands on its picosecond and a parked core costs nothing while a frame goes out. runtime.rs decodes the blink and rainbow examples' frames on GPIO48 and checks every high and low time against the WS2812B datasheet's windows.
The RMT's clock is off at reset (section 3.9).
Not modeled: the receive channels, DMA on channel 3, simultaneous start, RAM ownership and power, and the minimum-period rules (formulas 37.1 and 37.3).
3.9 SYSTEM (0x600C_0000) and the interrupt matrix (0x600C_2000)
| offset | register |
|---|---|
0x10 |
SYSTEM_CPU_PER_CONF_REG, stored |
0x18 |
SYSTEM_PERIP_CLK_EN0_REG |
0x1C |
SYSTEM_PERIP_CLK_EN1_REG, stored |
0x20 |
SYSTEM_PERIP_RST_EN0_REG |
0x24 |
SYSTEM_PERIP_RST_EN1_REG, stored |
Verified: the offsets and reset values (registers 17.3 to 17.7). LEDC (bit 11) and RMT (bit 9) are gated off at reset in SYSTEM_PERIP_CLK_EN0_REG. Here, a write to either block while its clock bit is clear, or while its bit in SYSTEM_PERIP_RST_EN0_REG holds it in reset, is dropped and counted, and a read returns 0; holding one in reset puts it back to power-on. The runtime sets both clock bits before anything else. The other blocks here are clocked at reset and are not gated in the model.
The interrupt matrix: source n's map register for core 0 is at 0x600C_2000 + 4n, and its low five bits name the CPU interrupt the source raises. Verified against table 9.3-1 and register summary 9.4, as are the four source numbers this model routes:
| source | number |
|---|---|
GPIO_INTERRUPT_CPU |
16 |
UART_INTR |
27 |
RMT_INTR |
40 |
USB_DEVICE_INT |
96 |
A source mapped to one of the core's internal interrupts (6, 7, 11, 15, 16 or 29) reaches nothing, which is how the manual says to disable a source (section 9.3.3.3). Every other map register is stored and read back and routes nothing.
3.10 Mokxi network mailbox (0x6FFF_E000)
This block is not the ESP32-S3's. Every other address in this document is
the chip's. This one is Mokxi's own: the simulated network of crates/net, at
an address the S3 leaves unmapped. The manual puts the peripherals at
"0x6000_0000~0x600D_0FFF" and RTC FAST memory ends at 0x600F_FFFF, so an image
using this block faults on silicon rather than reading some other
peripheral's register. That fault is the honest answer, because the thing
behind this block does not exist on the chip. It is the same address the
ESP32-C3 and C6 models use, because the three chips leave the same gap.
The block raises no interrupt at all. Every other source here reaches the
core through the interrupt matrix of section 3.9, and a matrix source number is
the chip's; there is no honest number to give a peripheral the chip has not
got, so the mailbox is given none. NET_INT_RAW_REG still reads, the firmware
polls, and a core parked in waiti is still woken when the network has
something to say, the same way it is woken for a PWM edge.
There is no radio, no 802.11 and no real internet here. Nothing in the model
transmits, scans, associates, encrypts or opens a socket: a password is compared
as a string, a name that is not under .mokxi does not resolve, and the
origins the network serves are functions in crates/net/src/http.rs. The
mailbox is the shape of the real seam rather than a fiction about a MAC: on a
real ESP32-S3 the WiFi hardware sits behind a closed binary blob with a command
interface, and Arduino's WiFi.h ends up on the other side of one of these.
docs/wifi.md is the contract for what is behind it; this section is only the
registers.
A command register, a status register, a 4096-byte ring each way and an
interrupt bit. Writing NET_CMD_REG runs the command; a command that takes
simulated time raises BUSY and clears it, with DONE in NET_INT_RAW_REG,
when it is finished.
| offset | name | bits |
|---|---|---|
0x00 |
NET_ID_REG |
reads 0x494B4F4D, and nothing else does. A sketch that wants to know whether it is on Mokxi reads this; on silicon the access faults |
0x04 |
NET_CMD_REG |
write a command code (table below); the write runs it |
0x08 |
NET_ARG_REG |
one argument word, read back: the port for LISTEN, the status for REPLY |
0x0C |
NET_STATUS_REG |
bit 0 BUSY, bit 1 ERR, bit 2 RX_READY, bit 3 LINK, bit 4 SERVE, bits 8..11 STATE |
0x10 |
NET_RESULT_REG |
what the last command answered |
0x14 |
NET_INT_RAW_REG |
bit 0 DONE, bit 1 EVENT |
0x18 |
NET_INT_ENA_REG |
the same two bits |
0x1C |
NET_INT_CLR_REG |
write 1 to clear, the same two bits |
0x20 |
NET_TX_LEN_REG |
bytes staged in the outbound ring |
0x24 |
NET_TX_DATA_REG |
write: push one byte (bits 0..7) into the outbound ring |
0x28 |
NET_RX_LEN_REG |
bytes waiting in the inbound ring |
0x2C |
NET_RX_DATA_REG |
read: pop one byte from the inbound ring, 0 when it is empty |
0x30 |
NET_RING_LEN_REG |
capacity of each ring in bytes, so firmware need not hard-code 4096 |
STATE is where the station is: 0 idle, 1 starting, 2 connecting, 3 connected,
4 failed, 5 no such network. NET_CMD_STATUS turns the same thing into the
Arduino wl_status_t code a sketch reads from WiFi.status().
NET_CMD_REG |
name | what it does |
|---|---|---|
| 0 | NOP |
nothing |
| 1 | RESET |
disconnect, drop both rings, drop any request |
| 2 | CONNECT |
the outbound ring holds ssid\0password; raises BUSY for one to three seconds |
| 3 | DISCONNECT |
drop the association and the address |
| 4 | STATUS |
NET_RESULT_REG = the wl_status_t code |
| 5 | IP |
NET_RESULT_REG = the leased address, first octet in the low byte |
| 6 | RSSI |
NET_RESULT_REG = the signal in dBm, sign extended |
| 7 | SSID |
the inbound ring gets the access point's name |
| 8 | MAC |
the inbound ring gets the board's six MAC bytes |
| 9 | REQUEST |
the outbound ring holds method\0url\0headers\0body; raises BUSY |
| 10 | POLL |
NET_RESULT_REG = 0 in flight, then the HTTP status, or a negative error with ERR up; the body lands in the inbound ring |
| 11 | LISTEN |
listen on the port in NET_ARG_REG |
| 12 | ACCEPT |
NET_RESULT_REG = 1 and the inbound ring holds method\0path\0query\0body, or 0 when nothing is waiting |
| 13 | REPLY |
answer the accepted request with the status in NET_ARG_REG; the outbound ring holds content-type\0body |
| 14 | STOP |
stop listening |
| 15 | WL_GPIO_SET |
drive the wireless part's own GPIOs from NET_ARG_REG. Not this board: this part's radio has no such pin, so it sets ERR and drives nothing. It is the path a Pico W's on-board LED takes (docs/picow.md section 3.8) |
| 16 | WL_GPIO_GET |
read them back, and the same ERR here |
The negative POLL codes: -1 the name does not resolve, -2 the station is not
associated, -3 https:// and TLS is not simulated, -4 a body longer than the
ring, -5 a URL this network cannot parse, -6 a request is already in flight.
firmware/include/HTTPClient.h turns them into the Arduino-shaped codes a
sketch compares against, and docs/wifi.md lists the mapping.
Timing, all of it this model's own and all of it stated in docs/wifi.md: an
association takes between one and three seconds, decided from the SSID so a
lesson takes the same time twice; a wrong password becomes WL_CONNECT_FAILED
after five seconds; a request to a simulated origin comes back 60 ms later; a
request into the sketch's own server that the sketch never answers is closed by
the network with a 504 after ten seconds.
4. The core: the LX7, which ABI, and how an interrupt arrives
The S3's two cores are Xtensa LX7s, and this model runs one of them on crates/xtensa-core, the interpreter the classic ESP32 and the ESP8266 already use, in a third configuration, Model::LX7.
For everything the compiler emits, the LX7 here is the LX6. The two toolchains' configuration headers (core-isa.h in xtensa-esp32-elf and xtensa-esp32s3-elf) agree on every integer option that decides what code a compiler emits: sixty-four address registers and the register window, the zero-overhead loop, the 32-bit multiply and its high half, the divider, MIN/MAX, SEXT, CLAMPS, S32C1I, MAC16, the booleans and the single-precision FPU. And the evidence is stronger than the headers: crates/xtensa-core/tests/programs.rs holds the S3 compiler's build of the test program the classic ESP32's compiler also built, and the two came out byte for byte the same. So the S3 runs the windowed ABI, exactly as the classic ESP32 does, with ENTRY, RETW, ROTW, MOVSP, L32E/S32E and the window exceptions through their own vectors (docs/esp32.md section 4 has the vector table, and it is the ISA's, not the chip's).
What the LX7 adds on this chip is the S3's vector extension, the EE.* instructions for AI and DSP work, which the assembler encodes in the op0 = 4 space the LX6 gives to MAC16. Neither is modeled, and both fault as illegal instructions (crates/xtensa-core/tests/encoding.rs holds two of the S3 assembler's encodings). GCC does not emit them from ordinary C.
Interrupts. INTENABLE masks; PS.INTLEVEL gates. Only level 1 is modeled. On the S3 the level-1 interrupts are 0 to 10, 12, 13, 17 and 18, mask 0x0006_37FF, and 19 to 21 are level 2: verified against the manual's table 9.3-2. The three CCOMPARE comparators raise CPU interrupts 6, 15 and 16, from the same table. The runtime routes GPIO to CPU interrupt 13, a level-1 peripheral interrupt, and takes its delay tick from CCOMPARE0 on CPU interrupt 6.
PRID reads 0xCDCD, as on the classic model: believed. What matters is that bit 13 is clear, which is how software tells core 0 from core 1.
The rest is the classic ESP32's section 4 unchanged: CCOUNT is the cycle count; a WSR to a CCOMPARE arms it and clears its interrupt; "reached or passed" rather than "equal"; a deadline already passed is not armed; MOVSP never raises Alloca; and every floating-point instruction faults with EXCCAUSE 32, because the FPU is not modeled. The runtime is soft float.
5. Time model, probe, poke and the host channel
The CPU runs at 240 MHz, one instruction per cycle. Time is recomputed from the cycle count, so a 2400-instruction slice is exactly 10 us. Inside a slice the instruction count is the clock: a timer read, a serial byte, a pin change and an RMT edge all land at the instruction that caused them. A core parked in WAITI costs nothing: the board asks to be woken at the earliest of its next CCOMPARE deadline, its next LEDC or RMT edge, a serial byte and the network, and a pin change or a host byte wakes it too.
probe is a bitmask with the LED's color in it:
| bits | meaning |
|---|---|
| 0 | running |
| 1 | parked in waiti, or stopped by the debugger |
| 2..19 | the RGB LED's color as it last latched a frame, six bits a channel: red in 14..19, green in 8..13, blue in 2..7 |
The LED is decoded by the board part from the edges the SoC reports on the LED's pad (GPIO48, or GPIO38 on v1.1), with the ws2812 part's windows: a high pulse under 125 ns is a glitch, up to 600 ns is a 0, up to 950 ns is a 1, anything longer breaks the frame, a low shorter than the bit allows breaks it too, and 50 us of low latches the first 24 bits, green first. The LED keeps its color through a reset of the chip, as a WS2812 does, because it is powered from the board. The color's low two bits a channel do not fit in a probe and are dropped.
poke: 1 holds BOOT (GPIO0 low), 0 releases it; 2 holds RST, 3 releases it.
The host channel carries both of the board's serial ports. A real DevKitC-1 has two sockets: "UART", through a USB-to-UART bridge to UART0, and "USB", the chip's own USB Serial/JTAG controller. Here the serial monitor is attached to both at once: host_read gives the bytes that have left UART0 and the bytes the USB port has delivered, in the order they arrived, and host_write puts what is typed into UART0's receive FIFO and into the USB port's host queue both. The bridge itself is not modeled, and neither is auto-reset over DTR and RTS.
Crash. 1000 consecutive traps with no forward progress and the core is declared crashed and halted, with where it died recorded and reported on the host channel.
6. What this model is sure of, and what it is not
Checked against the manual, the datasheet or the user guide
The memory windows (table 4.3-1) and every peripheral base (table 4.3-3). Every register offset and every bit named in section 3, block by block: GPIO (6.14 to 6.20), IO MUX (6.36 and table 6.12-1, including every pad's reset value and function 1 being GPIO on every pin), UART0 (26.6 to 26.19), USB Serial/JTAG (33.2 to 33.9 and the behavior in 33.3.3), SYSTIMER (11.1 to 11.5 and the 16 MHz clock), SENS (39.10 to 39.13), LEDC (35.1 to 35.13 and the PARA_UP lists), RMT (37.1 to 37.19, the RAM size, the pulse code format, the clock formula and table 37.3-1), SYSTEM (17.3 to 17.7, including LEDC and RMT being gated off at reset) and the interrupt matrix (table 9.3-1 and summary 9.4). The GPIO matrix signal numbers 73 to 80 for LEDC and 81 to 84 for RMT (table 6.11-1), and that both peripherals' output enables are tied high. The CPU interrupt levels and the comparators' interrupt numbers (table 9.3-2). The ADC1 channel map and the effective ranges of the four attenuations (datasheet). The header order, the RGB LED's GPIO on each revision, and the board's dimensions (user guide and dimension drawing). The integer ISA options (the toolchains' core-isa.h, and the S3 compiler's output byte for byte).
Believed, not verified
SCLK_DIV_NUM + 1as UART0's core clock divider (section 3.3). The runtime writes 0 there, which is 1 under this reading; the manual's printed formula has no plus one.- The RMT RAM's layout per channel,
0x800 + 192n(section 3.8). - The carrier's phase: high first, from the start of each code (section 3.8).
- The duty loading on
PARA_UP_CHnwithoutDUTY_START(section 3.7). - The USB packet timing: 12 Mbit/s plus thirteen bytes, and no host scheduling delay (section 3.4).
PRID=0xCDCD(section 4).
Every number the runtime depends on is named in one place (firmware/esp32s3/include/esp32s3.h), and crates/esp32s3/tests/contract.rs holds that header and crates/esp32s3/src/map.rs to the same numbers.
Modeled, but not to the chip's numbers
- The ADC reads 4095 at the top of the datasheet's effective range and is a straight line to it (section 3.6). A real S3 has some headroom above the range and a curve corrected from eFuse calibration.
- A conversion takes exactly 5 us, and an LEDC or RMT edge lands on the exact picosecond.
GPIO_STRAP_REGis a constant, not a latch of the pins at reset.RSTis a 10 k pull-up alone; the power-on capacitor is not modeled.- There is no boot ROM and no clock tree. The core is at 240 MHz and the APB clock at 80 MHz from the first instruction, and
SYSTEM_CPU_PER_CONF_REGis stored and changes nothing. - The USB pads are not taken over. GPIO19 and GPIO20 stay ordinary pins and the USB port keeps working even if a sketch drives them.
- U0TXD holds a steady high while the UART owns it; the UART's bits are the host channel, not a waveform on GPIO43.
CCOMPAREcompares "reached or passed", andMOVSPnever raises Alloca (section 4).
Deliberately absent
- The second core. Only core 0 runs; there is no cross-core interrupt and no FreeRTOS, so anything that starts a task on core 1 will not run.
- The FPU and the vector extension. Every floating-point and
EE.*encoding faults (section 4). The runtime is soft float. - SRAM0, the cache, the MMU, flash and PSRAM. Code runs from SRAM1.
- WiFi and Bluetooth radios, in every form. The WiFi a sketch reaches is the simulated network of section 3.10.
- The RTC, its memories, the ULP coprocessors, deep sleep and the power-management unit.
- ADC2, the digital ADC controller, DMA, the calibration eFuses, the temperature sensor and the touch sensor.
- The timer groups and every watchdog.
- UART1 and UART2, the I2C and SPI controllers, I2S, the camera and LCD controller, TWAI, the SD host, the pulse counter, the motor PWM, the USB OTG controller, GDMA and the crypto accelerators. The runtime's
WireandSPIare software on ordinary pins (SDA 8 and SCL 9, SCK 12, MISO 13, MOSI 11 and SS 10, the Arduino core's defaults for this board), exactly as on every other board here, because no board model here has those controllers. - The RMT's receive channels and the rest of section 3.8's list, and the LEDC fade hardware.
- The interrupt matrix beyond four sources, and every CPU interrupt level above one.
That is why vendor ESP-IDF and Arduino-core binaries do not run here, and firmware built for this document does.
The three Xtensa boards' images are not interchangeable. The catalog program strings differ (elf32 xtensa lx7 esp32s3 here, elf32 xtensa lx6 esp32 on the classic DevKit, elf32 xtensa lx106 esp8266 on the NodeMCU), and the difference is real: an image for the classic ESP32 runs the same instructions but loads at addresses this chip does not have, so the loader refuses it.
7. Firmware runtime (firmware/esp32s3/)
An Arduino-shaped runtime: setup() and loop(), pinMode, digitalWrite, digitalRead, shiftOut, analogRead (with analogReadResolution, analogSetAttenuation and analogSetPinAttenuation), analogWrite and the ledcAttach/ledcWrite/ledcRead family, rgbLedWrite and neopixelWrite, millis, micros, micros64, delay, delayMicroseconds, attachInterrupt, Serial, USBSerial, map, random. No Arduino core, no ESP-IDF, no libc. WiFi.h, HTTPClient.h and WebServer.h are the shared simulated-network runtime of docs/wifi.md.
LED_BUILTIN and RGB_BUILTIN are the Arduino core's number for the RGB LED: the GPIO count plus the LED's pin, 49 + 48. digitalWrite(LED_BUILTIN, HIGH) sends the LED one WS2812 frame, white at RGB_BRIGHTNESS (64 of 255), and LOW sends black, which is what the Arduino core does. rgbLedWrite(pin, r, g, b) takes the GPIO number or RGB_BUILTIN. Both use RMT channel 0 at 100 ns a tick: a 0 is 400 ns high and 800 ns low, a 1 is 800 ns high and 400 ns low, both inside the WS2812B datasheet's windows, with the carrier switched off. BOOT_BUTTON is 0. A0 to A9 are GPIO 1 to 10.
mokxi_init, before the static constructors, sets the LEDC and RMT clock gates, switches UART0 to the APB clock at 115200, and routes the GPIO interrupt source. Every write that changes an LEDC output ends with its update bit.
Serial is UART0. USBSerial is the USB Serial/JTAG port: bytes go into its 64-byte buffer and the driver writes WR_DONE at each newline and in flush(), waiting for the buffer to be free rather than losing a byte.
crt0.S and support64.c are the classic ESP32's, compiled again for this board rather than copied: the window handlers, the vector table and the exception entry are the ISA's and not the chip's. Six more files are the ESP32-C3's, as on the classic ESP32.
Built by Espressif's GCC for the S3. deploy/fetch-xtensa.sh esp32s3 puts xtensa-esp32s3-elf-gcc 8.4.0 (esp-2021r2-patch5) in tools/xtensa/, which is gitignored. GCC is GPL, and nothing it builds here links against libgcc or any other part of the toolchain (the runtime is -nostdlib with its own support code), so no GPL code reaches a shipped ELF. docs/compile.md has the full account.
Linked against firmware/esp32s3/link.ld: .text at the instruction-bus address of SRAM1, the data sections at the data-bus address of the first byte after it, stack at the top of SRAM2. The build refuses an image with more than 64 KB loadable, and elfcheck.py refuses one whose code and data would share RAM.
Eleven examples, each with a sketch the UI shows: blink (the RGB LED white and a wired LED on GPIO2), rainbow (the RGB LED round the color wheel), button (a switch to ground on GPIO21 with the pad's own pull-up, lighting the LED on GPIO2), serial (a tick a second on UART0, and an echo), usbserial (the same on the USB port), fade (an LED on GPIO2 on LEDC, eight bits at 1 kHz), knob (a pot on GPIO4, ADC1 channel 3, dimming the LED on GPIO2 with the raw reading printed), and the four WiFi examples: wificonnect, wifiweather, wifiapi and wifilamp.
crates/esp32s3/tests/runtime.rs boots those ELFs and measures: blink's half period within 5 ms of 500, the WS2812 frames on GPIO48 decoded and every bit's high and low time inside the datasheet's windows, the colors rainbow sends in green-first order, LEDC's period within 5 us of 1 ms, millis() within 60 ms of the kernel's clock over five seconds, no two UART0 bytes closer than a byte time at 115200, and a USB line arriving as one packet. crates/esp32s3/tests/wifi.rs runs the WiFi examples against the simulated network. Each test skips itself when the images are not built.