Electronics

Expressif S3 Linux Support

Adding Linux support to esp32s3

Expressif S3 Linux Support

Introduction

Embedded Linux on microcontrollers has long been a tantalizing goal for makers and engineers alike. The esp32s3-linux project by Ray Bello brings this vision closer to reality, enabling a full Linux 6.5+ system to run on the ESP32-S3 SoC. By leveraging Docker for a fully containerized build environment and integrating Espressif’s network adapter firmware, this repo streamlines the process of compiling, packaging, and flashing a Linux image—no host toolchain installation required.

In this deep-dive, we’ll explore:

  1. Why Linux on ESP32-S3?
  2. Repo Anatomy & Workflow
  3. Containerized Build System
  4. Toolchain & Kernel Configuration
  5. Partition Layout & Flashing
  6. Runtime & Networking
  7. Customization & Extensibility
  8. Troubleshooting & Best Practices
  9. Use Cases & Roadmap

1. Why Linux on ESP32-S3?

1.1 The ESP32-S3 Advantage

  • AI acceleration with vector instructions and support for TinyML.
  • Dual-core Xtensa LX7 at up to 240 MHz, 8 MB flash/PSRAM variants.
  • Built-in Wi-Fi & BLE, unlocking networked applications.

1.2 Beyond FreeRTOS

While FreeRTOS excels at real-time, single-application tasks, Linux offers:

  • Multi-process, multi-user support
  • Rich POSIX filesystem and permissions
  • Standard toolchains (GCC, BusyBox, Python, MQTT, etc.)
  • SSH, networking stacks, and user-space daemons

This allows turning an ESP32-S3 into a tiny edge server, secure gateway, or autonomous device with familiar Linux tooling.


2. Repo Anatomy & Workflow

esp32s3-linux/
├── Dockerfile
├── scripts/
│   ├── rebuild.sh
│   ├── flash.sh
│   └── ...
├── firmware/       ← prebuilt or output binaries
├── release/        ← final build artifacts
├── .vscode/        ← optional IDE configs
└── README.md
  • Dockerfile: Defines the Ubuntu 22.04-based container that compiles toolchains, kernel, and rootfs.
  • scripts/: Orchestrators for full rebuilds and flashing.
  • firmware/ & release/: Hold firmware blobs (ESP-IDF), kernel image, and filesystems.

Workflow:

  1. Clone repo & submodules (git submodule update --init --recursive).
  2. docker build -t esp32s3-linux .
  3. docker run --device=/dev/ttyUSB0 -v $PWD/release:/app/build/release -it esp32s3-linux bash
  4. Inside container: ./scripts/rebuild.sh → artifacts in /app/build/release.
  5. Exit & docker cp artifacts back to host.
  6. ./scripts/flash.sh or manual esptool.py + parttool.py.

3. Containerized Build System

3.1 Why Docker?

  • Isolation: No host pollution by cross-compilers or kernel sources.
  • Reproducibility: Exact environment versioned in Dockerfile.
  • Portability: Works on Linux, macOS, Windows (with Docker Desktop).

3.2 Key Dockerfile Stages

StagePurpose
Base SetupInstalls git, build-essential, Python, cmake
Crosstool-NGBuilds xtensa-esp32s3-uclibc toolchain
Kernel BuildClones Linux 6.5+, configures and compiles XIP kernel
RootFS & DriversBuildroot generates cramfs, merges Wi-Fi firmware
CleanupPrunes intermediate layers, retains /app/build
FROM ubuntu:22.04 AS builder
ENV DEBIAN_FRONTEND=noninteractive
 
# 1. Install dependencies
RUN apt-get update && \
    apt-get install -y git build-essential python3 python3-pip cmake libncurses-dev ...
 
# 2. Crosstool-NG for Xtensa
RUN git clone --depth=1 https://github.com/crosstool-ng/crosstool-ng.git && \
    cd crosstool-ng && ./bootstrap && ./configure --prefix=/usr/local && make -j && make install
 
# 3. Configure & build toolchain
COPY crosstool.config /xtensa.config
RUN ct-ng build
 
