Boards

The ESP32 DevKit V1

The classic ESP32: an Xtensa LX6 at 240 MHz on the thirty-pin DOIT board, with the LEDC PWM controller, a twelve-bit ADC and the six pins that are input only.

The overview, with examples running, is on the board page and The ESP32 DevKit model (every register).

What it is

The DOIT ESP32 DevKit V1, an ESP32-WROOM-32 module on a thirty-pin carrier, running on our own Xtensa core, the first board here whose instruction set is neither RISC-V, AVR nor Arm. Firmware is an Xtensa ELF talking to the real ESP32 addresses.

The board is 51.5 by 28.3 mm and its two rows of fifteen pins are 0.9 inch apart, so it straddles a breadboard's center channel with one row of holes left free on each side.

This is the thirty-pin DOIT board, not the 38-pin Espressif DevKitC. The DevKitC is a full inch between rows, leaves no row free at all, and brings out more pins. If your board has 38 pins, use the ESP32-DevKitC V4, which is a board of its own in the parts bin.

The pins

Fifteen down each long edge, the micro-USB socket at the bottom, the module and its PCB antenna at the top.

Left column, top to bottom: EN, VP, VN, D34, D35, D32, D33, D25, D26, D27, D14, D12, GND, D13, VIN.

Right column, top to bottom: D23, D22, TX0, RX0, D21, D19, D18, D5, TX2, RX2, D4, D0, D2, D15, 3V3.

Dn is GPIO n. VP is GPIO 36 and VN is GPIO 39, named on the silkscreen for their function rather than their number. EN is the chip enable, which is this board's reset. VIN is the 5 V rail from USB.

This is a 3.3 volt board and its pins are not 5 V tolerant.

Three things the pinout does to you

These are the hardware, not the model, and each one catches somebody:

  • GPIO 34 to 39 cannot be outputs. Ever. Those six pins have an input buffer and nothing else: no output driver and no internal pull-up or pull-down. pinMode(34, OUTPUT) writes the register, the bit does not stick, and the pin stays high-impedance. Four of them are on this header: D34, D35, VP and VN. Mokxi refuses the write the way the chip does, counts it, and puts a small amber mark on the board rather than letting you believe a pin is driving something. If you need a pull-up on one of these, it has to be a resistor.
  • GPIO 6 to 11 are not on the header at all, because they are the SPI flash the program is running from. On boards that do bring them out, driving one stops the chip.
  • GPIO 0, 2, 5, 12 and 15 are strapping pins. They decide how the chip boots, so what is wired to them at power-on matters. GPIO 2 is also the blue LED, so its state before pinMode runs varies from board to board.

The LED and the button

LED_BUILTIN is GPIO 2, an ordinary blue LED wired the ordinary way around: HIGH lights it. BOOT is GPIO 0, active low, and it is on the header as D0 as well, so you can wire to it; clicking it on the board grounds the pin.

analogRead: eight channels, and 4095 is not where a 3.3 V rail lands

ADC1's eight channels are not GPIO 0 to 7. They are, in order, GPIO 36, 37, 38, 39, 32, 33, 34 and 35, and 37 and 38 are not brought out on a WROOM-32, so what you can actually reach is VP, VN, D32, D33, D34 and D35.

Twelve bits, and at the default 11 dB attenuation the reading follows a real ESP32's curve rather than a straight line: 0 for the first 80 mV or so, close to linear through the middle, and 4095 from about 3.1 V. A potentiometer across this board's own 3.3 V rail therefore reaches 4095 a little before the end of its travel and reads about 1890, not 2048, in the middle. That is the real part, and the knob example prints the raw number so it is not a mystery. analogReadMilliVolts() undoes the curve and reads volts.

ADC2 is not modeled. On a real ESP32 ADC2 is shared with the WiFi radio and stops working when WiFi starts, which is why nobody uses it; here it is simply absent, so the eight channels above are all of them.

The straight-line conversion here is an approximation of a real converter that bends at both ends and is corrected from a calibration curve burned into the chip. Expect the model to be a little kinder than the silicon near 0 V and near the top of the range.

analogWrite is LEDC, and it costs the core nothing

