Write Xtensa assembly or aBelle, put it on the board and debug it step by step – without a JTAG probe, without esptool, without ESP-IDF.
The original ESP32 from Espressif has two Xtensa LX6 cores at 240 MHz and no USB port of its own. Asmonite assembles its code (.target esp32) and compiles programs in the language aBelle for it, loads a program into the RAM or writes it into the flash, and debugs it over the USB cable of the board: breakpoints, steps, registers, memory. On top come a console with files in the flash and the WiFi: an aBelle program connects to a network, gets an address by DHCP and speaks UDP and TCP, also while the debugger holds it at a breakpoint.
Tested with an ESP32 DevKit V1 (ESP32-D0WD, CP2102, 4 MB of flash, LED at GPIO 2) and a board with an ESP32-WROVER (CH9102, 8 MB of flash). The full guide is in the source repository as docs/en/esp32.md.
| Assembler | Xtensa with register windows, a literal pool and the 16-bit instructions; code in the IRAM, data behind .data in the DRAM. |
| aBelle | A small language with the look of Python; target "esp32" compiles for the same RAM, the debugger shows the source. |
| Debugger | Over the serial port: breakpoints, F7/F8, pausing a running program, registers, memory, the output in the tab Serial. |
| Flash | A program as an image of the boot ROM at 0x1000; it starts by itself after every reset. |
| Console | A file system in the flash, a window for the files, starting programs and an autostart. |
| WiFi | Station with WPA2 or open, DHCP, DNS, ping, UDP, TCP (client and server), mDNS (esp32.local). |
esptool flash_id.examples\esp32\blink_ram.asm and press F7: Asmonite puts the chip into the download mode, loads the program and stops at _start. F7 and F8 take a step, F9 runs, F5 sets a breakpoint, Debug > Pause stops.The download mode. The boot ROM waits for commands on UART0 when GPIO 0 is low at a reset. The boards do that over the lines DTR and RTS of the USB serial chip; Asmonite does it as esptool does and checks the first line of the boot ROM after each reset. On some boards that takes many attempts, because the capacitor at EN is too small. If the board does not get there within 10 seconds, Asmonite asks you to hold BOOT down. A capacitor of about 10 µF between EN and GND fixes it for good.
921600 baud. Big transfers go eight times as fast as at 115200: the flash over the boot ROM, a program of 16 KB or more under the debugger, files of 16 KB or more. Asmonite checks the rate with a short exchange and falls back to 115200 if the board does not manage it. A program of 98 KB loads in a little over 6 seconds. Before the program runs, the rate is 115200 again. The environment variable ASMONITE_ESP_BAUD sets another rate.
The LED at GPIO 2 blinks: set the pin up as a GPIO, make it an output, then switch it on and off in a loop. Files: examples\esp32\blink_ram.asm and examples\abelle\esp32\blink.abel (shortened).
.target esp32 .arch xtensa .imagebase 0x40080000 ; IRAM lit_gpio: dd 0x3FF44000 ; the GPIO registers lit_half: dd 20000000 ; half a second at 40 MHz ... _start: l32r a2, lit_gpio ... ; IO_MUX, output select movi a3, 4 ; 1 << 2: GPIO 2 s32i a3, a2, 0x24 ; an output l32r a7, lit_half loop: s32i a3, a2, 0x08 ; on call0 wait s32i a3, a2, 0x0C ; off call0 wait j loop wait: rsr a9, ccount wait_more: rsr a10, ccount sub a10, a10, a9 bltu a10, a7, wait_more ret
target "esp32" gpio: u32[0x140] at 0x3FF44000 iomux: u32[64] at 0x3FF49000 def ets_delay_us(us: u32) at 0x40008534 # ROM def main(): iomux[0x40 / 4] = 0x2800 # GPIO 2 as a GPIO gpio[0x538 / 4] = 0x100 # a simple output gpio[0x24 / 4] = 1 << 2 # an output while True: gpio[0x08 / 4] = 1 << 2 # on ets_delay_us(500000) gpio[0x0C / 4] = 1 << 2 # off ets_delay_us(500000)
Xtensa loads a 32-bit constant with l32r from a word in front of the code; ccount counts the cycles of the core. In aBelle the registers are an array at their address, and a function of the ROM is made known with at and its address. Both programs lie in the IRAM from 0x40080000 on, and the debugger goes through both line by line: in the assembly instruction by instruction, in aBelle line by line of the source, with the variables under the mouse and in the window Watch.
dice.abel on the ESP32 at a breakpoint: on the right the variables in the window Watch (counts, how often each number came), below in the tab Serial what the program writes over UART0.
F9 or F7 puts the chip into the download mode, the boot ROM loads Asmonite's debug monitor (a little over 2 KB at 0x4009E000), and the monitor loads the program and starts it at _start. It points VECBASE at its own vector table: break 1,15 (a breakpoint), the step of ICOUNT and every exception land there. It saves the registers and answers Asmonite's commands over UART0. It has the handlers of the register windows, so a chain of calls with call4, call8 and call12 deeper than the 64 registers hold works too.
Pause and stop. Debug > Pause stops a running program where it is, also in an endless loop. For that Asmonite sends a byte over UART0; the monitor routes the interrupt of the UART to the NMI of the core, above the interrupt level 5 the program runs at. Stop (Ctrl+F2) ends the session at once.
Exceptions. An illegal instruction, a load from an address without memory or a byte store into the IRAM stops the program at the instruction, with the cause in the output window. The core reads and writes the IRAM in words of 32 bits only: l32i and s32i work, l8ui and s8i do not.
.dataTexts, tables and buffers go behind .data: these lines land in the DRAM from 0x3FFB0000 on, where the core reads and writes bytes, halfwords and words. .code switches back to the code.
lit_text: dd text ; the address in the DRAM, for l32r _start: l32r a3, lit_text l8ui a2, a3, 0 ; a byte of the text ... .data text: db 'Hello, world!', 13, 10, 0 buffer: resb 64 ; room, filled with zeros when it is loaded
The debugger writes the bytes of .data into the DRAM after the code; a program for the flash has them as a second segment of the image, which the boot ROM copies at every start. The stack starts at 0x3FFC0000 and grows down; data that reach into it are an error.
.target esp32 | F9 loads the program into the IRAM from 0x40080000 (120 KB up to the monitor). Quick to try out; after a reset whatever is in the flash runs again. |
.target esp32 flash | F9 writes the program into the flash as an image of the boot ROM at 0x1000 and then debugs it in the RAM. It starts by itself after every reset. An image that does not fit into the flash of the board Asmonite refuses. |
| Write to Flash and Run (Shift+F9) | The same for a program with .target esp32: the same source without a change into the RAM (F9) or into the flash (Shift+F9). |
A program that the boot ROM starts from the flash has to switch off the watchdogs itself, else the chip resets after a few seconds; examples\esp32\blink_flash.asm shows how with a few lines at _start.
The tab Serial. What the program writes into UART0 comes out at the COM port, at 115200 baud. Under the debugger the monitor shares the port with the program, and the output shows in the tab Serial next to the output window. Without the debugger the tab is a small terminal: Connect, an input line and Reset, which resets the chip over RTS without closing the port. Otherwise the terminal leaves the lines DTR and RTS off, so that a program in the RAM is not gone when the port opens.
examples\esp32\console.asm makes the board a small computer with a file system in the flash, from 2 MB to the end of the flash: 2 MB on a flash of 4 MB, 6 MB on one of 8 MB. Any terminal program works with it (ls, cat, put, rm, df, format, run, load, boot, help), and Debug > ESP32 Board Files shows its files in a window.
| Write Console to Flash | Writes the console into the flash over the boot ROM; the first time Format makes the file system. The head line names the flash of the board. |
| Upload File, Download, Delete | Files of any kind between the PC and the board, each checked by its CRC-32: 100 KB up in a little under 4 seconds, down in 2. |
| Upload This Program, Run, Autostart | Compiles the program in the editor (assembly or aBelle) and uploads it as a file; Run starts it, Autostart starts it after every reset. |
| Reset Board | Resets the board and keeps it in the console (as BOOT or a key in the terminal right after the reset do). |
| Erase Flash | Erases the whole flash as on a new board (8 to 17 seconds on 4 MB). Then the boot ROM says "flash read err, 1000" until something is in the flash again. |
| Install WiFi | Puts the WiFi onto the board (see below). |
A program for F9 fits into the console as it is: its code may take 0x40080000..0x40097FFF, its data 0x3FFB0000..0x3FFCFFFF. The console does not run under the debugger, because the debug monitor takes every byte on UART0 as a pause.
The WiFi of the ESP32 is in libraries of Espressif without source. Asmonite links them once with its own linker into a fixed file, wlan.rom, and brings a small service, wlansvc, which starts them and offers simple functions to programs. No ESP-IDF is needed for it.
0x100000) goes into the flash over the boot ROM, wlan.rom and wlansvc into the files. This takes about 25 seconds.examples\abelle\esp32\inc\network.abel enter the name and the password of your network (empty for an open network, else WPA2, 2.4 GHz).ntp.abel and press F9. The program loads the service from the files of the board itself at its first WiFi call, with F9 as with run ntp in the console or as the autostart.Debugging while the WiFi runs. The service and the WiFi run on the second core of the ESP32, the program on the first. The debugger stops the first one only: while the program stands at a breakpoint, the station stays connected, and what comes in waits in a buffer of the service (6 frames). So you can go through a DHCP exchange or a TCP connection line by line.
ping.abel at a breakpoint after the pings: the connection stays up, in the tab Serial the replies, on the right the variables of the IP stack (address, mask, router, lease).
| Function | What it does |
|---|---|
wifi_scan, wifi_connect, wifi_status | find networks, connect (open or WPA2), read the state |
wifi_send, wifi_recv | Ethernet frames out and in |
dhcp_run | get an address, the mask, the router and the DNS server from the router |
dns_lookup | turn a name into an address |
ping_send | send a ping (the board answers pings to it by itself) |
udp_send, net_recv | UDP |
tcp_connect, tcp_listen, tcp_accept, tcp_send, tcp_recv, tcp_close | TCP, one connection at a time, as a client or a server |
net_set_name, mdns_start | the name of the board: the router learns it by DHCP, mDNS answers name.local (ping esp32.local) |
wifi_error_text | an error as a sentence, for example the hint to Install WiFi when the files are missing |
The functions are in inc\wifi.abel, inc\net.abel and inc\tcp.abel, written in aBelle: you can read DHCP, DNS, ARP, ICMP, UDP, TCP and mDNS there and follow them in the debugger.
In examples\esp32 are the examples in assembly: blink, hello_uart (each for the RAM and the flash), clock (the core at 80, 160 and 240 MHz) and the console. In examples\abelle\esp32 are the examples in aBelle: blink, hello_uart, chipinfo, clock, timer, dice, reaction, sieve, temperature, and for the WiFi scan, dhcp, dns, ping, wol, arpscan, ntp, udpecho, httpget, webserver and telnetd. The registers they use are in their comments, with the source.
| Area | What is there |
|---|---|
flash 0x001000 | the console (or the program of Write to Flash and Run) |
flash 0x100000 .. 0x152000 | the part of the WiFi for the flash (code and constants, through the cache) |
flash 0x200000 .. end | the file system of the console |
IRAM 0x40080000 .. 0x40097FFF | the program (with WiFi: its code up to 0x40093000) |
IRAM 0x40094000 | the service wlansvc, above it wlan.rom |
IRAM 0x4009C000 / 0x4009E000 | the console / the debug monitor |
DRAM 0x3FFB0000 .. | the data of the program (with WiFi: up to 0x3FFD8000), the stack from 0x3FFC0000 down |
DRAM 0x3FFD0000 | the data of the console |
DRAM 0x3FFD8000 .. 0x3FFDFFFF, 0x3FFE8000 .. | the data of the WiFi and its heap |
| Message or sign | What to do |
|---|---|
| "No board with an ESP32 is plugged in" | The USB cable (some carry power only), the driver of the USB serial chip |
| "… does not go into the download mode by itself: hold the BOOT button" | Hold BOOT down until the program runs |
| "COM14 is open in another program" | Close the terminal, then F9 again |
| "The console does not answer" (Board Files) | A program runs (Reset Board), or the console is not in the flash (Write Console to Flash) |
| "flash read err, 1000" at the COM port | The flash is empty (a new or erased board): Write Console to Flash |
| "wifi_connect: error 2041 (wlan.rom or wlansvc is not on the board …)" | Install WiFi in Board Files |
| "… the access point said no" / "no answer of the access point" | The password in inc\network.abel, the name of the network, a network on 2.4 GHz in reach |
| The board starts again every few seconds | A program in the flash that does not switch the watchdogs off |
| "Stopped: exception at …: load/store error" | A byte access to the IRAM, or an address without memory |
VECBASE, use UART0 for input, switch on interrupts above level 5 or switch off the CPU interrupt 14: the monitor needs them.Every example for the ESP32: what it shows, what it needs, how it starts.
The language with the look of Python, for the ESP32 and Windows (x86, x64).
Every Xtensa instruction with its bit encoding and pseudo code, for the ESP32 and the ESP32-S3.