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).