Ein einfacher I/O Treiber

Raspberry Pi

In diesem Artikel zeige ich euch wie ihr euch einen einfachen GPIO-Treiber programmieren könnt.
Dieser Treiber wird anschließend unter /dev eingebunden und kann dann mit einem einfachen kleinen Programm geöffnet werden. Durch das Schreiben einer 0 oder einer 1 kann dann ein Pin geschaltet werden.

-> Installieren der Kernelsources:

Sobald man Software kompilieren will die im Kernelspace, also direkt im Betriebssystemkern, läuft werden zusätzliche Dateien benötigt, welche sich in den Kernelsources befinden.
Um diese Dateien zu installieren wird als als erstes wird die aktualisiert:

Im nächsten Schritt muss die gcc Version angepasst werden:

Bei einer Version unter 4.7.2 sind die nachfolgenden Schritte auszuführen:

Jetzt öffnet sich ein Fenster und es kann die neueste Version ausgewählt werden:

Mit der Taste 2 wählt ihr anschließend die neueste Version aus. Danach können die Kernelsources runtergeladen werden:

Als letztes werden die Sources installiert:

Jetzt, da die Kernelsources installiert sind, kann mit der Programmierung des Treibers begonnen werden.

Achtung: Bei der Treiberprogrammierung arbeitet ihr direkt im Betriebssystemkern! Ihr solltet daher immer genau gucken was ihr tut, da ihr sonst ggf. einen Systemabsturz erzeugen könnt!

-> Die Entwicklung des Treibers:

In diesem Artikel werden keine Grundlagen über den Aufbau von Treibern und die Zusammenarbeit zwischen einem Treiber und einem Betriebssystem erklärt.
Wer dennoch etwas mehr dadrüber wissen möchte, kann dieses PDF lesen.
Als erstes schauen wir uns mal das Grundgerüst eines einfachen Treibers an:

Das sind ja ganz schön viele Funktionen…aber was machen die Funktionen den nun alles?
Gehen wir die Funktionen mal der Reihe nach durch:

  • (1) – Dies sind Informationen über den Programmierer des Treibers. Diese Informationen können, bis auf die Lizenz, auch weg gelassen werden.
  • (2) – Diese Funktion definiert wie sich der Treiber bei den Systemcall open verhalten soll.
  • (3) – Diese Funktion definiert wie sich der Treiber bei den Systemcall close verhalten soll.
  • (4) – Diese Funktion definiert wie sich der Treiber bei den Systemcall read verhalten soll.
  • (5) – Diese Funktion definiert wie sich der Treiber bei den Systemcall write verhalten soll.
  • (6) – Dieses Struct beinhaltet die Namen der einzelnen Funktionen innerhalb des Treibers
  • (7) – Diese Funktion wird aufgerufen wenn der Treiber im Betriebssystem angemeldet wird.
    Dies geschieht durch den Befehl insmod.
  • (8) – Wird ein Treiber durch den Befehl rmmod vom Betriebssystem abgemeldet, so wird diese Funktion aufgerufen.
  • (9) – Diese beiden Zeilen stellen ein Verweis auf die Funktionen zum Initialisieren und Abmelden des Treiber dar.

Dieses Gerüst ist nur ein Grundgerüst. Natürlich kann es noch erweitert werden, wodurch komplexere Treiber möglich sind.
Für unsere Zwecke reicht dieses Gerüst aber erst einmal aus.

-> Ein kurzer Einschub – Was ist ein Systemcall?:

Im vorherigen Abschnitt habe ich ja an mehreren Stellen den Begriff Systemcall verwendet. Was genau ist eigentlich ein Systemcall?
Wie ihr ja bereits wisst, ist der Kernel das Herzstück eines jeden Betriebssystems. Dieser Kernel stellt grundlegende Funktionen bereit, die jede andere Software verwenden darf.
Diese Funktionen werden Systemcall genannt. Eine Application, wie z.B. ein ausführbares Programm oder ein Treiber, können diese Systemcalls benutzen um mit dem Kernel zu kommunizieren.
Durch z.B. den Systemcall open teilt eine Anwendung dem Kernel mit, dass sie auf eine Datei zugreifen will.
Der Kernel erledigt nun alle notwendigen Schritte damit die Application diese Datei öffnen darf.

Eine Application greift also nicht direkt auf das Betriebssystem zu, sondern immer über vom Kernel bereit gestellte Funktionen die man Systemcall nennt.

Der aktuelle Linux Kernel verfügt über 190 Systemcalls, welche man sich hier alle genau anschauen kann.

