Skip to content
Zurück zum Blog
· 5 Min. Lesezeit

Linux-Treiber entwickeln: wann der Kernel nötig ist – und wann nicht

Bevor Sie einen Kernel-Treiber schreiben: Welche Geräte sich sauber aus dem Userspace bedienen lassen, wann ein Kernel-Treiber unvermeidlich wird, wie ein Platform-Treiber aufgebaut ist und welche Fehler beim Bring-up am meisten Zeit kosten.

Entscheidungsdiagramm zur Treiberentwicklung: Links die Frage, ob ein Gerät aus dem Userspace bedient werden kann, mit den Schnittstellen spidev, i2c-dev, libgpiod und IIO; rechts die Gründe für einen Kernel-Treiber – Interrupt-Latenz, DMA, Subsystem-Integration und Power Management – darunter der Aufbau eines Platform-Treibers von der of_match_table über probe bis zur devm-verwalteten Ressourcenfreigabe.

Kurz gesagt: Die teuerste Entscheidung in der Treiberentwicklung fällt, bevor die erste Zeile Code entsteht – nämlich die, ob es überhaupt einen Kernel-Treiber braucht. Ein erheblicher Teil der Geräte in Embedded-Projekten lässt sich sauber aus dem Userspace bedienen, mit deutlich weniger Aufwand über die Produktlebensdauer. Wenn der Kernel dann doch der richtige Ort ist, gibt es dafür klare Gründe: Latenz, DMA, gleichzeitiger Zugriff oder ein Subsystem, das eine standardisierte Schnittstelle liefert.

Die erste Frage: geht es aus dem Userspace?

Linux stellt für die gängigen Busse generische Schnittstellen bereit, mit denen eine Anwendung Hardware direkt anspricht:

  • spidev öffnet einen SPI-Chipselect als Gerätedatei. Transfers laufen über ioctl, inklusive Taktrate und Wortbreite.
  • i2c-dev macht dasselbe für I2C-Busse; Lesen und Schreiben geht über ioctl(I2C_RDWR) oder die SMBus-Hilfsfunktionen.
  • libgpiod bedient GPIOs über das Character-Device-Interface. Die alte sysfs-Schnittstelle unter /sys/class/gpio ist abgekündigt und sollte in neuen Projekten nicht mehr auftauchen.
  • IIO deckt Sensoren ab, für die es bereits einen Kernel-Treiber gibt – Werte kommen dann über sysfs oder einen gepufferten Character-Device-Kanal an, ohne dass Sie selbst etwas schreiben.

Der Vorteil liegt nicht nur im geringeren Aufwand. Userspace-Code lässt sich ohne Kernel-Neustart testen, stürzt im Zweifel nur selbst ab statt das System mitzunehmen, und er überlebt einen Kernel-Versionssprung unverändert. Für einen Temperatursensor, der einmal pro Sekunde gelesen wird, ist das die richtige Antwort.

Wann der Kernel unvermeidlich wird

Vier Gründe tragen die Entscheidung für einen Kernel-Treiber:

Interrupt-Latenz. Muss auf ein Ereignis in wenigen Mikrosekunden reagiert werden, führt kein Weg am Kernel vorbei. Der Umweg über einen Userspace-Prozess kostet Kontextwechsel und unterliegt dem Scheduler – auch auf einem System mit PREEMPT_RT bleibt eine Handlerroutine im Kernel die kürzere Strecke.

DMA. Sobald größere Datenmengen ohne CPU-Beteiligung bewegt werden sollen, braucht es kohärente Puffer, korrektes Cache-Handling und eine Verankerung im DMA-Framework. Das ist Kernel-Territorium.

Gleichzeitiger Zugriff. Wenn mehrere Prozesse dasselbe Gerät benutzen, muss jemand serialisieren. Ein Kernel-Treiber tut das an genau einer Stelle; im Userspace endet es in einem selbstgebauten Sperrprotokoll, an das sich alle Beteiligten halten müssen.

Subsystem-Integration. Ein Gerät, das als Netzwerkschnittstelle, Eingabegerät, Video- oder Sensorquelle auftreten soll, muss die Sprache des jeweiligen Subsystems sprechen. Der Gewinn ist beträchtlich: Ein Sensor im IIO-Framework funktioniert mit vorhandenen Werkzeugen, ohne dass jemand Ihre proprietäre Schnittstelle lernen muss.

Das passende Subsystem wählen

Ein eigener Character-Treiber mit selbst erfundenen ioctl-Nummern ist fast immer die schlechtere Wahl. Für die meisten Geräteklassen existiert ein Subsystem, das die halbe Arbeit abnimmt und dafür eine Schnittstelle vorgibt, die andere schon kennen: IIO für Sensoren und Wandler, input für Tasten, Encoder und Touch, V4L2 für Bildquellen, hwmon für Temperatur und Spannungsüberwachung, PWM, RTC, watchdog für die jeweiligen Klassiker.

Der Preis ist, sich in die Konventionen des Subsystems einzulesen. Der Ertrag ist eine Schnittstelle, die zu Standardwerkzeugen passt, dokumentiert ist und beim nächsten Entwicklerwechsel nicht neu erklärt werden muss.

Wie ein Platform-Treiber aufgebaut ist

Geräte, die fest auf der Platine sitzen und sich nicht selbst anmelden, werden über den Device Tree beschrieben und über einen platform_driver bedient. Das Grundgerüst ist überschaubar:

static int my_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct my_priv *priv;

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

    priv->base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(priv->base))
        return PTR_ERR(priv->base);

    priv->clk = devm_clk_get_enabled(dev, NULL);
    if (IS_ERR(priv->clk))
        return dev_err_probe(dev, PTR_ERR(priv->clk), "clock missing\n");

    platform_set_drvdata(pdev, priv);
    return 0;
}

static const struct of_device_id my_of_match[] = {
    { .compatible = "acme,widget-1" },
    { }
};
MODULE_DEVICE_TABLE(of, my_of_match);

static struct platform_driver my_driver = {
    .probe = my_probe,
    .driver = {
        .name = "acme-widget",
        .of_match_table = my_of_match,
    },
};
module_platform_driver(my_driver);

Drei Dinge daran sind wichtiger, als sie aussehen. Die of_match_table verbindet den Treiber mit dem compatible-String im Device Tree – ohne exakte Übereinstimmung passiert schlicht nichts. Die devm_-Varianten binden Ressourcen an die Lebensdauer des Geräts, sodass Aufräumpfade entfallen, die erfahrungsgemäß genau dann falsch sind, wenn sie gebraucht werden. Und dev_err_probe behandelt den häufigen Fall, dass eine Ressource noch nicht bereitsteht: Es protokolliert nur bei echten Fehlern und hält bei -EPROBE_DEFER die Logs sauber, während der Kernel es später erneut versucht.

Fehler, die im Bring-up am meisten Zeit kosten

Blockierende Aufrufe im Interrupt-Kontext. Ein Handler darf nicht schlafen. I2C-Transfers, Speicheranforderungen mit GFP_KERNEL oder Mutexe gehören in den Thread-Anteil – devm_request_threaded_irq mit einem kurzen Hard-Handler und einem Thread für den Rest ist das übliche Muster.

Fehlende Ressourcen im Device Tree. Ein Treiber, der nicht probed, ist selten ein Treiberproblem. Meist fehlen Clock, Regulator oder pinctrl-Eintrag, oder der Knoten steht noch auf disabled.

-EPROBE_DEFER als Fehler behandeln. Wenn ein Treiber vor seiner Abhängigkeit geladen wird, ist das kein Defekt, sondern der Normalfall. Wer diesen Rückgabewert wie einen echten Fehler behandelt, bekommt sporadisch nicht startende Geräte, deren Verhalten sich mit jeder Änderung an der Boot-Reihenfolge verschiebt.

Der Pflegeaufwand out-of-tree. Interne Kernel-APIs ändern sich ohne Rücksicht auf externe Module. Ein Treiber, der beim Kernel-Update nicht mehr baut, ist erwartbar – die Frage ist nur, ob dieser Aufwand eingeplant wurde. Wer den Treiber in seinem Yocto-Layer sauber versioniert und beim Versionssprung mitzieht, trifft das kalkuliert; wer ihn als Patch im Kernel-Baum hinterlegt, findet ihn beim nächsten Sprung nicht wieder.

Wie wir vorgehen

In Projekten beginnt die Arbeit fast immer mit derselben Bestandsaufnahme: Welche Bausteine hängen an der Platine, welche davon bedient der Kernel bereits, und welche brauchen tatsächlich eigenen Code? Häufig bleibt danach weniger übrig als erwartet – ein Sensor, für den es ein IIO-Modul gibt, und ein FPGA-Interface, das ohne DMA nicht auskommt.

Diese Trennung ist die eigentliche Ingenieursleistung. Sie entscheidet darüber, wie viel Code Sie über die nächsten Jahre durch Kernel-Versionen tragen müssen. Wenn Sie vor dieser Frage stehen, sehen wir uns das gern gemeinsam an – im Rahmen unserer Embedded-Linux-Entwicklung oder als Schulung für Ihr Team, in der genau diese Abwägung an Ihrer Hardware geübt wird.

Häufige Fragen

Braucht jedes eigene Gerät einen Kernel-Treiber?
Nein, und das ist die wichtigste Frage vor der ersten Zeile Code. Ein SPI- oder I2C-Baustein, den Ihre Anwendung nur gelegentlich liest, lässt sich über `spidev` beziehungsweise `i2c-dev` direkt aus dem Userspace bedienen; GPIOs laufen über libgpiod. Ein Kernel-Treiber lohnt sich, wenn Interrupts mit harten Latenzen verarbeitet werden müssen, DMA im Spiel ist, mehrere Prozesse gleichzeitig zugreifen oder das Gerät in ein Subsystem gehört, das eine standardisierte Schnittstelle nach oben bietet.
Was bedeutet die devm-Präfix bei Funktionen wie devm_kzalloc?
Diese Varianten binden eine Ressource an die Lebensdauer des Geräts: Wird der Treiber entladen oder schlägt probe fehl, gibt der Kernel sie automatisch frei. Das eliminiert die häufigste Fehlerquelle in Treibern – vergessene oder in der falschen Reihenfolge abgewickelte Aufräumpfade. Wo es eine devm-Variante gibt, sollte sie benutzt werden.
Out-of-tree als Modul oder upstream in den Kernel?
Ein Out-of-tree-Modul ist schnell gebaut, muss aber bei jedem Kernel-Update nachgezogen werden, weil interne APIs keine Stabilitätsgarantie haben – über die Laufzeit eines Produkts summiert sich das. Upstream aufgenommener Code wird bei API-Änderungen von denen mit angepasst, die sie vornehmen. Für Bausteine, die nur bei Ihnen verbaut sind, ist out-of-tree pragmatisch; für alles, was auch andere einsetzen könnten, zahlt sich der Weg upstream über die Produktlebensdauer aus.
Wie debuggt man einen Treiber, der nicht probed?
Zuerst `dmesg` nach dem Namen des Treibers durchsehen. Passiert dort gar nichts, wurde probe nie aufgerufen – dann stimmt das Matching nicht: `compatible`-String im Device Tree gegen die `of_match_table` halten und prüfen, ob der Knoten `status = "okay"` hat. Bricht probe mit einem Fehler ab, fehlt meist eine Ressource: Clock, Regulator, Interrupt oder pinctrl-Eintrag. Mit `dynamic_debug` lassen sich einzelne Dateien zur Laufzeit gesprächig schalten, ohne den Kernel neu zu bauen.

Alexander Nassian

Geschäftsführer, bitshift dynamics

Entwickelt mit seinem Team hardwarenahe Software für Embedded-Produkte – C++, Qt/QML, Embedded Linux und das Yocto Project. bitshift dynamics arbeitet seit 2005 in diesem Feld.

Verwandte Beiträge

· 6 Min. Lesezeit

Device Tree in der Praxis: Hardware beschreiben, statt Treiber zu raten

Wie der Device Tree in Embedded-Linux-Projekten wirklich funktioniert: compatible-Matching, pinctrl und Clocks, Overlays für Gerätevarianten und die Debug-Rezepte, mit denen sich Bring-up-Probleme eingrenzen lassen.

  • Embedded Linux
  • Device Tree
  • Kernel
Weiterlesen
· 6 Min. Lesezeit

Echtzeit mit Linux: was PREEMPT_RT leistet – und was nicht

Determinismus statt Tempo: wie PREEMPT_RT Linux echtzeitfähig macht, wie Sie Latenzen mit cyclictest belastbar messen und welche System- und Anwendungsfehler jede Konfiguration wieder zunichtemachen.

  • Echtzeit
  • PREEMPT_RT
  • Embedded Linux
Weiterlesen
· 4 Min. Lesezeit

systemd auf Embedded-Systemen: was sich lohnt und was schadet

systemd im Embedded-Einsatz: Units und Abhängigkeiten richtig schneiden, Boot-Zeit statt Reihenfolge optimieren, Dienste per Watchdog überwachen, das Journal vom Flash fernhalten und mit read-only Root-Dateisystem arbeiten.

  • Embedded Linux
  • systemd
  • Boot-Zeit
Weiterlesen

Steht bei Ihnen eine ähnliche Aufgabe an?

Wir begleiten Embedded-Teams genau bei solchen Fragestellungen – von der Architekturentscheidung bis zur Serienreife. Schildern Sie uns kurz Ihr Vorhaben.

Projekt anfragen