Kampis Elektroecke

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 Kernelsourcen:

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:

$ sudo apt-get update
$ sudo apt-get upgrade
$ sudo rpi-update

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

$ gcc --version | grep gcc

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

$ sudo apt-get install gcc-4.7 g++-4.7
$ sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.6 60 --slave /usr/bin/g++ g++ /usr/bin/g++-4.6
$ sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.7 40 --slave /usr/bin/g++ g++ /usr/bin/g++-4.7
$ sudo update-alternatives --config gcc

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

  Selection    Path              Priority   Status
------------------------------------------------------------
* 0            /usr/bin/gcc-4.6   60        auto mode
  1            /usr/bin/gcc-4.6   60        manual mode
  2            /usr/bin/gcc-4.7   40        manual mode

Press enter to keep the current choice[*], or type selection number:

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

$ sudo wget https://raw.githubusercontent.com/RPi-Distro/rpi-source/master/rpi-source 
$ sudo chmod +x /usr/bin/rpi-source 
$ /usr/bin/rpi-source -q --tag-update

Als letztes werden die Sources installiert:

$ rpi-source

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:

#include <linux/fs.h>
#include <linux/version.h>
#include <linux/module.h>
#include <linux/init.h>
#include <asm/uaccess.h>  
#include <linux/ioctl.h>
#include <asm/io.h>

(1)
MODULE_AUTHOR("Daniel Kampert");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Simple LED Treiber");
MODULE_SUPPORTED_DEVICE("Raspberry Pi");       

(2)
static int driver_open(struct inode* geraete_datei, struct file* instanz )
{
}

(3)
static int driver_close(struct inode* geraete_datei, struct file* instanz )
{
}

(4)
static ssize_t driver_read(struct file* instanz, char* User, size_t Count, loff_t* offset )
{
}

(5)
static ssize_t driver_write(struct file* Instanz, const char* Buffer, size_t Count, loff_t* offset)
{
}

(6)
static struct file_operations fops = {                     
    .owner= THIS_MODULE,
    .read= driver_read,
    .write= driver_write,
    .open= driver_open, 
    .release= driver_close,
};

(7)
static int __init Init(void)
{
    if(register_chrdev(DRIVER_MAJOR, NAME, &fops) == 0) 
    {
        return 0;
    }
    else
    {
        return -EIO;
    }
}

(8)
static void __exit Exit(void)
{
    unregister_chrdev(Major, Name);  
}

(9)
module_init(Init);
module_exit(Exit);

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) – Diese Struktur beinhaltet die Namen der einzelnen Funktionen innerhalb des Treibers
  • (7) – Diese Funktion wird aufgerufen wenn der Treiber im Betriebssystem angemeldet wird.
    Dies geschieht durch die Funktion insmod.
  • (8) – Wird ein Treiber durch die Funktion 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. 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

$ tail -f /var/log/syslog

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:

static int __init Init(void)
{
	if(register_chrdev(DRIVER_MAJOR, NAME, &fops) == 0x00) 
	{
		printk("Driver loaded...\n");
		return 0;
	}
	else
	{
		printk("Error during initialization!\n");
		return -EIO;
	}
}
}

static void __exit Exit(void)
{
    printk("Treiber wurde abgemeldet!\n");
    unregister_chrdev(DRIVER_MAJOR, NAME);  
}

Ü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:

Aug  8 20:01:47 MyRaspberry kernel: [170065.987600] Device loaded...

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:

static int driver_open(struct inode* geraete_datei, struct file* instanz )
{
    gpioRegs = (uint32_t *)ioremap(GPIO_Basis, 4096);	
    *(gpioRegs + 0x01) = (0x01 << 0x15);	
    printk("Open Device...\n");
	
    return 0;
}

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:

static int driver_close(struct inode* geraete_datei, struct file* instanz )
{
    printk("Close device...\n");

    return 0;
}

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:

static ssize_t driver_read(struct file *instanz, char* User, size_t Count, loff_t* offset)
{
    size_t BytesToCopy;
    size_t BytesNotCopied;
    uint32_t Status; 

    Status = *(gpioRegs + 0x0D);
    Status = Status & (0x01 << 0x11);
    Status = Status >> 0x11;

    printk("Read status: %i\n", Status);

    BytesToCopy = min((size_t)atomic_read(&BytesToRead), Count);
    BytesNotCopied = copy_to_user(User, &Status, BytesToCopy);
    *offset += BytesToCopy - BytesNotCopied;

    return BytesToCopy - BytesNotCopied;
}

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

Status = *(gpioRegs + 0x0D);
Status = Status & (0x01 << 0x11);
Status = Status >> 0x11;

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 wird über die Funktion copy_to_user(User, Wert, BytesToCopy) in den Userspace kopiert. Als Rückgabewert liefert die Funktion die Anzahl der Bytes, die nicht kopiert werden konnten. Für den dem Parameter User wird der Zeiger User aus den Übergabeparametern dem Aufruf der Funktion driver_read verwendet. Der Wert BytesToCopy ergibt sich aus der maximalen Anzahl an Bytes die gelesen werden dürfen (BytesToRead) und der Anzahl zu lesender Bytes (Count).

Die Funktion driver_read gibt anschließend einen Wert zurück, 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 Anwendung gegeben werden. Die entsprechende Lesefunktion der Application wertet diesen Rückgabewert dann aus.

Die letzte Funktion ist die write-Funktion:

static ssize_t driver_write(struct file* Instanz, const char* Buffer, size_t Count, loff_t* offsset)
{
    size_t BytesToCopy;
    size_t BytesNotCopied;

    char Input_Buffer[&BytesToWrite] = "";
	
    BytesToCopy = min((size_t)atomic_read(&BytesToWrite), Count);
    BytesNotCopied = copy_from_user(Input_Buffer, Buffer, BytesToCopy);
    *offset += BytesToCopy - BytesNotCopied;

    if(!(strcmp(Input_Buffer, "1")))
    {
        printk("Switch off\n");
	*(gpioRegs + 0x07) = (0x01 << 0x11);
    }
    else if(!(strcmp(Input_Buffer, "0")))
    {
	printk("Switch on\n");
        *(gpioRegs + 0x0A) = (0x01 << 0x11);
    }
    else
    {
        printk("Unknown command!\n");
    }
	
    return BytesToCopy - BytesNotCopied;
}

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

Der Rückgabewert der Funktion copy_from_user ist erneut die Anzahl der nicht kopierten Bytes und wie auch bei der Funktion driver_read wird die Anzahl der erfolgreich kopierten Bytes an den Userspace zurückgegeben.

Den Treiber testen:

Der Test des Treibers kann mittels Shellskript automatisiert werden. Das Skript führt die folgenden Schritte aus:

  • 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. Das Makefile zum Kompilieren des Treibers sieht folgendermaßen aus:

obj-m := Treiber.o

KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

all:
    $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules

Der Treiber wie ein normales C-Programm kompiliert, nur das mit der Zeile

obj-m := Treiber.o

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.

Der komplette Code ist bei GitHub verfügbar.

2 Kommentare

  1. Hello,
    please add a note that for a raspi 2 or 3 the value of BCM2708_PERI_BASE must be changed to 0x3F000000. It wasn´t that easy to find.
    Otherwise a very informative article!

    kind regards
    Bernd

Schreibe einen Kommentar

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