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.
This commit is contained in:
parent
968980fba2
commit
f54c3bcb65
3 changed files with 230 additions and 4 deletions
|
|
@ -4,7 +4,7 @@ NixOS profiles and modules for Raspberry Pi boards.
|
||||||
|
|
||||||
## What's here
|
## 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.
|
- `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.
|
- 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`
|
## `config.txt`
|
||||||
|
|
||||||
|
|
@ -51,6 +72,6 @@ Top-level attrs are conditional sections (`all`, `pi4`, `pi5`, `cm4`, and so on)
|
||||||
|
|
||||||
## Current limits
|
## 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. 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.
|
||||||
- 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.
|
- 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 `<nixos-hardware/raspberry-pi/...>`.
|
- 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 `<nixos-hardware/raspberry-pi/...>`.
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
imports = [
|
imports = [
|
||||||
./config-txt.nix
|
./config-txt.nix
|
||||||
./config-txt-defaults.nix
|
./config-txt-defaults.nix
|
||||||
|
./firmware.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
boot.initrd.availableKernelModules = [
|
boot.initrd.availableKernelModules = [
|
||||||
|
|
|
||||||
204
raspberry-pi/common/firmware.nix
Normal file
204
raspberry-pi/common/firmware.nix
Normal file
|
|
@ -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 <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
|
||||||
|
'';
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue