Kampis Elektroecke

XMega – SPI

XMega Blockdiagramm

In diesem Teil des Tutorials möchte ich das SPI der XMega Mikrocontroller vorstellen und zeigen wie man es benutzt. Ähnlich wie beim I2C Tutorial werde ich auch hier zeigen, wie der XMega als SPI Master, bzw. als SPI Slave genutzt werden kann.

XMega als SPI-Master:

USART als SPI-Master im normalen Modus

Neben den SPI-Modulen besteht die Möglichkeit, dass das USART-Interface des XMegas als SPI-Master konfiguriert wird. Im ersten Beispiel zeige ich, wie der USARTD0 des XMegas als SPI-Master genutzt werden kann. 

Das USART-Modul soll als Sender und Empfänger genutzt werden. Als erstes müssen die verwendeten I/Os entsprechend ihrer Funktion als Aus-, bzw. Eingang geschaltet werden, wobei in diesem Beispiel kein Slave Select verwendet wird

Signal Pin Richtung
SCK 1 Ausgang
MISO 2 Eingang
MOSI 3 Ausgang
PORTD.DIRSET = (0x01 << 0x03) | (0x01 << 0x01);
PORTD.DIRCLR = (0x01 << 0x02);

Damit der USART als SPI-Master genutzt werden kann, muss über das CTRLB-Register das Sendemodul und wahlweise das Empfangsmodul (wenn man Daten empfangen möchte) aktiviert werden.

USARTD0.CTRLB = USART_RXEN_bm | USART_TXEN_bm;

Über die CMODE-Bits des CTRLC-Registers wird das USART-Modul anschließend in den Master SPI Modus gesetzt und durch das UDORD-Bit wird festgelegt, ob das erste gesendete Bit das MSB oder das LSB eines Datenwortes ist. Wird das Bit gesetzt, so wird das LSB als erstes gesendet, andernfalls das MSB. In diesem Beispiel soll das MSB als erstes gesendet werden, weshalb das Bit nicht gesetzt wird.

Über das UCHPA-Bit wird dann der SPI-Modus ausgewählt. Dieser wird üblicherweise anhand der Datenblätter der Busteilnehmer ausgewählt und ergibt sich folgendermaßen:

Modus CPOL (Taktpolarität) CPHA (Taktphase) Beschreibung
0 0
Clock Idle Low
0
Daten bei erster Flanke
Daten werden bei steigender Flanke übernommen und bei fallender Flanke angelegt
1 0
Clock Idle Low
1
Daten bei zweiter Flanke
Daten werden bei steigender Flanke angelget und bei fallender Flanke übernommen
2 0
Clock Idle High
0
Daten bei erster Flanke
Daten werden bei fallender Flanke übernommen und bei steigender Flanke angelegt
3 0
Clock Idle High
1
Daten bei zweiter Flanke
Daten werden bei fallender Flanke angelegt und bei steigender Flanke übernommen

Im USART-SPI Modus des XMegas muss zusätzlich noch der SCK-Pin des USART-Moduls (D.1) invertiert werden, wenn der Modus 2 oder 3 genutzt wird.

In diesem Beispiel soll der SPI-Modus 0 genutzt werden. Daher muss weder der SCK-Pin invertiert, noch das UCPHA-Bit gesetzt werden. Somit ergibt sich für das CTRLC-Register:

USARTD0.CTRLC = USART_CMODE_MSPI_gc;

Als letztes muss noch die Geschwindigkeit über die BAUDCTRL-Register eingestellt werden. Der Wert für die Baudrate ergibt sich, ähnlich wie beim USART, über die Formel aus Tabelle 23-1 des Datenblattes.

BSEL = \frac{f_{PER}}{2 \cdot f_{BAUD}} - 1

Bei einer Taktfrequenz von 2 MHz und einer SPI-Frequenz von 250 kHz ergibt sich mit der genannten Formel der Wert 0x03. Dieser Wert wird nun in die BAUDCTRLA-Register kopiert.

USARTD0.BAUDCTRLA = 0x03;

Jetzt ist das Modul fertig konfiguriert und bereit für den ersten Einsatz.