analogWrite(pin, 0..255) attaches one of LEDC's eight high-speed channels to the pin the first time it is called: eight bits at 1 kHz, on any pin with an output driver, because the channel reaches the pad through the GPIO matrix rather than through a fixed pin. When the frequency matters, ledcAttach(pin, frequency, bits) and ledcWrite(pin, duty) are there, and channels asking for the same frequency and resolution share one of the four timers.

Because LEDC is real hardware, a fading LED here costs the core nothing at all: the sketch can sit in delay() and the edges still land on time. The ESP8266 NodeMCU is the interesting contrast: it has no PWM hardware, so its core does the work. Both boards are honest about it.

Serial

Serial is UART0, which is the port a DevKit's USB bridge is on, and it is wired to the serial monitor. Bytes leave at the baud rate you set, so a sketch that prints faster than 115200 baud can carry waits here exactly as it does on a desk.

Serial1 and Serial2 are UART1 and UART2, real ports on whatever pins you give them, sending and receiving frames bit by bit on the wire, so a GPS module, an HC-05, a PZEM-004T, an RS-485 transceiver or a second board on those pins hears them:

Serial2.begin(9600, SERIAL_8N1, 16, 17); // RX, TX: the pins marked RX2 and TX2

What they do is what the arduino-esp32 3.x core does:

  • With no pins, Serial1.begin(9600) uses GPIO 26 (RX) and 27 (TX) and Serial2.begin(9600) GPIO 4 and 25. Core 2.x used 16 and 17 for Serial2, which is why older tutorials leave the pins out; Mokxi says so once in the monitor when a sketch does. setPins(rx, tx) moves them, before or after begin().
  • The frame can be any of SERIAL_5N1 to SERIAL_8O2, and begin()'s invert turns both lines over.
  • Received bytes reach the sketch in chunks, as with ESP-IDF's driver: the UART moves its FIFO into a 256-byte ring (setRxBufferSize() before begin()) once 120 bytes have come in (setRxFIFOFull()) or the line has been quiet for one character time (setRxTimeout()). available() counts the ring, so a byte still in the FIFO is not available yet. A sketch that reads too slowly loses bytes the way it does on the desk.
  • onReceive() is called after each chunk, between loop()s and while the sketch waits in delay(), rather than from a FreeRTOS task.
  • write() waits while the 128-byte FIFO is full, and flush() until the last stop bit is out. setTxBufferSize() is taken, but there is no TX ring, so write() still waits on the FIFO.

Hardware flow control, RS-485 mode, IrDA and autobaud are not modeled. The TX pin needs an output driver, so GPIO 34 to 39 can only be RX. SoftwareSerial.h still works on any two pins as well.

One core, not two

A real ESP32 has two Xtensa LX6 cores, and an Arduino sketch here runs on one: no APP CPU, no cross-core interrupt and no FreeRTOS to schedule across them. A sketch written around setup() and loop() never notices. Anything that starts a task on core 1 will not run.

A firmware you built yourself is different: MicroPython and an ESP-IDF build run on a model of the whole chip, where both cores do run (below).

What is modeled

The Xtensa LX6 in its windowed ABI (the only ABI its compiler emits), with the register window, the window overflow and underflow handlers, and level-1 interrupts. Around it: the GPIO matrix and IO_MUX, UART0, timer group 0's timer 0 behind millis() and micros(), ADC1 one-shot through the SENS block, LEDC's high-speed group, and as much of DPORT's interrupt matrix as two sources need.

That is the set pinMode, digitalWrite, digitalRead, analogRead, analogWrite, ledcAttach, ledcWrite, millis, micros, delay, attachInterrupt and Serial need, at their real addresses.

WiFi, simulated

There is no radio. There never will be: an 802.11 MAC is not something this model could reproduce honestly.

What there is instead is a simulated network. A block that is Mokxi's own rather than the chip's (a mailbox at 0x3FEF_E000, where a real ESP32 has nothing mapped at all) carries WiFi.h, HTTPClient.h and WebServer.h, so the Arduino code a tutorial gives you runs here: the board joins an access point whose name and password are properties of the board, WiFi.status() walks the same states in the same one to three seconds, DHCP leases it 192.168.4.2, and a sketch can fetch a page or serve one.

What is behind it is software, and nothing is hidden about that. There is no 802.11, no channel, no scan, no WPA2, no TCP, no TLS and no real internet: four made-up names under .mokxi resolve and no others, and nothing a sketch does can reach the network your browser is on. WiFiClientSecure compiles and then fails, saying TLS is not simulated. The Web and Cloud panels along the bottom of the editor open with that sentence and do not scroll it away.

The one exception is Real internet, a switch in the Cloud panel for anyone signed in (more of it with Pro) that is off every time a page loads. With it on, a request for a name outside .mokxi, https:// included, goes out through Mokxi's gateway and the answer comes back to the sketch. See Real internet.

The address is in the gap the chip's data bus leaves between the external memory windows and the peripheral window, so an image that uses it faults on silicon rather than reading some other peripheral's register. Bluetooth is simulated through the same block, Classic and LE, with a phone in the editor's Bluetooth pane and no radio (Bluetooth, simulated).

Three examples ship for this board: WiFi: join the network, WiFi: fetch the weather and WiFi: a lamp on a web page.

WiFi, simulated is how to use it; Simulated WiFi is the contract, limit by limit.

What is not

A radio of any kind. There is no 802.11 and no Bluetooth radio in the model and no pretense of one; the WiFi and Bluetooth above are simulated and say so. The second core. The FPU: every floating-point instruction faults here rather than returning a wrong number, and the runtime is soft float, so float works and hard-float vendor binaries do not. The cache, flash and PSRAM: code runs from internal RAM. The RTC, the ULP coprocessor and deep sleep. ADC2, the touch sensor and the hall sensor. The I2C and SPI controllers, I2S and RMT. Wire and SPI in the runtime are software on ordinary pins, as on every board here: Wire.begin() puts SDA on 21 and SCL on 22, and SPI uses 18 (SCK), 19 (MISO), 23 (MOSI) and 5 (SS). Adafruit_NeoPixel.h drives a WS2812 strip by timing each bit on the core's cycle counter, since the RMT a real ESP32 would use is not here. The timer groups beyond timer group 0's first timer and its watchdog (which runs the task watchdog, as ESP-IDF does).

Vendor ESP-IDF and Arduino-core binaries will not run here: the runtime is ours.

Compiling your sketch

Edit the sketch and press Run: it compiles in the tab, like every other board here, with a real clang compiled to WebAssembly. For this chip that is Espressif's own clang, which has the Xtensa back end, so the first build on an ESP32 downloads a separate toolchain (clang-v3, about 80 MB, once) and later builds reuse it. Nothing is uploaded. An untouched example still runs its prebuilt program; change a line and Run builds yours.

Floating-point arithmetic works: this model has no FPU, so the build does float and double sums in software, as on the boards with no FPU at all. Compiling in the browser has the full account.

The full list of what the model implements, and a plain account of which register addresses came out of the technical reference manual and which are this model's belief, is in the ESP32 DevKit model. Section 6 is the one to read before taking a build from here to real silicon.

The stock Arduino libraries the other boards compile (Servo.h, Wire.h, EEPROM.h, Adafruit_SSD1306.h and the rest) are on the include path here too. Wire.h, Adafruit_NeoPixel.h and Keypad.h are checked on this board, with an I2C scan on 21 and 22, a lit strip and a 4x4 keypad; the others have not been checked here yet.

MicroPython

The board can run Python instead of a sketch. Pick MicroPython (main.py) in its Firmware panel and Run boots the official MicroPython v1.29.0 firmware for the ESP32 and runs your main.py. That runs on a model of the whole chip, booted from Espressif's mask ROM, so the limits above are the Arduino runtime's, not MicroPython's: machine.I2C, machine.SPI, the RMT behind neopixel and network.WLAN are there for it. See MicroPython on the ESP32.

The examples

Blink, Button, Serial, Fade, Knob, and the three WiFi ones: WiFi: join the network, WiFi: fetch the weather and WiFi: a lamp on a web page. Each one runs its prebuilt program until you change it, and then Run builds your version.

A firmware you built yourself

An Arduino sketch is built here, and it runs on one core. A program built somewhere else, with ESP-IDF or in Rust (esp-hal, esp-idf-hal), runs too, on a model of the whole chip with both cores: upload its .bin or .elf in the Firmware panel, and the board boots it as after idf.py flash. Wi-Fi and Bluetooth are the gap, since their drivers are Espressif's closed libraries: a firmware that starts either one stops with a line saying so. The steps and the whole list are in Run a prebuilt firmware.

Elsewhere