How do I get GPIO pins to work with RaspberryPi 4 using a mainline kernel?

Hi there!

I have been succesfully running a RaspberryPi4 using a mainline kernel (without using nixos-hardware) with UEFI/btrfs/systemd-boot with an external SSD for a while now, based on notes around the internet. I took me a while but eventually found this gem (and it underlying references):

Lots of other gems around the beautiful internet also helped me a lot, a subset of these are:

I am running nixpkgs-unstable, and am currently running the following kernel:

Linux << hostname >> 6.6.41 #1-NixOS SMP Thu Jul 18 11:21:27 UTC 2024 aarch64 GNU/Linux

I ran the following (installation) commands:

DISK=/dev/sda
parted --script $DISK mklabel gpt
parted --script $DISK mkpart ESP fat32 0% 1GiB
parted --script $DISK set 1 esp on
parted --script $DISK set 1 boot on

mkfs.fat -F32 -n BOOTTOFRMT $DISK1
parted --script $DISK mkpart primary 1GiB 100%
mkfs.btrfs $DISK2

mount $DISK2 /mnt/
mkdir -p /mnt/boot
mount $DISK1 /mnt/boot/

cd /mnt/boot/

nix-shell -p wget unzip
wget https://github.com/pftf/RPi4/releases/download/v1.33/RPi4_UEFI_Firmware_v1.33.zip
unzip RPi4_UEFI_Firmware_v1.33.zip
rm README.md
rm RPi4_UEFI_Firmware_v1.33.zip
rm firmware/Readme.txt

sync

# remember: remove 3GiB RAM limit from UEFI (see post)

# ensure disks are mounted
mount /dev/sda2 /mnt
mount /dev/sda1 /mnt/boot

# on build machine
nix build --print-out-paths '.#nixosConfigurations.<<hostname>>.config.system.build.toplevel'
nix-copy-closure --to root@<ipv4> ./result

# on Raspberry Pi
nixos-install --no-channel-copy --system <nix store path> --root /mnt

# remember, booting does not work with keyboard connected
reboot -h now

I have a system which is working fine with the following (relevant) config:

imports = [
  imports = [
    # inputs.nixos-hardware.nixosModules.raspberry-pi-4
    (modulesPath + "/profiles/headless.nix")
    # skip disko
    # does not work, because UEFI has to be manually pushed
    # ./disko.nix
  ];

  config = {

    # https://codeberg.org/kotatsuyaki/rpi4-usb-uefi-nixos-config
    boot.initrd.availableKernelModules = [
      "bcm2835_dma" # kernel module for GPIO
      "i2c_bcm2835" # kernel module for HAT
      "vc4" # for GPU
    ];

    boot.kernelParams = [
      # put kernel message in the first tty0
      "console=ttyS0,115200n8"
      "console=ttyAMA0,115200n8"
      "console=tty0"
      "cma=64M"
    ];

    boot.loader = {
      grub.enable = false;
      systemd-boot.enable = true;
      efi.canTouchEfiVariables = true;
    };

    environment.systemPackages = with pkgs; [
      libraspberrypi
      raspberrypi-eeprom
    ];
    
    # more config exists, but I don't think its relevant for my issue
}

If I want to configure a service which uses GPIO pins, like for example the following (I know I should use a systemd timer instead, that will be my next step if this works):

    systemd.services.fancontrol = {
      #enable = false;
      description = ''
        Service that control the fan on raspberrypi.
      '';
      script = ''
        ${pkgs.writers.writePython3 "fancontrol.py" { libraries = with pkgs; [
          python312Packages.gpiozero
          python312Packages.rpi-gpio
        ]; } ''
        import subprocess
        import time

        from gpiozero import OutputDevice

        ON_THRESHOLD = 65  # (degrees Celsius) Fan kicks on at this temperature.
        OFF_THRESHOLD = 55  # (degress Celsius) Fan shuts off at this temperature.
        SLEEP_INTERVAL = 5  # (seconds) How often we check the core temperature.
        GPIO_PIN = 14  # Which GPIO pin you're using to control the fan.


        def get_temp():
            """Get the core temperature.
            Run a shell script to get the core temp and parse the output.
            Raises:
                RuntimeError: if response cannot be parsed.
            Returns:
                float: The core temperature in degrees Celsius.
            """
            output = subprocess.run(["vcgencmd", "measure_temp"], capture_output=True)
            temp_str = output.stdout.decode()
            try:
                return float(temp_str.split("=")[1].split("'")[0])
            except (IndexError, ValueError):
                raise RuntimeError("Could not parse temperature output.")


        if __name__ == "__main__":
            # Validate the on and off thresholds
            if OFF_THRESHOLD >= ON_THRESHOLD:
                raise RuntimeError("OFF_THRESHOLD must be less than ON_THRESHOLD")

            fan = OutputDevice(GPIO_PIN)

            while True:
                temp = get_temp()

                # Start the fan if the temperature has reached the limit and the fan
                # isn't already running.
                # NOTE: `fan.value` returns 1 for "on" and 0 for "off"
                if temp > ON_THRESHOLD and not fan.value:
                    fan.on()

                # Stop the fan if the fan is running and the temperature has dropped
                # to 10 degrees below the limit.
                elif fan.value and temp < OFF_THRESHOLD:
                    fan.off()

                time.sleep(SLEEP_INTERVAL)
        ''}
      '';

      wantedBy = [
        "multi-user.target"
      ];
    };

If I enable this service, I am experiencing an issue with no GPIO pin connection being found:

aug 18 17:50:23 <<hostname>> systemd[1]: Started Service that control the fan on raspberrypi..
aug 18 17:50:25 <<hostname>> fancontrol-start[1224]: /nix/store/i52brqhpss0fzf2rz370s0vrh11kpdia-python3-3.12.4-env/lib/python3.12/site-packages/gpiozero/devices.py:300: PinFactoryFa>
aug 18 17:50:25 <<hostname>> fancontrol-start[1224]:   warnings.warn(
aug 18 17:50:25 <<hostname>> fancontrol-start[1224]: /nix/store/i52brqhpss0fzf2rz370s0vrh11kpdia-python3-3.12.4-env/lib/python3.12/site-packages/gpiozero/devices.py:300: PinFactoryFa>
aug 18 17:50:25 <<hostname>> fancontrol-start[1224]:   warnings.warn(
aug 18 17:50:25 <<hostname>> fancontrol-start[1224]: /nix/store/i52brqhpss0fzf2rz370s0vrh11kpdia-python3-3.12.4-env/lib/python3.12/site-packages/gpiozero/devices.py:300: PinFactoryFa>
aug 18 17:50:25 <<hostname>> fancontrol-start[1224]:   warnings.warn(
aug 18 17:50:25 <<hostname>> fancontrol-start[1224]: /nix/store/i52brqhpss0fzf2rz370s0vrh11kpdia-python3-3.12.4-env/lib/python3.12/site-packages/gpiozero/devices.py:300: PinFactoryFa>
aug 18 17:50:25 <<hostname>> fancontrol-start[1224]:   warnings.warn(
aug 18 17:50:25 <<hostname>> fancontrol-start[1224]: Traceback (most recent call last):
aug 18 17:50:25 <<hostname>> fancontrol-start[1224]:   File "/nix/store/mq1h0rby2gzws3yvqxlyd585ipg0bqln-fancontrol.py", line 34, in <module>
aug 18 17:50:25 <<hostname>> fancontrol-start[1224]:     fan = OutputDevice(GPIO_PIN)
aug 18 17:50:25 <<hostname>> fancontrol-start[1224]:           ^^^^^^^^^^^^^^^^^^^^^^
aug 18 17:50:25 <<hostname>> fancontrol-start[1224]:   File "/nix/store/i52brqhpss0fzf2rz370s0vrh11kpdia-python3-3.12.4-env/lib/python3.12/site-packages/gpiozero/devices.py", line 10>
aug 18 17:50:25 <<hostname>> fancontrol-start[1224]:     self = super().__call__(*args, **kwargs)
aug 18 17:50:25 <<hostname>> fancontrol-start[1224]:            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
aug 18 17:50:25 <<hostname>> fancontrol-start[1224]:   File "/nix/store/i52brqhpss0fzf2rz370s0vrh11kpdia-python3-3.12.4-env/lib/python3.12/site-packages/gpiozero/output_devices.py", >
aug 18 17:50:25 <<hostname>> fancontrol-start[1224]:     super().__init__(pin, pin_factory=pin_factory)
aug 18 17:50:25 <<hostname>> fancontrol-start[1224]:   File "/nix/store/i52brqhpss0fzf2rz370s0vrh11kpdia-python3-3.12.4-env/lib/python3.12/site-packages/gpiozero/mixins.py", line 75,>
aug 18 17:50:25 <<hostname>> fancontrol-start[1224]:     super().__init__(*args, **kwargs)
aug 18 17:50:25 <<hostname>> fancontrol-start[1224]:   File "/nix/store/i52brqhpss0fzf2rz370s0vrh11kpdia-python3-3.12.4-env/lib/python3.12/site-packages/gpiozero/devices.py", line 54>
aug 18 17:50:25 <<hostname>> fancontrol-start[1224]:     super().__init__(pin_factory=pin_factory)
aug 18 17:50:25 <<hostname>> fancontrol-start[1224]:   File "/nix/store/i52brqhpss0fzf2rz370s0vrh11kpdia-python3-3.12.4-env/lib/python3.12/site-packages/gpiozero/devices.py", line 24>
aug 18 17:50:25 <<hostname>> fancontrol-start[1224]:     Device.ensure_pin_factory()
aug 18 17:50:25 <<hostname>> fancontrol-start[1224]:   File "/nix/store/i52brqhpss0fzf2rz370s0vrh11kpdia-python3-3.12.4-env/lib/python3.12/site-packages/gpiozero/devices.py", line 27>
aug 18 17:50:25 <<hostname>> fancontrol-start[1224]:     Device.pin_factory = Device._default_pin_factory()
aug 18 17:50:25 <<hostname>> fancontrol-start[1224]:                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
aug 18 17:50:25 <<hostname>> fancontrol-start[1224]:   File "/nix/store/i52brqhpss0fzf2rz370s0vrh11kpdia-python3-3.12.4-env/lib/python3.12/site-packages/gpiozero/devices.py", line 30>
aug 18 17:50:25 <<hostname>> fancontrol-start[1224]:     raise BadPinFactory('Unable to load any default pin factory!')
aug 18 17:50:25 <<hostname>> fancontrol-start[1224]: gpiozero.exc.BadPinFactory: Unable to load any default pin factory!
aug 18 17:50:25 <<hostname>> systemd[1]: fancontrol.service: Main process exited, code=exited, status=1/FAILURE
aug 18 17:50:25 <<hostname>> systemd[1]: fancontrol.service: Failed with result 'exit-code'.

I presume it has to do something with missing kernel modules and/or GPIO firmware being unsupported on the mainline linux kernel. I have found this issue detailing part of my problem: [RPI4] modesetting overlay fails to build with latest kernels · Issue #631 · NixOS/nixos-hardware · GitHub

However, this is a different approach however as it tries to fix the issue by patching the kernel and doesn’t use UEFI firmware.

All this context finally leads me to my questions:

  • Is there a way to get GPIO pins working on a RaspberryPi4 on a mainline kernel booted with UEFI-firmware?
  • Am I right to assume that missing firmware/kernel module is the issue? If so which do I need?

NOTE: I noticed that my setup (which was fully inspired on others) is kind of the modern goal for the RaspberryPi 5, as detailed on the wiki, which is kinda cool!

NOTE2: If people would like more docs on how to run this, I’d be happy to contribute it somwhere. I think there might even be some way to use nixos-anywhere to automate the entire deployment, with some voodoo writing the UEFI firmware on install, but I haven’t bothered yet as my current setup works (aside from GPIO pins).

1 Like

Consider asking these in a more RaspberryPi-specific forum and when you have NixOS specific questions, I’m sure some would do their best to help here.