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.

Hi, I know it is an old post, but did you find Any solution, how to work with GPIO pins?

It’s been a while since I tinkered with this, but the last time I did not have functional pins.

As far as my understanding goes, it is because the driver that correctly connects to the pin is not available on the main line kernel.

If I get to it, I’ll double check whether this still is the case.