ajhahn.de
← FlashOS
Markdown 355 lines
<div align="center">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="assets/flashos_logo_dark.png">
    <img src="assets/flashos_logo_light.png" alt="FlashOS" width="280">
  </picture>

<h1>Setup</h1>

<p><i>Host toolchain, SD-card layout, serial console, QEMU, and the test runner.</i></p>

<p>
    <a href="README.md"><b>README</b></a> ·
    <a href="DOCUMENTATION.md"><b>Documentation</b></a> ·
    <b>Setup</b> ·
    <a href="PORT.md"><b>Port</b></a> ·
    <a href="VERSIONING.md"><b>Versioning</b></a> ·
    <a href="CHANGELOG.md"><b>Changelog</b></a> ·
    <a href="LICENSE.md"><b>License</b></a>
  </p>

<p>
    <b>English</b> ·
    <a href="docs/de/SETUP.md">Deutsch</a>
  </p>
</div>

---

This page covers the host toolchain, the SD-card layout the Raspberry
Pi 4 expects, the serial console, QEMU, and the test runner.

Reference:
[BCM2711 ARM Peripherals (RPi 4)](https://pip-assets.raspberrypi.com/categories/545-raspberry-pi-4-model-b/documents/RP-008248-DS-1-bcm2711-peripherals.pdf?disposition=inline).

## Contents

1. [Host toolchain](#1-host-toolchain)
2. [Building](#2-building)
3. [Running under QEMU](#3-running-under-qemu)
4. [SD-card layout](#4-sd-card-layout)
5. [Serial console](#5-serial-console)
6. [Helper shell functions](#6-helper-shell-functions)
7. [Host-side unit tests](#7-host-side-unit-tests)

## 1. Host toolchain

| Tool                       | Minimum version | Purpose                                   |
| :------------------------- | :-------------- | :---------------------------------------- |
| Zig                        | 0.16.0          | Compile Zig + assembly, run `build.zig` |
| `flashc`                   | pinned          | Transpile Flash (`.flash`) sources to Zig |
| `aarch64-elf-objcopy`    | 2.40+           | ELF → raw binary                         |
| `aarch64-elf-nm`         | 2.40+           | Symbol extraction for `populate-syms`   |
| `qemu-system-aarch64`    | 11.0.0+         | Run the kernel under QEMU                 |
| `screen` (or equivalent) | –              | Serial console for the Pi                 |

On macOS:

```bash
brew install zig aarch64-elf-binutils qemu
```

### Flash compiler (`flashc`)

FlashOS's source modules are written in
[Flash](https://github.com/ajhahnde/Flash) and transpiled to Zig at
build time. `build.zig` resolves the `flashc` binary at
`~/Flash/zig-out/bin/flashc-stage1` by default; override the path with
`-Dflashc=<path>`. Flash publishes no prebuilt binaries, so build the
pinned self-hosted compiler from source — run this from the FlashOS
checkout so the pin is read from `flash-toolchain.lock`:

```bash
git clone https://github.com/ajhahnde/Flash.git ~/Flash
git -C ~/Flash checkout "$(grep -oE '[0-9a-f]{40}' flash-toolchain.lock)"
( cd ~/Flash && zig build stage1 )   # → ~/Flash/zig-out/bin/flashc-stage1
```

`zig build stage1` — not the bare `zig build`, which emits only the
stage0 bootstrap seed `flashc` — produces `flashc-stage1`, the revision
pinned in `flash-toolchain.lock`. Rebuild it only when that pin moves.

## 2. Building

Every build transpiles the `.flash` source modules with `flashc`, so
build it first (see §1).

```bash
zig build                 # default: kernel8.img + armstub8.bin → zig-out/
```

```bash
./build.sh                # full two-pass build with optional deploy
```

`build.sh` invokes `zig build`, `zig build populate-syms`, then
`zig build` again, diff-checks that the symbol layout converged, and
optionally runs `zig build deploy`.

## 3. Running under QEMU

Two QEMU machines are wired up; pick by `-Dboard=`:

```bash
zig build -Dboard=rpi4b run        # Pi 4 model (raspi4b)
zig build -Dboard=virt  run-virt   # generic ARMv8 (virt)
```

For a self-validating run that exits 0 when the boot reaches the
interactive `fsh` prompt (the third `type 'help' for commands` homescreen
marker — see below) with no `[FAIL]` / `ERROR CAUGHT` and the expected free-page
checkpoints, and 1 on a failure or watchdog timeout (no manual QEMU
supervision):

```bash
zig build -Dboard=virt  test-virt
zig build -Dboard=rpi4b test-rpi4b  # (matches run)
```

To verify the Pi byte-identity baseline before flashing the SD card
(stashes `src/symbol_area.S`, cleans, rebuilds, diffs against
`scripts/pi_baseline.sha256`):

```bash
scripts/verify_pi_baseline.sh
```

`run` invokes
`qemu-system-aarch64 -M raspi4b -serial null -serial stdio -kernel zig-out/kernel8.img`
— the Mini-UART (UART1) is routed onto host stdio so the kernel's
output and the test harness's `[TEST]/[PASS]/[FAIL]` lines appear
directly on the controlling terminal. `run-virt` uses
`-M virt,gic-version=3 -cpu cortex-a72 -m 1G -nographic`, with the
PL011 routed onto host stdio.

A green run on either board lands `30/30 passed`, 34 per-scenario
free-page checkpoints (`0xbbff2` on rpi4b, `0x3be46` on virt) plus the
matching boot baseline (`0xbc000` / `0x3be54`), and 0 `ERROR CAUGHT`.
The boot then hands off to `/bin/login``/bin/fsh`; with the login
lifecycle fsh's homescreen marker (`type 'help' for commands`) appears
three times (two scripted `[TEST] login` sessions + the real boot
login), and the CI watchdog (`scripts/run_qemu_test.sh`) counts exactly
that. The free-page invariants are documented in
[Documentation §8](DOCUMENTATION.md#free-page-invariants).

QEMU is the authoritative inner-loop signal. The boot path matches
real hardware byte-for-byte, modulo timing.

## 4. SD-card layout

The Raspberry Pi 4 boots from a FAT32-formatted card whose root must
contain at least:

```text
config.txt              # ships in this repo
kernel8.img             # built by `zig build`
armstub8.bin            # built by `zig build`
bcm2711-rpi-4-b.dtb     # bundled in this repo
start4.elf              # bundled in this repo
fixup4.dat              # bundled in this repo
overlays/miniuart-bt.dtbo
```

The firmware blobs are bundled in this repo under `firmware/`
(`bcm2711-rpi-4-b.dtb`, `start4.elf`, `fixup4.dat`,
`overlays/miniuart-bt.dtbo`), taken from the official
[raspberrypi/firmware](https://github.com/raspberrypi/firmware/tree/master/boot)
project and kept here for convenience and license/credit clarity. The
deploy step points at that directory by default:

```bash
SD_BOOT=/Volumes/BOOT FIRMWARE=firmware zig build deploy
```

The deploy step reads two environment variables:

| Variable     | Default           | Purpose                                           |
| :----------- | :---------------- | :------------------------------------------------ |
| `SD_BOOT`  | `/Volumes/BOOT` | SD-card mount point on macOS                      |
| `FIRMWARE` | `firmware`      | Directory holding the bundled RPi firmware files  |

## 5. Serial console

The kernel has three console/debug channels on the Pi:

- **Mini-UART (UART1)** on GPIO 14 / 15 — main console (and fallback
  when USB is not enumerated).
- **PL011 (UART4)** on GPIO 8 / 9 — dedicated trace channel.
- **USB-C gadget console** — the interactive `fsh` console over the
  Pi's USB-C port; no adapter or jumper wires (see below).

GPIO 14/15 is shared with the firmware on purpose. `config.txt`
enables `uart_2ndstage=1` and `dtoverlay=miniuart-bt`, which routes
the firmware's PL011_0 to GPIO 14/15 so the `MESS:…` lines from
`start4.elf` are visible on the same cable. Once the kernel runs,
`mini_uart_init` (`src/board/rpi4b/uart.flash`) reconfigures the pins to alt5
(mini-UART) — last-write on the GPIO function selector wins, so the
firmware-side PL011_0 routing is silently replaced. This is a
sequential handoff, not a conflict.

### UART1 pinout (RPi 4 → USB-TTL adapter)

| RPi pin | Function      | USB-TTL pin |
| :------ | :------------ | :---------- |
| Pin 6   | GND           | GND         |
| Pin 8   | TXD (GPIO 14) | RXD         |
| Pin 10  | RXD (GPIO 15) | TXD         |

Do **not** connect VCC if the Pi is powered independently.

### Connecting on macOS

The PL2303G chip is supported natively. Find the device node and
open a session at 115200 baud:

```bash
ls /dev/cu.usbserial-*
```

```bash
screen /dev/cu.usbserial-XXXX 115200
```

Exit `screen` with `Ctrl-A`, then `K`, confirmed with `y`. To kill
a detached `picapture` session from a second terminal, run `piquit`
(see §6).

### USB-C console (single C-to-C cable)

The Pi's own USB-C port doubles as the console. The kernel brings the
BCM2711's DWC2 OTG controller up as a **CDC-ACM USB device**
(`src/board/rpi4b/usb.flash`), so one USB-C ↔ USB-C cable to the Mac
carries both **power and the interactive `fsh` console**. macOS binds
its built-in `AppleUSBCDCACM` driver — nothing to install.

```bash
ls /dev/cu.usbmodem*            # node appears once the gadget enumerates
```

```bash
screen /dev/cu.usbmodem00011 115200
```

Once enumerated, user/`fsh` output (the `# ` / `$ ` prompt, command output)
switches from the Mini-UART to the USB console automatically; kernel
`[Debug]` prints and the USB driver's own bring-up trace stay on the
Mini-UART. If the gadget never enumerates (no host attached, or under
QEMU, which does not emulate the DWC2 device path), the console falls
back to the Mini-UART and the GPIO flow above works unchanged.

The baud rate is cosmetic — there is no physical UART behind the USB
device; any rate works. Keystrokes typed into `screen` reach `fsh`
over USB bulk-OUT; replug / re-enumeration hardening is a known work
item, so if the console wedges after replugging the cable, power-cycle
the Pi.

## 6. Helper shell functions

The repo ships [`flashos.env.zsh`](flashos.env.zsh) with a handful of
helpers, exposed as two verb dispatchers — `pi <verb>` (serial console) and
`run <mode>` (build, emulate, or attach) — plus `build` and `flashos`. Source
it from `~/.zshrc` (`source ~/FlashOS/flashos.env.zsh`) to make them available
in every shell. The legacy flat names (`picapture`, `piconnect`, `piquit`,
`pilist`) remain as thin aliases for the corresponding `pi` verbs.

- **`picapture [usb|mu]`** — runs the canonical boot-capture flow,
  logging the session to `boot.log` in the repo root (regardless of
  the current directory; covered by the repo `.gitignore`).
  - `usb` (default): waits for the CDC gadget to enumerate on
    `/dev/cu.usbmodem*` (plugging in the C-to-C cable powers the Pi,
    so the node's appearance is itself the first boot signal), then
    probes the console once per second until the boot marker
    `type 'help' for commands` appears (fsh reached its interactive REPL).
  - `mu`: captures the Mini-UART trace adapter
    (`/dev/cu.usbserial-*`) until `type 'help' for commands` (the boot
    reached the shell on the MU fallback — no USB host attached) or
    `ERROR CAUGHT` appears. Power-cycle the Pi when prompted.
  - Kernel faults only ever print on the MU adapter — use `mu` mode
    (trace adapter + external power) for fault diagnosis.
- **`piconnect [usb|mu]`** — opens an interactive `screen` session on
  the Pi console at 115200 baud. With no argument it auto-picks the
  USB CDC console (`fsh`) when present, else the MU trace adapter;
  `usb` / `mu` force a specific channel.
- **`piquit`** — terminates the detached `pi_capture` screen session
  started by `picapture`. Use from a second terminal.
- **`pilist`** — lists attached console devices: the USB CDC console
  (`/dev/cu.usbmodem*`) and any USB-serial adapters
  (`/dev/cu.usbserial-*`, MU trace).
- **`pi log`** — pages the most recent `boot.log` capture.
- **`pi tail [N]`** — live-tails `boot.log` (last `N` lines, default 40),
  following across the next capture's log rotation.
- **`build`** — runs `./build.sh` from the repo root (works from any
  directory): clean, link pass 1, `populate-syms`, link pass 2,
  diff-check the symbol layout, optionally `deploy`. `BOARD=virt
  build` selects the virt board (deploy is skipped); `NM=llvm-nm
  build` overrides the symbol-dump binary.
- **`run <mode>`** — builds and runs a board, runs the boot watchdog, or
  attaches to hardware. `run qemu` (alias `auto`) builds and launches the
  rpi4b model in QEMU; `run virt` does the same for the virt board; `run test`
  runs the host unit tests (`run test --NAME` filters by name); `run hw`
  attaches to the Pi over serial (`--trace` selects the MU adapter).
- **`run watchdog [virt|rpi4b]`** — runs the unattended boot watchdog with the
  required `-Dci-login-seed=true` and `-Dboot-selftest=true` flags applied
  automatically; defaults to the virt board (`rpi4b` is a slower TCG run).
- **`flashos`** — lists the shell helpers defined in
  [`flashos.env.zsh`](flashos.env.zsh) and the available `zig build` steps —
  a quick inventory of targets.

The MU trace adapter is auto-detected from `/dev/cu.usbserial-*` and
the USB CDC console from `/dev/cu.usbmodem*`; override with
`PI_SERIAL_DEVICE=/dev/cu.usbserial-XXXX` /
`PI_USB_CONSOLE_DEVICE=/dev/cu.usbmodemXXXX` if multiple devices are
connected. The `picapture` timeouts default to 120 s (overall) and 30 s
(prompt probe); override with `PI_CAPTURE_TIMEOUT` / `PI_PROBE_TIMEOUT`.

### Auto-source on `cd` (optional)

To load `flashos.env.zsh` automatically whenever the shell enters
`~/FlashOS`, append a `chpwd` hook to `~/.zshrc`. The command below
is idempotent:

```bash
grep -q '_FLASHOS_LOADED' ~/.zshrc || cat >> ~/.zshrc <<'EOF'

# --- FlashOS auto-source on cd ---
autoload -Uz add-zsh-hook
load_flashos_env() {
  if [[ "$PWD" == "$HOME/FlashOS"* && -z "$_FLASHOS_LOADED" ]]; then
    [[ -f "$HOME/FlashOS/flashos.env.zsh" ]] && source "$HOME/FlashOS/flashos.env.zsh" && typeset -g _FLASHOS_LOADED=1
  fi
}
add-zsh-hook chpwd load_flashos_env
load_flashos_env
EOF
```

Open a new shell or run `source ~/.zshrc` to activate. Assumes the
repo lives at `~/FlashOS`.

## 7. Host-side unit tests

```bash
zig build test
```

Runs the host-side unit tests against pure-logic kernel modules.
Each module that has tests is its own test root, linked against
`tests/host_stubs.zig` (stubs for assembly-only externs). The
current suite covers 41 modules (468 host tests); it
finishes in well under a second and is the fastest signal that
core kernel logic still holds.

---

[← Prev: Documentation](DOCUMENTATION.md) · [Next: Port →](PORT.md)