# 4. Clone & build Linux kernel
RUN git clone --branch v6.5 https://github.com/torvalds/linux.git /linux && \
    cd /linux && make ARCH=xtensa esp32s3_defconfig && make -j
 
# 5. Buildroot + cramfs + firmware
...

Tip: If build times become excessive, you can leverage Docker build cache with minor tweaks to reorder install steps.


4. Toolchain & Kernel Configuration

4.1 Building the Xtensa Toolchain

  • Target triplet: xtensa-esp32s3-linux-uclibcfdpic

  • uClibc with fdpic (position-independent code) support, crucial for XIP.

  • Key ct-ng options:

    • CT_ARCH_XTENSA=y
    • CT_KERNEL_LINUX=y
    • CT_LIBC_UCLIBC=y
    • CT_LIBC_UCLIBC_FDPIC=y

4.2 Kernel Menuconfig Highlights

After make esp32s3_defconfig, run:

make ARCH=xtensa menuconfig
  • Processor type and features → Xtensa Settings

    • Enable CONFIG_MMU for Linux memory management.
  • Networking support → Wireless → esp32s3 wireless driver

    • Select ESP32S3 Wireless Firmware Loader to auto-bundle network_adapter.bin.
  • File systems → Compressed ROM file system support

    • CRAMFS for minimal footprint, or swap to JFFS2 if wear leveling needed.

5. Partition Layout & Flashing

5.1 Default Partition Table

PartitionTypeOffsetSizeDescription
bootloaderapp0x0000256 KBESP-IDF bootloader
networkdata0x400002 MBWi-Fi firmware (network_adapter.bin)
part-tabledata0x8000016 KBPartition definitions
linuxdata0xA00003.5 MBXIP kernel image
rootfsdata0x4200003.5 MBCRAMFS root filesystem
etcdata0x800000512 KBPersistent configs (e.g. wpa)

Pro tip: Adjust sizes via scripts/partition-table.csv to accommodate larger rootfs or JFFS2.

5.2 Flashing Steps

# 1. Write bootloader, network firmware, and partition table
esptool.py --chip esp32s3 -p /dev/ttyUSB0 -b 921600 \
  write_flash 0x0 bootloader.bin \
              0x40000 network_adapter.bin \
              0x80000 partition-table.bin
 
# 2. Use parttool for XIP+CRAMFS
parttool.py write_partition --partition-name linux  --input xipImage
parttool.py write_partition --partition-name rootfs  --input rootfs.cramfs
parttool.py write_partition --partition-name etc    --input etc.jffs2
  • esptool.py handles raw flash writes.
  • parttool.py (ESP-IDF) updates named partitions, preserving alignment and wear leveling.

6. Runtime & Networking

6.1 First Boot & Console

  • Serial console at 115200 baud; you should see kernel boot messages and mount of CRAMFS.
  • Default root user has no password; networking must be configured manually.

Capture of kernel boot

