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?
| Anforderung | User-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 |
| 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) cleanLokal 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;
}
|
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
doneCross-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
|
Zusammenfassung
| Aspekt | Kernaussage |
|---|---|
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 |
|