-> Der I/O-Treiber:

Das oben vorgestellte Grundgerüst verwenden wir nun um den I/O-Treiber zu programmieren. Die Vorgehensweise ist ähnlich wie im C-Programm für einen direkten Speicherzugriff:

-> Einfacher I/O-Zugriff

Da wir jetzt aber im Kernelspace, also auf Kernelebene, programmieren sind können einige der „Standard“ C-Funktionen, wie man sie kennt, nicht verwendet werden.

Ein Beispiel ist die Funktion printf(). Mit dieser Funktion können Strings in der Konsole ausgegeben werden. Diese Funktion macht im Grunde nichts anderes als das entsprechende Gerät für die Konsole zu öffnen (über einen open-Systemcall) und den Text dort rein zu schreiben (per write-Systemcall). Der Treiber für die Konsole gibt den Text nun in der Konsole aus (einfach ausgedrückt).
Im Kernelspace geht dies aber nicht. Der Kernel verfügt nur über die Möglichkeit mittels der Funktion printk() in das Systemlog unter /var/log/syslog zu schreiben.

Das Beispiel zeigt, dass Debuggen über die Konsole im Kernelspace über printk() ein bisschen schwieriger ist als im Userspace mittels printf().
Ich debugge immer so, dass ich eine zweite PuTTY-Session öffne und mir über

die letzten Zeilen im Systemlog ausgeben lasse. Diese Methode müllt das Systemlog zwar zu, aber für mich persönlich ist das Systemlog bei der Programmierung eh nicht ganz so wichtig.
Der erste Schritt bei dem Treiber sind die Funktionen für das Ab-und Anmelden an das Betriebssystem.
Da ich gerne wissen möchte ob die Anmeldung erfolgreich war, bzw. ob der Treiber abgemeldet wurde, füge ich in beiden Funktionen eine printk()-Funktion ein:

Über die If-Abfrage in der Init()-Funktion überprüfe ich zudem noch ob die Anmeldung erfolgreich war.
Diese beiden Funktionen sorgen nun dafür, dass jedesmal, wenn der Treiber eingebunden oder entfernt wird, ein entsprechender Eintrag im Systemlog erscheint:

Im nächsten Schritt definieren wir, wie sich der Treiber beim Öffnen verhalten soll.
Da der Treiber am Ende einen GPIO schalten soll wäre es sinnvoll, wenn in der open()-Funktion schon mal alle Vorkehrungen für den Zugriff auf die Register getroffen werden:

Die Funktion mmap() ist leider nur im Userspace verfügbar. Möchte man über den Kernelspace auf den Speicher zugreifen, so muss dies über die Funktion ioremap(Registeradresse, Bereich) gemacht werden.
Die Funktionsweise beider Funktionen ist identisch. Beide Funktionen geben einen Zeiger auf die Startadresse zurück, der für die direkte Adressierung der Register verwendet werden kann.
Im nächsten Schritt wird der GPIO 17 als Ausgang deklariert und da ich gerne jeden Schritt bestätigt haben will, lasse ich mir als letztes noch eine Ausgabe ins Systemlog schreiben.
Jetzt kann der Treiber bereits durch eine Anwendung im Userspace geöffnet werden. Allerdings kann die Anwendung bisher noch nicht vom Treiber lesen bzw. in ihn schreiben.
Danach programmiere ich noch das Verhalten wenn der Treiber geschlossen wird:

Hier wird einfach nur eine Ausgabe in das Systemlog geschrieben. Schauen wir uns im nächsten Schritt einen Lesezugriff auf den Treiber von einer Application im Userspace an.
Sobald der Treiber ausgelesen wird, wird folgende Funktion aufgerufen:

In dieser Funktion wird als erstes der Status des GPIO 17 eingelesen und herausgefiltert:

Im nächsten Schritt wird der aktuelle Status des GPIO in das Systemlog geschrieben.
Jetzt liegt der Status des GPIO als Wert in einer Variable vor, aber die Anwendung, welche den Treiber ausließt, hat keinen direkten Zugriff auf diesen Wert. Dieser Wert muss erst mit der Funktion copy_to_user(User, Wert, Bytes) in den Userspace kopiert werden. Der Rückgabewert dieser Funktion entspricht der Anzahl erfolgreich kopierter Bytes.
Als Wert für User wird dabei der Zeiger User aus den Übergabeparametern der Funktion driver_read() verwendet und für Bytes wird der Wert aus dem Übergabeparameter Count verwendet.
Diese Variable enthält nämlich die Anzahl der Bytes die eine Application lesen möchte und genau so viele muss der Treiber dann auch in den Userspace kopieren.
Die Funktion driver_read() muss anschließend einen Wert zurückgeben, der der Anzahl der gelesenen Bytes entspricht.
Mit Hilfe des Rückgabewertes der Funktion copy_to_user() kann man vergleichen ob die Anzahl Bytes die gelesen werden soll gleich der Anzahl tatsächlich gelesener Bytes ist. Dieser Wert kann dann über return zurück an die Application gegeben werden. Die entsprechende Lesefunktion der Application wertet diesen Rückgabewert dann aus.
Die letzte Funktion ist die write()-Funktion:

