Am Raspberry Pi lassen sich preiswerte Sensoren und Aktoren sehr einfach über das Serial Peripheral Interface (SPI) anschließen. Linux bringt das passende Subsystem und Treiber mit.
Eines der Erfolgsgeheimnisse von Arduino und Raspberry Pi besteht darin, es Entwicklern und Hobbyisten zu erlauben, mit eigener Hardware zu experimentieren. Der Büro-PC ist nur mit erheblichem Aufwand um Sensoren und Aktoren zu erweitern. Dagegen liegen bei Arduino und Raspberry Pi so genannte GPIOs auf einer frei zugänglichen Steckerleiste, der Zugriff ist ohne tiefe Kenntnisse über Peripheral Component Interconnect (PCI) oder Ähnliches möglich.
Im Fall des Raspberry Pi kommt vorteilhafterweise das ausgewachsene, quelloffene Betriebssystem hinzu, das eine bekannte Entwicklungsumgebung und vor allem ein umfangreiches Angebot von Middleware und Funktionalität bereitstellt, um selbst komplexe Anwendungen schnell zu realisieren.
Die einfachste Art, Hardware mit einem Rechner zu verbinden, bieten die erwähnten “General Purpose Input Output”-Leitungen (GPIO, [1]). Diese Pins lassen sich per Software auf eine Spannung von 0 Volt oder 3.3 Volt legen. Beim Einlesen wird eine Eins in ein Prozessorregister übernommen, falls am Pin 3,3 Volt anliegen, andernfalls steht eine Null.
Der Raspberry Pi bietet immerhin 54 derart programmierbare Ein-/Ausgabe-Leitungen, von denen der Entwickler 17 nutzen kann. Analogwerte, die etwa eine Temperatur oder Helligkeit repräsentieren, lassen sich damit ohne weitere Maßnahmen allerdings nicht einlesen.
Hierfür wird ein Analog-Digital-Konverter (ADC) benötigt, der bei einer Breite von 8 Bit (was bedeutet, dass eine anliegende analoge Spannung in einen numerischen Wert zwischen 0 und 255 überführt wird) bereits acht der vorhandenen Ein-/Ausgabe-Leitungen nutzen würde. Die ursprünglich 17 verfügbaren Leitungen sind auf diese Weise sehr schnell belegt.
Vier statt acht
Werden die acht Bits nicht gleichzeitig (parallel) übertragen, sondern nacheinander (seriell), reichen minimal zwei Leitungen aus. Natürlich dauert das Einlesen eines digital gewandelten Analogwerts dann achtmal so lange. In der Elektronik ist die serielle Hardware-Ankopplung längst etabliert und so haben sich angesichts des Wunsches, mit möglichst wenigen Leitungen viel Peripherie ansteuern zu können, neben der klassischen seriellen Schnittstelle zwei Standards durchgesetzt, die moderne Mikrocontroller unterstützen: I2C und SPI. I2C war bereits Thema in [2], das Serial Peripheral Interface (SPI) wird in dieser Kern-Technik vorgestellt. Weitere Informationen sind unter anderem in [3], [4] und [5] zu finden.
SPI besteht für die anzuschließenden Geräte (die so genannten Slaves, etwa AD-Wandler) aus drei Leitungen (siehe Abbildung 1): MOSI, MISO und SCLK. MOSI steht für “Master Out Slave In”, MISO für “Master In Slave Out” und SCLK ist der Schiebetakt, der vorgibt, zu welchem Zeitpunkt die Daten auf den anderen beiden Leitungen gültig sind.
Zusätzlich benötigt jeder Slave zum Aktivieren eine weitere Leitung (CE0, CE1, … CEn). Während der im Raspberry Pi eingebaute Controllerbaustein BCM 2835 drei SPI-Master mit jeweils drei Slaves unterstützen würde, nutzt der Raspberry Pi selber mit den auf die Steckerleiste P1 geführten Leitungen CE0 und CE1 lediglich einen Master mit zwei Slaves.
Das Hardwareprotokoll zu SPI beschreibt mit der so genannten Clock-Polarität sowie Clock-Phase, wie die Signale auf den Leitungen aussehen, damit Master und Slave die Daten übernehmen. Hierüber sind vier Betriebsmodi definiert, die der Programmierer abhängig vom Slave einstellt (Tabelle 1). Er legt ebenfalls die Übertragungsrate fest, die beim Raspberry Pi offiziell zwischen 7629 Hz und eher theoretischen 125 MHz liegt.
Tabelle 1
SPI-Betriebsmodi
|
Define |
Mode |
CPOL |
CPHA |
|---|---|---|---|
|
SPI_MODE_0 |
|||
|
SPI_MODE_1 |
1 |
1 |
|
|
SPI_MODE_2 |
2 |
1 |
|
|
SPI_MODE_3 |
3 |
1 |
1 |
Schreiben, um zu lesen
Für den Datentransfer mit am Bus angeschlossenen Geräten ist nur festgelegt, dass der Master 1 Byte schickt und grundsätzlich im Gegenzug 1 Byte zurückbekommt. Dieses Verfahren gilt auch dann, wenn nur gelesen werden soll: Um zu lesen, muss der Master also schreiben. Weitere Protokolle sind durch SPI selbst nicht festgelegt, sehr wohl aber vom jeweiligen Hersteller der einzelnen Slave-Bausteine und dort in den Datenblättern zu finden.
Nicht nur Hardware-, auch Software-technisch ist der Raspberry Pi bestens für den Umgang mit SPI-Geräten gerüstet. Linux bringt ein ausgeklügeltes SPI-Subsystem mit. Es unterscheidet Controller- und Protokoll-Treiber. Der Controller-Treiber koppelt einen spezifischen SPI-Controller an das Subsystem an, sodass ein Protokoll-Treiber oder auch direkt eine Applikation auf das Gerät zugreifen kann. Für den Raspberry Pi gibt es mit dem Modul »spibcm-2708« bereits einen geeigneten Controller-Treiber (Abbildung 2).
Es geht los
Eigene Tests mit SPI lassen sich einfach durchführen. Ein SPI-fähiger Baustein, ein Breadboard und ein paar Kabel (insgesamt zehn) genügen. Als SPI-fähiger Baustein bietet sich beispielsweise der Analog-Digital-Wandler TLC 549CP der Firma Texas Instruments an, der bei einschlägigen Elektronikhändlern für etwa 1,50 Euro zu kaufen ist.
Um für den Test den AD-Wandler mit einem Analogwert füttern zu können, ist ein Potentiometer hilfreich. Der Widerstand des Drehpotis sollte mehr als 1 Kiloohm betragen, um nicht unnötig Strom zu verheizen. In einer realen Anwendung würde sich am Analogeingang statt des Potentiometers beispielsweise ein temperaturabhängiger oder lichtabhängiger Widerstand befinden.
Der Bastler verschaltet den Raspberry Pi so, wie es in den Abbildungen 3, 4 und 5 dargestellt ist. Als Versorgungsspannung des Wandlers reichen die 3,3 Volt des Raspberry Pi, GND liegt an der Steckerleiste des Raspberry Pi auf Pin 6 oder auf Pin 25. Der eigentliche SPI-Bus – hier reichen die Signale SCLK, MISO und CS – wird mit den Pins 7, 6 und 5 des TLC 549CP verbunden.
Um mit dem Wandler sinnvolle Werte zu messen, erzeugt das Potentiometer die Analogspannung. Dazu wird für die beiden Referenzspannungen Ref+ und Ref- das 3,3-Volt- respektive das 0-Volt-Signal verwendet. Die beiden äußeren Anschlüsse des Potentiometers erhalten ebenfalls Verbindung mit Ref+ und Ref- (also mit 3,3 und 0 Volt), der mittlere Abgriff mit dem Eingang 2 des TLC 549CP.
Boot-Konfiguration
Ist der SPI-Baustein mit dem Raspberry Pi verbunden, kann dessen Besitzer loslegen. Zuerst lädt er den Treiber. Einem Raspbian liegt dieser bei und lässt sich durch Eingabe von »sudo modprobe spi-bcm2708« auf der Konsole laden. Bei neueren Kerneln heißt der Treiber »spi-bcm2835« : BCM 2708 ist eine Bausteinserie von Broadcom, 2835 die spezifische, im Raspberry Pi verbaute Ausprägung. Alternativ automatisiert der Bastler das Laden des Treibers. Dazu startet er auf einer Konsole den Befehl »sudo raspi-config« und wählt »Advanced Options« , »SPI« und »Yes« (Abbildung 6).
Sobald sich der Treiber im Kernel eingenistet hat, stehen die beiden Gerätedateien »/dev/spidev0.0« und »/dev/spidev0.1« zur Verfügung. Auf diese kann der Anwender beispielsweise mit Hilfe von »echo« schreiben respektive von diesen mit »cat« oder besser »hexdump« lesen (Abbildung 7). Dabei finden für den SPI-Modus und die Übertragungsrate Defaultwerte Verwendung, die allerdings nicht optimal auf den verwendeten AD-Wandler passen. Um diese Parameter einzustellen, kommt der Entwickler daher nicht um den C-Code herum.
Zur Konfiguration steht eine Reihe IO-Controls bereit (Tabelle 2), deren Anwendung im Beispielprogramm »adc.c« (Listing 2) zu sehen ist. Es liest nach der Initialisierung in einer Schleife über die »transfer()« genannte Funktion per klassischem »read()« die digitalisierten Analogwerte ein und gibt sie auf dem Bildschirm aus. Auf dem Raspberry Pi kompiliert das Programm problemlos, lässt sich aber nicht direkt linken.
Tabelle 2
IO-Controls für den SPI-Bus
|
IO-Control |
Funktion |
|---|---|
|
SPI_IOC_RD_MODE, SPI_IOC_WR_MODE |
Liest beziehungsweise schreibt den SPI-Modus. Für den SPI-Modus stehen die Konstanten »SPI_MODE_0« bis »SPI_MODE_3« zur Verfügung. Der Modus lässt sich alternativ über die Oder-Verknüpfung der Clock-Polarität (»SPI_CPOL« ) und der Clock-Phase (»SPI_CPHA« ) einstellen. |
|
SPI_IOC_RD_LSB_FIRST, SPI_IOC_WR_LSB_FIRST |
Liest beziehungsweise schreibt die Bitausrichtung bei der Übertragung. Null bedeutet höchstwertigstes Bit zuerst; ansonsten startet die Übertragung mit dem niederwertigsten. |
|
SPI_IOC_RD_BITS_PER_WORD, SPI_IOC_WR_BITS_PER_WORD |
Liest beziehungsweise schreibt die bei der Übertragung verwendete Bitanzahl. Null steht für einen 8-Bit Transfer. |
|
SPI_IOC_RD_MAX_SPEED_HZ, SPI_IOC_WR_MAX_SPEED_HZ |
Liest beziehungsweise schreibt die maximale Übertragungsrate in Hertz. Abhängig von der Hardware wird nicht jede Übertragungsrate unterstützt. |
|
SPI_IOC_MESSAGE(n) |
Führt den Datentransfer durch. Die zugehörigen Transferdaten und -informationen befinden sich in einem Feld aus n »struct spi_ioc_transfer« . |
Listing 2
Einlesen der Analogwerte (adc.c)
01 #include <stdio.h>
02 #include <fcntl.h>
03 #include <time.h>
04 #include <unistd.h>
05 #include <sys/ioctl.h>
06 #include <linux/spi/spidev.h>
07
08 static __u8 mode = 0x03;
09
10 static int transfer(int fd_spi)
11 {
12 __u8 analogvalue;
13 int ret;
14
15 ret = read(fd_spi, &analogvalue, sizeof(analogvalue));
16 if (ret<=0) {
17 fprintf(stderr,"can't read analogvalue\n");
18 return -1;
19 } else
20 printf("%d\n", analogvalue);
21 return 0;
22 }
23
24 int main( int argc, char **arv, char **envp )
25 {
26 int fd_spi, ret;
27 struct timespec sleeptime;
28
29 fd_spi = open("/dev/spidev0.0", O_RDWR);
30 if (fd_spi<0) {
31 fprintf(stderr,"can't open
/dev/spidev0.0\n");
32 return -1;
33 }
34 ret = ioctl(fd_spi, SPI_IOC_RD_MODE, &mode);
35 if (ret==-1) {
36 fprintf(stderr,"can't set mode %d\n", mode);
37 return -1;
38 }
39 ret = ioctl(fd_spi, SPI_IOC_WR_MODE, &mode);
40 if (ret==-1) {
41 fprintf(stderr,"can't set mode %d\n", mode);
42 return -1;
43 }
44 ret = ioctl(fd_spi,
SPI_IOC_RD_MAX_SPEED_HZ, 10000);
45 if (ret==-1) {
46 fprintf(stderr,"can't set rd-speed\n");
47 return -1;
48 }
49
50 sleeptime.tv_sec = 0;
51 sleeptime.tv_nsec= 250000000;
52 while( 1 ) {
53 transfer( fd_spi );
54 clock_nanosleep(CLOCK_MONOTONIC,
0,&sleeptime,NULL);
55 }
56 return 0;
57 }
Dem Linker fehlt die Funktion »clock_nanosleep()« , die sich in der Realzeit-Bibliothek »librt« befindet. Programmierer steuern daher die Generierung per »make LDLIBS=-lrt adc« , falls der Quellcode »adc.c« lautet (Abbildung 7). Das kompilierte Programm startet anschließend mit der Eingabe von »./ad« .
Spezielle Zugriffsfunktion
Bei einfachen Slaves, etwa dem verwendete ADC, reichen »read()« und »write()« für den Zugriff aus. Per Spezifikation besteht ein Transfer aber aus der Kombination Schreiben und Lesen, eine Funktionalität, die das klassische Interface für den Zugriff auf Dateien und Peripherie zunächst nicht bietet.
Daher stellen die Entwickler diese Funktion (Schreiben und Lesen in einem Durchlauf) ebenfalls über ein IO-Control bereit. Der Treiber bekommt dabei eine Struktur vom Typ »struct spi_ioc_transfer« übergeben, in der die Adressen für einen Lese- und einen Schreibpuffer abgelegt sind. Mehr noch: In dieser Struktur finden sich bereits alle Zugriffsparameter, inklusive der Übertragungsrate.
Dabei lassen sich dem Treiber mit einem Aufruf sogar mehrere dieser Blöcke und damit mehrere Aufträge übergeben. Jeder Auftrag wird mit seinen eigenen Übertragungsparametern ausgeführt. Die Anzahl der Transferaufträge und damit der übergebenen Strukturen erhält das IO-Control nicht über einen eigenen Parameter. Vielmehr ist der Name des IO-Control (SPI_IOC_MESSAGE) ein Makro, das die Anzahl der Strukturen selbst als Parameter übergeben bekommt.
Das Listing 1 stellt eine alternative Funktion »transfer()« vor, die den Zugriff über das IO-Control realisiert. Dass im Listing Sende- und Empfangspuffer 3 Byte groß sind, ist rein willkürlich und dient hier nur zur Demonstration.
Listing 1
Schreib-Lese-Transfer per IO-Control
01 #define ARRAY_SIZE(a) \
02 (sizeof(a) / sizeof((a)[0]))
03 static int transfer(int fd_spi)
04 {
05 int ret, i;
06 __u8 transmit_data[]={0x00,0x00,0x00};
07 __u8 receive_data[] ={0x00,0x00,0x00};
08 struct spi_ioc_transfer transfer_struct;
09
10 transfer_struct.tx_buf =
11 (unsigned long)transmit_data;
12 transfer_struct.rx_buf =
13 (unsigned long)receive_data;
14 transfer_struct.len =
15 ARRAY_SIZE(transmit_data);
16 transfer_struct.delay_usecs = 0;
17 transfer_struct.speed_hz = 10000;
18 transfer_struct.bits_per_word = 8;
19
20 ret = ioctl(fd_spi, SPI_IOC_MESSAGE(1),
21 &transfer_struct);
22 if (ret < 1) {
23 fprintf(stderr,"xfer failed\n");
24 return -1;
25 }
26 printf("Received: ");
27 for (i=0;i<ARRAY_SIZE(receive_data);
28 i++)
29 printf("0x%x ", receive_data[i]);
30 printf("\n");
31 return 0;
32 }
Die Größe und der Inhalt der Daten hängen letztlich vom angeschlossenen SPI-Slave ab und müssten im vorliegenden Fall eigentlich sogar auf eins limitiert sein. Der Linux-Treiber Spidev (»spi-bcm2708« ) implementiert so ein einfach zu nutzendes Userinterface. Wer keine hohen Ansprüche an Performance stellt, wird damit bereits glücklich.
Noch schneller mit DMA
Wer jedoch noch schneller sein will, der muss in den Kernel wandern und dann einen eigenen Protokolltreiber schreiben. Unter [6] finden Programmierer bereits einen funktionstüchtigen, DMA-fähigen Treiber. Er ist einsatzbereit, wird aber aktuell nicht mehr gepflegt.
Dafür gibt es allerdings auch einen ganz nachvollziehbaren Grund: Der Kernelentwickler Martin Sperl arbeitet nämlich daran, den Standardtreiber ebenfalls DMA-fähig zu machen. Da dies aber zugleich mit einer kompletten Überarbeitung des SPI-Subsystems einhergeht, kann es leider noch etwas dauern, bis das Projekt vollendet ist. (jcb)
Infos
- Quade, Kunst, “Kern-Technik”, Folge 70 zu Gerätetreiber für GPIO: Linux-Magazin 10/13, S. 102
- Quade, Kunst, “Kern-Technik”, Folge 72 zu I2C: Linux-Magazin 02/14, S. 90
- RPi SPI: http://elinux.org/RPi_SPI
- SPI-Artikel: http://www.mikrocontroller.net/articles/Serial_Peripheral_Interface
- Linux-Kernel-Dokumentation SPI: https://www.kernel.org/doc/ Documenta-tion/spi/
- Notro: DMA-fähiger SPI-Master-Treiber: https://github.com/notro/spi-bcm2708/wiki












