From f54c3bcb655780d48a637f966daaecf684bae14d Mon Sep 17 00:00:00 2001 From: Jamie Magee Date: Thu, 28 May 2026 22:35:24 -0700 Subject: [PATCH] raspberry-pi/common: add firmware-partition install module Adds hardware.raspberry-pi.firmware to stage the Pi's pre-Linux boot files (GPU boot code, DTBs, overlays, config.txt, optionally U-Boot) onto the firmware partition. Boards still boot via generic-extlinux-compatible; this only supplies the files it needs, through populateCmd for image builders or an opt-in activation script. DTB/overlay copy adapted from nvmd/nixos-raspberrypi. --- raspberry-pi/README.md | 29 ++++- raspberry-pi/common/default.nix | 1 + raspberry-pi/common/firmware.nix | 204 +++++++++++++++++++++++++++++++ 3 files changed, 230 insertions(+), 4 deletions(-) create mode 100644 raspberry-pi/common/firmware.nix diff --git a/raspberry-pi/README.md b/raspberry-pi/README.md index 8daebdda..bf96213b 100644 --- a/raspberry-pi/README.md +++ b/raspberry-pi/README.md @@ -4,7 +4,7 @@ NixOS profiles and modules for Raspberry Pi boards. ## What's here -- `common/` has the shared bits: the `linux-rpi` kernel build (vendor defconfig, matching firmware), the `config.txt` generation module, and a pinned wireless firmware. +- `common/` has the shared bits: the `linux-rpi` kernel build (vendor defconfig, matching firmware), the `config.txt` generation module, a pinned wireless firmware, and the firmware-partition install module. - `2/`, `3/`, `4/`, `5/` are the board profiles. Each one picks the right kernel and kernel params. Pi 4 and 5 also set DT filters and the initrd modules they need. - The extra files under `4/` are opt-in toggles for Pi 4 hardware: audio, dwc2, GPIO, I2C, LEDs, the PoE HATs, touchscreens, and so on. @@ -18,7 +18,28 @@ NixOS profiles and modules for Raspberry Pi boards. } ``` -These profiles assume the `generic-extlinux-compatible` bootloader (the NixOS module that writes an `extlinux.conf` for U-Boot to read), which is what aarch64 NixOS SD images use by default. There is no `boot.loader.raspberry-pi` module here. U-Boot itself still has to land on the boot partition somehow. Either your image builder does it, or you do. Nothing in here writes to `/boot/firmware/`. See [Current limits](#current-limits). +These profiles assume the `generic-extlinux-compatible` bootloader (the NixOS module that writes an `extlinux.conf` for U-Boot to read), which is what aarch64 NixOS SD images use by default. There is no `boot.loader.raspberry-pi` module here. U-Boot and the GPU boot code still have to land on the firmware partition somehow: either your image builder does it, or you use the firmware install module below. + +## Firmware install + +`hardware.raspberry-pi.firmware` stages the files the Pi firmware needs before Linux starts onto the firmware partition (default `/boot/firmware`): GPU boot code (`bootcode.bin`, `start*.elf`, `fixup*.dat`), vendor device trees and overlays, the rendered `config.txt`, and optionally U-Boot. It is not a new boot method; it just supplies the files the existing `generic-extlinux-compatible` + U-Boot path needs. + +When you build an SD image, the module sets `sdImage.populateFirmwareCommands` itself, so the firmware partition is populated at build time with no extra configuration. + +For a running system, set `hardware.raspberry-pi.firmware.enable = true`. An activation script then repopulates the firmware partition on every `nixos-rebuild switch`. It is off by default. + +To chainload U-Boot from the firmware, enable `uboot.enable`. It copies `u-boot.bin` to the firmware partition and sets `config.txt`'s `kernel` line for you: + +```nix +{ + hardware.raspberry-pi.firmware = { + enable = true; + uboot.enable = true; + }; +} +``` + +`uboot.enable` defaults `uboot.package` to nixpkgs' `pkgs.ubootRaspberryPiAarch64`, built from the upstream `rpi_arm64_defconfig`, which covers every 64-bit board (Pi 3/4/5). For a 32-bit board, override `uboot.package` with the matching U-Boot build. On the Pi 5 this boots from SD, but U-Boot can't drive USB/PCIe/RP1 yet, so USB boot, NVMe boot, and a USB keyboard at the U-Boot prompt don't work until Linux takes over. ## `config.txt` @@ -51,6 +72,6 @@ Top-level attrs are conditional sections (`all`, `pi4`, `pi5`, `cm4`, and so on) ## Current limits -- No firmware install: Nothing writes `start*.elf`, `bootcode.bin`, `fixup*.dat`, vendor DTBs, overlays, or `config.txt` to `/boot/firmware/`. The rendered config.txt is exposed at `hardware.raspberry-pi.configtxt.file`, but nothing on disk reads it yet. You either rely on the SD-image populate step or stage those files yourself. -- No bootloader module: There's no `boot.loader.raspberry-pi` here. Boards rely on `generic-extlinux-compatible` plus U-Boot. Pi 5 boots from SD via U-Boot, but USB, PCIe, and the RP1 don't come up until Linux takes over. So a USB keyboard at the U-Boot prompt won't work on Pi 5 today. +- No bootloader module: There's no `boot.loader.raspberry-pi` here. Boards rely on `generic-extlinux-compatible` plus U-Boot. Raspberry Pi OS has the GPU firmware load the kernel directly; we go through U-Boot so it reads `extlinux.conf`, which is what gives you the NixOS boot-generation menu and rollbacks. The firmware install module just stages the boot code and (optionally) U-Boot; it doesn't add a firmware-level direct-boot path. Pi 5 boots from SD via U-Boot, but USB, PCIe, and the RP1 don't come up until Linux takes over, so a USB keyboard at the U-Boot prompt won't work on Pi 5 today. +- Single pinned kernel: `common/kernel.nix` pins one `linux-rpi` version rather than matching each kernel to its firmware release. - No Pi 0/02/1 board profiles: `common/kernel.nix` accepts `rpiVersion = 1`, but there's no `0/`, `02/`, or `1/` directory wiring that kernel up into a profile you can import via ``. diff --git a/raspberry-pi/common/default.nix b/raspberry-pi/common/default.nix index f80e124b..8fc9f02f 100644 --- a/raspberry-pi/common/default.nix +++ b/raspberry-pi/common/default.nix @@ -2,6 +2,7 @@ imports = [ ./config-txt.nix ./config-txt-defaults.nix + ./firmware.nix ]; boot.initrd.availableKernelModules = [ diff --git a/raspberry-pi/common/firmware.nix b/raspberry-pi/common/firmware.nix new file mode 100644 index 00000000..a4bf8ba0 --- /dev/null +++ b/raspberry-pi/common/firmware.nix @@ -0,0 +1,204 @@ +# Firmware-partition install for Raspberry Pi. +# +# Stages the files the Pi needs before Linux starts onto the firmware partition +# (default /boot/firmware): GPU boot code (bootcode.bin, start*.elf, fixup*.dat), +# vendor device trees and overlays, the rendered config.txt, and optionally +# U-Boot. Not a new boot method: boards still use +# boot.loader.generic-extlinux-compatible (U-Boot reads extlinux.conf); this just +# provides the files that path needs, either at SD-image build time +# (sdImage.populateFirmwareCommands, wired automatically) or on a running system +# (the opt-in activation script). +# +# DTB/overlay copy adapted from nvmd/nixos-raspberrypi (MIT). + +{ + lib, + config, + options, + pkgs, + ... +}: + +let + cfg = config.hardware.raspberry-pi.firmware; + + # install-rpi-firmware + # Idempotent: copies via temp file + rename, and prunes stale DTBs/overlays. + installScript = pkgs.writeShellApplication { + name = "install-rpi-firmware"; + runtimeInputs = [ pkgs.coreutils ]; + text = '' + target="$1" + shopt -s nullglob + + firmwareBoot=${cfg.package}/share/raspberrypi/boot + ${ + if cfg.useGenerationDeviceTree then + '' + # Prefer the booted generation's device trees over the vendor ones. + dtbSrc=/run/current-system/dtbs + [ -d "$dtbSrc" ] || dtbSrc=$firmwareBoot + '' + else + '' + dtbSrc=$firmwareBoot + '' + } + + mkdir -p "$target/overlays" + + # Copy via a temp file then rename so an interrupted run can't leave a + # half-written file behind. The firmware partition is FAT, so the rename + # isn't truly atomic, but it still beats a partial copy when the + # activation script rewrites the partition on a live system. + copyForced() { + cp "$1" "$2.tmp" + mv "$2.tmp" "$2" + } + + # Track every file we copy this run, keyed by destination path, so the + # prune step below can delete stale device trees / overlays left behind + # by a previous generation. + declare -A kept + + echo "rpi-firmware: copying device trees from $dtbSrc" + for dtb in "$dtbSrc"/*.dtb "$dtbSrc"/broadcom/*.dtb; do + dst="$target/$(basename "$dtb")" + copyForced "$dtb" "$dst" + kept[$dst]=1 + done + + if [ -d "$dtbSrc/overlays" ]; then + for ovr in "$dtbSrc"/overlays/*; do + dst="$target/overlays/$(basename "$ovr")" + copyForced "$ovr" "$dst" + kept[$dst]=1 + done + fi + + # Prune stale device trees / overlays. + for fn in "$target"/*.dtb "$target"/overlays/*; do + if [ "''${kept[$fn]:-}" != 1 ]; then + rm -v -- "$fn" + fi + done + + echo "rpi-firmware: copying GPU boot code" + for src in "$firmwareBoot"/bootcode.bin "$firmwareBoot"/start*.elf "$firmwareBoot"/fixup*.dat; do + copyForced "$src" "$target/$(basename "$src")" + done + + ${lib.optionalString cfg.uboot.enable '' + echo "rpi-firmware: copying U-Boot" + copyForced ${cfg.uboot.package}/u-boot.bin "$target/u-boot.bin" + ''} + + echo "rpi-firmware: copying config.txt" + copyForced ${config.hardware.raspberry-pi.configtxt.file} "$target/config.txt" + + echo "rpi-firmware: done ($target)" + ''; + }; +in +{ + options.hardware.raspberry-pi.firmware = { + enable = lib.mkEnableOption '' + installation of the Raspberry Pi firmware partition on a running system. + + An activation script repopulates {option}`hardware.raspberry-pi.firmware.path` + on every system switch + ''; + + path = lib.mkOption { + type = lib.types.str; + default = "/boot/firmware"; + description = '' + Mount point of the Raspberry Pi firmware (FAT) partition. + + `/boot/firmware` matches the NixOS aarch64 SD-image layout, and most + configurations should leave it there. The activation script writes here + only when it is a mounted partition (checked with `mountpoint`); + otherwise it logs a warning and skips. + ''; + }; + + package = lib.mkOption { + type = lib.types.package; + default = pkgs.raspberrypifw; + defaultText = lib.literalExpression "pkgs.raspberrypifw"; + description = '' + Package providing the Raspberry Pi GPU boot code, vendor device trees, + and overlays under `''${package}/share/raspberrypi/boot`. + ''; + }; + + uboot = { + enable = lib.mkEnableOption '' + chainloading U-Boot from the Raspberry Pi firmware. + + Copies `u-boot.bin` from + {option}`hardware.raspberry-pi.firmware.uboot.package` to the firmware + partition and points `config.txt`'s `kernel` at it, so the GPU firmware + loads U-Boot, which then reads `extlinux.conf` + ''; + + package = lib.mkOption { + type = lib.types.package; + default = pkgs.ubootRaspberryPiAarch64; + defaultText = lib.literalExpression "pkgs.ubootRaspberryPiAarch64"; + description = '' + U-Boot package whose `u-boot.bin` is copied to the firmware + partition when {option}`hardware.raspberry-pi.firmware.uboot.enable` + is enabled. + + The default, nixpkgs' `pkgs.ubootRaspberryPiAarch64`, covers the + 64-bit boards (Pi 3/4/5). For a 32-bit board, override this with the + matching U-Boot package. + ''; + }; + }; + + useGenerationDeviceTree = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Copy device trees from the booted NixOS generation + (`/run/current-system/dtbs`) instead of the vendor firmware package. + Irrelevant when generating an SD image. + ''; + }; + }; + + config = lib.mkMerge [ + (lib.mkIf cfg.uboot.enable { + # Chainload U-Boot: the GPU firmware loads u-boot.bin, which then reads + # extlinux.conf. mkDefault so an explicit kernel setting still wins. + hardware.raspberry-pi.configtxt.settings.all = { + kernel = lib.mkDefault "u-boot.bin"; + # Default U-Boot is 64-bit, but the firmware loads kernel= in 32-bit + # mode unless arm_64bit=1. + arm_64bit = lib.mkDefault pkgs.stdenv.hostPlatform.isAarch64; + }; + + # The GPU firmware merges config.txt dtoverlays into the DTB it hands to + # U-Boot. The default (true) adds an FDTDIR line to extlinux.conf, so + # U-Boot reloads bare dtbs and drops the overlays. + boot.loader.generic-extlinux-compatible.useGenerationDeviceTree = lib.mkDefault false; + }) + # Stage the firmware partition at SD-image build time, only when an + # sd-image module is imported. mkForce so we override (not merge with) + # sd-image-aarch64.nix, which also sets this and would clobber config.txt. + (lib.optionalAttrs (options ? sdImage) { + sdImage.populateFirmwareCommands = lib.mkForce "${lib.getExe installScript} ./firmware\n"; + }) + (lib.mkIf cfg.enable { + system.activationScripts.raspberry-pi-firmware = lib.stringAfter [ "specialfs" ] '' + if mountpoint -q ${lib.escapeShellArg cfg.path}; then + ${lib.getExe installScript} ${lib.escapeShellArg cfg.path} + else + echo "rpi-firmware: ${cfg.path} is not a mounted partition, skipping firmware install" >&2 + fi + ''; + }) + ]; +}