πŸ”§ Hands-On Lab

Hands-On Lab: Linux 6.12 on ESP32-S31 β€” A Full MMU Kernel Inside a Microcontroller (Port Architecture & Flashing Guide)

SinoBot Editorial

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:

MMUPurpose
Espressif proprietary MMUMaps Flash/PSRAM into the address space (the foundation for XIP)
Sv32 MMUStandard 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_MEMMAP and 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

ModuleStatus
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:

  1. 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.

  2. 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.

  3. 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 all builds 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)