Kampis Elektroecke

Erstellen eines eigenen Treibers für den Raspberry Pi

Ich lese mich zur Zeit etwas intensiver in die Treiberentwicklung unter Linux ein und der Raspberry Pi ist zum Entwickeln von Treibern ein idealer Kandidat. Da die Codeentwicklung auf dem Raspberry Pi aber auf Grund der geringen Rechenleistung und der SD-Karte als Speicher alles andere als ideal ist, empfiehlt es sich hier, den Code auf einer anderen Maschine zu entwickeln und dann mit einem Cross Compiler für den Raspberry Pi zu kompilieren. 

Beschaffen der Kernel-Sourcen:

Für den Kompilierprozess des Kernel-Treibers sind die Kernel-Header des verwendeten Kernels wichtig. Linux-Treiber lassen sich nur laden, wenn die Versiosnummer der Kernel-Header mit der Version des verwendeten Kernels komplett übereinstimmt. Es muss also erst einmal die Kernelversion auf dem Raspberry Pi bestimmt werden:

$ root@Raspberry:/home/pi/Desktop# uname -r
4.14.34-v7+

Anschließend können die Kernelsourcen auf den Host (bei mir eine x86/x64 Maschine mit einem Ubuntu Betriebssystem) geladen werden:

$ cd ~
$ git clone https://GitHub.com/raspberrypi/linux

Nun beginnt die Suche nach den Sourcen für die verwendete Version im offiziellen Repository. Dazu sucht man unter Branches die Version 4.14 und wählt diese aus. Ein guter Anhaltspunkt für die aktuelle Versionsnummer ist das Makefile im obersten Verzeichnisses des Repository:

Der letzte Sublevel der Version 4.14 ist demnach 39 gewesen. Den Sublevel 34 findet man über die Releases, direkt neben den Branches.

Durch öffnen der Versionen und überprüfen der Makefiles lässt sich der Commit für den Sublevel 34 herausfinden. In diesem Fall ist der Sublevel 39 der direkte Nachfolgelevel zu 34 und somit ist der erste Commit direkt der richtige. Von diesem Commit wird nun die ID benötigt. Diese lässt sich durch Öffnen des Commits herausfinden:

Der benötigte Commit trägt in diesem Fall die ID

f70eae405b5d75f7c41ea300b9f790656f99a203

Mit Hilfe dieser ID können die Linux-Sourcen auf den gewünschten Stand gebracht werden.

$ cd linux
$ git checkout f70eae405b5d75f7c41ea300b9f790656f99a203

Im Makefile des Repositorys kann die korrekte Version überprüft werden:

# SPDX-License-Identifier: GPL-2.0
VERSION = 4
PATCHLEVEL = 14
SUBLEVEL = 34
EXTRAVERSION =

Der benötigte Cross-Compiler:

Für die Erstellung des Moduls wird noch ein Cross-Compiler benötigt. Dieser wird, passend zu den Kernel-Sourcen, ebenfalls bei Git angeboten.

$ cd ~
$ git clone https://GitHub.com/raspberrypi/tools.git

Der Cross-Compiler ist anschließend unter tools/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian-x64 zu finden. Damit der Compiler und die Kernel-Sourcen im weiteren Verlauf eleganter verwendet werden können, trage ich den Pfad des Cross-Compilers und der Kernel-Sourcen in die .bashrc ein. Anschließend wird die geänderte .bashrc neu geladen um die Pfade in der aktuelle Terminalsession nutzbar zu haben:

$ echo export Raspberry=~/tools/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian-x64/bin >> ~/.bashrc
$ echo export Raspberry_Kernel=~/linux >> ~/.bashrc 
$ source ~/.bashrc

Ein neuer Kernel wird benötigt…:

Aus einem (mir noch unbekannten Grund) hat der vorhandene Kernel den erzeugten Treiber trotz der korrekten Version permanent abgelehnt. Als „Ausweg“ habe ich den Kernel neu erstellt und auf mein Raspberry Pi kopiert. Danach ließ sich der Treiber ohne Probleme laden.

Um den Kernel für einen Raspberry Pi 3B+ zu kompilieren, wird in das Verzeichnis linux gewechselt und die Defaultkonfiguration für den Raspberry Pi erzeugt:

$ cd linux
$ KERNEL=kernel7
$ make ARCH=arm CROSS_COMPILE=${Raspberry}- bcm2709_defconfig

Danach kann der Kernel kompiliert werden:

$ make ARCH=arm CROSS_COMPILE=${Raspberry}- zImage modules dtbs

Dieser Vorgang dauert, je nach verwendeter Maschine, gut 15 Minuten oder länger. Am Ende purzelt das Kernelimage raus, welches in dem Verzeichnis arch/arm/boot zu finden ist. Benötigt wird in diesem Fall die Datei zImage, die nun in das /boot-Verzeichnis des Raspberry Pi kopiert werden kann:

$ scp arch/arm/boot/zImage root@<IP>:/boot

Auf dem Raspberry Pi wird der aktuelle Kernel entfernt (oder umbenannt, je nachdem ob man diesen noch behalten möchte):

$ sudo rm /boot/kernel.img
$ sudo cp /boot/zImage /boot/kernel.img
$ sudo rm /boot/zImage

Zuletzt muss der Raspberry Pi noch neu gestartet werden, damit der Kernel geladen wird:

$ sudo reboot

Erstellen des Treibers:

Nach dem Reboot ist der neue Kernel einsatzbereit und alle Vorbereitungen zur Erstellung eines Treibers für den Raspberry Pi sind abgeschlossen. Da es sich bei meiner Host-Maschine um ein Ubuntu-System handelt, erstelle ich den Treiber mit Visual Studio Code. 

Für die Erstellung eines solchen Projektes mit Visual Studio Code wird die C/C++ Erweiterung benötigt. Wenn die Erweiterung installiert ist, wird ein neuer Projektordner angelegt und dem Arbeitsplatz hinzugefügt. Über die Tastenkombination Strg + Shift + P wird das Menü Edit Configurations… geöffnet, um die Einstellungen des Compilers anzupassen:

Es wird nun eine Datei namens c_cpp_properties.json angelegt und geöffnet. Sämtliche Compilereinstellungen, wie z. B. der zu verwendende Compiler, die Include-Pfade oder die Intellisensepfade werden hier eingetragen. Da es sich bei diesem Projekt nur um ein Makefile-Projekt handeln wird, werden nur die Include- und die Intellisensepfade angepasst:

{
    "configurations": [
        {
            "name": "Raspberry Pi",
            "targetArchitecture": "arm",
            "intelliSenseMode": "clang-x64",
            "browse": {
                "path": [
                    "${workspaceFolder}",
                    "${Raspberry_Kernel}/include",
                    "${Raspberry_Kernel}arch/arm/include"
                ],
                "limitSymbolsToIncludedHeaders": true
            },
            "includePath": [
                "${workspaceFolder}",
                "${Raspberry_Kernel}/include",
                "${Raspberry_Kernel}/arch/arm/include"
            ]
        }
    ],
    "version": 4
}

Im nächsten Schritt wird über Tastenkombination Strg + Shift + P wird das Menü Configure Task aufgerufen. Jetzt wird die task.json angelegt und geöffnet. Diese Datei verwaltet die Aufgaben, die durchgeführt werden sollen:

{
    // See https://go.microsoft.com/fwlink/?LinkId=733558
    // for the documentation about the tasks.json format
    "version": "2.0.0",
    "tasks": [
        {
            "label": "Build",
            "type": "shell",
            "command": "make"
        },
        {
            "label": "Clean",
            "type": "shell",
            "command": "make clean"
        }
    ]
}

Hier wurden zwei Aufgaben definiert

  • Build – Führt das Makefile im Projektverzeichnis mit der Option all aus
  • Clean – Führt das Makefile im Projektverzeichnis mit der Option clean aus

Über Datei Einstellungen Tastenkombinationen lassen sich die beiden Label Build und Clean auf beliebige Tasten legen, sodass diese ganz einfach ausgeführt werden können. Dazu einfach in dem o. g. Menü die keybindings.json öffnen:

Diese Datei wird dann entsprechend angepasst:

// Place your key bindings in this file to overwrite the defaults
[
    {
        "key": "f6",
        "command": "workbench.action.tasks.runTask",
        "args": "Build"
    },
    {
        "key": "f7",
        "command": "workbench.action.tasks.runTask",
        "args": "Clean"
    }
]

Hier habe ich beispielhaft die Taste F6 mit dem Task Build und die Taste F7 mit dem Task Clean belegt.

Nun muss noch das Makefile erstellt werden. Dafür kann dieses Template genommen werden:

obj-m += SimpleDriver.o

PWD  := $(shell pwd)
KDIR := ${Raspberry_Kernel}

all:
	make ARCH=arm CROSS_COMPILE=$(Raspberry)- -C $(KDIR) M=$(PWD) modules

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

Das Template kann für jeden beliebigen Raspberry Pi Treiber verwendet werden. Alle wichtigen Pfade sind über Umgebungsvariablen abgedeckt. Einzig der rot markierte Abschnitt muss den selben Namen wie das verwendete Sourcefile sein.

Nun kann der Code für den Treiber erstellt werden. Hierzu wird eine neue Datei namens SimpleDriver.c angelegt und mit dem folgenden Inhalt gefüllt:

#include <linux/fs.h>
#include <linux/cdev.h>
#include <asm/uaccess.h>
#include <linux/device.h>
#include <linux/module.h>
#include <linux/uaccess.h>

#define DEVICENAME 			"Hello"
#define MESSAGE				"Hello World!\n"
#define HELLO_MINOR			0

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Daniel Kampert");
MODULE_DESCRIPTION("'Hello World' virtual device");

static dev_t HelloDevNumber;
static struct cdev* DriverObject;
static struct class* HelloClass;
static struct device* HelloDev;
static int MessageCount = 0;

static ssize_t DriverRead(struct file* instanz, char __user* user, size_t count, loff_t* offset);
static int DriverOpen(struct inode* geraete_datei, struct file* instanz);
static int DriverClose(struct inode* geraete_datei, struct file* instanz);

static struct file_operations FOPS = {
	.owner = THIS_MODULE,
	.read = DriverRead,
	.open = DriverOpen, 
	.release = DriverClose,
};

static int DriverOpen(struct inode* geraete_datei, struct file* instanz)
{
	dev_info(HelloDev, "'DriverOpen' called...\n");
	MessageCount = 0;
	return 0;
}

static int DriverClose(struct inode* geraete_datei, struct file* instanz)
{
	dev_info(HelloDev, "'DriverClose' called...\n");
	return 0;
}

static ssize_t DriverRead(struct file* instanz, char __user* user, size_t count, loff_t* offset)
{
	unsigned long not_copied, to_copy;

	if(MessageCount == 0)
	{
		to_copy = min(count, strlen(MESSAGE) + 1);
		not_copied = copy_to_user(user, MESSAGE, to_copy);
		*offset += to_copy - not_copied;
		MessageCount++;

		return to_copy - not_copied;
	}

	return 0;
}

static int __init Module_Init(void)
{
	if (alloc_chrdev_region(&HelloDevNumber, HELLO_MINOR, 1, DEVICENAME) < 0)
	{
		return -EIO;
	}

	DriverObject = cdev_alloc();
	if(DriverObject == NULL)
	{
		goto Jump_Free_DeviceNumber;
	}

	DriverObject->owner = THIS_MODULE;
	DriverObject->ops = &FOPS;

	if (cdev_add(DriverObject, HelloDevNumber, 1))
	{
		goto Jump_Free_cdev;
	}

	HelloClass = class_create(THIS_MODULE, DEVICENAME);
	if (IS_ERR(HelloClass)) 
	{
		pr_err("No udev support!\n");
		goto Jump_Free_cdev;
	}

	HelloDev = device_create(HelloClass, NULL, HelloDevNumber, NULL, "%s", DEVICENAME);
	if (IS_ERR(HelloDev)) 
	{
		pr_err("'device_create' failed!\n");
		goto Jump_Free_class;
	}

	return 0;

	Jump_Free_class:
		class_destroy(HelloClass);
	Jump_Free_cdev:
		kobject_put(&DriverObject->kobj);
	Jump_Free_DeviceNumber:
		unregister_chrdev_region(HelloDevNumber, 1);
		return -EIO;
}

static void __exit Module_Exit(void)
{
	device_destroy(HelloClass, HelloDevNumber);
	class_destroy(HelloClass);
	cdev_del(DriverObject);
	unregister_chrdev_region(HelloDevNumber, 1);

	return;
}

module_init(Module_Init);
module_exit(Module_Exit);

Die Funktion dieses Treibers ist es, ein Gerät mit dem Namen Hello anzulegen, welcher bei einem read-Systemcall einmalig den Text Hello World zurück gibt.

Über die eben belegte Taste kann der Treiber mit Hilfe des Makefiles kompiliert werden. Der fertig kompilierte Treiber wird dann auf den Raspberry Pi übertragen:

$ scp <ProjectDir>/SimpleDriver.ko root@<IP>:/home/pi/Desktop

Auf dem Raspberry Pi kann der Treiber dann geladen und mit einem cat wird ein read-Systemcall ausgelöst:

root@Raspberry:/home/pi/Desktop# insmod SimpleDriver.ko
root@Raspberry:/home/pi/Desktop# cat /dev/Hello
Hello World!

Über den Befehl rmmod kann der Treiber jederzeit wieder entladen werden:

root@Raspberry:/home/pi/Desktop# rmmod SimpleDriver

Sämtliche Debugausgaben des Treibers werden über printk in das Systemlog /var/log/syslog geschrieben und können dort eingesehen werden.

Wie immer gibt es das komplette Projekt in meinem Raspberry Pi Repository zum Download.

Schreibe einen Kommentar

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