In diesem Beispiel soll das USART-Modul gleichzeitig als Sender und als Empfänger dienen. Daher müssen die Pins Rx und Tx miteinander gebrückt werden. Anschließend können Daten gesendet werden, indem die Daten in das DATA-Register des USART-Moduls kopiert werden:

uint8_t USART_SPI_SendData(uint8_t Data)
{
	while(!(USARTD0.STATUS & USART_DREIF_bm));
	USARTD0.DATA = Data;

	while(!(USARTD0.STATUS & USART_TXCIF_bm));
	USARTD0.STATUS = USART_TXCIF_bm;
	return USARTD0.DATA;
}

Durch die Abfrage des DREIF-Bit im STATUS-Register wartet das Programm so lange, bis das DATA-Register leer ist (für den Fall, dass noch eine Übertragung aktiv ist) und kopiert anschließend die neuen Daten in das DATA-Register. Nach dem Schreiben wird die Übertragung sofort gestartet und es wird erneut gewartet, bis das DATA-Register leer und die Übertragung abgeschlossen ist. Anschließend wird das TXCIF-Bit gelöscht und das empfangenen Datenbyte ausgelesen und zurück gegeben.

USART als SPI-Master im Interruptbetrieb

Jetzt soll das erste Beispiel so erweitert werden, dass das USART-Modul im Interruptbetrieb betrieben wird. Als erstes wird die Konfiguration für das USART-Modul aus dem vorherigen Beispiel übernommen und um den TXC– und den RXC-Interrupt mit der Priorität Niedrig erweitert:

PORTD.DIRSET = (0x01 << 0x03) | (0x01 << 0x01);
PORTD.DIRCLR = (0x01 << 0x02);
	
USARTD0.CTRLA = USART_RXCINTLVL_LO_gc | USART_TXCINTLVL_LO_gc;
USARTD0.CTRLB = USART_RXEN_bm | USART_TXEN_bm;
USARTD0.CTRLC = USART_CMODE_MSPI_gc;
USARTD0.BAUDCTRLA = 0x03;

Jetzt muss noch ein SPI-Nachrichtenobjekt konstruiert werden. Da der SPI wie ein Ringpuffer funktioniert, können Daten nur gesendet werden, wenn auch gleichzeitig Daten eingelesen werden und andersrum. Dies führt dazu, dass die Menge der Daten, die gelesen, bzw. geschrieben wird, immer gleich ist. Daher muss die SPI-Nachricht je einen Sende- und einen Empfangspuffer, sowie einen Zähler besitzen. Zudem kommt noch die Länge der Übertragung, ein aktueller Übertragungsstatus und die Informationen für den Slave Select hinzu.

Mit diesen Randbedingungen ergibt sich das folgende Nachrichtenobjekt für die SPI-Kommunikation:

typedef struct
{
	uint8_t* BufferOut;
	uint8_t* BufferIn;
	uint8_t BytesProcessed;
	uint8_t Length;
	uint8_t Status;
	PORT_t* Port;
	uint8_t Pin;
} SPI_Message_t;
Feld Funktion
BufferOut Zeiger auf Schreibpuffer
BufferIn Zeiger auf Lesepuffer
Length Länge der Übertragung
BytesProcessed Bytezähler
Status Übertragungsstatus
Port
Zeiger auf Slave Select Port
Pin
Slave Select Pin

Jetzt müssen noch die Funktionen erstellt werden, um ein Gerät am Bus auszuwählen, bzw. abzuwählen. Dies geschieht durch einen Low- oder High-Pegel am SS-Pin des jeweiligen Gerätes.

void USART_SPI_Select(PORT_t* Port, uint8_t Pin)
{
	Port->OUTCLR = (0x01 << Pin);
}

void USART_SPI_Deselect(PORT_t* Port, uint8_t Pin)
{
	Port->OUTSET = (0x01 << Pin);
}

Vor jedem Datentransfer muss der Empfänger über die Select-Funktion angewählt und nach jedem Datentransfer über die Deselect-Funktion abgewählt werden.

Jetzt kann eine neue Übertragung gestartet werden. Dazu wird zuerst ein entsprechendes Nachrichtenobjekt initialisiert, wobei der Pin E.0 als Slave Select genutzt werden soll:

uint8_t SPI_TransmitBuffer[8];
uint8_t SPI_ReceiveBuffer[8];

