Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion raspberry-pi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,29 @@ Board profiles import `hardware.raspberry-pi.configtxt`, which renders `config.t

List values become repeated keys in the rendered file, so the `dtoverlay` above expands to:

```
```ini
dtoverlay=vc4-kms-v3d
dtoverlay=disable-bt
```

Top-level attrs are conditional sections (`all`, `pi4`, `pi5`, `cm4`, and so on). Nesting stacks filters. To drop a default, set the key to `null` with `mkForce`.

Use `configtxt.overlays` when order or parameter scope matters:

```nix
{
hardware.raspberry-pi.configtxt.overlays = [
{
name = "dwc2";
filters = [ "pi4" ];
params = [ "dr_mode=host" ];
}
];
}
```

`configtxt.settings` is rendered first. Each entry writes its filters, overlay, and parameters in order, then resets the overlay scope.

## Current limits

- 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.
Expand Down
132 changes: 129 additions & 3 deletions raspberry-pi/common/config-txt.nix
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,27 @@
let
cfg = config.hardware.raspberry-pi.configtxt;

overlayType = lib.types.submodule {
options = {
name = lib.mkOption {
type = lib.types.str;
description = "Firmware overlay name without the `.dtbo` suffix.";
};

params = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Ordered parameters applied to this overlay.";
};

filters = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Ordered config.txt conditional filters without brackets.";
};
};
};

mkValueString =
v:
if builtins.isInt v then
Expand Down Expand Up @@ -77,6 +98,66 @@ let
in
lib.concatMapStringsSep "\n[all]\n" mkGroup groups;

renderOverlay =
overlay:
lib.concatStringsSep "\n" (
[ "[all]" ]
++ map (filter: "[${filter}]") overlay.filters
++ [ "dtoverlay=${overlay.name}" ]
++ map (param: "dtparam=${param}") overlay.params
++ [ "dtoverlay=" ]
);

# Render settings first, followed by ordered overlays. The final [all] clears
# any filters left by the last overlay.
settingsConfig = toConfigTxt cfg.settings;
overlayConfig = lib.pipe cfg.overlays [
(map renderOverlay)
(overlays: overlays ++ [ "[all]" ])
(lib.concatStringsSep "\n")
(text: text + "\n")
];
generatedConfig =
settingsConfig
+ lib.optionalString (cfg.overlays != [ ]) (
lib.optionalString (settingsConfig != "") "\n" + overlayConfig
);
generatedFile = pkgs.writeText "config.txt" generatedConfig;

# Collect invalid values so assertion messages can show the original input.
hasLineBreak = value: lib.hasInfix "\n" value || lib.hasInfix "\r" value;
validOverlayName =
name:
name != ""
&& builtins.match "^[A-Za-z0-9][A-Za-z0-9._+-]*$" name != null
&& !lib.hasSuffix ".dtbo" name;
validFilter =
filter:
filter != "" && !hasLineBreak filter && !lib.hasInfix "[" filter && !lib.hasInfix "]" filter;

invalidNames = lib.pipe cfg.overlays [
(lib.filter (overlay: !validOverlayName overlay.name))
(map (overlay: overlay.name))
];
invalidFilters = lib.pipe cfg.overlays [
(lib.concatMap (overlay: overlay.filters))
(lib.filter (filter: !validFilter filter))
];
invalidParams = lib.pipe cfg.overlays [
(lib.concatMap (overlay: overlay.params))
(lib.filter hasLineBreak)
];

# Raspberry Pi firmware rejects config.txt lines longer than 98 characters.
longLines = lib.pipe generatedConfig [
(lib.splitString "\n")
(lib.filter (line: builtins.stringLength line > 98))
];

# A custom file bypasses generatedConfig. It cannot be combined with ordered
# overlays, and its lines are not validated here.
usesGeneratedFile = toString cfg.file == toString generatedFile;

in
{
options.hardware.raspberry-pi.configtxt = {
Expand Down Expand Up @@ -123,14 +204,59 @@ in
'';
};

overlays = lib.mkOption {
type = lib.types.listOf overlayType;
default = [ ];
description = ''
Ordered Raspberry Pi firmware overlays.

Entries are rendered after {option}`settings`. Each one writes its
filters, `dtoverlay`, and `dtparam` values in order, then resets the
overlay scope.
'';
example = lib.literalExpression ''
[
{
name = "dwc2";
params = [ "dr_mode=host" ];
filters = [ "pi4" ];
}
]
'';
};

file = lib.mkOption {
type = lib.types.path;
default = pkgs.writeText "config.txt" (toConfigTxt cfg.settings);
defaultText = lib.literalExpression ''pkgs.writeText "config.txt" (generated from settings)'';
default = generatedFile;
defaultText = lib.literalExpression ''pkgs.writeText "config.txt" (generated from settings and overlays)'';
description = ''
Path to the generated config.txt file. Defaults to the rendered output
of `settings`, but can be overridden to supply a custom config.txt.
of `settings` and `overlays`, but can be overridden to supply a custom
config.txt when `overlays` is empty.
'';
};
};

config.assertions = [
{
assertion = invalidNames == [ ];
message = "Raspberry Pi config.txt has invalid overlay names: ${builtins.toJSON invalidNames}";
}
{
assertion = invalidFilters == [ ];
message = "Raspberry Pi config.txt has invalid overlay filters: ${builtins.toJSON invalidFilters}";
}
{
assertion = invalidParams == [ ];
message = "Raspberry Pi config.txt overlay parameters contain line breaks: ${builtins.toJSON invalidParams}";
}
{
assertion = cfg.overlays == [ ] || usesGeneratedFile;
message = "Raspberry Pi config.txt overlays cannot be used with a custom configtxt.file.";
}
{
assertion = !usesGeneratedFile || longLines == [ ];
message = "Raspberry Pi config.txt lines exceed the 98-character limit: ${builtins.toJSON longLines}";
}
];
}