Linux 7.2 and OpenSBI 1.9 on the ESP32-S31-Korvo-1 board. Linux uses the 16 MiB octal PSRAM as its memory.
ESP ROM -> ESP-IDF 2nd stage -> loader -> OpenSBI -> Linux -> initramfs -> Buildroot rootfs (SD)
The ESP32-S31 is a microcontroller: two RV32 harts at 320 MHz, 512 KiB of
SRAM, Wi-Fi 6 and Bluetooth. It was not made for Linux. This project runs a
stock Linux 7.2 kernel with a small patch set on it, with a real userspace:
a shell on the LCD, Wi-Fi with wpa_supplicant, SSH, USB keyboards and
sticks, vim, gdb, strace, MicroPython and a text-mode web browser
(links).
You can use it in two ways:
- To try it. Flash a release, write the card image, and log in. It takes about ten minutes.
- To learn from it. The SoC has no PLIC, no Zicbom and no coherent DMA. Each of these has a solution here that you can read in a few hundred lines. docs/internals.md tells the full story for kernel developers.
- Get the files of the newest release from the releases page.
- Flash:
pip install "esptool>=5.4", connect the UART Type-C port, thenesptool --chip esp32s31 -p PORT -b 921600 write-flash 0x0 flash.bin. - Write
*-sdcard.img.xzto a microSD card (for example with balenaEtcher), and put the card into the board. - Open the console on the UART port at 115200 baud. Push reset. After about 30 seconds you are root.
- Connect to Wi-Fi (see Wi-Fi), then try
ssh,links,micropythonoriperf3.
The root password is korvo-bringup. Change it before you connect the board
to a network that you do not control.
A tag v* starts .github/workflows/release.yml. It builds everything on
a Linux runner (scripts/ci-build.sh), packages the files
(scripts/package-release.sh) and publishes a GitHub release. A tag with
a - (for example v0.2.0-rc1) makes a pre-release. Usual commits do not
start a build. To test the workflow without a release, run it by hand from
the Actions tab: the files are then workflow artifacts only.
The ESP-IDF commit is pinned in bootloader/esp-idf.version. Use the same
commit for a local build.
- UART console (
ttyS0) on the CP2102N bridge. - 800x480 LCD as
/dev/fb0withfbcon, and a login ontty1. - USB host (DWC2): keyboards, mice and mass storage.
- microSD (
dw_mmcwith internal DMA): FAT on p1, ext4 root on p2. - Wi-Fi as
wlan0(cfg80211 full-MAC) through the ESP-Hosted firmware on hart 0: station withwpa_supplicant(WPA2 and WPA3-SAE), access point withhostapd. iperf3 TCP gives approximately 46 Mbit/s TX and 41 to 43 Mbit/s RX. - Hardware RNG, GPIO,
rebootandpoweroff.
Espressif makes new chip revisions, and a different revision can change the behavior. This port is tested only on this board:
| Item | Value |
|---|---|
| Board | ESP32-S31-Korvo-1 |
| Chip | ESP32-S31, revision v0.0 (wafer major 0, minor 0) |
| Package version (eFuse) | 0 |
| eFuse block version | 0.0 |
| PSRAM (eFuse vendor 1, capacity 1) | 16 MiB octal |
| Flash | 16 MB NOR (JEDEC manufacturer 0x46, device 0x4018) |
| Crystal | 40 MHz |
To read these values from your board:
python -m esptool --chip esp32s31 -p PORT chip-id
python -m esptool --chip esp32s31 -p PORT flash-id
python -m espefuse --chip esp32s31 -p PORT summary- Harts. Hart 0 stays in M-mode. It runs the ESP-IDF loader, which stays resident as the Wi-Fi firmware. Linux runs on hart 1 only.
- Loader. The loader checks the size and CRC of the kernel. It copies the
kernel to
0x50000000, the initramfs to0x50800000and OpenSBI to0x50E00000. It starts the LCD, the SD slot and Wi-Fi. Then it releases hart 1 into OpenSBI. - Interrupts. OpenSBI gives the CLIC inputs to S-mode. If an input is not
in S-mode, Linux cannot see it. The S-mode trampoline must use
sretand restorescause.spil.mretlocksmintstatus.SILand masks all supervisor interrupts. - Cache. PSRAM is write-back. The panel DMA reads the frame buffer from
PSRAM without the cache, so OpenSBI makes the frame buffer range uncached
with PMA entries. No bus master is coherent with the data cache, and the
hart has no Zicbom. DMA uses the cache sync engine, which OpenSBI programs
for Linux through a vendor SBI extension
(
drivers/cache/esp32s31-cache.c). The I-cache refills from PSRAM, not from the D-cache, so Linux writes the D-cache back before eachfence.i. Coherent DMA memory comes from a 64 KiB pool in SRAM at0x2F040000. - Shared SRAM.
0x2F040000-0x2F060000is outside the ESP-IDF heap. The lower half is the DMA pool. The upper half holds the Wi-Fi rings (shared/esp32s31-hosted-shm.h). - Display. The frame buffer is at
0x50F40000. The loader DMA continues to scan it out after the handoff. Linux uses it as asimple-framebuffer.
| Connector | Use |
|---|---|
| Power Type-C | Power only. |
| UART Type-C | CP2102N to UART0: flash and Linux console (/dev/cu.usbserial-*). |
| USB Type-A | USB 2.0 host for Linux. |
| GPIO33/34 breakout | Native USB Serial/JTAG (/dev/cu.usbmodem*): D- white to GPIO33, D+ green to GPIO34, GND black. Do not connect 5 V. |
GPIO33/34 are also LCD data pins. You can use the LCD or JTAG, not both. The
default is the LCD (CONFIG_ESP_CONSOLE_SECONDARY_NONE in
bootloader/sdkconfig.defaults). For JTAG, set
CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG=y instead.
- macOS with Homebrew and GNU make (
brew install make). - ESP-IDF at
~/esp/esp-idf, at the commit inbootloader/esp-idf.version, with theriscv32-esp-elftoolchain and the ESP-Hosted ROM patch (make bootloadergives the command). - Apple
containerCLI. Runcontainer system startfirst.
On Linux, scripts/ci-build.sh builds everything without a container.
Buildroot does not run on macOS. It runs in the Debian image from
container/Containerfile. Its output/ and dl/ are in the esp32s31-br
volume.
git submodule update --init --recursive
make container-image
make check
make portsmake build
make flash FLASH_PORT=/dev/cu.usbserial-XXXX
make monitor SERIAL_PORT=/dev/cu.usbserial-XXXXmake buildbuilds the loader and OpenSBI on the Mac. Buildroot builds the kernel, the rootfs and the card images in the container.make monitorresets the board and waits for=== ESP32-S31 Linux / Buildroot ===. Then it stays open as a terminal. PushCtrl-]to exit. It writes a log tologs/.- Set
RESET_PORTif the RTS line ofSERIAL_PORTcannot reset the board. make helpshows all targets.
Without a prepared SD card, the initramfs starts a recovery shell on the UART and one on the LCD.
The card is MBR. p1 is FAT32 and is mounted on /mnt/sd. p2 is the ext4 root
(192 MiB). All card targets erase data. They ask for confirmation and sudo.
| Command | Use |
|---|---|
make sdpart SD_DISK=/dev/diskN |
Partition a large card. p1 gets SD_DATA_SIZE (28G). |
make sdwrite SD_DISK=/dev/diskN |
Write the full card image. Use for a small card. |
make sdroot SD_DISK=/dev/diskN |
Write only the root partition. Use for updates. |
If the root goes read-only, run e2fsck -f /dev/mmcblk0p2 from the console.
If the kernel says that p1 was not properly unmounted, the board was reset
with p1 mounted. Linux does not clear that flag again, also not at a clean
shutdown. To clear it:
umount /mnt/sd && fsck.fat -a /dev/mmcblk0p1
/etc/init.d/S10sdcard startfsck.fat does not check if p1 is mounted, and it must not run on a mounted
p1. If umount fails, a program still uses /mnt/sd.
fsck.fat needs approximately 8 bytes of memory for each cluster of p1. That
is less than 1 MB for the 64 MiB p1 of the card image, and more than the board
has for a 28 GiB p1. Check a large p1 on the host.
The root password is korvo-bringup. The serial and panel consoles log in
automatically. Dropbear SSH asks for the password.
Boot scripts in br2-external/board/esp32s31/rootfs-overlay:
S01clocksets the clock from/etc/timestamp. The board has no RTC.S05swapmakes a swap file of max. 64 MiB on the root.S10sdcardmounts p1 on/mnt/sd.S40wifiloads the Wi-Fi driver and startswpa_supplicantonwlan0.- The udhcpc hook sets the clock with NTP on each new lease.
S95autostartruns/mnt/sd/autostart.sh, if that file exists.S99bannerprints the line thatmake monitorwaits for.
login and SSH show /etc/motd, which lists the first commands.
| Use | Commands |
|---|---|
| System | htop, free, df -h, dmesg, file, strace, gdbserver |
| Network | ssh, scp, curl, links, iperf3, tcpdump, nslookup |
| Code | micropython, lua, vim |
| Hardware | gpioinfo, gpioget, gpioset (libgpiod 2), lsusb |
The shell is bash, with a history that stays after a reset.
- Copy files. From a computer,
scpandsftpwork with the root password:scp file root@<address>:/mnt/sd/. - Web server.
httpd -f -p 8080 -h /mnt/sdgives the files of the card to a browser, athttp://<address>:8080/<file name>. It shows no list of the files: a directory needs anindex.html. Stop it with Ctrl-C. - Start at boot. The board runs
/mnt/sd/autostart.shat each boot, as root. The file is on the FAT partition, so a computer can also write it, and it stays aftermake sdroot. - Examples in
/usr/share/examples:led.pysets the RGB LED from MicroPython,gpio.shreads or sets a pin,www/is a web page with a CGI script for the LED, andautostart.shis a start script.
wlan0 works with the standard wpa_supplicant. S40wifi starts it with
/etc/wpa_supplicant.conf, or with wpa_supplicant.conf on p1 when that
file exists. make sdroot does not delete p1, and a board without a console
can get its network from that file. At each connection, the wpa_cli action
script starts or renews the DHCP lease.
wpa_passphrase "<ssid>" "<passphrase>" >> /etc/wpa_supplicant.conf
/etc/init.d/S40wifi restart
wpa_cli status
wpa_cli scan; wpa_cli scan_resultsFor WPA3 and WPA2/WPA3 networks, the network block needs
key_mgmt=WPA-PSK SAE, ieee80211w=1 and the passphrase as psk="...".
The hex PSK from wpa_passphrase is not sufficient for SAE.
The ESP-Hosted firmware on hart 0 runs the 802.11 MAC. Linux loads the
ESP-Hosted host driver (esp32_shm) and runs the authentication and the
key handshakes in wpa_supplicant. hostapd with the nl80211 driver
starts an access point. Stop S40wifi first: wlan0 is a station or an
access point, not the two at the same time.
Buildroot builds a stock kernel release (BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE
in br2-external/configs/esp32s31_defconfig) with linux/patches/.
0000-esp32s31-add-source-files.patchis generated from the new files underlinux/andshared/. Edit those files, not the patch. Eachmake kernel,make rootfsandmake kernel-patchesmakes the patch again, and an edit of the patch is lost.0001and higher are manual patches to existing kernel files.- The kernel config is
br2-external/board/esp32s31/linux.config. Usemake kernel-menuconfigandmake kernel-saveconfig. make kernel-checkapplies the series at zero fuzz with GNU patch.make kernelandmake rootfsfind a changed series or config. Then they extract and patch the kernel again.make kernel-cleandoes this by hand.
| Offset | Contents |
|---|---|
0x002000 |
ESP-IDF second-stage bootloader |
0x008000 |
Partition table |
0x020000 |
Loader and Wi-Fi firmware |
0x220000 |
OpenSBI fw_jump |
0x2A0000 |
Linux Image (with built-in DTB) |
0xA1FFF4 |
Linux size and CRC manifest |
0xA20000 |
2 MiB initramfs |
The initramfs holds BusyBox and an init that mounts /dev/mmcblk0p2 and
does switch_root. If the card is not there, it starts a shell on the UART
and one on the LCD.
- Set the loader to JTAG (see above). The LCD then stays dark.
- Run
make flash, thenmake openocd. - Connect
riscv32-esp-elf-gdbto port 3333. Usebuild/opensbi.elf,build/bootloader/s31-linux-loader.elforbuild/vmlinux(make kernel-vmlinux).
- Linux is uniprocessor on hart 1.
- The hardware FPU is not used: the hart has F but not D, and Linux supports an FPU only with D. Floating point works, because userspace is built soft-float (ilp32), but it is slower.
- OpenSBI protects its memory and the TEE/APM registers from S-mode with PMP, and its memory from DMA with HP_APM. The Wi-Fi firmware on hart 0 runs in M-mode, so nothing protects OpenSBI from hart 0.
poweroffstops the hart. It does not remove power.- The coherent DMA pool is 64 KiB.
- There is no I2C, audio or camera driver.
- Bluetooth and IEEE 802.15.4 are off in the firmware. ESP-Hosted has host
code for the two (
hci0, the Spinel RCP), but this port does not build or test it yet. The Bluetooth core module loads only because the host driver links to it. - WPA2/WPA3 Enterprise is not tested.
LICENSE (MIT) applies to the build system, scripts and rootfs files. Other
files have SPDX headers: bootloader/ and the OpenSBI platform are
BSD-2-Clause, bootloader/components/esp_hosted/ is Apache-2.0, linux/
and br2-external/package/esp-hosted-linux/ are GPL-2.0,
shared/esp32s31-hosted-shm.h is GPL-2.0 or Apache-2.0.