SPI_Message_t Message = {
	.BufferIn = SPI_ReceiveBuffer,
	.BufferOut = SPI_TransmitBuffer,
	.Length = 8,
	.Port = &PORTE,
	.Pin = 0
};

Das Objekt wird anschließend an die Transmit-Methode übergeben, wodurch die Datenübertragung gestartet wird:

USART_SPI_Transmit(&Message);

Die Transmit-Methode setzt den Zähler zurück, selektiert den gewünschten Busteilnehmer und kopiert das erste Datenbyte in das DATA-Register des USART-Moduls:

void USART_SPI_Transmit(SPI_Message_t* Message)
{
	CurrentMessage = *Message;
	CurrentMessage.BytesProcessed = 0x00;
	CurrentMessage.Status = 0x00;
	USART_SPI_Select(CurrentMessage.Port, CurrentMessage.Pin);
	USARTD0.DATA = CurrentMessage.BufferOut[CurrentMessage.BytesProcessed];
}

Sobald das erste Datenbyte gesendet worden ist, springt der Mikrocontroller in die RXC-ISR, wo das empfangene Datenbyte ausgelesen und damit das Interruptflag gelöscht wird. Das ausgelesene Byte wird dann im Eingangsbuffer gespeichert.

ISR(USARTD0_RXC_vect)
{
	CurrentMessage.BufferIn[CurrentMessage.BytesProcessed] = USARTD0.DATA;
}

Sobald die ISR abgearbeitet ist, springt der Mikrocontroller in die TXC-ISR, wo das nächste Datenbyte in das DATA-Register kopiert wird. Falls alle Daten gesendet worden sind, wird der Busteilnehmer abgewählt und die Variable Status des Nachrichtenobjektes gesetzt, wodurch die Übertragung beendet wird.

ISR(USARTD0_TXC_vect)
{
	if(CurrentMessage.BytesProcessed < (CurrentMessage.Length - 1))
	{
		USARTD0.DATA = CurrentMessage.BufferOut[++CurrentMessage.BytesProcessed];
	}
	else
	{
		USART_SPI_Deselect(CurrentMessage.Port, CurrentMessage.Pin);
		CurrentMessage.Status = 0x01;
	}
}

SPI-Master im normalen Modus:

Im nächsten Beispiel zeige ich, wie das SPI-Modul des XMega als SPI-Master verwendet werden kann. Der XMega dient wieder gleichzeitig als Sender und als Empfänger für die Daten, indem MISO und MOSI vom verwendeten SPI (hier SPIC) miteinander verbunden werden.

Anders als bei Mikrocontrollern mit AVR8 Architektur (z. B. einem Mega32) und analog zum USART müssen bei den XMega-Mikrocontrollern die I/Os für den SPI-Betrieb ebenfalls separat konfiguriert werden. Der SPI verwendet beim XMega256A3BU immer die folgenden Pins:

Pin Funktion
4 SS (nur für SPI-Slave wichtig)
5 MOSI
6 MISO
7 SCK

Als SPI-Master wird MISO automatisch als Eingang deklariert, wohingegen die I/Os für MOSI, SCK und SS vom Anwender als Ausgang deklariert werden müssen. Falls die SPI-Modi 2 und 3 verwendet werden, muss der SCK-Pin zusätzlich noch auf High gezogen werden, da die Taktleitung bei im Idle-Zustand einen High-Pegel führen muss.

PORTC.DIRSET = (0x01 << 0x07) | (0x01 << 0x05) | (0x01 << 0x04);

Achtung:

Wenn der SS-Pin als Eingang deklariert ist und dann das MASTER-Bit im CTRL-Register gesetzt wird, wird dieses Bit automatisch wieder gelöscht!


Anschließend kann das Interface initialisiert werden. In meinem Beispiel soll der SPI als Master im Modus 0 und mit einer Taktfrequenz von 500 kHz betrieben werden. Das CLK2X-Bit zur Verdoppelung der Taktfrequenz soll nicht genutzt werden.

SPIC.CTRL |= (SPI_ENABLE_bm | SPI_MASTER_bm | SPI_MODE_0_gc | SPI_PRESCALER_DIV4_gc);

Damit ist die Konfiguration auch schon abgeschlossen und es können die ersten Daten gesendet werden. Wie auch beim USART ist die Senderoutine auch gleichzeitig eine Empfangsroutine und das DATA-Register des SPI-Moduls für das Senden und Empfangen von Daten zuständig.

uint8_t SPIM_SendData(uint8_t Data)
{
	SPIC.DATA = Data;
	while(!(SPIC.STATUS & SPI_IF_bm));
	return SPIC.DATA;
}

Durch die Abfrage des IF-Bits im STATUS-Register kann die Software überprüfen, ob das Datenbyte komplett übertragen und ein gesendetes Byte eingelesen worden ist.

Mit dieser Funktion kann auch schon die Übertragung eines Datenbytes gestartet werden:

SPIM_Init();
Received = SPIM_SendData(0x1C);

Durch die Brücke zwischen MOSI und MISO enthält die Variable Received am Ende der Übertragung den gesendete Wert 0xAA. Mehrere Datenbytes können über eine Schleife empfangen, bzw. gesendet werden.

SPI-Master im Interruptbetrieb:

Als nächstes zeige ich, wie eine Implementierung mit Interrupts aussehen könnte. Auch hier muss der SPI erst einmal initialisiert werden, wobei sich die Initialisierung nicht groß von der Initialisierung aus dem ersten Beispiel unterscheidet.

PORTC.DIRSET = (0x01 << 0x07) | (0x01 << 0x05) | (0x01 << 0x04);
	
SPIC.CTRL |= (SPI_ENABLE_bm | SPI_MASTER_bm | SPI_MODE_0_gc | SPI_PRESCALER_DIV4_gc);
SPIC.INTCTRL = SPI_INTLVL_LO_gc;

Der einzige Unterschied ist, dass mit der Zeile

SPIC.INTCTRL = SPI_INTLVL_LO_gc;

die SPI-Interrupts mit der Priorität Niedrig aktiviert werden. Als nächstes wird wieder ein neues Nachrichtenobjekt erzeugt und initialisiert. Anschließend wird das Objekt in die Transmit-Funktion des SPI-Masters gegeben, wodurch die Übertragung gestartet wird:

SPI_Message_t Message = {
	.BufferIn = SPI_ReceiveBuffer,
	.BufferOut = SPI_TransmitBuffer,
	.Length = 8,
	.Port = &PORTC,
	.Pin = 0
};

SPIM_Transmit(&Message);

Genau wie beim USART initialisiert die Transmit-Funktion wieder den Zähler, wählt den entsprechenden Busteilnehmer an und startet die Übertragung, indem das erste Datenbyte in das DATA-Register geladen wird:

void SPIM_Transmit(SPI_Message_t* Message)
{
	CurrentMessage = *Message;
	CurrentMessage.BytesProcessed = 0x00;
	CurrentMessage.Status = 0x00;
	SPIM_Select(CurrentMessage.Port, CurrentMessage.Pin);
	SPIC.DATA = CurrentMessage.BufferOut[CurrentMessage.BytesProcessed];
}

Sobald das Datenbyte gesendet worden ist, wird der SPI-Interruptvektor aufgerufen. Beim SPI wird der entsprechende Interruptvektor jedes mal ausgelöst, wenn ein Datenbyte erfolgreich gesendet, bzw. empfangen worden ist.

In der ISR wird das empfangene Datenbyte dann eingelesen und im Empfangspuffer gespeichert. Anschließend wird der Zähler inkrementiert und das nächste Datenbyte in das DATA-Register kopiert.

ISR(SPIC_INT_vect)
{
	Message.BufferIn[Message.BytesProcessed] = SPIC.DATA;
	
	if(Message.BytesProcessed < (Message.Length - 1))
	{
		SPIC.DATA = Message.BufferOut[++Message.BytesProcessed];
	}
	else
	{
		SPIM_Deselect(Message.Port, Message.Pin);
		Message.Status = 0x01;
	}
}

Wenn alle Daten gesendet worden sind, wird das Gerät abgewählt und der Status auf 0x01 gesetzt. Damit ist die Übertragung ist abgeschlossen.

XMega als SPI-Slave:

Im letzten Beispiel möchte ich zeigen, wie ein SPI-Slave implementiert werden kann. Für dieses Beispiel verwende ich das SPIC des XMegas als SPI-Slave und den USARTD0 als SPI-Master. Der SS-Pin des SPI-Slaves ist mit dem Pin E.0 verbunden. Anders als beim vorangegangenen Beispiel werde ich für den SPI-Slave nur eine interruptbasierende Lösung vorstellen.

Zu Anfang müssen wieder erst die I/Os konfiguriert werden. Zuerst wird der Pin E.0 als Ausgang geschaltet und auf High gesetzt:

PORTE.DIR |= (0x01 << 0x00);
PORTE.OUTSET = (0x01 << 0x00);

Dieser Pin dient von jetzt an als Slave Select für den SPI-Master. Als nächstes wird der SPI-Master, also der USARTD0, initialisiert und die Nachricht vorbereitet.

USART_SPI_InitInterrupt();

SPI_Message_t Message = {
	.BufferIn = SPI_ReceiveBuffer,
	.BufferOut = SPI_TransmitBuffer,
	.Length = SPI_BUFFER_SIZE,
	.Port = &PORTE,
	.Pin = 0
};

Für den Betrieb als SPI-Slave sind nur die Bits DORD, ENABLE und MODE interessant. Alle anderen Bits haben im Slave-Modus keine Funktion. Außerdem muss noch der MISO-Pin als Ausgang konfiguriert und die Interrupts mit der Priorität Niedrig aktiviert werden.

PORTC.DIRSET = (0x01 << 0x06);

SPIC.CTRL |= (SPI_ENABLE_bm | SPI_MODE_0_gc); 
SPIC.INTCTRL = SPI_INTLVL_LO_gc;

Für den SPI-Slave habe ich mir ebenfalls ein Objekt angelegt, welches alle notwendigen Informationen beinhaltet.

typedef struct 
{
	uint8_t* RxBuffer;
	uint8_t* TxBuffer;
	uint8_t BytesProcessed;
	uint8_t Status;
} SPI_Buffer_t;

static SPI_Buffer_t SlaveBuffer;
Member Funktion
RxBuffer Zeiger auf Empfangspuffer
TxBuffer Zeiger auf Sendepuffer
BytesProcessed Bytezähler
Status Übertragungsstatus

Solange der SS-Pin auf High liegt, kann die Software den Inhalt des DATA-Registers im Slave-Modus nach belieben modifizieren. Erst wenn der SS-Pin auf Low gezogen und damit der Busteilnehmer ausgewählt wird, werden die gespeicherten Daten übertragen. Daher muss vor der ersten Übertragung das erste Byte des Empfangspuffers in das DATA-Register kopiert werden:

SPIC.DATA = SlaveBuffer.TxBuffer[0x00];

Jetzt ist der SPI-Slave einsatzbereit und es kann eine Übertragung vom Master gestartet werden. Sobald das erste Byte empfangen worden ist, springt der Slave in die ISR des SPI-Moduls.

ISR(SPIC_INT_vect)
{
	SlaveBuffer.RxBuffer[SlaveBuffer.BytesProcessed] = SPIC.DATA;
		
	if(SlaveBuffer.BytesProcessed < (SPI_BUFFER_SIZE - 1))
	{
		SPIC.DATA = SlaveBuffer.TxBuffer[++SlaveBuffer.BytesProcessed];
	}
	else
	{
		SlaveBuffer.BytesProcessed = 0x00;
		SlaveBuffer.Status = 0x01;
	}
}

In der ISR werden die empfangene Bytes ausgelesen und im Empfangspuffer gespeichert. Sobald der Puffer einmal komplett vollgeschrieben wurde, wird Status gesetzt. Dadurch erkennt die Software, dass ein Pufferüberlauf erfolgt ist.

Die komplette Übertragung sieht auf einem Oszilloskop dann folgendermaßen aus:

Über den Pegel des SS-Pins kann der Zustand der Übertragung erkannt werden. Ein Low-Pegel bedeutet, dass der Slave noch ausgewählt ist und somit noch weitere Daten gesendet werden können. Bei einem High-Pegel ist der Slave nicht mehr ausgewählt, wodurch die Übertragung beendet ist.

uint8_t SPIS_ActiveTransmission(void)
{
	return (!(PORTC.IN & (0x01 << 0x04)) >> 0x04);
}

Das komplette Projekt mit allen Programmen steht in meinem GitHub-Repository zum Download bereit.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert