Dieser Post ist Teil der Serie über mein privates BeagleBone-Black-Projekt. Die Go-REST-API und die Rust-HAL greifen beide aus dem User-Space auf GPIO/I2C zu (/sys/class/gpio, /dev/i2c-*). Das reicht für die meisten Anwendungsfälle völlig aus. Dieser Post zeigt die tieferliegende Ebene: ein eigenes Kernel-Modul, für den Fall, dass User-Space-Zugriff nicht mehr genügt.

Wann reicht User-Space nicht mehr aus?

AnforderungUser-Space (sysfs//dev/i2c-*)

Kernel-Modul

Gelegentliches Lesen/Schreiben

Ausreichend

Unnötiger Aufwand

Ereignisgesteuerte Reaktion auf ein Signal (z. B. Taster-Interrupt)

Nur per Polling möglich — kostet CPU-Zeit und Latenz

request_irq reagiert ohne Polling, mit geringer Latenz

Harte Timing-Anforderungen

Nicht garantierbar (User-Space kann jederzeit verdrängt werden)

Deutlich bessere, wenn auch nicht harte Echtzeit-Garantien

Ein Kernel-Modul ist kein Allheilmittel — es bringt eigene Risiken mit (ein Absturz reißt das ganze System mit, nicht nur einen Prozess). Die Faustregel: User-Space zuerst versuchen, Kernel-Modul nur, wenn Polling oder Latenz tatsächlich zum Problem werden.

Grundgerüst eines Out-of-Tree-Moduls

// gpio_irq_demo.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

static int __init gpio_irq_demo_init(void)
{
    pr_info("gpio_irq_demo: loaded\n");
    return 0;
}

static void __exit gpio_irq_demo_exit(void)
{
    pr_info("gpio_irq_demo: unloaded\n");
}

module_init(gpio_irq_demo_init);
module_exit(gpio_irq_demo_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Paul Fleischmann");
MODULE_DESCRIPTION("GPIO interrupt demo character device");

Das zugehörige Makefile für ein Out-of-Tree-Modul:

obj-m += gpio_irq_demo.o

KDIR ?= /lib/modules/$(shell uname -r)/build

default:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean

Lokal auf einem Kernel mit passenden Headern reicht make. Für die BeagleBone Black wird stattdessen gegen den Yocto-Kernel cross-kompiliert (siehe unten).

Character-Device-Interface registrieren

Ein Kernel-Modul allein tut noch nichts — es braucht ein Interface, über das User-Space damit interagieren kann. Für ein einfaches Gerät bietet sich ein Character Device an:

// gpio_irq_demo.c (Fortsetzung)
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>

#define DEVICE_NAME "gpio_irq_demo"

static dev_t dev_num;
static struct cdev my_cdev;
static int irq_count = 0;

static ssize_t demo_read(struct file *file, char __user *buf,
                          size_t len, loff_t *offset)
{
    char msg[16];
    int msg_len = snprintf(msg, sizeof(msg), "%d\n", irq_count);

    if (*offset > 0)
        return 0;
    if (copy_to_user(buf, msg, msg_len))
        return -EFAULT;
    *offset += msg_len;
    return msg_len;
}

static const struct file_operations fops = {
    .owner = THIS_MODULE,
    .read  = demo_read,
};

demo_read liefert die Anzahl bisher aufgetretener Interrupts an User-Space zurück — ein cat /dev/gpio_irq_demo zeigt so den aktuellen Zählerstand.

GPIO-Interrupt-Handling

Der eigentliche Mehrwert gegenüber User-Space-Polling: der Kernel wird nur aktiv, wenn tatsächlich ein Ereignis eintritt.

// gpio_irq_demo.c (Fortsetzung)
#include <linux/gpio.h>
#include <linux/interrupt.h>

#define GPIO_PIN 60   /* z. B. P9_12 auf der BeagleBone Black */

static int irq_number;

static irqreturn_t gpio_irq_handler(int irq, void *dev_id)
{
    irq_count++;
    pr_info("gpio_irq_demo: interrupt #%d on GPIO %d\n", irq_count, GPIO_PIN);
    return IRQ_HANDLED;
}

static int setup_gpio_irq(void)
{
    int ret;

    ret = gpio_request(GPIO_PIN, "gpio_irq_demo");
    if (ret)
        return ret;

    gpio_direction_input(GPIO_PIN);
    irq_number = gpio_to_irq(GPIO_PIN);

    ret = request_irq(irq_number, gpio_irq_handler,
                       IRQF_TRIGGER_RISING, "gpio_irq_demo", NULL);
    if (ret) {
        gpio_free(GPIO_PIN);
        return ret;
    }
    return 0;
}

gpio_irq_handler läuft im Interrupt-Kontext — keine blockierenden Aufrufe, kein Schlafen, keine langlaufende Verarbeitung. Für alles, was länger dauert als ein paar Instruktionen, gehört die eigentliche Arbeit in einen Bottom-Half (Tasklet, Workqueue oder Threaded IRQ), nicht direkt in den Handler.

Im Vergleich dazu sieht die entsprechende User-Space-Lösung über sysfs so aus — mit dem entscheidenden Unterschied, dass hier aktiv gepollt werden muss:

echo 60 > /sys/class/gpio/export
echo in > /sys/class/gpio/gpio60/direction
echo rising > /sys/class/gpio/gpio60/edge

# Polling-Schleife statt echtem Interrupt:
while true; do
  cat /sys/class/gpio/gpio60/value
  sleep 0.1
done

Cross-Compilation gegen den Yocto-Kernel

Anders als das lokale make gegen die Host-Kernel-Header braucht die BeagleBone Black die Kernel-Header und Toolchain aus dem Yocto-Build:

source /opt/poky/*/environment-setup-cortexa8hf-neon-poky-linux-gnueabi

make -C $KERNEL_SRC M=$(pwd) modules

$KERNEL_SRC zeigt dabei auf den konfigurierten Kernel-Quellbaum des Yocto-Builds (tmp/work-shared/beaglebone-yocto/kernel-source), nicht auf einen generischen ARM-Kernel — das Modul muss exakt gegen die Kernel-Version und -Konfiguration gebaut werden, die auf dem Zielsystem läuft, sonst schlägt das Laden mit einem Versions-Mismatch fehl.

Laden, Entladen, Diagnose

sudo insmod gpio_irq_demo.ko
dmesg | tail -5

cat /dev/gpio_irq_demo   # aktueller Interrupt-Zähler

sudo rmmod gpio_irq_demo
dmesg | tail -5

dmesg ist bei einem fehlschlagenden insmod die erste Anlaufstelle — typische Fehlerbilder sind ein Versions-Mismatch (siehe oben), ein bereits durch ein anderes Modul belegter GPIO-Pin (gpio_request schlägt fehl) oder ein fehlender vorheriger gpio_free bei einem zuvor abgestürzten Modul.

Zusammenfassung

AspektKernaussage

Wann ein Kernel-Modul nötig ist

Wenn Polling aus dem User-Space (sysfs) an CPU-Last oder Latenz scheitert

Interrupt-Handler

Kurz und nicht-blockierend halten — lange Arbeit in einen Bottom-Half auslagern

Cross-Compilation

Muss exakt gegen die Kernel-Version/-Konfiguration des Yocto-Builds erfolgen

Diagnose

dmesg zuerst — Versions-Mismatch und belegte GPIOs sind die häufigsten Fehlerbilder