Raspberry Pi: SPI-Schnittstelle


Prof. Jürgen Plate

Raspberry Pi: SPI-Schnittstelle

Grundlagen

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 7
Das 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: Der Mikrocontroller unterstützt intern drei Chip-Select-Leitungen, es sind aber nur zwei (CS0, CS1) auf die Steckerleiste herausgeführt. Es lassen sich also direkt über den Treiber nur zwei Geräte ansprechen. Manuell könnte man aber auch andere GPIO-Pins für das CS-Signal weiterer SPI-Devices generieren, nur das muss dan auch im Programm "von Hand" erledigt werden.

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_bcm2835
Der 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 die Datenübertragung können 8-Bit-Worte (Normalfall) oder auch 9-Bit-Worte bei LoSSI-Modus) verwendet werden.

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.c
Danach starten Sie das Programm
./spidev_test -D /dev/spidev0.0
Das 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:

Das Senden und Empfangen geschieht immer gleichzeitig, bei jedem Takt wird ein Bit vom MOSI des Masters versendet und über MISO ein Bit des Slaves empfangen. Schickt also der Master ein Kommando an den Slave, erhält er die Antwort auf die vorherige Anforderung.

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

Programmierung mit C

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 port
Der 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;
  }

Programmierung mit Python

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:

Um den MCP23S17 ohne Bibliothek anzusteuern, werden jeweils drei Bytes geschrieben:
  1. Die Adresse des MCP23S17. Im folgenden Beispiel wird die Adresse 0x40 verwendet.
  2. Das Register, in das geschrieben wird. Im Beispiel ist dies das Register GPIOB für die Steuerung der acht Pins GPB0 bis GPB7.
  3. Der Wert, der in das Register geschrieben werden soll.
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 install
Wenn 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 spidev
Zum Testen, ob alles geklappt hat, kann man nun Python mit Root-Rechten starten (sudo python und interaktiv die Spidev-Library testen:
import spidev
Wenn 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

Liste der Spidev-Properties

PropertyFunktion
bits_per_wordSetzen/Lesen der Wortlänge (8..16)
cshighLesen/Setzen CS auf active high
lsbfirstLesen ob das LSB zuerst oder zuletzt gesendet wird
max_speed_hzLesen/Setze Datenrate
modeLesen/Setzen des SPI-Mode (Taktpolarität CPOL, Phase CPHA) (0b00..0b11)
threewireLesen/Setzen "I/SO signals shared" → nur eine Datenleitung (nur bei manchen Devices möglich)

Liste der Spidev-Methoden

MethodeFunktion
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 while
Durch 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:

Copyright © Hochschule München, FK 04, Prof. Jürgen Plate
Letzte Aktualisierung: