AVR SPI C Snippets/de
From OpenAVR
Contents |
[edit] Was ist SPI?
Eine generelle Beschreibung gibt es bei http://de.wikipedia.org/wiki/Serial_Peripheral_Interface.
Die beste Quelle für AVR spezifische Informationen ist das jeweilige Datenblatt des verwendeten Microcontrollers.
[edit] Alternativen
Für die Kommunikation über SPI werden mindestens drei Signalleitungen benötigt, MOSI, MISO und SCK. Optional gibt es noch eine Select Leitung pro angeschlossenem Device. Wenn das für Ihre Anwendung zu viel ist, sollten Sie TWI (I2C) als Alternative in Betracht ziehen. Dort werden nur zwei Signalleitungen benötigt. Allerdings ist SPI weniger kompliziert und grunsätzlich schneller als TWI, insbesondere wenn größere Datenmengen in beide Richtungen übertragen werden müssen.
[edit] Benutzung der Runtime Bibliothek
AvrLibC bietet keine fertigen Routinen für SPI. Allerdings enthält es eine Reihe von Definitionen, mit deren Hilfe der Code zwischen verschiedenen AVR Controllern portabel gehalten werden kann. Um die Bibliothek zu benutzen, muß die entsprechende Header Datei eingefügt werden.
#include <avr/io.h>[edit] SPI Initialisierung
[edit] Initialisierung des Masters
Zumindest zwei Bits müssen im SPI Control Register gesetzt werden, das SPI Enable Bit und das Master Mode Bit.
SPCR = _BV(SPE) | _BV(MSTR);
Im Gegensatz zu einigen anderen Peripheriebausteinen, werden bei SPI die Ausgänge nicht automatisch aktiviert. Wir müssen dazu explizit die entsprechenden Bits für SCK und MOSI im Datenrichtungsregister setzen. Beim ATmega2561 sind dies PB1 für SCK und PB2 für MOSI.
DDRB |= _BV(1) | _BV(2);
Ist der Master Modus aktiviert, führt ein Low Pegel am Pin SS dazu, dass automatisch in den Slave Modus umgeschaltet wird. Dies kann zu unvorhergesehenen Problemen führen, wenn der SS Pin nicht angeschlossen ist (floating). Für diesen Fall ist es eine gute Idee, den internen Pullup Widerstand an diesem Pin zu aktivieren. Beim ATmega2561 liegt SS an PB0.
/* Check whether PB0 is an input. */ if (bit_is_clear(DDRB, 0)) { /* If yes, activate the pull-up. */ PORTB |= _BV(0); }
Das gleiche gilt auch für den MISO Pin, wo verhindert wird, dass ein nicht angeschlossener Pin zum Empfang zufälliger Daten führt. Die Aktivierung des Pullup Widerstands stellt sicher, dass bei einer Unterbrechung der Leitung immer 0xFF empfangen wird. Beim ATmega2561 liegt MISO and PB3.
PORTB |= _BV(3);
Beim Start sollte sichergestellt sein, dass alle Status Flags im Grundzustand sind. Dies erreicht man durch Lesen des Status- und des Datenregisters.
unsigned char tmp; tmp = SPSR; tmp = SPDR;
Werden alle Teile zusammengefügt, könnte eine Routine zur Initialisierung so aussehen:
void SpiMasterInit(void) { unsigned char tmp; /* Check whether PB0 is an input. */ if (bit_is_clear(DDRB, 0)) { /* If yes, activate the pull-up. */ PORTB |= _BV(0); } /* Switch SCK and MOSI pins to output mode. */ DDRB |= _BV(1) | _BV(2); /* Enable MISO pull-up. */ PORTB |= _BV(3); /* Activate the SPI hardware. */ SPCR = _BV(SPE) | _BV(MSTR); /* Clear status flags. */ tmp = SPSR; tmp = SPDR; }
[edit] Initialisierung des Slaves
Die Initialisierung des Slaves entspricht der des Masters, außer dass das Master Mode Bit nicht gesetzt wird.
SPCR = _BV(SPE);
Selbstverständlich ändern sich außerdem die Datenrichtungen. MISO (PB3 beim ATmega2561) wird ein Ausgang, alle übrigen Leitungen sind Eingänge.
DDRB |= _BV(3);
Die endgültige Routine könnte so aussehen:
void SpiSlaveInit(void) { unsigned char tmp; /* Switch MISO pin to output mode. */ DDRB |= _BV(3); /* Enable MOSI pull-up. */ PORTB |= _BV(2); /* Activate the SPI hardware. */ SPCR = _BV(SPE); /* Clear status register flags. */ tmp = SPSR; tmp = SPDR; }
[edit] SPI Konfigurationen
Zwei Parameter sind bei der AVR Plattform konfigurierbar, der SPI Modus und die Übertragungsgeschwindigkeit.
[edit] SPI Modus
[edit] SPI Datenrate
[edit] Polling Mode Data Transfer
Die Übertragung startet automatisch, sobald ein Datenbyte in das SPI Datenregister geschrieben wird.
SPDR = tx_data;
Wir können das SPIF Bit im Status Register abfragen, um festzustellen, wann die Übertragung des Datenbytes beendet ist.
loop_until_bit_is_set(SPSR, SPIF);
Machen Sie sich nochmal klar, dass bei SPI immer ein Byte glecihzeitig gesendet und empfangen wird. Nach jedem Sendevorgang ist das gleichzeitig empfangene Datenbyte im SPI Datenregister verfügbar. Beim Lesen des Registers wird das SPIF Bit im Statusregister automatisch gelöscht.
rx_data = SPDR;
Wir fügen nun alles zu einer brauchbaren Routine zusammen, der wir ein Byte zum Senden übergeben und die das empfangene Byte als Funktionswert zurückliefert:
unsigned char SpiByte(unsigned char data) { SPDR = data; loop_until_bit_is_set(SPSR, SPIF); return SPDR; }
Das war einfach. Falls wir nur senden, aber nichts empfangen wollen, können wir den zurückgegeben Wert einfach ignorieren. Die folgende Routine sendet den Inhalt eines Strings. Bei C sind Strings durch den Zeichencode NUL (ASCII 0x00) terminiert.
void SpiSendString(const char *str) { while(*str) { SpiByte((unsigned char)(*str)); str++; } }
Drei Anmerkungen dazu: Erstens wird ein type cast verwendet, um char in unsigned char zu konvertieren, da der Compiler sonst eine Warnung ausgibt. Als Parameter wurde absichtlich ein char Pointer verwendet, da dies unter C für Strings üblich ist. Zweitens wird der Pointer nicht in der Parameterliste selbst, sondern auf einer gesonderten Zeile inkrementiert. Wir hätten auch
SpiByte((unsigned char)(*str++));
schreiben können, was in unserem Fall auch keinen Unterschied macht. Allerdings ist es immer kritisch, dies in einer Parameterliste zu tun. Wir wissen hier zwar, dass SpiByte() eine Routine ist, aber diese Information ist nicht immer sofort verfügbar oder etwas könnte sich später ändern. Wäre SpiByte nämlich ein Makro, der unseren Pointer mehrfach referenziert, würde der Pointer bei jeder Referenzierung inkrementiert werden. Es ist leicht vorstellbar, dass solche Fehler später sehr schwer aufzuspüren sind.
Drittens sollte die Verwendung von const beachtet werden. Dieses Attribut legt fest, dass die Routine den Inhalt des Strings nicht verändern wird und hilft damit dem Compiler bei der Code Optimierung.
Zurück zu SPI. Was, wenn wir nur empfangen, aber nichts senden wollen? In diesem Fall senden wir einfach irgend etwas, bevorzugt 0x00 oder 0xFF.
Die folgende Routine kann dazu verwendet werden, eine bestimmte Anzahl Datenbytes zu empfangen:
void SpiRead(void *buffer, int len) { unsigned char *bp = (unsigned char *)buffer; while(len--) { *bp++ = SpiByte(0xFF); } }
Den Minimalisten unter uns mag die Funktion komplizierter als nötig erscheinen. Wir haben aber bewußt einen void Pointer als Parameter gewählt, um zu signalisieren, dass jeder beliebige Typ akzeptabel ist.
[edit] Interrupt Driven Data Transfer
Der Befehl zum Pollen des SPI Status Registers
loop_until_bit_is_set(SPSR, SPIF);
kann ohne weiteres einige tausend Schleifen ausführen, was natürlich eine Verschwendung von Prozessorleistung ist. Wenn wir den SPI Interrupt nutzen, kann die CPU andere Aufgaben erledigen, während die Daten im Hintergrund übertragen werden.
Eine entsprechende Interrupt Routine könnte so aussehen:
volatile size_t icnt; volatile unsigned char * volatile iptr; ISR(SPI_STC_vect) { /* Empfangenes Byte im Puffer ablegen. */ *iptr++ = SPDR; /* Zähler dekrementieren. */ icnt--; /* Falls Zähler noch nicht 0 ist... */ if (icnt) { /* ...nächstes Byte senden. */ SPDR = *iptr; } else { /* ...andernfalls Interrupt Modus deaktivieren. */ SPCR &= ~_BV(SPIE); } }
Mit dieser Routine kann der Interrupt-gesteuerte Transfer gestartet werden:
void StartTransfer(unsigned char *data, size_t len) { /* Interrupt Datenpointer und Zähler setzen. */ iptr = data; icnt = len; /* SPI Interrupt aktivieren und Transfer anstossen. */ SPCR |= _BV(SPIE); SPDR = *iptr; }
Da die Interrupt Routine den SPI Interrupt Modus automatisch deaktiviert, wenn alle Bytes übertragen wurden, kann die Abfrage des Interrupt Enable Bits dazu verwendet werden, um das Ende des Transfers festzustellen. Die folgende Funktion gibt 0 zurück, wenn der Transfer abgeschlossen ist.
int TransferIsBusy(void) { return SPCR & _BV(SPIE); }
Hier nun ein Anwendungsbeispiel, mit dem ein bestimmter Sektor eines seriellen DataFlash der AT45 Serie beschrieben werden kann:
void PageWrite(unsigned int pa, unsigned int ba, unsigned char *buf, size_t len) { unsigned char cmd[8]; /* Kommando zum direkten Schreiben, inkl. Löschen. */ cmd[0] = 0x82; cmd[1] = (unsigned char)(pa >> 7); cmd[2] = (unsigned char)((pa << 1) | (ba >> 7)); cmd[3] = (unsigned char)ba; StartTransfer(cmd, 8); while(TransferIsBusy()); /* Daten senden. */ StartTransfer(buf, len); }
Als Parameter werden die Page Adresse pa, die Byte Adresse innerhalb der Page ba, ein Pointer buf auf den Buffer, der die zu schreibenden Daten enthält, sowie die Anzahl der zu schreibenden Bytes len übergeben. Die Routine kehrt zum Aufrufer zurück, während der Transfer zum DataFlash läuft.
[edit] SPI Software Emulation
Werden zusätzliche Schnittstellen benötigt oder falls die CPU keine SPI Hardware aufweist, kann man SPI an ganz normalen I/O Pins per Software emulieren.
[edit] External Links
AVR151: Setup and use of the SPI
Atmel Application Note über SPI für AVR Microcontroller. In englischer Sprache.
spi.c File Reference
SPI Quellcode der Procyon AVRlib.

