Tutorial STM32 CubeMX – SPI

Eingesetzte Hardware

Board: STM32F429I-DISC1 (SPI Master und Slave onboard)

  • SPI-Master: STM32F429ZI
  • SPI-Slave: L3GD20 MEMS motion sensor

Eingesetzte Software

  • System Workbench for STM32 (SW4STM32)
  • STM32CubeMX Eclipse plug in

Verwendete Abkürzungen

  • SCK, SPC: Serial Clock
  • SS, CS: Slave-Select bzw. Chip-Select
  • SDO, MISO: Slave-Data-Out bzw. Master-In-Slave-Out
  • SDI, MOSI: Slave-Data-In bzw. Master-Out-Slave-In
  • GPIO: General Purpose Input Output
  • DMA: Direct Memory Access
  • FIFO: First In First Out
  • ADC: Analog to Digital Converter
  • SRAM: Static Random Access Memory

Einrichten der Entwicklungsumgebung

Installation

Erstellung des Projekts

Das CubeMX-Plugin kann unter Eclipse z.B. über das “Quick Access” Feld oben links erreicht werden.

CubeMX_0

Anschließend sollte der Startbildschirm erscheinen, in dem dann “New Project” gewählt werden kann.

CubeMX_1

Wie weiter oben bereits erwähnt, wird bei diesem Artikel ein verfügbares Board von ST verwendet. Dies hat den Vorteil, dass zu diesen Boards bereits fertige Hardware-Konfigurationen in CubeMX hinterlegt sind. Ausgewählt werden können diese über den Reiter “Board Selector”.

CubeMX_2

Hat man das entsprechende Board aus der Datenbank ausgewählt, bestätigt man dies durch den OK-Button, woraufhin sich eine Übersicht über den Chip und die aktuelle Hardware-Konfiguration öffnet. In diesem Artikel möchten wir uns auf die Konfiguration der Peripherie-Einheit SPI5 beschränken (rot im Bild markiert)

CubeMX_4

Die Einstellungen zur Code-Generierung kann (nachdem z.B. Änderungen vorgenommen worden sind) über das Menü in der Kopfzeile von CubeMX geöffnet werden (Project -> Generate Code).

CubeMX_5

Nachdem man einen Projekt-Namen und Speicherort angeben hat, muss (bei der Verwendung der Entwicklungsumgebung OpenSTM32) bei Toolchain die Option SW4STM32 ausgewählt werden. Anschließend kann die Generierung durch einen Klick auf den OK-Button gestartet werden. Das Projekt kann danach direkt in den OpenSTM32-Workspace importiert werden (File -> Import ->Existing Projects into Workspace). Folgende Kapitel zeigen Schritt für Schritt die zur Verfügung stehenden Konfigurationsmöglichkeiten.

Konfiguration im Menü „Pinout“

CubeMX_SPI_PIC_1

Mode

Die folgende Tabelle liefert eine Übersicht zu den Optionen, welche im Feld “Mode” gewählt werden können. IC_X repräsentiert dabei eine beliebige, SPI-fähige Komponente. Grau markierte Signale sind bei dem entsprechendem Modus nicht in Verwendung und können damit eingespart werden.

Modus Signal STM32F4 IC_X
Full-Duplex-Master SCK X
SS X
MOSI X
MISO X
Full-Duplex-Slave SCK X
SS X
MOSI X
MISO X
Transmit Only Master SCK X
SS X
MOSI X
MISO
Transmit Only Slave SCK X
SS X
MOSI
MISO X
Receive Only Master SCK X
SS X
MOSI
MISO X
Receive Only Slave SCK X
SS X
MOSI X
MISO

Die Modi Half-Duplex-Slave und Half-Duplex-Master werden zu einem späteren Zeitpunkt in einem gesonderten Artikel behandelt.

Alle Beispiele (Code und Grafiken) beziehen sich im folgenden auf den Full-Duplex-Master-Mode.

Hardware NSS Signal

Hier wird definiert, ob das SS-Signal von der SPI-Peripherie gesteuert wird, oder in der Software gesetzt werden muss. Möchte man das Signal selbst über ein beliebigen GPIO-Pin steuern wählt man die Option ‘Disable’.

Manuelle Steuerung

