Arduino SPI-bus – Serial Peripheral Interface

Vom SPI-Bus

SPI (Serial Peripheral Interface) ist ein synchrones serielles Datenübertragungsprotokoll, das von Mikrocontrollern verwendet wird, um über kurze Distanzen schnell miteinander oder mit Peripheriegeräten zu kommunizieren.

Der SPI-Bus ist eine unidirektionale Hochgeschwindigkeits-kommunikationsleitung, sodass eine bidirektionale Kommunikation gleichzeitig erfolgen kann, sodass Geräte gleichzeitig Daten senden und empfangen können.

Der Arduino Mega 2560 Mikrocontroller-Board

Arduino Mega 2560
Werbung – Arduino Mega 2560

Bei der SPI-Datenübertragung gibt es immer einen Controller, meist einen Mikrocontroller, der die Kommunikation überwacht und die Peripheriegeräte steuert. Zuvor war das Steuergerät als „Master“ bekannt, während die Peripheriegeräte als „Slave“ bekannt waren. Arduino unterstützt die Verwendung dieser Terminologie nicht mehr. In jüngerer Zeit wurden sie als „Controller“ und „Peripheral“ bezeichnet.

Werfen wir einen Blick auf die Leitungen des SPI-Busses

Arduino UNO - Spi-Bus-Anschlussstifte

„COPI“ (Controller Out Peripheral In), die Leitung zum Senden von Daten vom Controller an Peripheral geräte, wurde früher „MOSI“ (Master Out Slave In) genannt.

„CIPO“ (Controller In, Peripheral Out), die Leitung zur Datenübertragung vom Peripheral gerät zum Controller, war früher „MISO“ (Master In Slave Out).

„SCK“ Serial Clock (SCK), die vom Controller erzeugten Taktimpulse, die die Datenübertragung synchronisieren. Der Name der Serial Clock hat sich nicht geändert.

„CS“ (Chip Select Pin), jedes Peripheriegerät verfügt über eine Leitung, die vom Controller zum Aktivieren und Deaktivieren von Peripheriegeräten verwendet wird. Standardmäßig ist dieser Pin hoch. Wenn der „CS“-Pin eines Peripheriegeräts auf Low geht, kommuniziert es mit dem Controller. Früher bekannt als „SS“ (Slave Select).

Ein einfacher SPI-Bus zwischen einem Controller-Gerät und einem Peripheriegerät

Im Allgemeinen gibt es drei gemeinsame Leitungen auf dem SPI-Bus zwischen dem Controller-Gerät und den Peripheral geräten, und von der Controller-Seite aus ist eine Leitung mit jedem Peripheriegerät verbunden, mit der der Controller auswählt, mit welchem ​​Peripheriegerät er kommunizieren möchte.

Während der Kommunikation hält der Controller die CS-Leitung auf einem niedrigen Signalpegel. Der Controller sendet Daten über die COPI-Leitung (MOSI) und das für die Kommunikation verwendete Taktsignal über die SCK-Leitung. Wenn das Controller-Gerät (Master) auf eine Antwort vom Peripheral gerät (Slave) wartet, sendet es weiterhin Taktsignale, bis Daten am CIPO-Pin (MISO) empfangen werden. Dies ist die unabhängige Slave/Peripheral konfiguration.

SPI-Bus – unabhängige Slave-/Peripheriekonfiguration.

In einer Daisy-Chain-Konfiguration benötigt das Controller-Gerät nur einen CS-Pin, um mit allen Peripheriegeräten zu kommunizieren. Der Controller zieht die CS-Leitung auf Low, um die Kommunikation zu starten, und signalisiert allen Peripheriegeräten, sich auf den Empfang von Daten an den COPI-Pins vorzubereiten. Der Controller hält die CS-Leitung für die Dauer der Kommunikation niedrig.

Das Controller-Gerät sendet dann Daten über den COPI-Pin an das erste Peripheral Gegerät in der Kette. Der Controller sendet das für die Kommunikation verwendete Taktsignal auf der SCK-Leitung. Die vom Controller gesendeten Daten fließen dann von Peripheriegerät zu Peripheriegerät. Das Controller-Gerät muss genügend Taktsignale erzeugen, damit die Daten das letzte Peripheriegerät in der Kette erreichen.

Beim Warten auf eine Antwort von einem Peripheriegerät taktet das Controller-Gerät weiter, bis alle Daten am CIPO-Pin empfangen wurden.

SPI-Bus – Daisy-Chain-Konfiguration

