Skip to content

Latest commit

 

History

History
430 lines (335 loc) · 15.9 KB

File metadata and controls

430 lines (335 loc) · 15.9 KB

Building why2025-linux

Waveshare XC branch: use WAVESHARE_XC.md. The original WHY2025 flash offsets and C6 instructions later in this file do not apply to the Waveshare board.

Three artifacts, in order:

  1. Kernel + rootfs via Buildroot 2025.02.15 (Linux 6.18.35 LTS).
  2. Boot shim via ESP-IDF v5.5.3 — loads the kernel into PSRAM and jumps to it.
  3. C6 slave firmware via ESP-IDF — Wi-Fi/BT/backlight coprocessor.

(1) needs Linux — Buildroot doesn't run on macOS. (2) and (3) build fine natively on macOS; the reference setup builds them in the same Linux VM as (1) for one toolchain, but that's not required.

Hitting an error that doesn't look like it belongs to you? Check Troubleshooting first — several of these are host- environment gotchas, not bugs in this repo.

Getting the source

Clone Buildroot as buildroot/ inside this checkout — .gitignore and every path below assume this layout, and post-image.sh publishes flashable images to buildroot/output/images/ relative to it:

git clone -b 2025.02.15 https://gitlab.com/buildroot.org/buildroot

2025.02.x is Buildroot's current LTS, supported until ~March 2028 — stay on its point releases rather than the quarterly 2025.05+/2026.xx releases, which churn toolchain defaults.

Then patch the fresh clone — upstream Buildroot refuses to offer wpa_supplicant on NOMMU, and without this make olddefconfig silently drops it and the image ships without Wi-Fi:

for p in patches/buildroot/buildroot-tree/00[0-9][0-9]-*.patch; do
    patch -p1 -d buildroot -i "$p"
done

Re-apply after any re-clone or Buildroot version bump (this step was once a local-only edit and got lost in exactly such a bump). The matching wpa_supplicant source fix rides along automatically via BR2_GLOBAL_PATCH_DIRpatches/buildroot/global-patches/.

Buildroot's .config and CONFIG_EXTRA_FIRMWARE_DIR only accept absolute paths, so configs/why2025_defconfig and patches/linux/kernel.config ship with the token @WHY2025_LINUX@ wherever this repo's path is needed. Substitute it from the repo root:

./setup-paths.sh

Idempotent — safe to re-run after git pull. Confirms itself: grep -r '@WHY2025_LINUX@' configs patches/linux/*.config should return nothing. By hand: sed -i.bak "s|@WHY2025_LINUX@|$PWD|g" configs/why2025_defconfig patches/linux/kernel.config.

Environment setup

  • Linux host for step 1 (Ubuntu 24.04 reference). On macOS, use a VM:

    • OrbStackorb create ubuntu rv32dev. Shares the Mac filesystem into the VM at the same paths, so this checkout is reachable from inside the VM unchanged. Run one-shot commands with orb -m rv32dev bash -c '...'.
    • Lima, Multipass, UTM work the same way.

    Build Buildroot's output/ on the VM's native disk, not the shared macOS mount — see Troubleshooting if you skip this and hit Too many open files.

    USB passthrough to a VM is unreliable, so flash and monitor serial from the host OS, not the VM — the badge enumerates there directly.

  • Host packages (Ubuntu/Debian):

    sudo apt update && sudo apt install -y \
      build-essential file bc cpio rsync unzip git wget \
      libncurses-dev libssl-dev python3 perl

    A minimal/cloud image is usually missing several of these — see Troubleshooting.

  • ESP-IDF v5.5.3 for the boot shim (step 2), per https://docs.espressif.com/projects/esp-idf/en/v5.5.3/esp32p4/get-started/. Set IDF_PATH, then source $IDF_PATH/export.sh in every shell before running idf.py — it doesn't persist across shells. The commands below repeat it for that reason; skip it if you've already sourced it in the current shell. The C6 slave (step 3) manages its own separate ESP-IDF checkout — see that step.

  • Flash tools, on whichever host the badge is plugged into: pip install esptool pyserial, and tio for the serial console.

1. Kernel + rootfs

cd /path/to/why2025-linux/buildroot
cp ../configs/why2025_defconfig .config
make olddefconfig
make -j$(nproc)

The defconfig wires BR2_LINUX_KERNEL_PATCH, BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE, BR2_ROOTFS_OVERLAY, and the post-build/post-image scripts to this repo already. To override at the command line instead of running setup-paths.sh:

make BR2_LINUX_KERNEL_PATCH=/path/to/why2025-linux/patches/linux \
     BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE=/path/to/why2025-linux/patches/linux/kernel.config \
     ...

patches/buildroot/post-image.sh publishes to buildroot/output/images/ after every build, in-tree or via the O=<dir> layout in Troubleshooting:

  • Image — flat kernel image (~6 MB)
  • rootfs.squashfs — read-only root (~4.5 MB)
  • esp32p4-why2025.dtb — pulled out of the kernel build tree, since the kernel packaging here doesn't install DTBs into output/images/

The rootfs overlay (patches/buildroot/overlay/) applies automatically. patches/buildroot/post-build.sh chmods init scripts.

Faster rebuilds (prefix O=~/br-output too if you're using that layout):

# Just the kernel after a patch change
make linux-rebuild all -j$(nproc)

# Re-apply kernel.config after editing it
make linux-reconfigure all -j$(nproc)

Note: the overlay rsync only adds files. Removing something from patches/buildroot/overlay/ leaves it in output/target/ from the previous build. Run rm -rf output/target (or make clean) after shrinking the overlay.

2. Boot shim

cd /path/to/why2025-linux/linux-native
. $IDF_PATH/export.sh
idf.py build

Output: build/linux-native.bin at flash offset 0x10000.

The boot shim:

  • Maps the rootfs partition into the IDF cache window.
  • Prewalks it (coarse 64 KB stride, fine 64 B stride over the last 64 KB) to avoid near-EOF squashfs metadata read errors — see linux-native/main/main.c.
  • Patches the DTB's flash@deadbeef placeholder with the real VA + size.
  • Jumps to 0x48000000.

3. C6 slave firmware

A small fork of esp-hosted-ng v1.0.6, not vendored here — clone upstream and apply our patches. This one needs its own ESP-IDF, not the v5.5.3 from Environment setup — esp_driver/setup.sh clones and patches the pinned version esp-hosted-ng actually builds against:

git clone -b release/ng-1.0.6 https://github.com/espressif/esp-hosted.git
cd esp-hosted/esp_hosted_ng/esp/esp_driver
./setup.sh

cd network_adapter
for p in /path/to/why2025-linux/patches/c6-slave/00[0-9][0-9]-*.patch; do
    patch -p1 -i "$p"
done

. ../esp-idf/export.sh
rm -f sdkconfig          # else set-target keeps a stale config
idf.py set-target esp32c6
idf.py build

Patch 0005 pins the transport to SPI (esp-hosted-ng defaults the C6 to SDIO, which won't talk to the P4's SPI host — no wlan0). Confirm from the C6's boot banner on its USB console that it reads Transport used :: SPI only, not SDIO only.

Flash via the bottom USB-C port (the C6's native USB — see HARDWARE.md; find the port with ls /dev/cu.*). Easiest:

idf.py -p /dev/cu.usbmodem<your-c6-port> flash

Or with esptool directly — all four images at their offsets from build/flasher_args.json:

esptool --chip esp32c6 -p /dev/cu.usbmodem<your-c6-port> \
  --before default-reset --after hard-reset \
  write-flash --flash-mode dio --flash-size 4MB --flash-freq 80m \
  0x0     build/bootloader/bootloader.bin \
  0x8000  build/partition_table/partition-table.bin \
  0xd000  build/ota_data_initial.bin \
  0x10000 build/network_adapter.bin

Warning: do NOT flash network_adapter.bin alone at 0x0 (an older revision of this doc said to) — that's the app image, and at 0x0 it overwrites the C6's bootloader, leaving the C6 unbootable until reflashed correctly.

In-band OTA from Linux also works once Wi-Fi is up — see drivers/net/wireless/espressif/esp_hosted/main.c's ota_file= module parameter.

See patches/c6-slave/README.md for the per-patch breakdown.

Flashing

Badge plugged in (side USB-C reaches the P4 via CH340; close tio first or esptool fails with "port busy"; find your port with ls /dev/cu.*):

cd /path/to/why2025-linux/buildroot

esptool --chip esp32p4 -p /dev/cu.wchusbserial<your-p4-port> -b 460800 \
  --before default-reset --after hard-reset \
  write-flash --flash-mode dio --flash-size 16MB --flash-freq 40m \
  0x2000   ../linux-native/build/bootloader/bootloader.bin \
  0x8000   ../linux-native/build/partition_table/partition-table.bin \
  0x10000  ../linux-native/build/linux-native.bin \
  0x90000  output/images/Image \
  0x710000 output/images/rootfs.squashfs \
  0xF10000 output/images/esp32p4-why2025.dtb

Kernel-only change: drop the rootfs/dtb lines. Userspace-only change: flash rootfs alone (offset 0x710000).

Release images

tools/mkrelease.sh merges each chip's artifacts into a single 0x0-based image, so a release user runs one write-flash per chip:

tools/mkrelease.sh buildroot/output/images linux-native/build \
    /path/to/esp-hosted/.../network_adapter/build out/

esptool --chip esp32p4 -p /dev/cu.<p4-port> -b 460800 write-flash 0x0 out/esp32p4.bin
esptool --chip esp32c6 -p /dev/cu.<c6-port> -b 460800 write-flash 0x0 out/esp32c6.bin

Two ports, not one: the side USB-C is the P4 (CH340), the bottom USB-C is the C6's native USB. Pass - as the third argument to skip the C6 image. Flash the P4 before the C6 and power-cycle afterwards: a C6 flashed while the factory P4 firmware still runs can be left stuck in download mode (black screen; hardware-observed 2026-07-06 — the kernel's c6-kick reset pulse does not recover it, a power cycle or reflash does).

Running sensorpanel

sensorpanel renders live BME680 + BMI270 readings to the DSI panel. Not started at boot by default:

# Manual one-shot. Ctrl+C to exit.
/usr/bin/sensorpanel
# Auto-start on every boot. Recovery: rm /etc/sensorpanel.enable
touch /etc/sensorpanel.enable

Gated by /etc/init.d/S98sensorpanel, which no-ops without the gate file.

Running fbDOOM

The saved config enables BR2_PACKAGE_FBDOOM=y + BR2_PACKAGE_DOOM_WAD=y.

Note: fbdoom is not an upstream Buildroot package — it is vendored by patches/buildroot/buildroot-tree/0002-package-fbdoom-new- package.patch (applied by the buildroot-tree patch loop above), which also widens upstream doom-wad's Config.in gate so the shareware WAD installs alongside. If either defconfig option silently disappears at olddefconfig, the buildroot-tree patches weren't applied to your Buildroot clone.

After login:

fbdoom -iwad /usr/share/games/doom/doom1.wad -mb 4

(or pick DOOM from the launcher menu.)

Takes over /dev/fb0 — fbcon stops drawing until it exits. Keypad: F1..F6 + arrows (see the DTS keymap for the full set), Backspace exits.

Use -mb 4: fbdoom's zone allocation is single-shot (no auto-shrink; Unable to allocate N MiB means it exited), and with the resident userspace the NOMMU pool no longer has a contiguous 6 MB hole.

Monitoring

tio /dev/cu.wchusbserial<your-p4-port>  # interactive
python3 tools/bootcap.py 25             # 25 s boot capture (close tio first)
python3 tools/freezetest.py 30 90       # 30-cycle reliability test

The tools/*.py scripts hardcode /dev/cu.wchusbserial10 internally — edit the device path in the script if yours differs.

See tools/README.md for the full harness.

Layout reference

Offset Size Content
0x002000 24 KB ESP-IDF bootloader
0x008000 4 KB Partition table
0x009000 24 KB NVS
0x00f000 4 KB PHY init
0x010000 0.5 MB Boot shim app
0x090000 6.5 MB Linux Image
0x710000 8 MB rootfs (squashfs, must be POW2)
0xF10000 64 KB Device tree blob

Troubleshooting

Missing host packages

A minimal/cloud Ubuntu image (a fresh orb create ubuntu included) is usually missing packages Buildroot's dependency check requires, or that the kernel build needs later:

sudo apt update && sudo apt install -y \
  build-essential file bc cpio rsync unzip git wget \
  libncurses-dev libssl-dev python3 perl

file is usually the first one missing — fails make at the dependencies target before any real build starts (Buildroot's required-package list: https://buildroot.org/downloads/manual/manual.html#requirement). libssl-dev shows up later: without it the kernel build fails at certs/extract-cert.c: openssl/bio.h: No such file or directory — its module-signing cert-extraction tool needs OpenSSL headers regardless of this project's kernel config.

/usr/bin/install is uutils-coreutils, not GNU coreutils

Ubuntu releases past 24.04 LTS default some tools to the Rust uutils-coreutils reimplementation, and Buildroot's dependency check rejects install from it (known gaps: uutils/coreutils#12166). The distro ships the GNU binary alongside it for this transition:

sudo update-alternatives --install /usr/bin/install install /usr/bin/gnuinstall 100
sudo update-alternatives --set install /usr/bin/gnuinstall

The --set is required — --install alone doesn't take effect if install is already alternatives-managed. Verify with install --version (should say "GNU coreutils"). Hitting this means your host isn't actually 24.04 LTS — watch for the same issue on other coreutils-backed tools.

"Too many open files" building on a Mac-shared VM mount

Building Buildroot's output/ on the shared macOS↔VM mount is several times slower than native VM disk, and on OrbStack a make -j$(nproc) kernel build can fail outright there: fixdep: error opening file: ... Too many open files, gcc: ... Too many open files.

Not a guest limit — ulimit -n and /proc/sys/fs/file-max inside the VM can look completely unconstrained and it still happens, because the ceiling is on the macOS host: OrbStack's virtiofs bridge (Apple's VirtioFS) holds one file descriptor open per accessed inode until the guest sends FUSE_FORGET, which lags under heavy concurrency, and that host process is bound by macOS's kern.maxfilesperproc (check with sysctl kern.maxfilesperproc on macOS, not the VM). A parallel kernel build opens enough headers at once to blow past it.

Two fixes, cheapest first:

  1. make -j8 — gives FUSE_FORGET time to keep up. No setup, slower.

  2. Move output/ off the shared mount, via Buildroot's O=<dir>:

    mkdir -p ~/br-output
    cp /path/to/why2025-linux/configs/why2025_defconfig ~/br-output/.config
    cd /path/to/buildroot
    make O=~/br-output olddefconfig
    make O=~/br-output -j$(nproc)

    The Buildroot checkout can stay where it is. ~/br-output sits on the VM's native disk, taking build/, host/, staging/, target/, images/ off virtiofs with it. BR2_ROOTFS_POST_IMAGE_SCRIPT (patches/buildroot/post-image.sh) still publishes to <repo>/buildroot/output/images/ regardless, so the build/flashing commands above don't change.

    Raising kern.maxfiles/kern.maxfilesperproc on macOS and restarting OrbStack works too, but it's host-wide and doesn't survive a reboot.

    Pass O=~/br-output on every subsequent command too — menuconfig, savedefconfig, clean, the rebuild targets, all of it. Buildroot's Makefile only honors O from the command line (ifneq ("$(origin O)", "command line")); export O=~/br-output is silently ignored and falls back to the in-tree output/ — empty, no .config — so it looks like the build vanished. Alias it instead:

    alias mbr='make O=~/br-output'

    Lima/Multipass/UTM have the same tradeoff with their own shared-folder mechanisms, though the exact failure differs.