Write a simple LED driver for Raspberry Pi 5


Write a simple LED driver with DeviceTree for Raspberry Pi 5  

In this article, I will explore how Linux handles hardware on the Raspberry Pi 5. I will cover three main topics:
  1. What is a Device Tree?
  2. Method 1: How to use the existing generic LED driver to control a GPIO.
  3. Method 2: How to write a custom LED driver from scratch to control a GPIO.

1.What is Device Tree

The Device Tree (DT) is the industry-standard data structure used by the Linux kernel to describe hardware that cannot be automatically discovered by the CPU (such as GPIOs, I2C devices, and on-chip controllers).Think of the Device Tree as a hardware blueprint. It separates the hardware description from the kernel binary code.Think of the Device Tree as a hardware blueprint. It separates the hardware description from the kernel binary code.

  • Old Way (Pre-DT): Every board had a hardcoded C file inside the kernel source. Changing a single GPIO pin required recompiling the entire kernel.
  • Modern Way (With DT): The hardware layout is written in a text file (.dts). This is compiled into a binary (.dtb) that the bootloader (like U-Boot) passes to the kernel at startup.

The Development Workflow

You rarely write a Device Tree from scratch. The typical process looks like this:

  1. Start with Vendor Source: You get a basic .dts file from the chip maker (the SoC vendor) that describes the CPU core.
  2. Customize for Your Board: You create a new .dts file for your specific target board. Inside this file, you include the vendor's basic file (e.g., #include "bcm2712.dtsi") and then add your specific hardware design (like your LEDs, sensors, or screen).
  3. Compile: You use the Device Tree Compiler (DTC) to convert your human-readable .dts into a machine-readable .dtb.
  4. Boot: The bootloader loads this .dtb into memory and starts the kernel.
How it works on Raspberry Pi 5

The DT is a hierarchical tree of nodes. On a Raspberry Pi 5, it describes the BCM2712 SoC, the PCIe bus, and the RP1 southbridge. The main source file is located at arch/arm64/boot/dts/broadcom/bcm2712-rpi-5-b.dts. Because the device tree is modular, you will see it includes other files like #include "bcm2712-dtsi".
How to get raspberry pi kerenl source code.

Key Terminology

  • DTS (Device Tree Source): The human-readable text file where developers describe the hardware.

  • DTB (Device Tree Blob): The binary version that the kernel actually reads.

  • DTC (Device Tree Compiler): The tool that converts .dts to .dtb.


2.The Easy Way (Using the Existing Driver)  

Goal: Configure GPIO 27 as an LED using the built-in gpio-leds driver.

Step 1: Create the Overlay (Host Computer)

Create a file named rpi_gpio27_led.dts. This overlay tells the kernel: "Hey, there is an LED connected to GPIO 27, please manage it using the standard driver."

$vi rpi_gpio27_led.dts

/dts-v1/;
/plugin/;
/ {
    compatible = "brcm,bcm2712";
    fragment@0 {
        target-path = "/";
        __overlay__ {
            my_led_device {
                compatible = "gpio-leds";
                led27 {
                    label = "rpi5::gpio27";
                    gpios = <&rpi1_gpio 27 0>; /* 0 is active high */
                    default-state = "off";
                };
            };
        };
    };
};

Code Explanation:

  • /dts-v1/; Declares the file as Version 1 of the Device Tree syntax.
  • /plugin/; This is critical. It informs the compiler that this is an Overlay, designed to be "grafted" onto the existing live system device tree rather than replacing it entirely.
  • compatible = "brcm,bcm2712"; Specifies compatibility with the Raspberry Pi 5's SoC (BCM2712). This serves as a safety check to prevent loading the overlay onto incompatible hardware like a Pi 4.
  • fragment@0 Overlays consist of multiple fragments. This is the first logical block of changes.
  • target-path = "/"; Specifies that the new node should be inserted at the Root of the main device tree.
  • __overlay__ Everything defined inside this block will be "injected" into the target path once the overlay is loaded.
  • compatible = "gpio-leds"; The most important property. It instructs the kernel to load the standard leds-gpio driver. This triggers the gpio_led_probe function built into Linux.
  • led27 The child node name representing this specific LED in the device tree structure.
  • label = "rpi5::gpio27"; This is the name that will appear in userspace. After loading, you will find the directory at /sys/class/leds/rpi5::gpio27/.
  • gpios = <&rp1_gpio 27 0>;
  • &rp1_gpio: Points to the GPIO controller on the RP1 southbridge chip (for Pi 5).
  • 27: The GPIO pin number.
  • 0: The flag bit. 0 represents Active High.
  • default-state = "off"; Ensures the LED is initialized to an "off" state when the driver is first loaded.

Step 2: Transfer to Target

Copy the rpi_gpio27_led.dts file from your host PC to the Raspberry Pi 5.

$scp rpi_gpio27_led.dts    linda@10.1.xx.xxx:/home/linda/

Step 3: Compile and Apply (Target Board)

Log into your Raspberry Pi 5 and compile the file:

# Compile DTS to DTBO (Overlay Blob)
$dtc -@ -I dts -O dtb -o rpi_gpio27_led.dtbo rpi_gpio27_led.dts
# Apply the overlay
$sudo dtoverlay rpi_gpio27_led.dtbo

Step 4: Verification and Test the LED

Check if the system recognizes the new LED:
$ls /sys/class/leds/
# You should see: rpi5::gpio27

