Temperatur und Luftdruck lassen sich preiswert über per SPI angeschlossene Sensoren erfassen. Eigene Kernel-Treiber gewähren dabei auch ohne Userland-Magic optimierten Zugriff.
Ob es um Temperatur, Luftdruck, Luftfeuchte, Durchfluss, Lage, Position, Motoren, Displays oder LED-Felder geht: Sensoren und Aktoren steuert man typischerweise seriell an. Das spart gegenüber der parallelen Datenübertragung Schaltungs- beziehungsweise Verdrahtungsaufwand und damit Kosten. Verwendet man standardisierte serielle Bussysteme wie I2C oder SPI (Serial Peripheral Interface), bleibt auch der softwaretechnische Aufwand überschaubar, denn die meisten Mikrocontroller unterstützen diese beiden Bussysteme per se. Nicht anders ist es beim Raspberry Pi. Schon in einer früheren Folge [1] dieser Serie haben wir den Aufbau eines I2C-Kernel-Treibers detailliert vorgestellt. Diesmal beschäftigen wir uns mit einem Pendant für per SPI angekoppelte Sensoren und Aktoren.
Lange Leitung
Bei SPI ist die Peripherie – man spricht hier von Slaves – über vier Leitungen hardwaretechnisch mit dem Rechner verbunden (Abbildung 1): MOSI, MISO, SCLK und CE.
Während alle Slaves am seriellen Bus die ersten drei Leitungen gemeinsam nutzen, ist jeder über eine exklusive Chip-Enable-Leitung mit dem Controller verbunden. Über sie wählt der Master den Slave aus, der gerade den Bus benutzt (Slave Select). Befinden sich also drei Geräte am Bus, muss es auch drei CE-Leitungen geben: (CE0, CE1 und CE2). Im Duplexbetrieb transferiert der Controller (Master) über MOSI (Master Out Slave In) Daten zum Slave und via MISO (Master In Slave Out) der Slave Daten zum Master. Der Schiebetakt SCLK gibt vor, zu welchem Zeitpunkt die Daten auf den Leitungen MOSI und MISO gültig sind.
Allerdings legt das Hardwareprotokoll zu SPI das Aussehen der Signale auf den Leitungen nicht eindeutig fest. Es definiert eine Clock-Polarität sowie eine Clock-Phase und darüber abhängig vom Slave einzustellende Betriebsmodi. Die Übertragungsrate lässt sich ebenfalls konfigurieren.
Eigentlich könnte der Controller im Raspberry Pi 4B prinzipiell eine Vielzahl von Slaves per SPI ansteuern. Ein Blick auf die Belegung der Pins am Stecker P1 mit den Signalen CE0 und CE1 zeigt aber, dass er zunächst an einem Bus nur zwei Geräte unterstützt.
Schreiben, um zu lesen
Um mit einem angeschlossenen Gerät zu kommunizieren, wählt der Master es über das CE-Signal aus. Dann schickt er Daten zum Slave und erwartet im Gegenzug Daten zurück. Auch wenn er nur Daten lesen will, muss der Master mindestens ein Byte schreiben. Welche Bedeutung die transferierten Bytes dabei haben, legen die Hersteller der Sensoren und Aktoren fest.
Das SPI-Subsystem im Linux-Kernel unterscheidet Controller- und Protokolltreiber. Der Controller-Treiber integriert die SPI-Hardware in den Kernel, sodass der Protokolltreiber die Funktionen des SPI-Subsystems zur Kommunikation mit den Slaves verwenden kann. Im Fall des Raspberry Pi 4B findet sich dieser Code beispielsweise im Modul spi-bcm2835 (Abbildung 2).
Der Bosch BMP280, ein gängiger Sensor zur Messung von Temperatur und Luftfeuchtigkeit, kostet bei einschlägigen Quellen nur wenige Euro (Abbildung 3). Es gibt noch andere Varianten wie den etwas teureren BME280, der zusätzlich die Luftqualität misst. Tatsächlich existiert im Kontext von Industrial IO (IIO) im Linux-Kernel ein professioneller Treiber, den wir bereits vorgestellt haben [2].
Treiberarchitektur
Ein Kernel-Treiber für SPI besteht aus zwei semantisch zu unterscheidenden Teilen. Der eine implementiert die Integration des Treibers in den Kernel. Er übernimmt das Anmelden des Treibers beim SPI-Subsystem unter Angabe der Slaves, für die der Treiber verantwortlich ist, und den Funktionen, die der Kernel aufrufen soll, wenn er passende Slaves identifiziert (Abbildung 4).
Der andere Teil im Kernel-Treiber für SPI erledigt den Zugriff auf die unterstützten Slaves selbst, also das Auslesen eines Sensors oder das Ansteuern eines Aktors. Das kann man beispielsweise klassisch über eine Gerätedatei oder über das Proc-Filesystem realisieren.
Treiber anmelden
Das Anmelden beim SPI-Subsystem gestaltet sich vom Grundsatz her gradlinig und einfach. Sie rufen einfach die Funktion »spi_register_driver()« auf und übergeben ihr die Datenstruktur »struct spi_driver«, die alle notwendigen Informationen enthält. Dazu gehören die Kennung für die Identifikation der vom Treiber unterstützten Geräte (Element »id_table«) sowie die Adressen zweier Funktionen: »device_probe()« ruft das SPI-Subsystem auf, sobald der Kernel einen passenden Slave identifiziert, »device_remove()« übernimmt das Entfernen des Slaves. Um den Kernel-Treiber wieder vom SPI-Subsystem abzukoppeln, rufen Sie die Funktion »spi_unregister_driver()« auf.
Das An- und Abmelden des Treibers erfolgt typischerweise in je einer Initialisierungs- und Deinitialisierungsfunktion, die jedes Kernel-Modul mitbringen muss. In Listing 1 sind das »sensor_init()« und »sensor_exit()«. Erledigen diese Modulfunktionen lediglich das An- und Abmelden beim SPI-Subsystem über die genannten Funktionen, kann man das dank des Makros »module_spi_driver()« sogar mit einer einzelnen, simplen Zeile Code abkürzen.
Listing 1
Beschreibung eines SPI-Gerätetreibers (spi-probe-remove.c)
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/spi/spi.h>
static struct spi_device *sensor_device;
static int sensor_probe(struct spi_device *spi) {
sensor_device = spi;
pr_info("sensor_probe()\n");
return 0;
}
static void sensor_remove(struct spi_device *spi) {
pr_info("sensor_remove()\n");
}
static const struct spi_device_id bmp280_spi_id[] = {
{ "mybmp280", 0 },
{ "mybme280", 0 },
{ }
};
MODULE_DEVICE_TABLE(spi, bmp280_spi_id);
static struct spi_driver sensor_driver = {
.driver = {
.name = "bmp280",
.owner = THIS_MODULE,
},
.id_table = bmp280_spi_id,
.probe = sensor_probe,
.remove = sensor_remove,
};
static int __init sensor_init(void) {
int ret;
ret = spi_register_driver(&sensor_driver);
if (ret < 0) {
pr_err("spi_register_driver() failed: %d\n", ret);
return ret;
}
return 0;
}
static void __exit sensor_exit(void) {
spi_unregister_driver(&sensor_driver);
}
module_init(sensor_init);
module_exit(sensor_exit);
MODULE_LICENSE("GPL");
Erkennungsmerkmal
Die Gerätekennung selbst ist in der Struktur »struct spi_device_id« in Form einer Liste festgelegt, da ein Treiber ja für mehrere Slaves ähnlichen Typs verantwortlich sein kann. Den Slave repräsentieren dabei ein String, auf den wir später noch zurückkommen, sowie ein frei wählbarer Parameter, den Sie der Funktion »device_probe()« beim Aufruf übergeben.
Die zu programmierende Funktion »device_probe()« gibt dem Kernel durch einen Rückgabewert von »0« zu verstehen, dass der Treiber das Gerät bedient. Es empfiehlt sich, vorab zu testen, ob das Gerät überhaupt existiert und ob es erwartungsgemäß reagiert. Eventuell fallen vorbereitend einige Konfigurationsarbeiten an.
Essenziell ist der Parameter (»struct spi_device«), den Sie der Funktion »device_probe()« übergeben. Diese Struktur repräsentiert konkret den identifizierten Sensor respektive Aktor im Kernel und wird später für die eigentlichen Zugriffe benötigt. Folglich sollten Sie sie abspeichern. Die Funktion »device_remove()« wird beim späteren Entfernen des Sensors oder Aktors aufgerufen. Sie gibt zuvor reservierte Ressourcen wieder frei.
Den Quellcode von »spi-probe-remove.c« aus Listing 1 kompilieren Sie mithilfe eines passenden Makefiles durch Eingabe von »make« zu einem Kernel-Modul, das Sie anschließend per »sudo insmod spi-probe-remove.ko« in den Kernel laden. Damit die Funktion »device_probe()« aufgerufen wird, müssen Sie noch den Devicetree-Overlay-Blob »bmp280.dts« aus Listing 2 per »make bmp280.dtbo« generieren und via »sudo dtoverlay bmp280.dtbo« in den Kernel des RasPi integrieren. Ein in einem separaten Terminal aufgerufenes »dmesg –follow« visualisiert die Abläufe. Damit haben Sie aber bisher nur den ersten Teil eines SPI-Treibers implementiert.
Listing 2
Devicetree für den BMP280 (bmp280.dts)
/dts-v1/;
/plugin/;
/ {
compatible = "brcm,bcm2835";
fragment@0 {
target = <&spidev0>;
__overlay__ {
status = "disabled";
};
};
fragment@1 {
target = <&spi0>;
__overlay__ {
status = "okay";
#address-cells = <1>;
#size-cells = <0>;
bme280@0 {
compatible = "bosch,mybmp280";
reg = <0x0>;
spi-max-frequency = <1000000>;
default-oversampling = <1>;
status = "okay";
};
};
};
};
Zugriff
Der zweite Teil des Kernel-Treibers für SPI-Geräte implementiert die Zugriffsfunktionen. Das lässt sich über Proc-Dateien oder auch in Form eines zeichenorientierten Geräts realisieren. Das Anlegen der Proc- oder der Gerätedatei kann innerhalb von »device_probe()« erfolgen oder direkt beim Laden des Kernel-Moduls innerhalb der Funktion »mod_init()« (im Beispielcode »sensor_init()«). Entsprechend findet der Rückbau innerhalb von »device_remove()« oder »mod_exit()« statt.
Für den Zugriff über eine zeichenorientierte Gerätedatei müssen Sie Routinen wie »device_open()«, »device_read()«, »device_write()« und »device_close()« implementieren.
Für die eigentlichen Zugriffe auf den Sensor oder Aktor bietet das SPI-Subsystem eine Reihe von Funktionen an, darunter »spi_write_then_read()«, »spi_w8r8()«, »spi_read()« oder »spi_write()«. Typischerweise kommen sie innerhalb der Lese- und Schreibfunktionen »device_read()« und »device_write()« zum Einsatz. Sie transferieren Sensor- und Aktordaten zwischen Hardware und Kernel und dürfen nicht im Interrupt-Kontext aufgerufen werden. Den Weitertransport zwischen Kernel und Applikation übernehmen dann Routinen wie »copy_to_user()« und »copy_from_user()«.
Gerät anmelden
Es bleibt noch die Frage zu klären, wie der Kernel denn einen neuen Sensor oder Aktor erkennt – SPI als solches bietet dafür keine Technik an.
Die Antwort: Die Erkennung erfolgt über einen Devicetree, wie wir ihn in der letzten Folge dieser Serie thematisiert haben [3]. Hier geben Sie insbesondere das Attribut »reg« an, das das Chip-Enable des Controllers zuordnet. Der unter dem Attribut »compatible« abgelegte String muss (nach dem Komma) mit der in der »id_table« spezifizierten Zeichenkette übereinstimmen. Wir haben hier »mybmp280« gewählt, da der Raspberry Pi »bmp280« bereits verwendet.
Der Raspberry Pi erzeugt übrigens standardmäßig für jedes CE bereits ein generell aus dem Userland nutzbares Gerät (»/dev/spidev0.0« und »/dev/spidev0.1«). Da wir ohne Userland-Magic auskommen wollen, gilt es, diese abzumelden. Auch das bewerkstelligen wir über den Devicetree (siehe Listing 2).
An die Tasten
In Listing 3 finden Sie Fragmente aus einem Treiber für einen per SPI an einen Raspberry Pi 4B angeschlossenen BMP280. Den vollständigen Code laden Sie aus dem Download-Bereich zu diesem Artikel herunter. Er baut auf dem von Bosch zur Verfügung gestellten Treibercode [4] auf, den wir plump ans Ende des eigenen Quellcodes kopiert haben. Der Treiber erstellt die Gerätedatei »/dev/bmp280« und gibt bei einem Lesezugriff Luftdruck und Temperatur in einer Struktur »struct bmp2_data« in Form zweier 32-Bit-Werte zurück.
Listing 3
Treiber für den BMP280 (Auszug)
...
static s8 spi_reg_write(u8 reg_addr, const u8 *reg_data,
u32 length, const void *intf_ptr) {
u8 tx_data[100] = {0};
u16 i;
int ret;
tx_data[0] = reg_addr;
for(i=0; i<length; i++)
tx_data[i + 1] = reg_data[i];
ret = spi_write(sensor_device, tx_data, length + 1);
if ( ret>=0 )
return 0;
return ret;
}
static s8 spi_reg_read(u8 reg_addr, u8 *reg_data,
u32 length, const void *intf_ptr) {
int ret;
ret = spi_write_then_read( sensor_device, ®_addr,
1, reg_data, length );
if ( ret>=0 )
return 0;
return ret;
}
static void mydelay( u32 period, void *intf_ptr ) {
udelay( period );
}
static int device_probe(struct spi_device *spi) {
u8 id;
s8 result=0;
sensor_device = spi; // needed for reading and writing
spi->bits_per_word = 8;
result = spi_setup(spi);
if (result < 0) {
dev_err(&spi->dev, "spi_setup failed!\n");
return result;
}
bmp.delay_us = mydelay;
bmp.chip_id = 0;
bmp.intf = BMP2_SPI_INTF;
bmp.read = spi_reg_read;
bmp.write = spi_reg_write;
result = bmp2_init(&bmp);
if (result < 0) {
pr_info("bmp2_init() failed : %d\n", result);
return -1;
}
result = bmp2_get_config(&conf, &bmp);
if (result < 0) {
pr_info("bmp2_get_config() failed : %d\n", result);
return -1;
}
conf.filter = BMP2_FILTER_COEFF_2; // filter coefficient
conf.os_pres = BMP2_OS_4X; // oversampling
conf.os_temp = BMP2_OS_4X;
conf.odr = BMP2_ODR_1000_MS; // output data rate
result = bmp2_set_config(&conf, &bmp);
if ( result<0 ) {
pr_info("bmp2_set_config() failed : %d\n", result);
return -1;
}
result = bmp2_set_power_mode(BMP2_POWERMODE_NORMAL, &conf, &bmp);
if ( result<0 ) {
pr_info("bmp2_set_power_mode() failed : %d\n", result);
return -1;
}
return 0;
}
...
static ssize_t device_read( struct file *instanz,
char __user *user, size_t count, loff_t *offset ) {
unsigned long not_copied, to_copy;
int result;
struct bmp2_data comp_data;
if (sensor_device==NULL) // no device
return -EIO;
result = bmp2_get_sensor_data(&comp_data, &bmp);
if ( result<0 ) {
pr_info("bmp2_get_sensor_data() failed : %d\n", result);
return -EIO;
}
printk("result: %d pressure: %d temperature: %d\n",
result, comp_data.pressure, comp_data.temperature );
to_copy = min( count, sizeof(comp_data) );
not_copied=copy_to_user( user, &comp_data, to_copy);
*offset += (to_copy-not_copied);
return to_copy - not_copied;
}
Der Beispieltreiber für den BMP280 sieht vor, dass in der Funktion »mod_init()« mit den Funktionen »device_probe()« und »device_remove()« die Registrierung beim SPI-Subsystem erfolgt. Daneben wird die Gerätedatei »/dev/bmp280« erstellt und dem Kernel die Adresse der Zugriffsfunktion »device_read()« mitgeteilt.
Außerdem sieht der Bosch-Quellcode zum Treiber vor, eine Struktur des vorgegebenen Typs »struct bmp2_dev« zu instanziieren und dann per »bmp2_init()« dem sonstigen Bosch-Code bekanntzugeben. Dazu müssen Sie die Interface-Funktionen »spi_reg_read()«, »spi_reg_write()« und »delay_us()« implementieren. Die Struktur beschreibt außerdem, ob der Sensor per I2C oder SPI (hier »BMP2_SPI_INTF«) angekoppelt ist. Diese Initialisierung und die Kopplung mit dem Bosch-Treibercode erfolgt in »device_probe()«. Hier findet auch probehalber ein Zugriff auf das Gerät durch das Auslesen der Chip-ID über die Funktion »spi_w8r8()« statt.
Die Verknüpfung zwischen dem Bosch-Treibercode und den Kernel-Funktionen des SPI-Subsystems erfolgt in den Funktionen »spi_reg_write()« und »spi_reg_read()«. Bosch hat diese Funktionen so definiert, dass der erste Parameter die Nummer eines internen Sensorregisters enthält, der zweite die Adresse der Daten und der dritte die Anzahl der Bytes im Datenfeld. Zum Versand muss aber bei Linux die Nummer des Sensorregisters vor den zugehörigen Daten stehen, sodass wir aus diesen beiden Informationen ein neues Datenpaket zusammenschrauben. Erst danach erledigt die Kernel-Funktion »spi_write()« den Transfer.
Das Lesen von Daten berücksichtigt, dass bei SPI zuvor Daten gesendet werden müssen. Linux unterstützt Sie hier mit der Kernel-Funktion »spi_write_then_read()«. Die vom Interface erwartete Funktion »delay_us()« lässt sich übrigens einfach realisieren und besteht lediglich aus einem Aufruf der Kernel-Funktion »udelay()«.
Ausprobiert
Um eigene Luftdruck- und Temperaturmessungen vorzunehmen, verbinden Sie den BMP280 mit dem Raspberry Pi. Dazu stellen Sie die in der Tabelle “Raspberry Pi und BMP280 koppeln” aufgeführten Verbindungen her.
|
Sensor |
RasPi |
|---|---|
|
VCC |
3.3V (Pin 17) |
|
GND |
GND (Pin 25) |
|
SCL/SCK |
SPI_CLK (Pin 23) |
|
SDA/MOSI |
SPI_MOSI (Pin 19) |
|
CSB/CS |
SPI_CE0_N (Pin 24) |
|
SDO/MISO |
SPI_MISO (Pin 21) |
Den vollständigen Code des BMP280-Treibers laden Sie zusammen mit dem Devicetree, den notwendigen Header-Dateien, dem Makefile sowie einer Testapplikation aus dem Download-Bereich zu diesem Artikel herunter. Ein »make« im Verzeichnis des Quellcodes generiert das Modul »sensor.ko«, das Sie mit »sudo insmod sensor.ko« in den Kernel laden (Abbildung 5). Am besten beobachten Sie in einem zweiten Terminalfenster durch Eingabe von »dmesg –follow« die Log-Ausgaben des Treibers.
Um den Kernel mit dem Gerät bekanntzumachen, generieren Sie per »make bmp280.dtbo« den Devicetree-Overlay-Blob, den Sie dann per »sudo dtoverlay bmp280.dtbo« laden. Im Kernel-Log erscheinen daraufhin erste Luftdruck- und Temperaturwerte. Dieselben Ausgaben sehen Sie bei jedem Lesezugriff auf die Gerätedatei im Log.
Da unser Treiber über die Gerätedatei die Daten binär und nicht ASCII-kodiert liefert, übersetzen Sie per »make readtemp« noch die beiliegende Testapplikation und starten sie. Sie liest den Sensor sekündlich aus und gibt Temperatur und Luftdruck als Klartext im Terminal aus.
Fazit
Der hier vorgestellte Treiber implementiert nur rudimentäre Zugriffe auf den Sensor. Dabei bietet Linux noch diverse Möglichkeiten der Optimierung: Es gibt nicht nur alternative Zugriffsfunktionen, sondern über eine sogenannte Regmap lassen sich Zugriffe über I2C und SPI noch besser abstrahieren. Da die Dokumentation dazu nicht gerade üppig ausfällt, kommen Sie dazu aber um ein Studium des Quellcodes nicht herum. (jlu)
Der Autor
Jürgen Quade, Professor an der Hochschule Niederrhein, bietet auch für Unternehmen Schulungen zu den Themen Treiberprogrammierung und Embedded Linux an.
Infos
- Kern-Technik: Eva-Katharina Kunst, Jürgen Quade, “I2C-Treiber”, LM 02/2014, S. 90, https://www.lm-online.de/31268
- Kern-Technik: Eva-Katharina Kunst, Jürgen Quade, “Schatzsuche”, LM 09/2021, S. 72, https://www.lm-online.de/44574
- Kern-Technik: Eva-Katharina Kunst, Jürgen Quade, “Fruchtbarer Baum”, LM 07/2024, S. 72, https://www.lm-online.de/50454
- BME280 Sensor API: https://github.com/boschsensortec/BME280_SensorAPI