ESP-ROM:esp32s3-20210327
Build:Mar 27 2021
rst:0x1 (POWERON),boot:0x2b (SPI_FAST_FLASH_BOOT)
SPIWP:0xee
mode:DIO, clock div:1
load:0x3fce3808,len:0x1064
load:0x403c9700,len:0x9e0
load:0x403cc700,len:0x291c
entry 0x403c98d8
etc ptr = 0x420b0000
linux ptr = 0x42120000
rootfs ptr = 0x42480000
[    0.000000] Ignoring boot parameters at (ptrval)
[    0.000000] Linux version 6.5.0 (esp32@buildkitsandbox) (xtensa-esp32s3-linux-uclibcfdpic-gcc (crosstool-NG UNKNOWN) 14.0.0 20231029 (experimental), GNU ld (crosstool-NG UNKNOWN) 2.40.50.20230424) #1 PREEMPT Sat Dec 30 22:10:43 UTC 2023
[    0.000000] config ID: c2f0fffe:23090f1f
[    0.000000] earlycon: esp32s3uart0 at MMIO32 0x60000000 (options '115200n8,40000000')
[    0.000000] printk: bootconsole [esp32s3uart0] enabled
[    0.000000] **********************************************************
[    0.000000] **   NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE   **
[    0.000000] **                                                      **
[    0.000000] ** This system shows unhashed kernel memory addresses   **
[    0.000000] ** via the console, logs, and other interfaces. This    **
[    0.000000] ** might reduce the security of your system.            **
[    0.000000] **                                                      **
[    0.000000] ** If you see this message and you are not debugging    **
[    0.000000] ** the kernel, report this immediately to your system   **
[    0.000000] ** administrator!                                       **
[    0.000000] **                                                      **
[    0.000000] **   NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE   **
[    0.000000] **********************************************************
[    0.000000] Zone ranges:
[    0.000000]   Normal   [mem 0x000000003d800000-0x000000003dffffff]
[    0.000000] Movable zone start for each node
[    0.000000] Early memory node ranges
[    0.000000]   node   0: [mem 0x000000003d800000-0x000000003dffffff]
[    0.000000] Initmem setup node 0 [mem 0x000000003d800000-0x000000003dffffff]
[    0.000000] pcpu-alloc: s0 r0 d32768 u32768 alloc=1*32768
[    0.000000] pcpu-alloc: [0] 0 
[    0.000000] Kernel command line: earlycon=esp32s3uart,mmio32,0x60000000,115200n8,40000000 console=ttyS0,115200n8 debug rw root=mtd:rootfs no_hash_pointers 
[    0.000000] Dentry cache hash table entries: 1024 (order: 0, 4096 bytes, linear)
[    0.000000] Inode-cache hash table entries: 1024 (order: 0, 4096 bytes, linear)
[    0.000000] Built 1 zonelists, mobility grouping off.  Total pages: 2032
[    0.000000] mem auto-init: stack:off, heap alloc:off, heap free:off
[    0.000000] virtual kernel memory layout:
[    0.000000]     lowmem  : 0x3d800000 - 0x3e000000  (    8 MB)
[    0.000000]     .text   : 0x42120000 - 0x4234eb30  ( 2234 kB)
[    0.000000]     .rodata : 0x4234f000 - 0x423a9000  (  360 kB)
[    0.000000]     .data   : 0x3d800000 - 0x3d87c6a0  (  497 kB)
[    0.000000]     .init   : 0x3d87c6a0 - 0x3d87f890  (   12 kB)
[    0.000000]     .bss    : 0x3d87f890 - 0x3d8b6410  (  218 kB)
[    0.000000] Memory: 7340K/8192K available (2234K kernel code, 497K rwdata, 360K rodata, 88K init, 218K bss, 852K reserved, 0K cma-reserved)
[    0.000000] SLUB: HWalign=16, Order=0-3, MinObjects=0, CPUs=1, Nodes=1
[    0.000000] rcu: Preemptible hierarchical RCU implementation.
[    0.000000] rcu: RCU calculated value of scheduler-enlistment delay is 10 jiffies.
[    0.000000] NR_IRQS: 33
[    0.000000] rcu: srcu_init: Setting srcu_struct sizes based on contention.
[    0.000000] Calibrating CPU frequency 160.00 MHz
[    0.000000] clocksource: ccount: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 11945377789 ns
[    0.000072] sched_clock: 32 bits at 160MHz, resolution 6ns, wraps every 13421772796ns
[    0.009515] Calibrating delay loop (skipped)... 160.00 BogoMIPS preset
[    0.013414] pid_max: default: 4096 minimum: 301
[    0.021355] Mount-cache hash table entries: 1024 (order: 0, 4096 bytes, linear)
[    0.025454] Mountpoint-cache hash table entries: 1024 (order: 0, 4096 bytes, linear)
[    0.080105] rcu: Hierarchical SRCU implementation.
[    0.080894] rcu: 	Max phase no-delay instances is 1000.
[    0.094345] devtmpfs: initialized
[    0.120763] clocksource: jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 19112604462750000 ns
[    0.122090] futex hash table entries: 16 (order: -5, 192 bytes, linear)
[    0.140884] NET: Registered PF_NETLINK/PF_ROUTE protocol family
[    0.172668] platform soc: Fixed dependency cycle(s) with /soc/intc@600c2000
[    0.258813] clocksource: Switched to clocksource ccount
[    0.313741] NET: Registered PF_INET protocol family
[    0.322241] IP idents hash table entries: 2048 (order: 2, 16384 bytes, linear)
[    0.348130] tcp_listen_portaddr_hash hash table entries: 1024 (order: 0, 4096 bytes, linear)
[    0.351909] Table-perturb hash table entries: 65536 (order: 6, 262144 bytes, linear)
[    0.356779] TCP established hash table entries: 1024 (order: 0, 4096 bytes, linear)
[    0.365414] TCP bind hash table entries: 1024 (order: 1, 8192 bytes, linear)
[    0.372388] TCP: Hash tables configured (established 1024 bind 1024)
[    0.381244] UDP hash table entries: 256 (order: 0, 4096 bytes, linear)
[    0.384350] UDP-Lite hash table entries: 256 (order: 0, 4096 bytes, linear)
[    0.395525] NET: Registered PF_UNIX/PF_LOCAL protocol family
[    0.408650] RPC: Registered named UNIX socket transport module.
[    0.410967] RPC: Registered udp transport module.
[    0.411567] RPC: Registered tcp transport module.
[    0.414526] RPC: Registered tcp-with-tls transport module.
[    0.420964] RPC: Registered tcp NFSv4.1 backchannel transport module.
[    0.492979] Initialise system trusted keyrings
[    0.497341] workingset: timestamp_bits=30 max_order=11 bucket_order=0
[    0.508093] NFS: Registering the id_resolver key type
[    0.511341] Key type id_resolver registered
[    0.512153] Key type id_legacy registered
[    0.514392] jffs2: version 2.2. (NAND) © 2001-2006 Red Hat, Inc.
[    0.520392] Key type asymmetric registered
[    0.522123] Asymmetric key parser 'x509' registered
[    0.528317] io scheduler mq-deadline registered
[    0.531553] io scheduler kyber registered
[    2.546851] 60000000.serial: ttyS0 at MMIO 0x60000000 (irq = 1, base_baud = 2500000) is a ESP32S3 UART
[    2.550374] printk: console [ttyS0] enabled
[    2.550374] printk: console [ttyS0] enabled
[    2.554144] printk: bootconsole [esp32s3uart0] disabled
[    2.554144] printk: bootconsole [esp32s3uart0] disabled
[    2.590659] random: crng init done
[    2.629608] physmap-flash 42000000.flash: physmap platform flash device: [mem 0x42000000-0x42ffffff]
[    2.641292] 6 esp32 partitions found on MTD device 42000000.flash
[    2.642209] Creating 6 MTD partitions on "42000000.flash":
[    2.642660] 0x00000000a000-0x00000000f000 : "nvs"
[    2.647174] mtd: partition "nvs" doesn't start on an erase/write block boundary -- force read-only
[    2.693280] 0x00000000f000-0x000000010000 : "phy_init"
[    2.694124] mtd: partition "phy_init" doesn't start on an erase/write block boundary -- force read-only
[    2.732090] 0x000000010000-0x0000000b0000 : "factory"
[    2.766305] 0x0000000b0000-0x000000120000 : "etc"
[    2.801095] 0x000000120000-0x000000480000 : "linux"
[    2.834277] 0x000000480000-0x000000800000 : "rootfs"
[    2.882388] ESP chipset detected [esp32-s3]
[    2.931293] NET: Registered PF_INET6 protocol family
[    3.022795] Segment Routing with IPv6
[    3.024454] In-situ OAM (IOAM) with IPv6
[    3.031104] sit: IPv6, IPv4 and MPLS over IPv4 tunneling driver
[    3.057075] NET: Registered PF_PACKET protocol family
[    3.058493] Key type dns_resolver registered
[    3.270662] Loading compiled-in X.509 certificates
[    3.366752] cfg80211: Loading compiled-in X.509 certificates for regulatory database
[    3.405487] Loaded X.509 cert 'sforshee: 00b28ddf47aef9cea7'
[    3.410518] clk: Disabling unused clocks
[    3.422854] platform regulatory.0: Direct firmware load for regulatory.db failed with error -2
[    3.423965] cfg80211: failed to load regulatory.db
[    3.427494] cramfs: checking physical address 0x42480000 for linear cramfs image
[    3.435310] cramfs: linear cramfs image on mtd:rootfs appears to be 3196 KB in size
[    3.444737] VFS: Mounted root (cramfs filesystem) readonly on device 31:5.
[    3.451448] devtmpfs: mounted
[    3.453509] Freeing unused kernel image (initmem) memory: 8K
[    3.456449] This architecture does not have kernel memory protection.
[    3.464430] Run /sbin/init as init process
[    3.466950]   with arguments:
[    3.471466]     /sbin/init
[    3.472581]   with environment:
[    3.475670]     HOME=/
[    3.478015]     TERM=linux
Starting syslogd: OK
Starting klogd: OK
Running sysctl: OK
seedrng: can't create directory '/var/lib/seedrng': Read-only file system
Starting network: Successfully initialized wpa_supplicant
OK
Starting inetd: OK