# Turn the LED on
$echo 1 | sudo tee /sys/class/leds/rpi5::gpio27/brightness

3.The Custom Way (Writing Your Own Driver)

Goal: Write a custom kernel module to control GPIO 17.

Prerequisites: You need a configured Linux kernel build environment. (Refer to "Write the Very first HelloWorld driver." for setup).

Step 1: Explain The Driver Code "led17_driver_sys.c" (Host Computer)

The demo source code is on my github, if need to download.

// 1. The ID Table (What hardware do we support?)
static const struct of_device_id pi5_led_ids[] = {
    { .compatible = "pi5-led17-sysfs", }, //must match with the name shows in device tree
    { }
};
// 2. Export the Table (Enable auto-loading)
MODULE_DEVICE_TABLE(of, pi5_led_ids); //of stands for Open Firmware. This tells the macro that the table uses Device Tree matching (searching for compatible strings--"pi5-led17-sysfs").

// 3. The Probe Function (Runs when insert the driver)
static int pi5_led_probe(struct platform_device *pdev) {
    struct pi5_led_data *led;
    struct device *dev = &pdev->dev;
    int ret;

    led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL);
    if (!led) return -ENOMEM;

    // Init GPIO 17 which get from Device Tree 
    led->gpiod = devm_gpiod_get(dev, "led", GPIOD_OUT_LOW);
    if (IS_ERR(led->gpiod)) return PTR_ERR(led->gpiod);

    // Set LED Classe attribute
    led->cdev.name = "pi5::gpio17";  // It will show at /sys/class/leds/
    led->cdev.brightness_set = pi5_led_brightness_set;
    led->cdev.max_brightness = LED_FULL;

    // Register LED device type
    ret = devm_led_classdev_register(dev, &led->cdev);
    if (ret) return ret;

    platform_set_drvdata(pdev, led);
    dev_info(dev, "Pi5 LED class driver register success\n");
    return 0;
}

// 4. The Remove Function (Runs when driver is unloaded)
static void pi5_led_remove(struct platform_device *pdev) {
    dev_info(&pdev->dev, "Pi5 LED class driver removed\n");
}

// 5. The Driver Structure
static struct platform_driver pi5_led_driver = {
    .probe = pi5_led_probe, // Function called when insert the driver
    .remove = pi5_led_remove, // Function called when remove the driver
    .driver = {
        .name = "pi5_led17_sysfs", // Name under /sys/bus/platform/drivers/
        .of_match_table = pi5_led_ids, // Links to our ID table. 
    },
};
// 6. Register the Driver
module_platform_driver(pi5_led_driver);

// 7. Function to control brightness
// set brightness through echo 1> /sys/class/leds/rpi5::gpio17/brightness
static void pi5_led_brightness_set(struct led_classdev *led_cdev, enum led_brightness brightness) {
    struct pi5_led_data *led = container_of(led_cdev, struct pi5_led_data, cdev);
    gpiod_set_value(led->gpiod, brightness ? 1 : 0);
}

Step 2: The Device Tree Overlay "rpi_gpio17_led.dts"(Host Computer)

/dts-v1/;
/plugin/;

/ {
    fragment@0 {
        target-path = "/";
        __overlay__ {
            /* This name is what shows up in /sys/bus/platform/devices/ */
            my_pi5_led {
                /* CRITICAL: This MUST match your driver's of_match_table */
                compatible = "raspberrypi,pi5-led17-sysfs";
                led-gpios = <&rp1_gpio 17 0>;
                status = "okay";
            };
        };
    };
};

Step 3: Compile and Deploy (Host Computer)

Compile the Driver:

Download the Makefile from my github, and modify the KDIR to your raspberry pi kernel path. Run make on your host machine to generate led17_driver_sys.ko. 
$make 
make[1]: Entering directory '/home/rita/fun/RB_Pi5/linux'
  CC [M]  /home/rita/fun/led_driver/led17_driver_sys.o
  MODPOST /home/rita/fun/led_driver/Module.symvers
  CC [M]  /home/rita/fun/led_driver/led17_driver_sys.mod.o
  CC [M]  /home/rita/fun/led_driver/.module-common.o
  LD [M]  /home/rita/fun/led_driver/led17_driver_sys.ko
make[1]: Leaving directory '/home/rita/fun/RB_Pi5/linux'

Transfer Files: Copy the .ko file and the .dts file to your Raspberry Pi 5.

$scp led17_driver_sys.ko rpi_gpio17_led.dts linda@10.12.XX.XXX:/home/linda/

Step 4:Compile, Apply and  Insert Driver (Target Board)

# Compile DTS to DTBO (Overlay Blob)
$dtc -@ -I dts -O dtb -o rpi_gpio17_led.dtbo rpi_gpio17_led.dts 
# Apply the overlay
$sudo dtoverlay rpi_gpio17_led.dtbo
# Load the software driver
$sudo insmod led17_driver_sys.ko 

Step 5: Verification and Test the LED

Check the kernel logs to confirm the probe function ran successfully:
$dmesg |tail
[ 1750.918629] pi5_led17_sysfs my_pi5_led: Pi5 LED class driver register success

Check if the system recognizes the new LED:
ls /sys/class/leds/
# You should see: rpi5::gpio17

Now control your custom LED:
$echo 1 |sudo tee /sys/class/leds/pi5::gpio17/brightness

Reference

Comments