Bei manueller Steuerung kann man jeden beliebigen GPIO-Pin als SS-Signal-Pin definieren. Möchte man mehrere SPI-Slaves am Bus betreiben und individuell ansprechen können, muss für jeden Slave ein SS-Signal-Pin bereitgestellt werden, wodurch die Hardware-Option nicht mehr verwendet werden kann, da bei diesen Controllern nur ein Signal pro Peripherie unterstützt wird (im Gegensatz zu anderen µController-Familien, wie zum Beispiel XMC von Infineon).

Der Pseudocode zur manuellen Steuerung dazu könnte folgendermaßen aussehen:

// Senden von Daten an Slave 1

lock_SS_01();           // Setze das SS-Signal für Slave 1
SPI_sendData(data);     // Sende Daten an Slave 1
release_SS_01();            // Setze das SS-Signal für Slave 1 zurück

// Senden von Daten an Slave 2

lock_SS_02();           // Setze das SS-Signal für Slave 2
SPI_sendData(data);     // Sende Daten an Slave 2
release_SS_02();            // Setze das SS-Signal für Slave 2 zurück

Steuerung durch die Peripherie

Aktiviert man die Option “Hardware NSS Output Signal” wird das Slave-Select-Signal direkt von der SPI-Peripherie gebildet. Dazu ist folgender Abschnitt aus dem Reference Manual interessant (RM0090, Rev12, S.880/1744) interessant:

“[…] This configuration is used only when the device operates in master mode. The NSS signal is driven low when the master starts the communication and is kept low until the SPI is disabled. […]”

Achtung: Das bedeutet, dass die Hardware dieses Signal nur auf low zieht (kein Push-Pull). Ohne einen entsprechenden Pull-Up-Widerstand bleibt der Pegel dieses Signals ständig auf low, wodurch sich zwei Optionen ergeben:

  • Einbauen eines externen Pull-Up-Widerstands in die Schaltung (aufwändig + teuer)
  • Aktivierung des internen Pull-Ups

Außerdem ist das Slave-Select-Signal so lange aktiv (low), bis die Peripherie deaktiviert wurde (siehe Code-Beispiele im Abschnitt Frame-Format)

Folgendes Bild zeigt, wie und wo der interne Pull-Up des Slave-Select-Pins zu aktiveren ist.

CubeMX_6

Konfiguration im Menü „Configuration“

Nachdem die grundsätzlichen HW-Einstellungen vorgenommen worden sind, muss nun das Interface ean den oder die Teilnehmer angepasst werden. Im folgenden ist beispielsweise das entsprechende Konfigurationsfenster für den IC L3DG20, welcher auf dem verwendeten Board verbaut ist, abgebildet.

 

CubeMX_SPI_Configuration_1

Frame-Format

Falls der Nutzer im Reiter “Pinout” bei “Hardware NSS Signal” die Option “Disable” gewählt hat, ist hier nur der Modus “Motorola” möglich. Bei den folgenden Code-Beispielen wird das Slave-Select-Signal durch die Peripherie erzeugt.

Motorola-Format

Dieses Format ist für die meißten Anwendungen relevant. Im folgenden ist zuerst der Programmcode  und anschließend die resultierenden Bussignale abgebildet:

//Erzeugung eines Arrays mit Testdaten
uint8_t data[4];
data[0] = 1;
data[1] = 2;
data[2] = 3;
data[3] = 4;

//Senden des gesamten Array innerhalb eines Zyklus via SPI5
__HAL_SPI_ENABLE(&hspi5);
HAL_SPI_Transmit(&hspi5, data, 4, 100); 
__HAL_SPI_DISABLE(&hspi5);

 

Motorola_MODE

Eigenschaften:

  • Slave-Select-Signal für die gesamte Dauer der Übertragung auf low
  • Wahlweise MSB oder LSB

TI-Format

Zum TI-Format ist allgemein wenig Information verfügbar. Dieses Format ist nur verfügbar, falls das Slave-Select-Signal direkt von der SPI-Peripherie erzeugt wird. Im folgenden ist zuerst der Programmcode und anschließend die resultierenden Bussignale (mit Anmerkungen) abgebildet:

//Erzeugung eines Arrays mit Testdaten
uint8_t data[4];
data[0] = 1;
data[1] = 2;
data[2] = 3;
data[3] = 4;

//Senden des gesamten Array innerhalb eines Zyklus via SPI5
__HAL_SPI_ENABLE(&hspi5);
HAL_SPI_Transmit(&hspi5, data, 4, 100); 
__HAL_SPI_DISABLE(&hspi5);

 

SPI_TI_MODE

Eigenschaften:

  • Puls auf der Slave-Select-Leitung markiert den Beginn des Pakets, wobei das erste Bit nicht gültig ist (vgl. RM0090, Rev12, S.884/1744)
  • Übergang zwischen den Bytes ist zusätzlich durch einen Puls zum Zeitpunkt des vorrangehenden LSB markiert
  • Nur MSB möglich

Data Size

Anzahl der pro Transfer übertragenen Bits.

First Bit

Damit kann der Nutzer die Bit-Order anpassen.

Prescaler

Entsprechend der Takteinstellungen des Mikrocontrollers, läuft die Peripherie, welche die SPI-Funktion abbildet, intern mit bestimmten Takt (z.B. 25 Mhz). Dieser Takt wird unter anderem dazu verwendet, um den Takt auf der SCK-Leitung zu generieren. Die SCK-Frequenz kann dabei über einen Teilerfaktor eingestellt werden. Die Taktrate muss so eingestellt werden, dass der (kleinste) maximal spezifizierte Wert aller angeschlossenen Teilnehmer nicht überschritten wird.

Clock-Polarity

Gibt den logischen Zustand des Clk-Signals im inaktiven Zustand an. In folgender Abbildung ist beispielsweise ein Transfer für eine Clock-Polarity low gezeigt:

CubeMX_SPI_Configuration_2

Clock-Phase

Gibt an, an welcher Flanke des Clock-Signals die Daten gelesen werden. Der Hintergrund ist, dass das Signal Zeit auf der Leiterbahn (Signallaufzeit) und zum Pegel-Wechsel benötigt. Liegen der Pegelwechsel beim Sender und der Abtastzeitpunkt des Empfängers auf der gleichen Flanke so ist nicht sicher gestellt, dass der Pegelwechsel vor dem Abtasten abgeschlossen ist und es kann zu Bitfehlern kommen. In den folgenden Bildern wird beides Mal das gleiche Byte übertragen, einmal aber zum falschen Zeitpunkt abgetastet (die Pfeile zeigen die Zeitpunkte der Abtastung an).

Richtige Clock-Polarity
Richtige Clock-Polarity
Falsche Clock-Polarity
Falsche Clock-Polarity

 

 

 

 

 

CRC-Calculation

Ist diese Option aktiv, wird jedem Transfer ein zusätzliches Prüf-Bit (bei Datasize gleich 8) oder Byte (bei Datasize gleich 16) angehängt. Folgender Code erzeugt mit der Einstellung X1+X3 die folgenden Signale:

//Erzeugung eines Arrays mit Testdaten
uint8_t data[4];
data[0] = 1;
data[1] = 2;
data[2] = 3;
data[3] = 4;

//Senden des gesamten Array innerhalb eines Zyklus via SPI5
__HAL_SPI_ENABLE(&hspi5);
HAL_SPI_Transmit(&hspi5, data, 4, 100); 
__HAL_SPI_DISABLE(&hspi5);

 

SPI_CRC

Berechnung einer CRC-Prüfsumme: z.B. http://www-stud.rbi.informatik.uni-frankfurt.de/~haase/crc.html)

NSS-Signal Type

Diese Einstellung wird durch die Wahl im Reiter “Pinout” festgelegt und kann an dieser Stelle nicht mehr verändert werden.

 

Codebeispiel ohne DMA

Folgender Code-Abschnitt initialisiert die Peripherie SPI5 und sendet anschließend 4 Byte. Die Antwort des Slaves wird dabei in ein separates Array gespeichert. Die Steuerung des Slave-Select-Signals erfolgt manuell. Zusätzlich wird der Peripherie max. 100 ms Zeit gegeben die Daten zu versenden. Da das Senden ohne DMA oder Interrupts stattfindet, blockiert die Funktion in einer while-Schleife für die Dauer des Sendevorgangs, weswegen ein Timeout an dieser Stelle sinnvoll ist (falls es z.B. zu einem Absturz der Peripherie kommt).

//Speicher für Senden und Empfangen
uint8_t txData[4];
txData[0] = 1;
txData[1] = 2;
txData[2] = 3;
txData[3] = 4;
  
uint8_t rxData[4];

//Enitialisierung der verwendeten Peripherie
__HAL_SPI_ENABLE(&hspi5);
  
//Senden und Empfangen
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_1, GPIO_PIN_RESET);
HAL_SPI_TransmitReceive(&hspi5, txData, rxData, 4, 100);
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_1, GPIO_PIN_SET);

 

Codebeispiel mit DMA

Dieser Abschnitt zeigt kurz, wie der DMA (Direct Memory Acess) für das Senden und Empfangen von SPI-Nachrichten verwendet werden kann. Alle Einstellungen können dabei im Konfigurationsfenster unter dem Reiter “DMA Settings” vorgenommen werden (siehe Screenshot):

CubeMX_SPI_DMA

Mode

  • Normal: Aktivert der Nutzer den DMA (z.B. durch die Sende-Funktion im Code-Beispiel) werden die Daten, auf welche der DMA zeigt, genau einmal übertragen
  • Circular: Die gleichen Daten werden wiederholt gesendet oder gelesen, bis der DMA deaktiviert wird. Beim SPI evtl. weniger sinnvoll als bei einem ADC zum Beispiel.

Increment Address

  • Peripheral: Diese Option ist zwar abgebildet, kann aber nicht gewählt werden. Bei anderen Peripherien können durch diese Option beispielsweise nacheinander verschiedene Kanäle eines ADCs gelesen werden
  • Memory (Am Beispiel des Sendens erklärt): Dem DMA muss mitgeteilt werden, wie viele Bytes via der entsprechenden Peripherie übertragen werden sollen. Falls die Option “Memory” nicht aktiviert ist, wird für die angegebene Länge das gleiche Byte gesendet anstatt nacheinander die Indizes eines Arrays abzuarbeiten.
  • Data Width: In diesem Feld kann die Schrittweite der Inkremente angeben werden (üblicherweise Byte)
  • Use Fifo: Ist diese Option nicht aktiviert, arbeitet die Peripherie im sogenannten “Direct Mode”. Das Fifo kann benutzt werden, um die SRAM-Zugriffe minimal zu halten, die Bandbreite des internen Busses zu optimieren (durch Burst-Transfers) oder um die Daten an die Speicherbreite der Zielperipherie anzupassen.
  • Threshold: Jeder Stream hat ein max. 4 Word (4 Byte * 4) tiefes Fifo. Die Länge bzw. die Schwelle kann über diesen Parameter von 1 – 4 Word variiert werden.
  • Burst Size: Gibt an, wie viele Pakete (definiert durch das Feld “Data Width”) zu einem Burst zusammengefasst werden.

Achtung: Die Anzahl an Bytes pro Burst geteilt durch die Anzahl der Bytes im Fifo muss ein Integer (Ganzzahl) sein. Deswegen sind nur bestimmte Parameter-Kombinationen erlaubt (siehe RM0090, Rev12, S.320/1744).

Beispiel: Senden und Empfangen

In diesem Modus wird sowohl das Senden als auch Empfangen von der DMA-Peripherie übernommen.

Folgender Code-Ausschnitt könnte z.B. in der main-Funktion vor der while-Schleife (jedoch nach der Initialisierung) eingefügt werden.

[...]

//Arrays für Senden und empfangen
uint8_t dataTx[256];
uint8_t dataRx[256];

//Generierung von Dummy-Daten
for (uint16_t i = 0; i < 256; i++) {
	dataTx[i] = i;
	dataRx[i] = 0;
}

//Abschalten beider LEDs auf dem Board
HAL_GPIO_WritePin(GPIOG, GPIO_PIN_14, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOG, GPIO_PIN_13, GPIO_PIN_RESET);

//Senden von 256 Bytes mithilfe der DMA-Peripherie
HAL_SPI_TransmitReceive_DMA(&hspi3, dataTx, dataRx, 256);

[...]

Nachdem der Transfer abgeschlossen ist, gibt es die Möglichkeit durch die Definition einer Callback-Funktion (ist an anderer Stelle als weak deklariert) im Programm entsprechend zu reagieren. Im vorliegenden Beispiel wird nach Abschluss eine LED eingeschaltet.

[...]
#include "stm32f4xx_hal.h"
[...]

//Kann an einer beliebigen Stelle deklariert werden, wodurch das Ereignis
//im Programm entsprechend berücksichtigt wird
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) {
	if(hspi->Instance == hspi3.Instance){
		HAL_GPIO_WritePin(GPIOG, GPIO_PIN_14, GPIO_PIN_SET);
	}

}

 

de_DEDeutsch
en_GBEnglish (UK) de_DEDeutsch