Xtensa-Assembler oder aBelle schreiben, aufs Board bringen und Schritt für Schritt debuggen – ohne JTAG-Probe, ohne esptool, ohne ESP-IDF.
Der ursprüngliche ESP32 von Espressif hat zwei Xtensa-LX6-Kerne mit 240 MHz und keinen eigenen USB-Anschluss. Asmonite assembliert seinen Code (.target esp32) und übersetzt Programme in der Sprache aBelle für ihn, lädt ein Programm ins RAM oder schreibt es in den Flash und debuggt es über das USB-Kabel des Boards: Breakpoints, Einzelschritte, Register, Speicher. Dazu kommen eine Konsole mit Dateien im Flash und das WLAN: Ein aBelle-Programm verbindet sich mit einem Netz, holt eine Adresse per DHCP und spricht UDP und TCP, auch während der Debugger es an einem Breakpoint anhält.
Getestet mit einem ESP32 DevKit V1 (ESP32-D0WD, CP2102, 4 MB Flash, LED an GPIO 2) und einem Board mit ESP32-WROVER (CH9102, 8 MB Flash). Die ausführliche Anleitung steht im Quelltext-Repository unter docs/de/esp32.md.
| Assembler | Xtensa mit Registerfenstern, Literal-Pool und den 16-Bit-Befehlen; Code im IRAM, Daten hinter .data im DRAM. |
| aBelle | Eine kleine Sprache mit der Schreibweise von Python; target "esp32" übersetzt für dasselbe RAM, der Debugger zeigt den Quelltext. |
| Debugger | Über den seriellen Port: Breakpoints, F7/F8, Pause eines laufenden Programms, Register, Speicher, die Ausgabe im Reiter Serial. |
| Flash | Ein Programm als Image des Boot-ROMs bei 0x1000; es startet nach jedem Reset von selbst. |
| Konsole | Ein Dateisystem im Flash, ein Fenster für die Dateien, Programme starten und als Autostart. |
| WLAN | Station mit WPA2 oder offen, DHCP, DNS, Ping, UDP, TCP (Client und Server), mDNS (esp32.local). |
esptool flash_id.examples\esp32\blink_ram.asm öffnen und F7 drücken: Asmonite setzt den Chip in den Download-Modus, lädt das Programm und hält bei _start. F7 und F8 gehen einen Schritt, F9 lässt laufen, F5 setzt einen Breakpoint, Debug > Pause hält an.Der Download-Modus. Das Boot-ROM wartet auf Befehle über UART0, wenn GPIO 0 bei einem Reset low ist. Die Boards machen das über die Leitungen DTR und RTS des USB-Seriell-Chips; Asmonite macht es wie esptool und prüft nach jedem Reset die erste Zeile des Boot-ROMs. Auf manchen Boards braucht das viele Versuche, weil der Kondensator an EN zu klein ist. Kommt das Board nicht innerhalb von 10 Sekunden dorthin, bittet Asmonite, BOOT gedrückt zu halten. Ein Kondensator von etwa 10 µF zwischen EN und GND behebt es dauerhaft.
921600 Baud. Große Übertragungen laufen achtmal so schnell wie mit 115200: der Flash über das Boot-ROM, ein Programm ab 16 KB unter dem Debugger, Dateien ab 16 KB. Asmonite prüft die Rate mit einem kurzen Austausch und fällt auf 115200 zurück, wenn das Board sie nicht schafft. Ein Programm von 98 KB lädt in gut 6 Sekunden. Bevor das Programm läuft, gilt wieder 115200. Die Umgebungsvariable ASMONITE_ESP_BAUD setzt eine andere Rate.
Die LED an GPIO 2 blinkt: den Pin als GPIO einstellen, als Ausgang schalten, dann in einer Schleife an und aus. Dateien: examples\esp32\blink_ram.asm und examples\abelle\esp32\blink.abel (gekürzt).
.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 lädt eine 32-Bit-Konstante mit l32r aus einem Wort vor dem Code; ccount zählt die Takte des Kerns. In aBelle liegen die Register als Feld an ihrer Adresse, und eine Funktion des ROM wird mit at und ihrer Adresse bekannt gemacht. Beide Programme liegen im IRAM ab 0x40080000, und der Debugger geht durch beide Zeile für Zeile: im Assembler Befehl für Befehl, in aBelle Zeile für Zeile des Quelltexts, mit den Variablen unter der Maus und im Fenster Watch.
dice.abel auf dem ESP32 an einem Breakpoint: rechts die Variablen im Fenster Watch (counts, wie oft jede Zahl kam), unten im Reiter Serial, was das Programm über UART0 schreibt.
F9 oder F7 setzt den Chip in den Download-Modus, das Boot-ROM lädt den Debug-Monitor von Asmonite (gut 2 KB bei 0x4009E000), und der Monitor lädt das Programm und startet es bei _start. Er setzt VECBASE auf seine eigene Vektortabelle: break 1,15 (ein Breakpoint), der Schritt von ICOUNT und jede Ausnahme landen bei ihm. Er sichert die Register und beantwortet die Befehle von Asmonite über UART0. Er hat die Handler der Registerfenster, so geht auch eine Aufrufkette mit call4, call8 und call12, die tiefer ist, als die 64 Register fassen.
Pause und Stop. Debug > Pause hält ein laufendes Programm an, wo es gerade ist, auch in einer Endlosschleife. Asmonite schickt dafür ein Byte über UART0; der Monitor leitet den Interrupt des UART auf das NMI des Kerns, über der Interrupt-Stufe 5, mit der das Programm läuft. Stop (Strg+F2) beendet die Sitzung sofort.
Ausnahmen. Ein ungültiger Befehl, ein Laden von einer Adresse ohne Speicher oder ein Byte-Speichern ins IRAM hält das Programm am Befehl an, mit dem Grund im Ausgabefenster. Das IRAM liest und schreibt der Kern nur in Wörtern zu 32 Bit: l32i und s32i gehen, l8ui und s8i nicht.
.dataTexte, Tabellen und Puffer kommen hinter .data: Diese Zeilen landen im DRAM ab 0x3FFB0000, wo der Kern Bytes, Halbwörter und Wörter liest und schreibt. .code schaltet zurück zum 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
Der Debugger schreibt die Bytes von .data nach dem Code ins DRAM; ein Programm für den Flash hat sie als zweites Segment im Image, das das Boot-ROM bei jedem Start kopiert. Der Stack beginnt bei 0x3FFC0000 und wächst nach unten; Daten, die in ihn hineinreichen, sind ein Fehler.
.target esp32 | F9 lädt das Programm ins IRAM ab 0x40080000 (120 KB bis zum Monitor). Schnell zum Ausprobieren; nach einem Reset läuft wieder, was im Flash ist. |
.target esp32 flash | F9 schreibt das Programm als Image des Boot-ROMs bei 0x1000 in den Flash und debuggt es dann im RAM. Nach jedem Reset startet es von selbst. Ein Image, das nicht in den Flash des Boards passt, weist Asmonite ab. |
| Write to Flash and Run (Shift+F9) | Dasselbe für ein Programm mit .target esp32: derselbe Quelltext ohne Änderung ins RAM (F9) oder in den Flash (Shift+F9). |
Ein Programm, das das Boot-ROM aus dem Flash startet, muss die Watchdogs selbst abschalten, sonst setzt sich der Chip nach ein paar Sekunden zurück; examples\esp32\blink_flash.asm zeigt es mit ein paar Zeilen bei _start.
Der Reiter Serial. Was das Programm in UART0 schreibt, kommt am COM-Port heraus, mit 115200 Baud. Unter dem Debugger teilt sich der Monitor den Port mit dem Programm, und die Ausgabe erscheint im Reiter Serial neben dem Ausgabefenster. Ohne Debugger ist der Reiter ein kleines Terminal: Connect, eine Eingabezeile und Reset, das den Chip über RTS zurücksetzt, ohne den Port zu schließen. Die Leitungen DTR und RTS lässt das Terminal sonst aus, damit ein Programm im RAM beim Öffnen des Ports nicht verschwindet.
examples\esp32\console.asm macht aus dem Board einen kleinen Rechner mit einem Dateisystem im Flash, von 2 MB bis zum Ende des Flash: 2 MB bei 4 MB Flash, 6 MB bei 8 MB. Jedes Terminalprogramm arbeitet mit ihr (ls, cat, put, rm, df, format, run, load, boot, help), und Debug > ESP32 Board Files zeigt ihre Dateien in einem Fenster.
| Write Console to Flash | Schreibt die Konsole über das Boot-ROM in den Flash; beim ersten Mal legt Format das Dateisystem an. Die Kopfzeile nennt den Flash des Boards. |
| Upload File, Download, Delete | Dateien jeder Art zwischen PC und Board, jede an ihrer CRC-32 geprüft: 100 KB hoch in knapp 4 Sekunden, herunter in 2. |
| Upload This Program, Run, Autostart | Übersetzt das Programm im Editor (Assembler oder aBelle) und lädt es als Datei hoch; Run startet es, Autostart startet es nach jedem Reset. |
| Reset Board | Setzt das Board zurück und hält es in der Konsole (wie BOOT oder eine Taste im Terminal gleich nach dem Reset). |
| Erase Flash | Löscht den ganzen Flash wie bei einem neuen Board (8 bis 17 Sekunden bei 4 MB). Danach meldet das Boot-ROM „flash read err, 1000“, bis wieder etwas im Flash ist. |
| Install WiFi | Bringt das WLAN aufs Board (siehe unten). |
Ein Programm für F9 passt unverändert in die Konsole: Sein Code darf 0x40080000..0x40097FFF belegen, seine Daten 0x3FFB0000..0x3FFCFFFF. Unter dem Debugger läuft die Konsole nicht, denn der Debug-Monitor nimmt jedes Byte auf UART0 als Pause.
Das WLAN des ESP32 steckt in Bibliotheken von Espressif ohne Quelltext. Asmonite bindet sie einmal mit seinem eigenen Linker zu einer festen Datei zusammen, wlan.rom, und bringt einen kleinen Dienst mit, wlansvc, der sie startet und Programmen einfache Funktionen anbietet. ESP-IDF braucht es dafür nicht.
0x100000) geht über das Boot-ROM in den Flash, wlan.rom und wlansvc in die Dateien. Das dauert etwa 25 Sekunden.examples\abelle\esp32\inc\network.abel den Namen und das Passwort deines WLAN eintragen (leer für ein offenes Netz, sonst WPA2, 2,4 GHz).ntp.abel öffnen und F9 drücken. Das Programm lädt den Dienst beim ersten WLAN-Aufruf selbst aus den Dateien des Boards, mit F9 genauso wie mit run ntp in der Konsole oder als Autostart.Debuggen, während das WLAN läuft. Der Dienst und das WLAN laufen auf dem zweiten Kern des ESP32, das Programm auf dem ersten. Der Debugger hält nur den ersten an: Steht das Programm an einem Breakpoint, bleibt die Station verbunden, und was ankommt, wartet in einem Puffer des Dienstes (6 Frames). So kannst du durch einen DHCP-Austausch oder eine TCP-Verbindung Zeile für Zeile gehen.
ping.abel an einem Breakpoint nach den Pings: Die Verbindung steht weiter, im Reiter Serial die Antworten, rechts die Variablen des IP-Stacks (Adresse, Maske, Router, Lease).
| Funktion | Was sie tut |
|---|---|
wifi_scan, wifi_connect, wifi_status | Netze suchen, sich verbinden (offen oder WPA2), den Zustand lesen |
wifi_send, wifi_recv | Ethernet-Frames hinaus und herein |
dhcp_run | eine Adresse, die Maske, den Router und den DNS-Server vom Router holen |
dns_lookup | einen Namen in eine Adresse auflösen |
ping_send | einen Ping schicken (Pings an das Board beantwortet es von selbst) |
udp_send, net_recv | UDP |
tcp_connect, tcp_listen, tcp_accept, tcp_send, tcp_recv, tcp_close | TCP, eine Verbindung zur Zeit, als Client oder Server |
net_set_name, mdns_start | der Name des Boards: Der Router lernt ihn per DHCP, mDNS beantwortet name.local (ping esp32.local) |
wifi_error_text | ein Fehler als Satz, zum Beispiel der Hinweis auf Install WiFi, wenn die Dateien fehlen |
Die Funktionen stehen in inc\wifi.abel, inc\net.abel und inc\tcp.abel, in aBelle geschrieben: DHCP, DNS, ARP, ICMP, UDP, TCP und mDNS kannst du dort nachlesen und im Debugger verfolgen.
In examples\esp32 liegen die Beispiele in Assembler: blink, hello_uart (je für RAM und Flash), clock (der Kern mit 80, 160 und 240 MHz) und die Konsole. In examples\abelle\esp32 liegen die Beispiele in aBelle: blink, hello_uart, chipinfo, clock, timer, dice, reaction, sieve, temperature, und für das WLAN scan, dhcp, dns, ping, wol, arpscan, ntp, udpecho, httpget, webserver und telnetd. Die Register, die sie benutzen, stehen in ihren Kommentaren, mit der Quelle.
| Bereich | Was dort liegt |
|---|---|
Flash 0x001000 | die Konsole (oder das Programm von Write to Flash and Run) |
Flash 0x100000 .. 0x152000 | der Teil des WLAN für den Flash (Code und Konstanten, über den Cache) |
Flash 0x200000 .. Ende | das Dateisystem der Konsole |
IRAM 0x40080000 .. 0x40097FFF | das Programm (mit WLAN: sein Code bis 0x40093000) |
IRAM 0x40094000 | der Dienst wlansvc, darüber wlan.rom |
IRAM 0x4009C000 / 0x4009E000 | die Konsole / der Debug-Monitor |
DRAM 0x3FFB0000 .. | die Daten des Programms (mit WLAN: bis 0x3FFD8000), der Stack ab 0x3FFC0000 abwärts |
DRAM 0x3FFD0000 | die Daten der Konsole |
DRAM 0x3FFD8000 .. 0x3FFDFFFF, 0x3FFE8000 .. | die Daten des WLAN und sein Heap |
| Meldung oder Anzeichen | Was zu tun ist |
|---|---|
| „No board with an ESP32 is plugged in“ | Das USB-Kabel (manche führen nur Strom), der Treiber des USB-Seriell-Chips |
| „… does not go into the download mode by itself: hold the BOOT button“ | BOOT gedrückt halten, bis das Programm läuft |
| „COM14 is open in another program“ | Das Terminal schließen, dann noch einmal F9 |
| „The console does not answer“ (Board Files) | Ein Programm läuft (Reset Board), oder die Konsole ist nicht im Flash (Write Console to Flash) |
| „flash read err, 1000“ am COM-Port | Der Flash ist leer (ein neues oder gelöschtes 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“ | Das Passwort in inc\network.abel, der Name des Netzes, ein Netz mit 2,4 GHz in Reichweite |
| Das Board startet alle paar Sekunden neu | Ein Programm im Flash, das die Watchdogs nicht abschaltet |
| „Stopped: exception at …: load/store error“ | Ein Bytezugriff auf das IRAM, oder eine Adresse ohne Speicher |
VECBASE nicht ändern, UART0 nicht für Eingaben nutzen, keine Interrupts über Stufe 5 einschalten und den CPU-Interrupt 14 nicht abschalten: Der Monitor braucht sie.Jedes Beispiel für den ESP32: was es zeigt, was es braucht, wie es startet.
Die Sprache mit der Schreibweise von Python, für den ESP32 und Windows (x86, x64).
Jeder Xtensa-Befehl mit Bitkodierung und Pseudocode, für den ESP32 und den ESP32-S3.