Getting a Graphical Desktop Working on the Raspberry Pi 3B+ with NixOS 25.11 and the 6.18 Kernel

On a recent kernel, the Raspberry Pi 3B+ stops at boot when vc4 KMS starts. The fix is a one-line device tree overlay. This post describes the cause on NixOS and how to add the overlay to an image.

Getting a Graphical Desktop Working on the Raspberry Pi 3B+ with NixOS 25.11 and the 6.18 Kernel

TL;DR

The Raspberry Pi 3B+ stops at boot when vc4 KMS starts, on the 6.18 kernel with NixOS 25.11. The v3d node in the firmware device tree has no clocks property. So clk_disable_unused disables the V3D clock, and the driver locks the CPU. The fix is a one-line device tree overlay that adds the clock: &v3d { clocks = <&firmware_clocks 5>; }. Add the overlay to the firmware partition. The steps are at the end of this post.

The problem

The Raspberry Pi 3B+ can run a graphical desktop with NixOS. The desktop needs the vc4 KMS driver and a /dev/dri/card0 device. On a recent kernel, this does not work by default. The 3B+ stops at boot when vc4 KMS starts. This post describes the cause and a fix.

The configuration is nixos-25.11 stable, nixos-hardware, the linux-rpi 6.18 kernel, and the standard firmware-KMS path (dtoverlay=vc4-kms-v3d).

The symptom

When vc4 starts the display, the 3B+ stops at boot. There is no card0, there is no HDMI output, and the boot does not finish. A serial console shows the cause. Route the PL011 UART to the GPIO header with dtoverlay=disable-bt, because the mini-UART is not reliable on the 3B+. The vc4 component master binds its parts and then stops:

[   16.15] vc4-drm soc:gpu: bound 3f400000.hvs (ops vc4_hvs_ops [vc4])
[   16.15] vc4-drm soc:gpu: bound 3f902000.hdmi (ops vc4_hdmi_ops [vc4])
[   16.17] vc4-drm soc:gpu: bou                        <- the log stops here

The log stops in the middle of a line. This is the sign of a hard lockup. The CPU stops on a bus access. It is not a clean -EPROBE_DEFER. The kernel parameter clk_ignore_unused makes the 3B+ boot. This is a large clue, but the parameter keeps every unused clock enabled, which is not a precise fix.

The cause

The vc4 driver uses the component framework. It starts only after all of its sub-devices bind: HVS, HDMI, the pixelvalves, and V3D. On the 3B+, the boot stops at V3D.

The V3D node in the device tree has no clocks property. So vc4_v3d gets no clock, and it depends on the firmware to keep the V3D clock enabled. The firmware keeps it enabled, until clk_disable_unused runs early in boot (about 2.4 seconds). This function finds a clock that no driver holds, and disables it. vc4_v3d binds later (about 16 seconds) and reads the V3D_IDENT registers. The V3D block has no clock, and the CPU stops on the read. clk_ignore_unused prevents the problem, because it does not disable the clock.

The first hypothesis was the HDMI state-machine clock and the simpledrm framebuffer. Both were wrong. The cause is V3D.

The correct fix is in the upstream kernel. raspberrypi/linux #7156 gives the node its clock:

&v3d {
    clocks = <&firmware_clocks 5>;   /* 5 = RPI_FIRMWARE_V3D_CLK_ID */
};

Now vc4_v3d holds the clock and enables it again. clk_disable_unused then has no effect.

Why the problem happens on NixOS

The fix is already in the 6.18 kernel source. It is in the kernel's own compiled DTBs. But the 3B+ still stops at boot. The reason is the source of the device tree.

The NixOS Pi sd-image sets useGenerationDeviceTree = false. With this setting, the board uses the firmware DTB from the raspberrypifw package, not the kernel DTB. In nixos-25.11, raspberrypifw has the date April 2025. This date is before #7156. So the kernel is from June 2026, and the device tree is from April 2025. The board uses the old device tree.

This is a version mismatch. It is not a kernel bug or a NixOS bug. The kernel is new, and the firmware DTB is old.

The fix: patch the firmware DTB with an overlay

One option is to use the kernel DTB. Do not use this option. The kernel DTB fixes V3D, but it describes the USB controller (dwc_otg) in a way that the April firmware does not expect. In a test, a USB sound card lost audio data continuously. There were 4 FIQ errors at boot with the firmware DTB, and 22 continuous errors with the kernel DTB. Keep the firmware DTB, and add only the missing property with a small overlay.

v3d-clocks-overlay.dts:

/dts-v1/;
/plugin/;
/ {
    compatible = "brcm,bcm2835";
    fragment@0 {
        target = <&v3d>;
        __overlay__ {
            clocks = <&firmware_clocks 5>;
        };
    };
};

In the NixOS sd-image, compile the overlay at build time. Copy it to the firmware partition. Add a line to config.txt after dtoverlay=vc4-kms-v3d:

let
  v3dClocksOverlay = pkgs.runCommand "v3d-clocks.dtbo" { } ''
    ${pkgs.dtc}/bin/dtc -@ -I dts -O dtb -o "$out" ${./v3d-clocks-overlay.dts}
  '';
in {
  sdImage.postBuildCommands = ''
    # After you finalize config.txt with `dtoverlay=v3d-clocks` added:
    $MT/mcopy -o -i "$img@@$OFF" ${v3dClocksOverlay} ::/overlays/v3d-clocks.dtbo
  '';
}

The overlay is on the firmware partition. The overlay becomes active after a reflash, not after a deploy-rs push. After the reflash, the 3B+ boots correctly. The card0 device appears, and the desktop starts. There is no kernel parameter, and the sound card works.

Fix in steps

These commands create a small flake that builds a graphical sd-image for the Pi 3B+ with the fix. Run them in a new, empty directory. The flake uses nixos-hardware's raspberry-pi-3 module, the sd-image-aarch64.nix module, and a minimal XFCE desktop.

1. Create the overlay source file.

cat > v3d-clocks-overlay.dts <<'EOF'
/dts-v1/;
/plugin/;
/ {
    compatible = "brcm,bcm2835";
    fragment@0 {
        target = <&v3d>;
        __overlay__ {
            clocks = <&firmware_clocks 5>;
        };
    };
};
EOF

2. Create a NixOS module that compiles the overlay and adds it to the image.

cat > vc4-fix.nix <<'EOF'
{ pkgs, lib, ... }:
let
  v3dClocksOverlay = pkgs.runCommand "v3d-clocks.dtbo" { } ''
    ${pkgs.dtc}/bin/dtc -@ -I dts -O dtb -o "$out" ${./v3d-clocks-overlay.dts}
  '';
in
{
  sdImage.compressImage = lib.mkForce false;
  sdImage.postBuildCommands = lib.mkAfter ''
    eval "$(${pkgs.util-linux}/bin/partx "$img" -o START,SECTORS --nr 1 --pairs)"
    OFF=$((START * 512))
    MT=${pkgs.mtools}/bin
    $MT/mcopy -o -i "$img@@$OFF" ${v3dClocksOverlay} ::/overlays/v3d-clocks.dtbo
    cfg=$(mktemp)
    $MT/mtype -i "$img@@$OFF" ::config.txt > "$cfg"
    printf '\n[all]\ndtoverlay=v3d-clocks\n' >> "$cfg"
    $MT/mcopy -o -i "$img@@$OFF" "$cfg" ::config.txt
  '';
}
EOF

3. Create the flake. It builds a graphical (XFCE) image and imports the fix module.

cat > flake.nix <<'EOF'
{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
    nixos-hardware.url = "github:NixOS/nixos-hardware";
  };

  outputs = { self, nixpkgs, nixos-hardware, ... }: {
    nixosConfigurations.rpi3 = nixpkgs.lib.nixosSystem {
      system = "aarch64-linux";
      modules = [
        nixos-hardware.nixosModules.raspberry-pi-3
        "${nixpkgs}/nixos/modules/installer/sd-card/sd-image-aarch64.nix"
        ./vc4-fix.nix
        ({ pkgs, ... }: {
          system.stateVersion = "25.11";

          # A minimal graphical desktop, auto-logged-in.
          services.xserver.enable = true;
          services.xserver.displayManager.lightdm.enable = true;
          services.xserver.desktopManager.xfce.enable = true;
          services.displayManager.autoLogin = {
            enable = true;
            user = "pi";
          };

          users.users.pi = {
            isNormalUser = true;
            initialPassword = "raspberry";
            extraGroups = [ "wheel" "video" ];
          };

          # 1 GB board: add swap.
          swapDevices = [{ device = "/swapfile"; size = 1024; }];
        })
      ];
    };
  };
}
EOF

4. Build the image. The Pi 3B+ is aarch64. On an x86_64-linux builder, set boot.binfmt.emulatedSystems = [ "aarch64-linux" ] first, to build with QEMU. If the directory is a git repository, run git add . first, because flakes ignore untracked files.

nix build .#nixosConfigurations.rpi3.config.system.build.sdImage

Note on the kernel. This flake tracks nixos-hardware, so the linux-rpi kernel can be newer than the binary cache. If the cache does not have the kernel, the build compiles it. Under QEMU emulation on an x86_64 builder, this can take a few hours. To avoid a long build, pin nixos-hardware to a revision whose kernel is in the cache. Add the commit to the flake URL, for example nixos-hardware.url = "github:NixOS/nixos-hardware/<commit>";.

5. Find the image file.

ls result/sd-image/

6. Find the SD card device name. The name is like /dev/sda or /dev/mmcblk0.

lsblk

7. Write the image to the SD card.

CAUTION: The next command erases all data on the target device. Make sure that the device is the SD card, and not a disk. Replace /dev/sdX with the device name from step 6.

sudo dd if=result/sd-image/*.img of=/dev/sdX bs=4M conv=fsync status=progress

8. Boot the board. Insert the SD card into the 3B+, and start it. The XFCE desktop starts and logs in as user pi. The card0 device is present.

Summary

If the 3B+ stops at boot when KMS starts on a recent kernel, examine the v3d node in the device tree. If the node has no clocks property, the firmware DTB is older than the kernel. A one-line overlay is a better fix than clk_ignore_unused or a full DTB change. The complete fix is in the nixpkgs domain. Keep raspberrypifw at the same version as the kernel.