Die Funktion ist der read()-Funktion von der groben Funktionsweise ziemlich ähnlich.
Als erstes müssen die übergebenen Daten aus dem Userspace in den Kernelspace kopiert werden.
Dies geschieht mit der Funktion copy_from_user(Kernelspace, Userspace, Bytes).
Die Variable Input_Buffer ist dabei der Speicherort für die Daten innerhalb des Treibers und Buffer beinhaltet die Daten aus dem Userspace.
Mit der darauf folgenden If-Abfrage wird der übergebene String nun verglichen:

  • String = 1 -> I/O wird eingeschaltet
  • String = 0 -> I/O wird abgeschaltet
  • Rest -> Fehlermeldung

Und auch diese Funktion erwartet als Rückgabewert die Anzahl erfolgreich verarbeiteter Bytes. Da ich auch hier auf eine Fehlerauswertung verzichte, gebe ich auch hier die Anzahl der Bytes die meine Userspace Application lesen möchte als Anzahl gelesener Bytes zurück.

-> Der Treiber wird getestet:

Für den Test des Treibers habe ich mir ein kleines Shellskript geschrieben, welches folgende Schritte automatisch ausführt:

  • Treiber mittels Makefile kompilieren
  • Treiber beim Betriebssystem anmelden
  • Eine Gerätedatei für den Treiber anlegen
  • Ein Testprogramm für einen Zugriff auf den Treiber kompiliert
  • Das Testprogramm startet
  • Den Treiber vom Betriebssystem abmeldet
  • Die Gerätedatei wieder entfernt

Wie man sieht, sind eine ganze Menge Schritte notwendig um den Treiber zu testen. Es ist also durchaus sinnvoll diese Schritte automatisiert durchlaufen zu lassen.
Das Makefile zum kompilieren des Treibers sieht so aus:

Im Grunde wird der Treiber wie ein normales C-Programm kompiliert, nur das mit der Zeile

dem C-Compiler mitgeteilt wird, dass der die gleichnamige C-Datei (also Treiber.c) als Modul kompilieren soll. In Folge dessen werden einige weitere Dateien erzeugt, die für den Treiber benötigt werden.
Das Makefile wird einfach über den Befehl make aufgerufen.
Nach dem Kompilieren wird das kompilierte Treibermodul Treiber.ko mit dem Befehl insmod im Betriebssystem angemeldet.
Jetzt kann für den Treiber eine Gerätedatei angelegt werden. Dies geschieht mit dem Befehl mknod. Dieser Befehl erwartet folgende Parameter:

  • Pfad für die Gerätedatei
  • Eine Kennzeichnung des Treibertyps. Hier ist es c für ein Character Device, sprich ein Gerät, welches Byteweise beschrieben wird.
  • Eine Majoritäts– und Minoritätsnummer über die der dazu gehörige Treiber identifiziert wird. Die Marjoritätsnummer unseres Treiber ist 240. Gebt ihr eine andere Nummer ein, wird die Gerätedatei zwar angelegt, allerdings könnt ihr den Treiber dann nicht öffnen, da die Gerätedatei mit einem Treiber gepaart ist der die selbe Majoritätsnummer besitzt wie die Gerätedatei.

In den nächsten beiden Zeilen wird ein Testprogramm für den Treiber kompiliert und ausgeführt.
Dieses C-Programm macht nichts anderes als die Gerätedatei zu öffnen und verschiedene Werte in die Gerätedatei zu schreiben und diese auszulesen.
Ein kompletter Durchlauf des Testskriptes sieht dann so aus:

Einfacher_IO-Treiber(1)
Die komplette Ausführung wird zudem noch im Systemlog dokumentiert:

Einfacher_IO-Treiber
Damit wäre euer erster eigener Treiber fertig programmiert und einsatzbereit.

 

Dokumentation:

 

-> Zurück zu Low Level C Programmierung

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Time limit is exhausted. Please reload CAPTCHA.