{ config, lib, pkgs, namespace, ... }: let cfg = config.${namespace}.hardware.raspberry-pi; dt_ao_overlay = _final: prev: { deviceTree = prev.deviceTree // { applyOverlays = _final.callPackage ./apply-overlays-dtmerge.nix { }; }; }; # installs raspberry's firmware independent of the nixos generations # sometimes referred to as "boot code" raspberryPiFirmware = { pkgs, firmware, configTxt, }: pkgs.replaceVarsWith { src = ./generational/install-firmware.sh; isExecutable = true; replacements = { inherit (pkgs) bash; path = pkgs.lib.makeBinPath [ pkgs.coreutils ]; inherit firmware configTxt; }; }; kernelbootGenBuilder = { pkgs, deviceTreeInstaller, }: pkgs.replaceVarsWith { src = ./generational/kernelboot-gen-builder.sh; isExecutable = true; replacements = { inherit (pkgs) bash; path = pkgs.lib.makeBinPath [ pkgs.coreutils ]; installDeviceTree = deviceTreeInstaller; }; }; deviceTree = { pkgs, firmware, }: pkgs.replaceVarsWith { src = ./generational/install-device-tree.sh; isExecutable = true; replacements = { inherit (pkgs) bash; path = pkgs.lib.makeBinPath [ pkgs.coreutils ]; inherit firmware; }; }; mkBootloader = pkgs: bootloader { inherit pkgs; inherit (cfg) nixosGenerationsDir; firmwareInstaller = "${raspberryPiFirmware { inherit pkgs; firmware = pkgs.${namespace}.raspberrypifw; configTxt = pkgs.writeTextFile { name = "config.txt"; text = '' # Do not edit! # This configuration file is generated from NixOS configuration # options `hardware.raspberry-pi.config`. # Any manual changes will be overwritten on the next configuration # switch. ${config.${namespace}.hardware.raspberry-pi.config-generated} ''; }; }}"; nixosGenBuilder = "${kernelbootGenBuilder { inherit pkgs; deviceTreeInstaller = let cmd = deviceTree { inherit pkgs; firmware = cfg.firmwarePackage; }; args = lib.optionalString (!cfg.useGenerationDeviceTree) " -r"; in "${cmd} ${args}"; }}"; }; bootloader = { pkgs, nixosGenerationsDir, firmwareInstaller, nixosGenBuilder, }: pkgs.replaceVarsWith { src = ./generational/nixos-generations-builder.sh; isExecutable = true; replacements = { inherit (pkgs) bash; path = pkgs.lib.makeBinPath [ pkgs.coreutils pkgs.gnused ]; # NixOS-generations -independent installFirmwareBuilder = firmwareInstaller; # NixOS-generations -dependent inherit nixosGenerationsDir nixosGenBuilder; }; }; # Builders used to write during system activation ubootBuilder = import ./uboot-builder.nix { inherit pkgs; ubootPackage = if (cfg.variant == "5") then pkgs.${namespace}.uboot-pi5 else pkgs.${namespace}.uboot-pi4; firmwareBuilder = firmwarePopulateCmd; extlinuxConfBuilder = config.boot.loader.generic-extlinux-compatible.populateCmd; }; uefiBuilder = import ./uefi-builder.nix { inherit pkgs; uefiPackage = if (cfg.variant == "5") then pkgs.${namespace}.uefi-rpi5 else pkgs.${namespace}.edk2.override { MODEL = "4"; }; firmwareBuilder = firmwarePopulateCmd; }; # Builders exposed via populateCmd, which run on the build architecture populateFirmwareBuilder = import ./firmware-builder.nix { pkgs = pkgs.buildPackages; configTxt = pkgs.writeTextFile { name = "config.txt"; text = '' # Do not edit! # This configuration file is generated from NixOS configuration # options `hardware.raspberry-pi.config`. # Any manual changes will be overwritten on the next configuration # switch. ${config.${namespace}.hardware.raspberry-pi.config-generated} ''; }; firmware = pkgs.${namespace}.raspberrypifw; }; firmwarePopulateCmd = "${populateFirmwareBuilder} ${firmwareBuilderArgs}"; firmwareBuilderArgs = lib.optionalString (!true) " -r"; # these will receive the top-level path as an argument when invoked as # system.build.installBootloader builder = { # system.build.installBootLoader uboot = "${ubootBuilder} -f /boot/firmware -b /boot -c"; uefi = "${uefiBuilder} -f /boot/firmware -b /boot -c"; kernel = builtins.concatStringsSep " " [ "${mkBootloader pkgs}" "-g ${toString 10}" "-f /boot/firmware" "-c" ]; }; # firmware: caller must provide `-c ` and `-f ` # boot: caller must provide `-c ` and `-b ` in { options.${namespace}.hardware.raspberry-pi = { enable = lib.mkEnableOption "Raspberry Pi common configuration"; variant = lib.mkOption { type = lib.types.enum [ "4" "5" ]; description = "Raspberry Pi variant (4 or 5)"; }; bootType = lib.mkOption { type = lib.types.enum [ "uefi" "uboot" "kernel" ]; default = "uefi"; }; nixosGenerationsDir = lib.mkOption { type = lib.types.str; default = "/boot/nixos-generations"; description = "Directory on the boot partition where NixOS generations are stored (kernel bootType only)."; }; firmwarePackage = lib.mkOption { type = lib.types.package; default = null; defaultText = lib.literalExpression "pkgs.\${namespace}.raspberrypifw"; description = "Raspberry Pi firmware package used for device-tree installation (kernel/uboot bootType)."; }; useGenerationDeviceTree = lib.mkOption { type = lib.types.bool; default = true; description = "Whether to install a per-generation device tree alongside the kernel (kernel bootType only)."; }; apply-overlays-dtmerge = { enable = lib.mkEnableOption "" // { description = '' Whether replace deviceTree.applyOverlays implementation to use dtmerge from libraspberrypi. This can resolve issues with applying dtbs for the pi. ''; }; }; }; imports = [ ./audio.nix ./bluetooth.nix ./config.nix ./i2c.nix ./leds.nix ./modesetting.nix ./pwm.nix ./wifi.nix ]; config = lib.mkIf cfg.enable { assertions = [ { assertion = cfg.bootType != "uboot" || cfg.firmwarePackage != null; message = "mjallen.hardware.raspberry-pi.firmwarePackage must be set when bootType is \"uboot\"."; } { assertion = cfg.bootType != "kernel" || cfg.firmwarePackage != null; message = "mjallen.hardware.raspberry-pi.firmwarePackage must be set when bootType is \"kernel\"."; } { assertion = cfg.nixosGenerationsDir != ""; message = "mjallen.hardware.raspberry-pi.nixosGenerationsDir must be a non-empty path."; } ]; boot = { initrd.availableKernelModules = [ "usbhid" "usb-storage" ] ++ ( if (cfg.variant == "5") then [ "nvme" ] else [ "vc4" "pcie-brcmstb" # required for the pcie bus to work "reset-raspberrypi" # required for vl805 firmware to load ] ); loader = { generic-extlinux-compatible = { enable = lib.mkDefault (if cfg.bootType == "uefi" then false else true); useGenerationDeviceTree = lib.mkOverride 60 (if cfg.bootType == "uefi" then false else true); }; systemd-boot = { enable = if cfg.bootType == "uefi" then true else false; extraInstallCommands = let bootloaderInstaller = builder."${cfg.bootType}"; in '' ${bootloaderInstaller} -f /boot/firmware -b /boot -c ''; }; grub.enable = lib.mkForce false; }; }; # Common Raspberry Pi packages environment.systemPackages = with pkgs; [ dconf i2c-tools raspberrypi-eeprom pkgs.${namespace}.raspberrypi-utils pkgs.${namespace}.raspberrypifw pkgs.${namespace}.raspberryPiWirelessFirmware raspberrypi-armstubs erofs-utils fex squashfuse squashfsTools ]; # Common Bluetooth configuration systemd = { services.btattach = { before = [ "bluetooth.service" ]; after = [ "dev-ttyAMA0.device" ]; wantedBy = [ "multi-user.target" ]; serviceConfig = { ExecStart = "${lib.getExe' pkgs.bluez "btattach"} -B /dev/ttyAMA0 -P bcm -S 3000000"; }; }; tmpfiles.packages = [ pkgs.${namespace}.udev-rules ]; }; ${namespace}.hardware.raspberry-pi = { config = { all = { options = { os_prefix = lib.mkIf (cfg.bootType == "kernel") { enable = true; value = "${cfg.nixosGenerationsDir}/default/"; # "nixos//" }; kernel = lib.mkIf (cfg.bootType == "kernel" || cfg.bootType == "uboot") { enable = true; value = if cfg.bootType == "uboot" then "u-boot.bin" else if cfg.bootType == "kernel" then "kernel.img" else ""; }; }; }; }; extra-config = let # https://www.raspberrypi.com/documentation/computers/config_txt.html#initramfs ramfsfile = "initrd"; ramfsaddr = "followkernel"; # same as 0 = "after the kernel image" in '' [all] initramfs ${ramfsfile} ${ramfsaddr} ''; }; # Common hardware settings hardware = { deviceTree = { filter = lib.mkDefault (if (cfg.variant == "5") then "bcm2712*.dtb" else "bcm2711*.dtb"); package = lib.mkOverride 80 config.boot.kernelPackages.kernel; overlays = lib.optionals (cfg.variant == "4") [ { name = "rpi4-cpu-revision"; dtsText = '' /dts-v1/; /plugin/; / { compatible = "brcm,bcm2711"; fragment@0 { target-path = "/"; __overlay__ { system { linux,revision = <0x00d03114>; }; }; }; }; ''; } ]; }; firmware = [ pkgs.${namespace}.raspberryPiWirelessFirmware ]; graphics.enable32Bit = lib.mkForce false; i2c.enable = lib.mkDefault true; }; system = { nixos.tags = [ "raspberry-pi-${cfg.variant}" # config.boot.loader.raspberry-pi.bootloader config.boot.kernelPackages.kernel.version ]; }; # Common programs programs.kdeconnect.enable = lib.mkDefault false; # Root user shell configuration users = { users.root.shell = pkgs.zsh; extraGroups = { gpio = { }; i2c = { }; input = { }; plugdev = { }; spi = { }; video = { }; }; }; services = { udev.packages = [ pkgs.${namespace}.udev-rules ]; xserver.extraConfig = let identifier = "rp1"; driver = "rp1-vec|rp1-dsi|rp1-dpi"; in '' Section "OutputClass" Identifier "${identifier}" MatchDriver "${driver}" Driver "modesetting" Option "PrimaryGPU" "true" EndSection ''; }; nixpkgs.overlays = ( if cfg.variant == "5" then [ (_final: prev: { # https://github.com/nvmd/nixos-raspberrypi/issues/64 # credit for the initial version of this snippet goes to @micahcc jemalloc = prev.jemalloc.overrideAttrs (old: { # --with-lg-page=(log2 page_size) # RPi5 (bcm2712): since our page size is 16384 (2**14), we need 14 configureFlags = let pageSizeFlag = "--with-lg-page"; in (prev.lib.filter (flag: !(prev.lib.hasPrefix pageSizeFlag flag)) old.configureFlags) ++ [ "${pageSizeFlag}=14" ]; }); }) ] else [ ] ) ++ (if cfg.apply-overlays-dtmerge.enable then [ dt_ao_overlay ] else [ ]); }; }