Welcome to Buildroot

buildroot login: 

6.2 Wi-Fi Setup

  1. Create /etc/wpa_supplicant.conf:

    ctrl_interface=/var/run/wpa_supplicant
    network={
        ssid="YourSSID"
        psk="YourPassword"
        key_mgmt=WPA-PSK
    }
  2. Bring up interface:

    ip link set espsta0 up
    wpa_supplicant -B -i espsta0 -c /etc/wpa_supplicant.conf
    udhcpc -i espsta0  # DHCP client
    ping 8.8.8.8       # Verify connectivity

7. Customization & Extensibility

7.1 Adding Packages

  • Enter the container, then make menuconfig under the Buildroot directory.
  • Enable additional BusyBox applets, Python support, MQTT clients, or even a lightweight SSH daemon.

7.2 Kernel Modules

  • Configure external modules via Makefile overlays in linux/drivers/.

  • Rebuild only the modules to speed iteration:

    cd /linux
    make modules
    make modules_install INSTALL_MOD_PATH=/app/build/release

7.3 GUI & Edge-AI

  • While XIP kernels limit RAM use, you can mount an SD card via SPI for larger workloads.
  • Experiment with TinyML frameworks by cross-compiling TensorFlow Lite Micro.

8. Troubleshooting & Best Practices

IssueRemedy
Permission denied on /dev/ttysudo usermod -aG dialout $USER & relogin
Bad partition tableVerify scripts/partition-table.csv offsets and sizes
Page allocation failures (dmesg)Reduce init-time workload, or add swap-like filesystem via JFFS2
Wi-Fi firmware load errorEnsure network_adapter.bin matches kernel’s CONFIG version
  • Enable logging: Append loglevel=8 to kernel cmdline via parttool.py.
  • Serial break: If kernel panics, use screen’s “Ctrl-A k” to reset board.

9. Use Cases & Roadmap

  1. IoT Gateway: Run MQTT, Node-RED, or Home Assistant Light on S3-sized hardware.
  2. Edge Compiler: Compile small C apps directly on device via gcc in rootfs.
  3. Secure Controller: Leverage Linux capabilities (namespaces, cgroups) for containerized tasks.
  4. Tiny GUI: Add an SPI-connected LCD and run a minimal X server or framebuffer app.

Future Directions

  • Upstream integration of S3 support in Buildroot and mainline Linux.
  • Automated CI pipelines for nightly Docker image builds.
  • SD-card based expandable storage and U-Boot bootloader support.
  • Prebuilt Docker images on Docker Hub with tags per kernel version.

Conclusion

The esp32s3-linux repository encapsulates a robust, Docker-powered toolchain and build system that brings Linux 6.5+ to the ESP32-S3 platform. By modularizing kernel, rootfs, and firmware builds—and automating flashing—this project lowers the barrier for embedded Linux enthusiasts. Whether you’re experimenting with edge AI, lightweight servers, or simply pushing the limits of microcontrollers, Ray Bello’s fork provides a solid foundation to build upon.

Feel free to clone the repo, customize the kernel, add new user-space packages, and share your findings with the community!