10.nixos-hardware/raspberry-pi/common/firmware.nix
Jamie Magee f54c3bcb65 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.
2026-07-09 23:03:00 -07:00

204 lines
7.1 KiB
Nix

# 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 <target-dir>
# 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
'';
})
];
}