W odniesieniu do Arduino Uno, Mega2560, Leonardo i podobnych płyt:
- Jak działa SPI?
- Jak szybki jest SPI?
- Jak połączyć między urządzeniem nadrzędnym a urządzeniem podrzędnym?
- Jak zrobić SPI slave?
Uwaga: jest to pytanie referencyjne.
W odniesieniu do Arduino Uno, Mega2560, Leonardo i podobnych płyt:
Uwaga: jest to pytanie referencyjne.
Odpowiedzi:
Interfejs szeregowej magistrali interfejsu peryferyjnego (SPI) służy do komunikacji między wieloma urządzeniami na krótkich dystansach i z dużą prędkością.
Zwykle jest to jedno „urządzenie nadrzędne”, które inicjuje komunikację i dostarcza zegar sterujący szybkością przesyłania danych. Może być jeden lub więcej niewolników. W przypadku więcej niż jednego urządzenia podrzędnego każdy ma własny sygnał „wyboru urządzenia podrzędnego”, opisany później.
W pełnym systemie SPI będziesz mieć cztery linie sygnałowe:
Kiedy do urządzenia MISO podłączonych jest wiele urządzeń podrzędnych, oczekuje się, że potróją (utrzymują wysoką impedancję) tę linię MISO, dopóki nie zostaną wybrane przez potwierdzenie Slave Select. Normalnie Slave Select (SS) przechodzi w stan niski, aby go potwierdzić. Oznacza to, że jest niski. Po wybraniu określonego urządzenia podrzędnego powinien on skonfigurować linię MISO jako wyjście, aby mógł wysyłać dane do urządzenia nadrzędnego.
Ten obraz pokazuje sposób wymiany danych podczas wysyłania jednego bajtu:
Zauważ, że trzy sygnały są wyjściami z urządzenia nadrzędnego (MOSI, SCK, SS), a jeden to wejście (MISO).
Sekwencja zdarzeń jest następująca:
SS
idzie nisko, aby to potwierdzić i aktywować niewolnikaSCK
Linia przełącza się wskazywać, kiedy linie danych należy pobrać próbkiSCK
(przy użyciu domyślnej fazy zegara)SCK
(używając domyślnej fazy zegara), zmieniając MISO
/ w MOSI
razie potrzebySS
do stanu wysokiego, aby anulować potwierdzenieUwaga:
Ponieważ dane są wysyłane i odbierane w tym samym impulsie zegarowym, urządzenie podrzędne nie może natychmiast zareagować na urządzenie nadrzędne. Protokoły SPI zwykle oczekują, że master zażąda danych podczas jednej transmisji i otrzyma odpowiedź na kolejną.
Korzystając z biblioteki SPI na Arduino, pojedynczy transfer wygląda tak:
byte outgoing = 0xAB;
byte incoming = SPI.transfer (outgoing);
Przykład tylko wysyłania (ignorowanie jakichkolwiek przychodzących danych):
#include <SPI.h>
void setup (void)
{
digitalWrite(SS, HIGH); // ensure SS stays high
SPI.begin ();
} // end of setup
void loop (void)
{
byte c;
// enable Slave Select
digitalWrite(SS, LOW); // SS is pin 10
// send test string
for (const char * p = "Fab" ; c = *p; p++)
SPI.transfer (c);
// disable Slave Select
digitalWrite(SS, HIGH);
delay (100);
} // end of loop
Powyższy kod (który wysyła tylko) może być użyty do sterowania wyjściowym szeregowym rejestrem przesuwnym. Są to urządzenia tylko wyjściowe, więc nie musimy się martwić o przychodzące dane. W ich przypadku pin SS może być nazywany pinem „store” lub „zatrzask”.
Przykładem tego jest szeregowy rejestr przesuwny 74HC595 i różne paski LED, żeby wymienić tylko kilka. Na przykład ten 64-pikselowy wyświetlacz LED napędzany przez układ MAX7219:
W tym przypadku widać, że twórca płyty użył nieco innych nazw sygnałów:
Większość desek będzie miała podobny wzór. Czasami DIN to po prostu DI (Data In).
Oto kolejny przykład, tym razem 7-segmentowy wyświetlacz LED (również oparty na układzie MAX7219):
Używa dokładnie takich samych nazw sygnałów, jak na drugiej płycie. W obu tych przypadkach widać, że płytka potrzebuje tylko 5 przewodów, trzy do SPI, plus moc i uziemienie.
Istnieją cztery sposoby próbkowania zegara SPI.
Protokół SPI pozwala na zmianę polaryzacji impulsów zegara. CPOL jest polaryzacją zegara, a CPHA jest fazą zegara.
Są one zilustrowane na tej grafice:
Aby uzyskać prawidłową fazę i polaryzację, należy zapoznać się z arkuszem danych urządzenia. Zwykle jest schemat pokazujący, jak próbkować zegar. Na przykład z arkusza danych dla układu 74HC595:
Jak widać zegar jest normalnie niski (CPOL = 0) i jest próbkowany na zboczu wiodącym (CPHA = 0), więc jest to tryb SPI 0.
Możesz zmienić polaryzację zegara i fazę w kodzie w ten sposób (oczywiście wybierz tylko jedną):
SPI.setDataMode (SPI_MODE0);
SPI.setDataMode (SPI_MODE1);
SPI.setDataMode (SPI_MODE2);
SPI.setDataMode (SPI_MODE3);
Ta metoda jest przestarzała w wersjach 1.6.0 Arduino IDE. W przypadku najnowszych wersji zmieniasz tryb zegara w SPI.beginTransaction
rozmowie, w ten sposób:
SPI.beginTransaction (SPISettings (2000000, MSBFIRST, SPI_MODE0)); // 2 MHz clock, MSB first, mode 0
Domyślnie jest to bit najbardziej znaczący jako pierwszy, jednak można powiedzieć sprzętowi, aby najpierw przetworzył bit o najmniejszym znaczeniu:
SPI.setBitOrder (LSBFIRST); // least significant bit first
SPI.setBitOrder (MSBFIRST); // most significant bit first
Ponownie jest to przestarzałe w wersjach 1.6.0 Arduino IDE. W przypadku najnowszych wersji zmieniasz kolejność bitów w SPI.beginTransaction
wywołaniu, w następujący sposób:
SPI.beginTransaction (SPISettings (1000000, LSBFIRST, SPI_MODE2)); // 1 MHz clock, LSB first, mode 2
Domyślnym ustawieniem dla SPI jest użycie szybkości zegara systemowego podzielonej przez cztery, to znaczy jeden impuls zegarowy SPI co 250 ns, przy założeniu zegara procesora 16 MHz. Możesz zmienić dzielnik zegara, używając setClockDivider
następującego sposobu:
SPI.setClockDivider (divider);
Gdzie „dzielnik” jest jednym z:
Najszybsza szybkość to „dzielenie przez 2” lub jeden impuls zegarowy SPI co 125 ns, przy założeniu zegara procesora 16 MHz. W związku z tym przesłanie jednego bajtu wymagałoby 8 * 125 ns lub 1 µs.
Ta metoda jest przestarzała w wersjach 1.6.0 Arduino IDE. W przypadku najnowszych wersji zmieniasz szybkość przesyłania w SPI.beginTransaction
rozmowie, w ten sposób:
SPI.beginTransaction (SPISettings (4000000, MSBFIRST, SPI_MODE0)); // 4 MHz clock, MSB first, mode 0
Jednak testy empiryczne pokazują, że konieczne jest posiadanie dwóch impulsów zegarowych między bajtami, więc maksymalna szybkość, z jaką bajty mogą zostać wyrejestrowane, wynosi 1,125 µs każdy (z dzielnikiem zegara 2).
Podsumowując, każdy bajt może być wysyłany z maksymalną szybkością 1 na 1,125 µs (z zegarem 16 MHz), co daje teoretyczną maksymalną szybkość transferu 1 / 1,125 µs lub 888,888 bajtów na sekundę (z wyłączeniem narzutu, jak ustawienie niskiego SS i tak dalej na).
Podłączanie za pomocą pinów cyfrowych 10 do 13:
Łączenie za pomocą nagłówka ICSP:
Podłączanie za pomocą pinów cyfrowych od 50 do 52:
Możesz także użyć nagłówka ICSP, podobnego do powyższego Uno.
Leonardo i Micro nie odsłaniają pinów SPI na pinach cyfrowych, w przeciwieństwie do Uno i Mega. Jedyną opcją jest użycie pinów nagłówka ICSP, jak pokazano powyżej dla Uno.
Mistrz może komunikować się z wieloma niewolnikami (jednak tylko jednym naraz). Robi to, zapewniając SS jednemu niewolnikowi i odznaczając go dla wszystkich pozostałych. Slave, który potwierdził SS (zwykle oznacza to LOW) konfiguruje swój pin MISO jako wyjście, aby slave i ten slave sam mogli odpowiedzieć na master. Inni niewolnicy ignorują wszelkie przychodzące impulsy zegara, jeśli SS nie jest zapewnione. Dlatego potrzebujesz jednego dodatkowego sygnału dla każdego urządzenia podrzędnego, takiego jak ten:
Na tej grafice widać, że MISO, MOSI, SCK są współużytkowane przez oba urządzenia podrzędne, jednak każde urządzenie podrzędne ma własny sygnał SS (wybór urządzenia podrzędnego).
Specyfikacja SPI nie określa protokołów jako takich, więc ustalenie znaczenia danych zależy od indywidualnych par master / slave. Chociaż możesz wysyłać i odbierać bajty jednocześnie, odebrany bajt nie może być bezpośrednią odpowiedzią na wysłany bajt (ponieważ są one składane jednocześnie).
Byłoby więc bardziej logiczne, aby jeden koniec wysłał żądanie (np. 4 może oznaczać „wylistuj katalog dysku”), a następnie wykonać transfery (być może po prostu wysyłając zera na zewnątrz), aż otrzyma pełną odpowiedź. Odpowiedź może zakończyć się znakiem nowej linii lub znakiem 0x00.
Przeczytaj arkusz danych dla urządzenia slave, aby zobaczyć, jakich sekwencji protokołów oczekuje.
Wcześniejszy przykład pokazuje Arduino jako urządzenie nadrzędne, wysyłające dane do urządzenia podrzędnego. Ten przykład pokazuje, jak Arduino może być niewolnikiem.
Połącz dwa Arduino Unos razem z następującymi pinami połączonymi ze sobą:
13 (SCK)
+ 5 V (jeśli wymagane)
W Arduino Mega piny to 50 (MISO), 51 (MOSI), 52 (SCK) i 53 (SS).
W każdym razie MOSI na jednym końcu jest podłączony do MOSI na drugim, nie zamieniasz ich (to znaczy, że nie masz MOSI <-> MISO). Oprogramowanie konfiguruje jeden koniec MOSI (koniec master) jako wyjście, a drugi koniec (koniec slave) jako wejście.
#include <SPI.h>
void setup (void)
{
digitalWrite(SS, HIGH); // ensure SS stays high for now
// Put SCK, MOSI, SS pins into output mode
// also put SCK, MOSI into LOW state, and SS into HIGH state.
// Then put SPI hardware into Master mode and turn SPI on
SPI.begin ();
// Slow down the master a bit
SPI.setClockDivider(SPI_CLOCK_DIV8);
} // end of setup
void loop (void)
{
char c;
// enable Slave Select
digitalWrite(SS, LOW); // SS is pin 10
// send test string
for (const char * p = "Hello, world!\n" ; c = *p; p++)
SPI.transfer (c);
// disable Slave Select
digitalWrite(SS, HIGH);
delay (1000); // 1 seconds delay
} // end of loop
#include <SPI.h>
char buf [100];
volatile byte pos;
volatile bool process_it;
void setup (void)
{
Serial.begin (115200); // debugging
// turn on SPI in slave mode
SPCR |= bit (SPE);
// have to send on master in, *slave out*
pinMode (MISO, OUTPUT);
// get ready for an interrupt
pos = 0; // buffer empty
process_it = false;
// now turn on interrupts
SPI.attachInterrupt();
} // end of setup
// SPI interrupt routine
ISR (SPI_STC_vect)
{
byte c = SPDR; // grab byte from SPI Data Register
// add to buffer if room
if (pos < sizeof buf)
{
buf [pos++] = c;
// example: newline means time to process buffer
if (c == '\n')
process_it = true;
} // end of room available
} // end of interrupt routine SPI_STC_vect
// main loop - wait for flag set in interrupt routine
void loop (void)
{
if (process_it)
{
buf [pos] = 0;
Serial.println (buf);
pos = 0;
process_it = false;
} // end of flag set
} // end of loop
Niewolnik jest całkowicie sterowany przerwaniami, więc może robić inne rzeczy. Przychodzące dane SPI są gromadzone w buforze i ustawiana flaga, gdy nadejdzie „znaczący bajt” (w tym przypadku nowa linia). To mówi slave'owi, aby wszedł i zaczął przetwarzać dane.
Kontynuując powyższy kod, który wysyła dane z Master SPI do Slave, poniższy przykład pokazuje wysyłanie danych do Slave, każąc mu coś z tym zrobić i zwrócić odpowiedź.
Mistrz jest podobny do powyższego przykładu. Ważną kwestią jest jednak to, że musimy dodać niewielkie opóźnienie (około 20 mikrosekund). W przeciwnym razie urządzenie podrzędne nie ma szansy zareagować na przychodzące dane i coś z tym zrobić.
Przykład pokazuje wysłanie „polecenia”. W tym przypadku „a” (dodaj coś) lub „s” (odejmij coś). Ma to pokazać, że slave faktycznie robi coś z danymi.
Po potwierdzeniu wyboru slave (SS) w celu zainicjowania transakcji, master wysyła polecenie, a następnie dowolną liczbę bajtów, a następnie podnosi SS, aby zakończyć transakcję.
Bardzo ważną kwestią jest to, że niewolnik nie może odpowiedzieć na nadchodzący bajt w tym samym momencie. Odpowiedź musi być w następnym bajcie. Wynika to z faktu, że wysyłane bity i odbierane bity są wysyłane jednocześnie. Aby dodać coś do czterech liczb, potrzebujemy pięciu przelewów, takich jak to:
transferAndWait ('a'); // add command
transferAndWait (10);
a = transferAndWait (17);
b = transferAndWait (33);
c = transferAndWait (42);
d = transferAndWait (0);
Najpierw prosimy o działanie na numer 10. Ale nie otrzymujemy odpowiedzi do następnego przelewu (ten dla 17). Jednak „a” zostanie ustawione na odpowiedź na 10. W końcu wysyłamy „manekina” numer 0, aby uzyskać odpowiedź za 42.
#include <SPI.h>
void setup (void)
{
Serial.begin (115200);
Serial.println ();
digitalWrite(SS, HIGH); // ensure SS stays high for now
SPI.begin ();
// Slow down the master a bit
SPI.setClockDivider(SPI_CLOCK_DIV8);
} // end of setup
byte transferAndWait (const byte what)
{
byte a = SPI.transfer (what);
delayMicroseconds (20);
return a;
} // end of transferAndWait
void loop (void)
{
byte a, b, c, d;
// enable Slave Select
digitalWrite(SS, LOW);
transferAndWait ('a'); // add command
transferAndWait (10);
a = transferAndWait (17);
b = transferAndWait (33);
c = transferAndWait (42);
d = transferAndWait (0);
// disable Slave Select
digitalWrite(SS, HIGH);
Serial.println ("Adding results:");
Serial.println (a, DEC);
Serial.println (b, DEC);
Serial.println (c, DEC);
Serial.println (d, DEC);
// enable Slave Select
digitalWrite(SS, LOW);
transferAndWait ('s'); // subtract command
transferAndWait (10);
a = transferAndWait (17);
b = transferAndWait (33);
c = transferAndWait (42);
d = transferAndWait (0);
// disable Slave Select
digitalWrite(SS, HIGH);
Serial.println ("Subtracting results:");
Serial.println (a, DEC);
Serial.println (b, DEC);
Serial.println (c, DEC);
Serial.println (d, DEC);
delay (1000); // 1 second delay
} // end of loop
Kod dla urządzenia podrzędnego w zasadzie robi prawie wszystko w procedurze przerwania (wywoływanej, gdy nadchodzą przychodzące dane SPI). Pobiera przychodzący bajt i dodaje lub odejmuje zgodnie z zapamiętanym „bajtem polecenia”. Zauważ, że odpowiedź zostanie „zebrana” następnym razem przez pętlę. Dlatego mistrz musi wysłać jeden końcowy „fałszywy” transfer, aby uzyskać ostateczną odpowiedź.
W moim przykładzie używam pętli głównej, aby po prostu wykryć, kiedy SS idzie w górę, i wyczyścić zapisane polecenie. W ten sposób, gdy SS ponownie zostanie obniżone do następnej transakcji, pierwszy bajt jest uważany za bajt polecenia.
Bardziej niezawodnie można to zrobić z przerwą. Oznacza to, że fizycznie podłączysz SS do jednego z wejść przerwań (np. Na Uno, podłączysz pin 10 (SS) do pinu 2 (wejście przerwania) lub użyjesz przerwania zmiany pin na pinie 10.
Następnie przerwanie może być wykorzystane do zauważenia, kiedy SS jest ściągane na niskie lub wysokie.
// what to do with incoming data
volatile byte command = 0;
void setup (void)
{
// have to send on master in, *slave out*
pinMode(MISO, OUTPUT);
// turn on SPI in slave mode
SPCR |= _BV(SPE);
// turn on interrupts
SPCR |= _BV(SPIE);
} // end of setup
// SPI interrupt routine
ISR (SPI_STC_vect)
{
byte c = SPDR;
switch (command)
{
// no command? then this is the command
case 0:
command = c;
SPDR = 0;
break;
// add to incoming byte, return result
case 'a':
SPDR = c + 15; // add 15
break;
// subtract from incoming byte, return result
case 's':
SPDR = c - 8; // subtract 8
break;
} // end of switch
} // end of interrupt service routine (ISR) SPI_STC_vect
void loop (void)
{
// if SPI not active, clear current command
if (digitalRead (SS) == HIGH)
command = 0;
} // end of loop
Adding results:
25
32
48
57
Subtracting results:
2
9
25
34
Adding results:
25
32
48
57
Subtracting results:
2
9
25
34
To pokazuje czas pomiędzy wysłaniem a odbiorem w powyższym kodzie:
Wersja 1.6.0 IDE do pewnego stopnia zmieniła sposób działania SPI. Państwo nadal trzeba zrobić SPI.begin()
przed użyciem SPI. To konfiguruje sprzęt SPI. Jednak teraz, kiedy masz zamiar zacząć komunikować się z niewolnika Państwo również zrobić SPI.beginTransaction()
, aby skonfigurować SPI (w tym slave) z popraw:
Kiedy skończysz komunikować się z niewolnikiem, zadzwonisz SPI.endTransaction()
. Na przykład:
SPI.beginTransaction (SPISettings (2000000, MSBFIRST, SPI_MODE0));
digitalWrite (SS, LOW); // assert Slave Select
byte foo = SPI.transfer (42); // do a transfer
digitalWrite (SS, HIGH); // de-assert Slave Select
SPI.endTransaction (); // transaction over
Dodałbym jedno wstępne pytanie: kiedy / dlaczego miałbyś używać SPI? Potrzeba konfiguracji multi-master lub bardzo dużej liczby slaveów przechyliłaby skalę w kierunku I2C.
To doskonałe pytanie. Moje odpowiedzi to:
Obie metody mają swoje miejsce. I 2 C pozwala podłączyć wiele urządzeń do jednej magistrali (dwa przewody plus uziemienie), więc byłby to preferowany wybór, gdybyś potrzebował przesłuchać znaczną liczbę urządzeń, być może dość rzadko. Jednak szybkość SPI może być bardziej istotna w sytuacjach, w których trzeba szybko wysyłać dane (np. Pasek LED) lub szybko (np. Konwerter ADC).
Moja strona o SPI - zawiera również szczegółowe informacje na temat SPI bit-banged i używania USART do uzyskania drugiego SPI sprzętowego na chipie Atmega328.
Are you going to cover the weirdness that is the Due's SPI?
- Nic nie wiem o interfejsie SP Due (oprócz założenia, że ogólny protokół jest taki sam). Możesz dodać odpowiedź dotyczącą tego aspektu.