Die SPI-Bibliothek muss nicht separat installiert werden, sie ist Teil der Arduino-IDE. Fügen wir es einfach am Anfang der Skizze ein.

#include <SPI.h>

SPISettings

SPISettings wird verwendet, um den SPI-Port des SPI-Geräts zu konfigurieren. Das SPISettings-Objekt verfügt über 3 Parameter: die Kommunikationsgeschwindigkeit; die Bitreihenfolge; sowie Taktphase und Polarität.

Der erste Parameter ist die Übertragungsgeschwindigkeit des SPI, diese muss in Hertz angegeben werden. Ermöglicht das Festlegen der SPI-Baudrate, die von der SPI-Klasse verwendet wird. Der Wert kann bei den meisten Arduinos 4, 8, 16, 32, 64, 128 oder 256 MHz betragen.

Der zweite Parameter, die Bitreihenfolge, bestimmt, mit welchem ​​Bit die Übertragung beginnt (LSBFIRST oder MSBFIRST).

Der dritte Parameter ist einer von vier Übertragungsmodi: SPI_MODE0, SPI_MODE1, SPI_MODE2, SPI_MODE3

Diese Übertragungsmodi bestimmen, ob die Datenübertragung erfolgt mit der steigenden oder fallenden Flanke des Takts(Taktphase) und ob der Takt im Leerlauf hoch oder niedrig ist (dies ist die Taktpolarität). 

Die Polarität und Phase des Taktsignals gemäß den vier Übertragungsmodi finden wir in der Tabelle:

ÜbertragungsmodusTaktpolarität (CPOL)Taktphase (CPHA)Ausgabedaten-OffsetEingabedatenaufzeichnung
SPI_MODE000Fallenden FlankeSteigende Flanke
SPI_MODE101Steigende FlankeFallenden Flanke
SPI_MODE210Steigende FlankeFallenden Flanke
SPI_MODE311Fallenden FlankeSteigende Flanke

CPOL bestimmt die Polarität des Taktsignals.

CPOL=0 ist das Taktsignal, das standardmäßig 0 ist und alle Impulse 1 sind. Die Vorderflanke ist also eine steigende Flanke. 

Wenn CPOL=1, ist der Takt bei 1 im Leerlauf und jeder Zyklus besteht aus 0 Impulsen. Das bedeutet, dass die Vorderkante eine Fallenden Flanke ist.

Der CPHA bestimmt den zeitlichen Ablauf (dh die Phase) der Datenbits in Bezug auf die Taktimpulse

Bei CPHA=0 schaltet die Ausgangsseite die Daten an der fallenden Flanke des vorherigen Taktzyklus um, während die Empfangsseite die Daten an der ansteigenden Flanke des Taktzyklus (oder kurz danach) erfasst. Auf der Ausgangsseite sind die Daten bis zur Fallenden Flanke des aktuellen Taktzyklus gültig. Im ersten Zyklus muss das erste Bit vor dem Lead-In-Takt auf der COPI-Leitung liegen.

Bei CPHA=1 taktet die Ausgangsseite die Daten an der steigenden Flanke des aktuellen Taktzyklus weiter, während die Empgangsseite die Daten an (oder kurz danach) der abfallenden Flanke des Taktzyklus erfasst die Daten. Auf der Ausgangsseite sind die Daten bis zur steigenden Flanke des nächsten Taktzyklus gültig. Im letzten Zyklus hält das Peripheriegerät die CIPO-Wartesclange, bis die Auswahl des Peripheriegeräts aufgehoben wird.

Phase und Polarität des SPI-Bus-Takts

Das SPISettings-Objekt muss als Parameter für SPI.beginTransaction() verwendet werden .

Zurück zum Inhaltsverzeichnis.

begin()

begin() initialisiert den SPI-Bus. Dies muss in setup() erfolgen, bevor andere SPI-Funktionen verwendet werden. Die Funktion begin() hat keinen Rückgabewert.

void setup()
{
  SPI.begin();

  .....
}

Zurück zum Inhaltsverzeichnis.

beginTransaction()

Die Funktion beginTransaction() initialisiert den SPI-Bus mithilfe des SPISettings– Objekts.

Die Funktion beginTransaction() wird normalerweise vor der SPI-Kommunikation aufgerufen, um die Kommunikationsparameter festzulegen. Zu diesen Parametern gehören die SPI-Busgeschwindigkeit, die Bitreihenfolge und die Kommunikationsmodi.

Die Verwendung eines konstanten Werts als Parameter führt zu kleinerem und schnellerem Code.

SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));

Wenn einer der Parameter variabel ist, können wir eine Instanz des SPISettings-Objekts erstellen, um die Variablen für die drei Einstellungen zu speichern.

Anschließend können wir die Objektinstanz an die Funktion SPI.beginTransaction() übergeben. Diese Methode ist möglicherweise effizienter, wenn die Einstellungen nicht konstant sind.

SPISettings mySetting(speedMaximum, dataOrder, dataMode);

SPI.beginTransaction(mySettings);

Die Funktion beginTransaction() hat keinen Rückgabewert.

Zurück zum Inhaltsverzeichnis.

endTransaction()

Um die SPI-Kommunikation zu beenden, verwenden wir die Funktion SPI.endTransaction() in Ihrem Arduino-Code. Diese Funktion schließt die aktuelle SPI-Transaktion und gibt die SPI-Hardware für neue Transaktionen frei.

Die Verwendung der Funktion SPI.endTransaction() ist optional, da die SPI-Kommunikation automatisch beendet wird, wenn der Arduino-Code ausgeführt wird. Wenn die SPI-Kommunikation jedoch lange dauert oder während der SPI-Kommunikation ein Fehler auftritt, empfiehlt es sich, die Funktion SPI.endTransaction() zu verwenden, um die Kommunikation zu beenden und die Hardware freizugeben.

Es hat keinen Rückgabewert.

SPI.endTransaction();

Zurück zum Inhaltsverzeichnis.

end()

Die Funktion SPI.end() deaktiviert den SPI-Bus (die Pin-Modi bleiben unverändert). Es hat keinen Rückgabewert. Der Controller kann nicht mehr mit diesem Gerät kommunizieren, bis er die SPI-Kommunikation neu initialisiert.

SPI.end();

Zurück zum Inhaltsverzeichnis.

setBitOrder()

Diese Funktion ist veraltet! In neuen Projekten verwenden wir stattdessen die Funktion SPI.beginTransaction() mit dem SPISettings- Objekt, um die Parameter zu konfigurieren.

In der Arduino SPI-Bibliothek bestimmt diese Funktion setBitOrder(), mit welchem ​​Bit die Übertragung gestartet werden soll. Sein Parameter kann LSBFIRST oder MSBFIRST sein. Die Funktion setBitOrder() hat keinen Rückgabewert.

SPI.setBitOrder(order);

Zurück zum Inhaltsverzeichnis.

setClockDivider()

Diese Funktion ist veraltet! In neuen Projekten verwenden wir stattdessen die Funktion SPI.beginTransaction() mit dem SPISettings-Objekt, um die Parameter zu konfigurieren.

Mit der Funktion SPI.setClockDivider() kann die SPI-Kommunikationsgeschwindigkeit eingestellt werden.

Die Funktion SPI.setClockDivider() verfügt über einen einzelnen Parameter, der den SPI-Taktteiler relativ zur Systemuhr festlegt. Verfügbare Teiler auf AVR-basierten Karten sind 2, 4, 8, 16, 32, 64 oder 128.

Die Standardeinstellung ist SPI_CLOCK_DIV4, wodurch der SPI-Takt auf ein Viertel der Systemtaktfrequenz eingestellt wird. Bei einem Motherboard mit einem 16-MHz-Takt beträgt der SPI-Bus-Takt also 4 MHz.

SPI.setClockDivider(divider);

Die Parameter der Funktion können sein:

SPI_CLOCK_DIV2
SPI_CLOCK_DIV4
SPI_CLOCK_DIV8
SPI_CLOCK_DIV16
SPI_CLOCK_DIV32
SPI_CLOCK_DIV64
SPI_CLOCK_DIV128

Die Funktion setClockDivider() hat keinen Rückgabewert.

Zurück zum Inhaltsverzeichnis.

setDataMode()

Diese Funktion ist veraltet! In neuen Projekten verwenden wir stattdessen die Funktion SPI.beginTransaction() mit dem SPISettings- Objekt, um die Parameter zu konfigurieren.

Die Funktion setDateMode() legt den SPI-Datenmodus, die Taktpolarität und die Phase fest. Sein Parameter ist einer von vier Übertragungsmodi: SPI_MODE0, SPI_MODE1, SPI_MODE2, SPI_MODE3

SPI.setDataMode(mode);

Zurück zum Inhaltsverzeichnis.

transfer()

Mit der Funktion SPI.transfer() kann der Controller (Master) Daten an das Peripheriegerät (Slave) senden und Daten vom Peripheriegerät empfangen.

Die Verwendung der Funktion SPI.transfer() ist sehr einfach. Die Funktion sendet oder empfängt ein einzelnes Byte auf dem SPI-Bus. Die Funktionssyntax lautet wie folgt:

byte SPI.transfer(byte data);

Der einzige Eingabeparameter der Funktion sind die zu sendenden Daten und sie gibt ein Byte zurück, das die empfangenen Daten enthält.

// Daten an Peripheral-gerät senden
SPI.transfer(byte data);

// Daten von Peripheral-gerät empfangen
byte response = SPI.transfer(byte data);

Bei der SPI.transfer(buffer, size)-Übertragung werden die empfangenen Daten lokal im Puffer gespeichert. Während der SPI-Transaktion werden die Daten im Puffer mit den empfangenen Daten überschrieben, sodass die gesendeten Daten durch die empfangenen Daten ersetzt werden.

Die Funktion erwartet zwei Parameter, „buffer“, in dem die Daten gespeichert werden, und „size“, der die Größe der Daten angibt.

// Daten über den SPI-Bus senden
byte buffer[] = {0x01, 0x02, 0x03, 0x04};
int size = sizeof(buffer);
SPI.transfer(buffer, size);
  
// Datenempfang auf dem SPI-Bus
byte receivedData[size];
SPI.transfer(receivedData, size);

Zurück zum Inhaltsverzeichnis.

usingInterrupt()

Die Verwendung der Funktion SPI.usingInterrupt() kann hilfreich sein, wenn der Arduino während der SPI-Kommunikation andere Aufgaben ausführen muss. Durch die Handhabung von SPI-Kommunikationsinterrupts kann der Arduino Datenübertragungen und Fehler effizienter verarbeiten.

Die Funktion SPI.usingInterrupt() ermöglicht es dem Arduino, Interrupts während der SPI-Kommunikation zu verwenden. Der Parameter interruptNumber gibt die Sequenznummer des Interrupts an, normalerweise 0 oder 1, je nachdem, welche Interrupt-Leitung verwendet wird.

Der im Funktionsparameter usingInterrupt() angegebene Interrupt wird beim Aufruf von beginTransaction() deaktiviert und nach dem Aufruf von endTransaction() wieder aktiviert .

SPI.usingInterrupt(interruptNumber)

Die Funktion usingInterrupt() hat keinen Rückgabewert.

Zurück zum Inhaltsverzeichnis.

SPI-Beispielcodes

Sehen wir uns zunächst ein Beispiel für die Implementierung der SPI-Kommunikation zwischen zwei Arduino UNOs an. Die SPI-Port-Pins sind Pin 13 ->SCK, Pin 12-> CIPO und Pin 11-> COPI. Im Beispiel wird Pin 10 die CS-Leitung sein, wir können sie frei wählen. Es ist wichtig, die GND-Pins auch zu verbinden!

Verbinden wir die beiden Geräte gemäß der Abbildung unten.

SPI-Kommunikation zwischen zwei Arduino Unos

Das Controller-Gerät steuert die Kommunikation und sendet die Daten an das Peripheriegerät. Das Peripheriegerät kann nur dann Daten senden, wenn das Controller-Gerät dies angefordert hat. Bei der SPI-Kommunikation müssen beide Geräte die gleichen Einstellungen verwenden, wie z. B. Baudrate und Kommunikationsmodi.

Im folgenden einfachen Beispiel sendet der Controller ein Byte an das Peripheriegerät und wartet dann 1 Sekunde. Perpheral empfängt den Wert und gibt ihn dann an den seriellen Monitor aus.

Beispielcode für das Controller-Gerät, laden wir ihn auf eines der Arduino UNOs hoch.

#include <SPI.h>

const char chipSelectPin = 10;

void setup()
{
  pinMode(chipSelectPin, OUTPUT);
  digitalWrite(chipSelectPin, HIGH);
  SPI.begin();
  Serial.begin(9600);
}

void loop()
{
  digitalWrite(chipSelectPin, LOW);
  SPI.transfer(0x01);
  digitalWrite(chipSelectPin, HIGH);
  delay(1000);
}

Und das ist der Code für das Peripheral-Gerät, das auf dem zweiten Arduino UNO läuft.

#include <SPI.h>

const char chipSelectPin = 10;
int data = 0;

void setup()
{
  pinMode(chipSelectPin, INPUT);
  SPI.begin();
  Serial.begin(9600);
}

void loop()
{
  if (digitalRead(chipSelectPin) == LOW)
  {
    data = SPI.transfer(0x00);
    Serial.println(data);
  }
}

Das folgende Beispiel zeigt die SPI-Kommunikation zwischen einem Arduino Uno und einem 74HC595-Schieberegister-IC. Der 74HC595 IC ist eine der einfachsten und günstigsten Porterweiterungs-lösungen.

In der Funktion setup() stellen wir die SPI-Kommunikation ein und legen den Arduino-Pin 10 als Ausgang fest. Dadurch wird die für die SPI-Kommunikation verwendete CS-Leitung (Chip Select) geladen. Diese ist mit dem Latch-Pin des 74HC595 IC verbunden.

In der Funktion „loop()“ erstellen wir eine Bytevariable, dann senden wir im Zyklus „for()“ das Byte an den 74HC595-IC, verschieben dann die Bits des Bytes um 1 Bit nach links und senden es im nächsten Zyklus erneut . Die for()-Schleife wird 8 Mal ausgeführt und beginnt von vorne.

Um die Kommunikation zu starten, wird der Latch-Pin des 74HC595 IC auf Low gesetzt, damit der IC Daten empfangen kann, und dann werden die Daten mithilfe der Funktion SPI.transfer() übertragen. Abschließend wird der Latch-Pin auf High gesetzt, damit die übertragenen Daten am Ausgang des 74HC595 IC angezeigt werden können.

SPI-Kommunikation zwischen einem Arduino Uno und einem 74HC595-Schieberegister-IC

Sehen wir uns den Beispielcode an:

#include <SPI.h>

const char latchPin = 10;            // 74HC595 IC latch pin

void setup()
{
  SPI.begin();                       // SPI-bus Initialisierung
  pinMode(latchPin, OUTPUT);         // Latch-Pin als Ausgang festlegen
}

void loop()
{
  byte data = 0b00000001;            // Initialisierung des Ausgabe-Byte
  for (int i = 0; i < 8; i++)
  {
    digitalWrite(latchPin, LOW);     // Latch-Pin auf niedrig stellen
    SPI.transfer(data);              // Datenübertragung über SPI-bus
    digitalWrite(latchPin, HIGH);    // Latch-Pin auf hoch stellen
    data = data << 1;                // 1 Bit nach links verschieben
    delay(300);                      // 300ms warten
  }
}

An die mit dem 74HC595 IC verbundenen LEDs können auch Relais angeschlossen werden, sodass wir mit einem geeigneten Arduino-Diagramm viele Geräte mit ein paar Arduino-Pins verbinden können.

Im dritten Beispiel verbinden wir eine 7-Segment-Anzeige mit gemeinsamer Kathode über den IC 74HC595 mit dem SPI-Bus.

Verbinden wir den 74HC595 IC und den Arduino UNO basierend auf dem vorherigen Beispiel. Die LEDs werden nun durch eine 7-Segment-Anzeige ersetzt.

Verbinden wir die Pins Q0-Q6 des 74HC595 IC mit den Pins a bis g der 7-Segment-Anzeige über einen 330-Ohm-Widerstand und den Q7-Pin ebenfalls über einen 330-Ohm-Widerstand mit dem dp-Pin der 7-Segment-Anzeige.

Wir verbinden eine 7-Segment-Anzeige über den 74HC595 IC am SPI-Bus mit einem Arduino Uno

Die folgende Skizze druckt die Ziffern 0 bis 9 auf der 7-Segment-Anzeige. Aus Gründen der Übersichtlichkeit fügen wir zwischen der Anzeige der Ziffern eine Verzögerung von einer Sekunde in die for()-Schleife ein.

#include <SPI.h>

const char latchPin = 10;

byte digits[] = {
  B11111100,      // 0
  B01100000,      // 1
  B11011010,      // 2
  B11110010,      // 3
  B01100110,      // 4
  B10110110,      // 5
  B10111110,      // 6
  B11100000,      // 7
  B11111110,      // 8
  B11110110       // 9
};

void setup()
{
  pinMode(latchPin, OUTPUT);
  SPI.begin();
}

void loop()
{
  for (int i = 0; i < 10; i++)
  {
    digitalWrite(latchPin, LOW);

    SPI.transfer(digits[i]);
    digitalWrite(latchPin, HIGH);
    delay(1000);
  }
}

Anhand dieser wenigen Beispiele können wir erkennen, wie einfach wir die Möglichkeiten des Arduino SPI-Busses nutzen können.

Zurück zum Inhaltsverzeichnis.