![]() |
Raspberry Pi: SPI-SchnittstelleProf. Jürgen Plate |
Der Raspberry Pi kann über den digitalen GPIO-Port nicht nur per I2C, UART oder bitweise kommunizieren, sondern hat auch eine SPI-Schnittstelle. Beim Modell B/B+ und folgende sind die SPI-Pins auf der GPIO-Steckerleiste über folgende Pins erreichbar:
SPI MOSI – PIN 19, GPIO 10 SPI MISO – PIN 21, GPIO 9 SPI SCLK – PIN 23, GPIO 11 SPI CS0 – PIN 24, GPIO 8 SPI CS1 – PIN 26, GPIO 7Das Serial Peripheral Interface (kurz SPI, oder SPI-Bus) ist ein synchrones Datenbus-System, das von Motorola (heute Freescale) entwickelt wurde, und mit dem digitale Schaltungen nach dem Master-Slave-Prinzip miteinander kommunizieren können. Der Bus wird für kurze Distanzen verwendet und arbeitet prinzipiell wie ein Schieberegister. Er hat vier logische Signale:
Die Datenübertragung erfolgt zwischen den beiden Busteilnehmern also seriell und synchron über die Leitungen MISO und MOSI, wobei der Master die Kommunikation steuert und den Takt über SCLK-Leitung (Serial-Clock) vorgibt.
Der Zugriff im Betriebssystem Raspbian erfolgt über die Gerätedateien /dev/spidev0.0 (Gerät an CS0) und /dev/spidev0.1 (Gerät an CS1), in die man direkt schreiben und aus denen man auch direkt lesen kann. Das Freischalten der Treiber wurde schon in der Installationsanleitung vorgenommen und ist dort beschrieben. Mit dem Befehl sudo modprobe spi-bcm2708 können Sie überprüfen, ob das Treibermodul geladen wurde.
Ab Kernelversion 3.18 ist der Eintrag dtparam=spi=on in der Datei /boot/config.txt notwendig. Die Zeile kann mit raspi-config eingetragen werden. Ausserdem scheint es einen neueren Treiber zu geben, spi_bcm2835 statt des spi_bcm2708. Sie können das einfach ausprobieren, indem Sie den Treiber manuell laden. Wenn es beim spi_bcm2708 schief geht, nemen Sie den spi_bcm2835:
modprobe spi_bcm2708 modprobe: ERROR: could not insert 'spi_bcm2708': No such device modprobe spi_bcm2835Der Treiber unterstützt folgende Geschwindigkeiten:
cdiv speed cdiv speed 2 125.0 MHz 4 62.5 MHz 8 31.2 MHz 16 15.6 MHz 32 7.8 MHz 64 3.9 MHz 128 1953 kHz 256 976 kHz 512 488 kHz 1024 244 kHz 2048 122 kHz 4096 61 kHz 8192 30.5 kHz 16384 15.2 kHz 32768 7629 Hz
Es werden folgende Modi unterstützt (Mode bits):
Für eine ersten Test können Sie den Loopbacktest verwenden. Dazu werden die Pins MOSI und MISO miteinadner verbunden. Laden Sie sich die aktuelle Version des Testprogramms auf den Raspberry und compilieren Sie es:
wget https://raw.githubusercontent.com/torvalds/linux/master/tools/spi/spidev_test.c gcc -o spidev_test spidev_test.cDanach starten Sie das Programm
./spidev_test -D /dev/spidev0.0Das Programm bietet zahlreiche Kommandozeilenoptionen:
-D --device device to use (default /dev/spidev1.1) -s --speed max speed (Hz) -d --delay delay (usec) -b --bpw bits per word -l --loop loopback -H --cpha clock phase -O --cpol clock polarity -L --lsb least significant bit first -C --cs-high chip select active high -3 --3wire SI/SO signals shared -v --verbose Verbose (show tx buffer) -p Send data (z.B. 1234\xde\xad) -N --no-cs no chip select -R --ready slave pulls low to pause -2 --dual dual transfer -4 --quad quad transfer);Das gleiche Programm finden Sie auch unter der Adresse https://raw.githubusercontent.com/raspberrypi/linux/rpi-3.10.y/Documentation/spi/spidev_test.c .
Das Senden von Daten an die Schnittstell funktioniert auch auf der Kommandozeile z. B.:
echo -ne "\x01\x02\x03" > /dev/spidev0.0
Es gibt vier verschiedene SPI-Modes für den Takt, der Raspi benutzt nur Mode 0,0, was bedeutet:
Sie können aber auch dauerhaft dafür sorgen, dass die Zugriffsrechte beim Bootvorgang von System festgelegt werden. Hier stützt man sich auf das udev-Subsystem. Normalerweise erstellt es für jedes neue Gerät eine Gerätedatei im Verzeichnis /dev. Man kann aber auch weitere Regeln angeben, indem man im Verzeichnis /etc/udev/rules.d eine Steuerdatei anlegt. Der numerische Präfix des Dateinamens regelt dabei die Reihenfolge der Abarbeitung der Dateien. Für SPI legen Sie im o. g. Verzeichnis die Datei 51-i2c.rules an und tragen darin die folgende Regel ein:
SUBSYSTEM=="spidev", GROUP="users", MODE="0660"Damit sind die entsprechenden Devices für die Gruppe "users" mit Lese- und Schreibrecht versehen. Jetzt müssen Sie nur noch den udev-Daemon von den Änderungen wissen lassen (beim nächsten Reboot passiert das dann automatisch):
sudo service udev restart
Wenn Sie den SPI-Bus in C ansprechen wollen, brauchen Sie auf jeden Fall die folgenden Headerdateien:
#include <fcntl.h> // Needed for SPI port #include <sys/ioctl.h> // Needed for SPI port #include <linux/spi/spidev.h> // Needed for SPI portDer Zugriff unter C hat recht klassische Programmierweise. Nach dem Öffnen des Devices werden die Parameter eingestellt und danach ist das System bereit für den Datentransfer. Bei dem folgenden Programmfragment wird neben dem Setzen der Parameter auch gezeigt, wie man die Parameter wieder abfragen kann:
static const char *device = "/dev/spidev0.0"; static uint8_t mode; static uint8_t bits = 8; static uint32_t speed = 500000; static uint16_t delay; int ret, fd; /* Device oeffen */ if ((fd = open(device, O_RDWR)) < 0) { perror("Fehler Open Device"); exit(1); } /* Mode setzen */ ret = ioctl(fd, SPI_IOC_WR_MODE, &mode); if (ret < 0) { perror("Fehler Set SPI-Modus"); exit(1); } /* Mode abfragen */ ret = ioctl(fd, SPI_IOC_RD_MODE, &mode); if (ret < 0) { perror("Fehler Get SPI-Modus"); exit(1); } /* Wortlaenge setzen */ ret = ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits); if (ret < 0) { perror("Fehler Set Wortlaenge"); exit(1); } /* Wortlaenge abfragen */ ret = ioctl(fd, SPI_IOC_RD_BITS_PER_WORD, &bits); if (ret < 0) { perror("Fehler Get Wortlaenge"); exit(1); } /* Datenrate setzen */ ret = ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed); if (ret < 0) { perror("Fehler Set Speed"); exit(1); } /* Datenrate abfragen */ ret = ioctl(fd, SPI_IOC_RD_MAX_SPEED_HZ, &speed); if (ret < 0) { perror("Fehler Get Speed"); exit(1); } /* Kontrollausgabe */ printf("SPI-Device.....: %s\n", device); printf("SPI-Mode.......: %d\n", mode); printf("Wortlaenge.....: %d\n", bits); printf("Geschwindigkeit: %d Hz (%d kHz)\n", speed, speed/1000);
Für das Schreiben und gleichzeitige Lesen von Daten reicht eine einzige Funktion. Der Puffer-Parameter data enthält die zu sendenden Daten und er wird mit den Empfangsdaten überschrieben. Er verhält sich damit wie in an MOSI und MISO angeschlossenes Schieberegister.
int SpiWriteRead (int fd, unsigned char *data, int length) /* Schreiben und Lesen auf SPI. Parameter: * fd Devicehandle * data Puffer mit Sendedaten, wird mit Empfangsdaten überschrieben * length Länge des Puffers */ { struct spi_ioc_transfer spi[length]; /* Bibliotheksstruktur fuer Schreiben/Lesen */ uint8_t bits = 8; /* Datenlaenge */ uint32_t speed = 500000; /* Datenrate */ int i, ret; /* Zaehler, Returnwert */ /* Wortlaenge abfragen */ ret = ioctl(fd, SPI_IOC_RD_BITS_PER_WORD, &bits); if (ret < 0) { perror("Fehler Get Wortlaenge"); exit(1); } /* Datenrate abfragen */ ret = ioctl(fd, SPI_IOC_RD_MAX_SPEED_HZ, &speed); if (ret < 0) { perror("Fehler Get Speed"); exit(1); } /* Daten uebergeben */ for (i = 0; i < length; i++) { spi[i].tx_buf = (unsigned long)(data + i); // transmit from "data" spi[i].rx_buf = (unsigned long)(data + i); // receive into "data" spi[i].len = sizeof(*(data + i)); spi[i].delay_usecs = 0; spi[i].speed_hz = speed; spi[i].bits_per_word = bits; spi[i].cs_change = 0; } ret = ioctl(fd, SPI_IOC_MESSAGE(length), &spi) ; if(ret < 0) { perror("Fehler beim Senden/Empfangen - ioctl"); exit(1); } return ret; }
Die andernorts schon besprochene Bibliothek WiringPi bietet zur Lösung der meisten Probleme, die sich beim Öffnen von SPI-Geräten und dem Senden und Empfangen von Bytes ergeben. Der Code der Bibliothek ist recht übersichtlich und so sollten Sie in der Lage, diesen als Grundlage für Ihren eigenen Code verwenden.
Das folgende Beispiel zeigt exemplarisch das Auslesen eines 3-Achsen-Beschleunigungssensors ADXL 362 mit der Bibliothek. Der anfängliche Vorspann setzt wieder die Busparameter (in diesem Fall alle auf den Default). Danach erfolgen dann erst die Initialisierung des Sensors und dann das Auslesen der Daten. Die gesendeten Befehle muss man sich aus dem Datenblatt herausfischen. Je nach Sensor oder Device ist hier oftmals Versuch und Irrtum angesagt, bis man zu Ziel gelangt.
#include <bcm2835.h> #include <stdio.h> /* ggf. weiter includes */ int main(int argc, char **argv) { char buf [10]; if (!bcm2835_init()) return 1; /* Bibliothek initialisieren */ /* Schnittstenneparameter setzen */ bcm2835_spi_begin(); bcm2835_spi_setBitOrder(BCM2835_SPI_BIT_ORDER_MSBFIRST); /* default */ bcm2835_spi_setDataMode(BCM2835_SPI_MODE0); /* default */ bcm2835_spi_setClockDivider(BCM2835_SPI_CLOCK_DIVIDER_65536); /* default */ bcm2835_spi_chipSelect(BCM2835_SPI_CS0); /* default */ bcm2835_spi_setChipSelectPolarity(BCM2835_SPI_CS0, LOW); /* default */ /* Device-ID abfragen */ buf[0] = 0x0B; buf[1] = 0x00; buf[2] = 0x00; bcm2835_spi_transfern(buf, 3); /* buf enthaelt die gelesenen Daten */ printf("Device ID: %02X \n", buf[2]); /* Soft-Reset des Sensors */ buf[0] = 0x0A; buf[1] = 0x1F; buf[2] = 0x52; bcm2835_spi_transfern(buf, 3); delay(1000); /* Setup for Measure */ buf[0] = 0x0A; buf[1] = 0x2D; buf[2] = 0x02; bcm2835_spi_transfern(buf, 3); delay(1000); /* X-Achse auslesen */ buf[0] = 0x0B; buf[1] = 0x0E; buf[2] = 0x00; buf[3] = 0x00; bcm2835_spi_transfern(buf, 4); printf("X-Achse: %02X %02X \n", buf[3], buf[2]); delay(1000); bcm2835_spi_end(); return 0; }
Auch wenn Python schon standardmäßig in der Raspbian-Distribution installiert ist, wird SPI leider noch nicht gleich mit unterstützt. Ohne spezielle Bibliotheken wird die SPI-Schnittstelle /dev/spidev0.0 wie eine Datei behandelt.
Als Beispiel soll der Port-Expander MCP23S17 dienen, der unter anderem zwei 8-Bit-Ports bietet. Das IC wird per SPI am RasPi angeschlossen und ist recht einfach zu programmieren. Gegebenfalls hilft ein Blick ins Datenblatt weiter:
from time import sleep SPIDEV = '/dev/spidev0.0' ADDRESS = 0x40 DELAY = 0.1 def write(DEV, Addr, Register, Byte): # SPI-Device, Adresse, Register, Daten handle = open(DEV, 'w+') try: data = chr(Addr)+chr(Register)+chr(Byte) handle.write(data) handle.close return True except: print("Error writing to SPI Bus") return False # MCP23S17-Register GPIOB auf Output schalten write(SPIDEV,ADDRESS,0x01,0x00) while True: write(SPIDEV,ADDRESS,0x13,0xff) sleep(DELAY) write(SPIDEV,ADDRESS,0x13,0) sleep(DELAY)Etwas einfacher wird die Programmierung, wenn man die passende SPI-Library für Python nachinstallieren. Dafür gibt es zwei Wege. Wenn man wirklich nur die eine Bibliothek braucht, führt man im Terminal folgendes aus:
sudo su apt-get install python-dev mkdir python-spi cd python-spi wget https://github.com/JoBergs/RaspiContent/raw/master/spidev/setup.py wget https://github.com/JoBergs/RaspiContent/raw/master/spidev/spidev_module.c python setup.py installWenn Sie aber schon wissen, dass Sie öfter mit Python arbeiten werden und auch sicher öfter mal etwas nachinstallieren müssen, ist es günstiger, sich den Installer für Python-Software pip einmal zu installieren. Das Laden und Installieren von Bibliotheken etc. ist damit dann wesentlich einfacher, denn pip erledigt das Herunterladen und Installieren in einem Aufwasch:
sudo su apt-get install git-core python-dev apt-get install python-pip pip install spidevZum Testen, ob alles geklappt hat, kann man nun Python mit Root-Rechten starten (sudo python und interaktiv die Spidev-Library testen:
import spidevWenn eine Fehlermeldung kommt, sollten Sie auf prüfen, ob der Treiber freigegeben ist (lsmod). Die Gerätedateien listet das Kommando ls /dev/spi*. Damit kann man prüfen, ob zwei SPI-Geräte gefunden werden (je eines pro CS-Signal). Es sollte sich also ergeben:
/dev/spidev0.0 /dev/spidev0.1
Property | Funktion |
---|---|
bits_per_word | Setzen/Lesen der Wortlänge (8..16) |
cshigh | Lesen/Setzen CS auf active high |
lsbfirst | Lesen ob das LSB zuerst oder zuletzt gesendet wird |
max_speed_hz | Lesen/Setze Datenrate |
mode | Lesen/Setzen des SPI-Mode (Taktpolarität CPOL, Phase CPHA) (0b00..0b11) |
threewire | Lesen/Setzen "I/SO signals shared" → nur eine Datenleitung (nur bei manchen Devices möglich) |
Methode | Funktion |
---|---|
spi.open(0,0) | Öffnet den SPI-Bus 0 mit CS0 |
spi.open(0,1) | Öffnet den SPI-Bus 0 mit CS1 |
spi.close() | Schliesst den SPI-Bus |
spi.readbytes(len) | Liest len Bytes vom SPI-Slave |
spi.writebytes([array of bytes]) | Sendet ein Byte-Array zum SPI-Slave |
spi.xfer([array of bytes]) | Sendet ein Byte-Array, CEx wird vor jedem Byte aktiv und dann wieder inaktiv |
spi.xfer2([array of bytes]) | Sendet ein Byte-Array, dabei bleibt CEx dauerhaft aktiv |
Das folgende Demoprogramm öffnet die zweite SPI-Schnittstelle und blinkt mit 2 Hz:
#!/usr/bin/python import spidev from time import sleep DELAY = 0.5 spi = spidev.SpiDev() spi.open(0,0) while True: spi.xfer([0,0,0]) # turn all LEDs off time.sleep(DELAY) spi.xfer([1,255,255]) # turn all LEDs on time.sleep(DELAY) # end while
Neben dem Portexpander ist wohl der A/D-Wandler MCP3008 einer der beliebtesten SPI-Bausteine. Der Raspberry Pi hat von Haus aus keine analogen Ein- und Ausgänge. Der MCP3008 wandelt analoge Spannungen an seinen acht Eingängen in binäre Daten um und überträgt sie per SPI zum Raspberry Pi. Der MCP3008 hat eine Auflösung von 10 Bit. Die zu messende, analoge Spannung wird in 1024 Schritte unterteilt. Die kleinstmögliche Einheit errechnet sich damit zu Step = VRef/1024. Bei einer Referenzspannung von 3,3 V ergibt sich: Step = 3,3/1024 = 0,003222, also ca. 3,22 mV. Das folgende Bild zeigt die Konfigurationsbits zum Ansprechen der einzelnen Kanäle und den Ablauf der Kommunikation:
Dazu ein Beispiel für Kanal 0. Zuerst muss das CS-Signal auf Low gezogen werden um den Chip anzusprechen, was die Methoden xfer() oder xfer2() automatisch erledigen. Nun senden wir zuerst das Startbit (1) und anschliessend das SGL/DIFF Bit (1). Die nächsten drei Bits bestimmen den Kanal (0 0 0). Alle darauffolgend gesendeten Bits sind "don't care". Zuletzt muss ein Dummybyte (0x00) gesendet werden. Daraus ergibt sich der folgende Python-Aufruf: spi.xfer([0x01, 0x80,0x00]) . Ein komplettes Python-Programm könnte folgendermassen aussehen:
#!/usr/bin/python import spidev import time spi = spidev.SpiDev() spi.open(0,1) while True: antwort = spi.xfer([1,128,0]) time.sleep(0.01) # Wandlung abwarten wert = ((antwort[1] * 256) + antwort[2]) * 0.00322 print wert ," V" time.sleep(1) # end whileDurch die Muliplikation mit 0.00322 wird der Dezimalwert in die ensprechende Spannung umgerechnet.
Das folgende Bild zeigt den "kleine Bruder", den MCP3004 mit nur vier Eingängen als komplette Schaltung. Ausgangsseitig ist das IC direkt mit dem SPI-Interface des Raspberry verbunden. Die Schaltung kann man gut auf einer Lochrasterplatine aufbauen. Der MCP3004 arbeitet mit 3,3 V.
Man darf nur keine zu großen Spannungen anschließen. Deshalb sind an allen vier Eingängen Spannungsteiler vorgesehen, mit denen das Eingangssignal abgeschwächt werden kann. versehen. Bei Kanal 1 sind dies R1 und R2. Das Eingangssignal wird daher abgeschwächt: Vout = R2/(R1 +R2) * Vin. Für einen Messbereich von 0 bis 5 V nehmen Sie R1 = R2 = 10 kΩ. Das ergibt dann eine Spannung am ADC-Eingang von 0 bis 2,5 V. Für einen Messbereich von 0 bis 10 V können Sie R1 = 10 kΩ und R2 = 22 kΩ, was am ADC-Eingang maximal 3,125 V ergibt. Für einen Messbereich von 0 bis 3,3 V nehmen Sie für R1 = 1 kΩ, R2 entfällt. Um Signalstörungen zu reduzieren, können Sie noch je einen kleinen Kondensator von 1 bis 10 nF für C1 bis C4 bestücken. Wer ganz sicher sein will, dass dem IC nichts geschieht, kann für ZD1 bis ZD4 noch Z-Dioden mit einer Nennspannung von 3,6 V als Überspannungsschutz bestücken.
Das Programm zum Auslesen ist kurz und übersichtlich. Da der Wandler 10 Bit Auflösung hat, werden zwei Bytes gelesen, wobei vom MSB nur die unteren zwei Bit verwendet werden. Der Wert setzt sich dann zusammen, indem das MSB mit 3 und-verknüpft und mit 256 multipliziert [(rcv[1] & 3) << 8] und dazu das LSB addiert wird. Das Beispiel zeigt das Lesen von Kanal 1, bei den anderen Kanälen funktioniert es auf die gleiche Weise.
#!/usr/bin/python import spidev import time # SPI-Instance erzeugen und den Bus oeffen spi = spidev.SpiDev() spi.open(0,0) def readadc(channel = 0): adc = self.spi.xfer2([1, (8 + channel) << 4, 0]) data = ((adc[1] & 3) << 8) + adc[2] return data # Einleseschleife while True: # Lese ADC-Kanal 0 adcval = readadc(0) # Wert ausgeben Print "ADC = ", adcval # etwas warten time.sleep(1)
Wenn Sie sehen wollen, wie die Bausteine ohne Spilib angesprochen werden könne, sehen Sie sich die beiden folgenden Links mal an: