Hands-On Lab: Linux 6.12 on ESP32-S31 β A Full MMU Kernel Inside a Microcontroller (Port Architecture & Flashing Guide)
Hands-On Lab: Linux 6.12 on ESP32-S31 β A Full MMU Kernel Inside a Microcontroller
Bottom line up front: If a chip has an MMU, Linux has a fighting chance β the ESP32-S31 proves it with a RISC-V microcontroller: Linux 6.12 runs on Core 1 with Sv32 virtual memory and XIP execute-in-place, while Core 0 keeps running an ESP-IDF/FreeRTOS app, booting into a BusyBox shell in 1.25 seconds. The price: most peripheral drivers are unadapted, and the project is explicitly βexperimental hardware bring-up, definitely not for production.β
1. Why βLinux on a microcontrollerβ is worth taking seriously
In one line: Conventional wisdom says βLinux needs an application processorβ (Raspberry Pi, RK3588, i.MX); the ESP32-S31 is an MMU-equipped RISC-V MCU, and this port proves there is no physical barrier between MCUs and Linux β only an engineering tradeoff of βis it worth it.β
For robotics developers, the value lies in cost structure: application processors cost and consume several to dozens of times more than ESP32-class parts, yet the Linux ecosystemβs mature network stack (TCP/IP, TLS), filesystem, process model, and vast software catalog (BlueZ, Dropbear, tcpdump) are hard to replace on bare-metal RTOS. An MMU-equipped RISC-V MCU sits exactly in the middle ground between βRTOS too bareβ and βapplication processor too expensive.β
Project quick facts:
- Linux 6.12.0, riscv32-unknown-linux-musl toolchain (GCC 16.1.0 / Binutils 2.46)
- 98 stars on GitHub, 89 points on Hacker News
- Hardware: ESP32-S31-WROOM-3 module (16MB Flash), Core Board/Korvo dev board
- Boot time: 1.25s from initrd release to executing
/init
2. Hardware foundation: why the S31 can run Linux
The ESP32-S31 stands out with a dual-MMU design:
| MMU | Purpose |
|---|---|
| Espressif proprietary MMU | Maps Flash/PSRAM into the address space (the foundation for XIP) |
| Sv32 MMU | Standard RISC-V virtual memory; SATP CSR is writable β this is what Linux runs on |
The CPU architecture is RV32IMAFBCNSUX: RV32 base + M/A/F/B/C extensions + N/S/U privilege architecture + X (Espressif proprietary extensions, including xesploop and xespv 2p2). Notably, the S31 does not declare xespdsp, Zcb, Zcmp, or Zcmt β those are intentionally disabled.
- Dual HP cores (Core 0 / Core 1) plus a low-power LP CPU
- Linux uses the soft-float ilp32 ABI (the F extension can be used in userspace while staying syscall/libc-ABI compatible with ilp32)
- N/S/U privilege levels are implemented by S31-specific OpenSBI and Linux privileged/trap paths, not appended to GCCβs -march
π Key insight: The real gatekeeper for βrunning Linuxβ isnβt CPU performance β itβs the trio of virtual memory (MMU) + privilege modes (S/U) + interrupt architecture. The S31 satisfies the first two with Sv32; the CLIC/CLINT interrupt path is bridged by OpenSBI.
3. Port architecture: dual-core split + OpenSBI XIP layout
This is the most elegant part of the project β one chip running two systems at once:
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β ESP32-S31 β
β ββββββββββββββββ ββββββββββββββββββββββββββ β
β β Core 0 β β Core 1 β β
β β ESP-IDF β β OpenSBI β Linux 6.12 β β
β β factory app β β (Sv32, XIP, S-mode) β β
β β (FreeRTOS) β β Buildroot userspace β β
β ββββββββββββββββ ββββββββββββββββββββββββββ β
β Single-core boot; loads firmware, maps partitions β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
Boot chain: ESP-ROM β ESP-IDF 2nd-stage bootloader β factory app (hello_world, Core 0 single-core) β OpenSBI (Core 1) β Linux 6.12 β /init β BusyBox shell
OpenSBI Flash XIP layout (the address contract β the core engineering decision):
- OpenSBI text/rodata stays in Flash and executes through the Flash MMU window (FW_TEXT_START=0x40030000)
- OpenSBIβs writable data/BSS/scratch/stack/heap lives in the top 1MB of the cached PSRAM alias (FW_RW_START=0x50F00000..0x51000000)
- Linux only gets the lower 15MB of the cached alias (0x50000000..0x50F00000, DT memory node)
- OpenSBI partition fixed at 512KB
- The factory app uses
CONFIG_SPIRAM_USE_MEMMAPand explicitly disables USE_CAPS_ALLOC/USE_MALLOC
π Why this design: XIP means kernel code never has to be staged into RAM first β with only 16MB Flash and limited PSRAM, this is what turns βit can runβ into βit fits.β Squeezing OpenSBIβs RW data into the top 1MB frees contiguous memory for Linux.
Actual boot log (measured):
[0.000000] Linux version 6.12.0-gb4777fc146b9-dirty (riscv32-unknown-linux-musl-gcc)
[0.000000] Kernel command line: earlycon=esp32s3uart,mmio32,0x2038a000 console=ttyS0,115200 rdinit=/init
[0.057390] Memory: 8744K/16384K available (1870K kernel code ...)
[1.254583] Run /init as init process
~ # β BusyBox shell, login successful
4. Build & flash: one-command make
The project uses a unified Makefile managing fully out-of-tree builds, with all artifacts in build/:
# One-shot build: download+verify prebuilt toolchain β opensbi β linux β rootfs
make all
# Toolchain (prebuilt, SHA256-verified)
make toolchain # riscv32-esp-linux-musl-gcc (GCC 15.2 / binutils 2.45 / musl)
# Component builds
make opensbi # OpenSBI + dynamically compiled DTB β build/fw_payload.bin
make linux # xipImage + esp32s31_generic.dtb (out-of-tree)
make rootfs # Buildroot: BusyBox/BlueZ/Dropbear/iproute2/tcpdump/memtester/CoreMark β rootfs.sqfs
# Flash (esptool parses the partition table dynamically)
pip install esptool
esptool -p /dev/ttyUSB0 -b 2000000 erase-flash
esptool -p /dev/ttyUSB0 -b 2000000 write-flash \
--flash-mode dio --flash-freq 80m --flash-size 16MB \
0x2000 bootloader.bin \
0x8000 partition-table.bin \
0x17000 ota_data_initial.bin \
0x20000 hello_world.bin \
0x220000 fw_payload.bin \
0x2A0000 xipImage \
0xA20000 rootfs.sqfs
Partition table (16MB Flash, QIO 80MHz): nvs β otadata β phy_init β factory (2MB) β opensbi (512KB) β linux (7.5MB) β rootfs (~6MB)
Userspace highlights: BusyBox (HTTPS wget with a size-optimized internal TLS client) + BlueZ Bluetooth tools + Dropbear SSH + iproute2 + tcpdump + memtester + CoreMark β a surprisingly complete embedded Linux distribution.
5. Porting status & the βreality checkβ list
| Module | Status |
|---|---|
| Buildroot rootfs / reboot / poweroff / UART0 console | π’ Stable |
| Wireless (ESP-Hosted) | π‘ Untested |
| AXI/AHB GDMA, Cache, TRNG, eFuse, Watchdog, PWM, CLIC/CLINT interrupt driver | π‘ Untested |
| Timers, clock tree, security accelerators | π WIP |
| USB | π WIP |
| LP subsystem & IPC, PMP/APM | π΄ Not implemented |
| GPIO, pinctrl/GPIO matrix, SDMMC, GMAC Ethernet | π‘ Untested |
| I2C | π΄ Not implemented |
| Dual-hart SMP | β« Not planned (Core 1 is taken by FreeRTOS) |
π§ Honest assessment: This is a research bring-up project, not a production-ready solution. The only currently usable peripheral is the UART console; Wi-Fi/Bluetooth depend on untested ESP-Hosted; GPIO/I2C arenβt wired up yet β βit bootsβ and βitβs usableβ are separated by a mountain of driver work. The authorβs own warning: βDefinitely not something you want for production.β
6. What robotics developers should take away
Bottom line: Donβt plan a production robot controller around the ESP32-S31 anytime soon, but this route points to three trends:
-
MMU is moving down-stack: MMU-equipped (Sv32) RISC-V MCUs will multiply, and the Linux/RTOS boundary will shift from βchip typeβ to βapplication need.β Need a mature network stack, filesystem, and multi-process isolation? Linux. Need hard real-time and low power? RTOS. The same chip can do both.
-
XIP is the key that unlocks Linux on MCUs: code executes directly from external flash, no RAM staging, and 16MB of flash fits kernel + rootfs. That matters for cost-sensitive edge nodes β sensor gateways, joint-controller upstreams.
-
Dual-core heterogeneous (FreeRTOS + Linux) is a practical architecture: Core 0 handles hard real-time (motor control, encoder reads), Core 1 runs Linux (communication, OTA, logging, AI inference scheduling) β the classic robot-controller division of labor. The S31 has already paved this path with bootloader + OpenSBI; next-generation chips just need to finish the peripheral drivers.
Practical advice for DIY hackers: if you want a low-cost taste of βLinux on a microcontroller,β this is a superb learning project β
make allbuilds the complete toolchain + kernel + rootfs in one command, one USB cable and esptool to flash, and the BusyBox shell 1.25 seconds later is a satisfaction no emulator can match.
π GitHub: GrieferPig/esp32-s31-linux | HN discussion
Hands-On Lab by SinoBot Editorial | Dual polish pending (Gemini + ChatGPT)