Apache-2.0 · ESP32 · ESP32-S3 · ESP32-C3 · ESP32-C6
ESPIDFORTH is a Forth interpreter running natively on ESP-IDF — no Arduino layer. Open a serial console and the firmware becomes editable at runtime: define a word, run it, redefine it, roll it back.
============================================ ESPIDFORTH v0.5.0 Build: Sep 19 2026 13:02:41 ============================================ No PSRAM, using 100 KB Forth heap Forth engine initialized. ok> 2 3 + . 5 ok> : square dup * ; ok> 12 square . 144 ok> : square dup dup * * ; \ redefine it, live ok> 12 square . 1728 ok> chip-info Chip: ESP32-C3 rev 4, 1 core(s) Features: WiFi BLE MAC: 12:34:56:78:9a:bc Free heap: 198432 bytes ok>
You need PlatformIO. Pick the environment that matches your board; every one of the six builds in CI.
# clone and flash a C3 devkit, then attach the monitor git clone https://github.com/IoTone/ESPIDFORTH.git cd ESPIDFORTH pio run -e esp32c3 -t upload -t monitor # other targets pio run -e esp32s3 -t upload -t monitor # S3, PSRAM, 512 KB dictionary pio run -e esp32c6 -t upload -t monitor pio run -e esp32 -t upload -t monitor # classic Xtensa, UART0 console # _release variants strip the on-device test suites (~5.5 KB) pio run -e esp32c3_release -t upload
When the monitor attaches you get an ok> prompt. Type words to list the vocabulary, test to run 58 built-in assertions on the board, chip-info to see what you are sitting on, and bye to leave.
Using it as a component. components/forth/ is a self-contained ESP-IDF component with no dependencies beyond the SDK. Copy it in, give it two character-I/O callbacks, and call forth_init() — the whole public API is fourteen functions in one header.
All three run on the same silicon. They differ in what a change costs you and what you give up to get it.
| ESP-IDF in C | MicroPython | ESPIDFORTH | |
|---|---|---|---|
| Cost of one change | Edit, rebuild, flash, reboot — tens of seconds, and you lose all runtime state | Paste at the REPL; instant | Type at the REPL; instant |
| Flash footprint | Only what you write | ~1.5 MB for a standard build† | 183 KB dev · 178 KB release |
| Static RAM | Yours to budget | Interpreter + GC heap, ~100 KB and up† | 67 KB |
| Redefining live behaviour | Not without a reflash | Rebind a name; no rollback primitive | A dictionary savepoint rolls a bad patch back word-for-word |
| Timing determinism | Fully deterministic | Garbage collector can pause you | No GC — fixed dictionary and a bump allocator |
| Reaching an ESP-IDF API | Call it | Write a C module, rebuild the firmware | A five-line C wrapper, registered at boot |
| Floating point | Full | Full | None. Integer cells only |
| Library ecosystem | The entire vendor SDK | Large — drivers for most common parts | Essentially none. You write the wrappers |
| Learning curve | C — familiar | Python — familiar | Postfix and an explicit stack — unfamiliar to most |
† ESPIDFORTH figures are measured from this repository at v0.5.0 (esp32c3 and esp32c3_release). MicroPython figures are approximate for a standard ESP32 port and will vary with the build.
ESPIDFORTH wins on exactly one axis and it is a narrow one: the cost of changing a running system. If the board is on your bench and you are trying to find out what a sensor actually does, or it is deployed somewhere inconvenient and you need to adjust its behaviour without shipping a firmware image, that axis is the whole game.
If you need floating-point maths, an I²C driver someone else already wrote, or a team that can read the code without learning a new evaluation order, you want MicroPython or C. Those are good answers and this project does not pretend otherwise.
Current engine is a stub. The interpreter in components/forth/ is an original implementation of the core ANS word set written for this project — roughly 90 words. The full ESP32forth v7.0.8.0 engine is vendored in third_party/ and will replace it once its Arduino dependencies are stripped. Everything on this site is written against the stub, which is what ships today.
New board, undocumented peripheral. Poke it from the prompt until you understand it, then write the C once you know what you are writing.
Ship one firmware image, then send small Forth bundles over the air. forth_eval_rollback() applies a bundle line by line and reverts the whole thing if any line fails.
Per-unit test sequences as text over the serial link, changeable without re-flashing the fixture between product revisions.
67 KB of RAM and 178 KB of flash leaves room on parts where a Python runtime simply will not fit.
The entire language — parser, compiler, interpreter — is one readable C++ file. Students can modify the language itself in an afternoon.
The engine is a component, not an application. Register your own words and drive it from whatever already owns your main loop.
ESPIDFORTH was extracted from Project MagNET, an IoT data-sync and control platform, and MagNET remains its largest consumer. Most of the engine's API exists because one of these asked for it.
A swarm of small ESP32 nodes under a coordinating "ruler". A node joins the hive, requests a role, and is sent a role bundle — a signed JSON envelope carrying Forth source, typically under 2 KB, that the node installs and runs without reflashing. That is the mechanism tutorial 10 teaches, in production.
| Measure | Scale | Notes |
|---|---|---|
| Reference designs on the engine | 17 | Cameras, environment sensors, LED matrices, audio, mmWave presence, e-ink scribes |
| Registered FFI words | 170 | Against ~90 in the stock engine — the wrapper pattern scales |
| Typical role bundle | < 2 KB | Fits one hive KV_DATA frame |
Vocabularies are built per device from the same five-line wrapper shown in tutorial 8 — cam-snap, temp?, ws-px, mr60-status, scribe-store. A bundle written against the capabilities a node advertises runs on any chip family that offers them.
話す — peer-to-peer mesh chat over Thread and CoAP on the ESP32-C6, with no border router and no WAN. Its serial link is dual-mode: a line is either a structured HCP command or raw Forth, dispatched on the fly. That is why forth_set_io() exists — the link owns the input stream and drives the engine line by line with forth_eval() rather than surrendering to the blocking REPL.
Hanasu also shows the engine's portability boundary. It carries the role-bundle component but not the hive-KV words, which were deliberately split out so the bundle engine has no WiFi dependency and ports to Thread. Thirty-one mn-* words cover mesh state, peers, time sync and chat.
A nice piece of Forth reasoning from that project: botmode is a colon definition rather than an FFI primitive, because an FFI word cannot distinguish an empty stack from a pushed 0 — this engine's pop() returns 0 on underflow, so a bare botmode would read as 0 botmode and arm bot 0. depth can see the caller's stack and disambiguate:
: botmode depth 0> if mn-bot! else mn-bots then ;
forth_eval_rollback() (0.4.0) — because a role bundle arriving over the air must apply completely or not at all.forth_word_exists() (0.5.0) — so a host can ask "did that bundle define role-tick?" without evaluating anything.forth_error_count() — because forth_eval() returns 0 whether or not the text made sense.forth_set_io() — Hanasu's dual-mode link, as above.Forth is old, small, and unusually well documented. These are the sources worth your time, starting with the project this one is built from.
ffef117 (v7.0.8.0) in third_party/esp32forth/, under Apache-2.0, as the reference for the ongoing port. If you want a mature, feature-complete ESP32 Forth today, use this rather than waiting for us.
components/forth/forth_core.h is the API surface and is heavily commented; forth_core.cpp is the whole language. third_party/esp32forth/PROVENANCE.md records the upstream revision and licence handling.