Pengantar SPI
The Peripheral Interface Serial Bus (SPI) antarmuka digunakan untuk komunikasi antara beberapa perangkat jarak pendek, dan pada kecepatan tinggi.
Biasanya ada perangkat "master" tunggal, yang memulai komunikasi dan memasok jam yang mengontrol laju transfer data. Mungkin ada satu atau lebih budak. Untuk lebih dari satu budak, masing-masing memiliki sinyal "pilih budak" sendiri, dijelaskan nanti.
Sinyal SPI
Dalam sistem SPI full-blown Anda akan memiliki empat jalur sinyal:
- Master Out, Slave In ( MOSI ) - yang merupakan data dari master ke slave
- Master In, Slave Out ( MISO ) - yang merupakan data dari slave ke master
- Serial Clock ( SCK ) - saat ini mengaktifkan master dan sampel budak bit berikutnya
- Pilih Budak ( SS ) - ini memberitahu budak tertentu untuk "aktif"
Ketika banyak budak terhubung ke sinyal MISO, mereka diharapkan melakukan tri-state (tetap pada impedansi tinggi) garis MISO sampai mereka dipilih oleh Slave Select. Normally Slave Select (SS) menjadi rendah untuk menegaskannya. Artinya, aktif rendah. Setelah budak tertentu dipilih, ia harus mengkonfigurasi garis MISO sebagai output sehingga dapat mengirim data ke master.
Gambar ini menunjukkan cara pertukaran data saat satu byte dikirim:
Perhatikan bahwa tiga sinyal adalah output dari master (MOSI, SCK, SS) dan satu adalah input (MISO).
Pengaturan waktu
Urutan acara adalah:
SS
pergi rendah untuk menegaskannya dan mengaktifkan budak
- The
SCK
garis matikan untuk menunjukkan ketika garis data harus sampel
- Data sampel oleh kedua master dan slave pada terkemuka tepi
SCK
(menggunakan clock phase default)
- Baik master maupun slave bersiap untuk bit berikutnya pada trailing edge
SCK
(menggunakan fase clock default), dengan mengubah MISO
/ MOSI
jika perlu
- Setelah transmisi selesai (mungkin setelah beberapa byte telah dikirim) kemudian
SS
pergi tinggi untuk menegaskan kembali itu
Perhatikan bahwa:
- Bit paling signifikan dikirim terlebih dahulu (secara default)
- Data dikirim dan diterima pada saat yang sama (dupleks penuh)
Karena data dikirim dan diterima pada pulsa clock yang sama, tidak mungkin bagi budak untuk segera menanggapi master. Protokol SPI biasanya mengharapkan master untuk meminta data pada satu transmisi, dan mendapatkan respons pada transmisi berikutnya.
Menggunakan perpustakaan SPI di Arduino, melakukan satu transfer terlihat seperti ini dalam kode:
byte outgoing = 0xAB;
byte incoming = SPI.transfer (outgoing);
Kode sampel
Contoh pengiriman saja (mengabaikan data yang masuk):
#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
Pengkabelan untuk SPI khusus keluaran
Kode di atas (yang hanya mengirim) dapat digunakan untuk mendorong register shift serial keluaran. Ini adalah perangkat keluaran saja, jadi kami tidak perlu khawatir tentang data yang masuk. Dalam kasus mereka, pin SS mungkin disebut pin "store" atau "latch".
Contoh dari ini adalah register geser serial 74HC595, dan berbagai strip LED, hanya untuk menyebutkan beberapa. Misalnya, tampilan LED 64 piksel yang digerakkan oleh chip MAX7219:
Dalam hal ini Anda dapat melihat bahwa pembuat papan telah menggunakan nama sinyal yang sedikit berbeda:
- DIN (Data Masuk) adalah MOSI (Master Out, Slave In)
- CS (Chip Select) adalah SS (Slave Select)
- CLK (Jam) adalah SCK (Serial Clock)
Sebagian besar papan akan mengikuti pola yang sama. Terkadang DIN hanya DI (Data In).
Berikut adalah contoh lain, kali ini papan display LED 7-segmen (juga didasarkan pada chip MAX7219):
Ini menggunakan nama sinyal yang persis sama dengan papan lainnya. Dalam kedua kasus ini Anda dapat melihat bahwa papan hanya membutuhkan 5 kabel untuk itu, tiga untuk SPI, ditambah daya dan ground.
Fase jam dan polaritas
Ada empat cara Anda dapat mencicipi jam SPI.
Protokol SPI memungkinkan variasi pada polaritas pulsa clock. CPOL adalah polaritas jam, dan CPHA adalah fase jam.
- Mode 0 (standar) - jam biasanya rendah (CPOL = 0), dan data diambil sampelnya pada transisi dari rendah ke tinggi (tepi depan) (CPHA = 0)
- Mode 1 - jam biasanya rendah (CPOL = 0), dan data diambil sampel pada transisi dari tinggi ke rendah (trailing edge) (CPHA = 1)
- Mode 2 - jam biasanya tinggi (CPOL = 1), dan data diambil sampelnya pada transisi dari tinggi ke rendah (leading edge) (CPHA = 0)
- Mode 3 - jam biasanya tinggi (CPOL = 1), dan data diambil sampel pada transisi dari rendah ke tinggi (trailing edge) (CPHA = 1)
Ini diilustrasikan dalam grafik ini:
Anda harus merujuk ke lembar data untuk perangkat Anda agar fase dan polaritasnya benar. Biasanya akan ada diagram yang menunjukkan cara mengambil sampel jam. Misalnya, dari datasheet untuk chip 74HC595:
Seperti yang Anda lihat, jam biasanya rendah (CPOL = 0) dan disampel di tepi terdepan (CPHA = 0) sehingga ini adalah mode SPI 0.
Anda dapat mengubah polaritas jam dan fase dalam kode seperti ini (pilih satu saja, tentu saja):
SPI.setDataMode (SPI_MODE0);
SPI.setDataMode (SPI_MODE1);
SPI.setDataMode (SPI_MODE2);
SPI.setDataMode (SPI_MODE3);
Metode ini sudah usang dalam versi 1.6.0 dan seterusnya dari Arduino IDE. Untuk versi terbaru, Anda mengubah mode jam dalam SPI.beginTransaction
panggilan, seperti ini:
SPI.beginTransaction (SPISettings (2000000, MSBFIRST, SPI_MODE0)); // 2 MHz clock, MSB first, mode 0
Pesanan data
Standarnya adalah bit yang paling signifikan terlebih dahulu, namun Anda dapat memberitahu perangkat keras untuk memproses bit yang paling signifikan terlebih dahulu seperti ini:
SPI.setBitOrder (LSBFIRST); // least significant bit first
SPI.setBitOrder (MSBFIRST); // most significant bit first
Sekali lagi, ini sudah usang dalam versi 1.6.0 dan seterusnya dari Arduino IDE. Untuk versi terbaru, Anda mengubah urutan bit dalam SPI.beginTransaction
panggilan, seperti ini:
SPI.beginTransaction (SPISettings (1000000, LSBFIRST, SPI_MODE2)); // 1 MHz clock, LSB first, mode 2
Kecepatan
Pengaturan default untuk SPI adalah menggunakan kecepatan jam sistem dibagi dengan empat, yaitu, satu pulsa clock SPI setiap 250 ns, dengan asumsi jam CPU 16 MHz. Anda dapat mengubah pembagi jam dengan menggunakan setClockDivider
seperti ini:
SPI.setClockDivider (divider);
Di mana "pembagi" adalah salah satu dari:
- SPI_CLOCK_DIV2
- SPI_CLOCK_DIV4
- SPI_CLOCK_DIV8
- SPI_CLOCK_DIV16
- SPI_CLOCK_DIV32
- SPI_CLOCK_DIV64
- SPI_CLOCK_DIV128
Laju tercepat adalah "bagi 2" atau satu pulsa clock SPI setiap 125 ns, dengan asumsi clock CPU 16 MHz. Oleh karena itu, diperlukan 8 * 125 ns atau 1 μs untuk mengirimkan satu byte.
Metode ini sudah usang dalam versi 1.6.0 dan seterusnya dari Arduino IDE. Untuk versi terbaru, Anda mengubah kecepatan transfer dalam SPI.beginTransaction
panggilan, seperti ini:
SPI.beginTransaction (SPISettings (4000000, MSBFIRST, SPI_MODE0)); // 4 MHz clock, MSB first, mode 0
Namun pengujian empiris menunjukkan bahwa perlu memiliki dua pulsa clock antara byte, sehingga tingkat maksimum di mana byte dapat clocked adalah masing-masing 1,125 μs masing-masing (dengan pembagi jam 2).
Untuk meringkas, setiap byte dapat dikirim pada tingkat maksimum satu per 1,125 μs (dengan clock 16 MHz) memberikan laju transfer maksimum teoritis 1 / 1,125 μs, atau 888.888 byte per detik (tidak termasuk overhead seperti pengaturan SS rendah dan sebagainya) di).
Menghubungkan ke Arduino
Arduino Uno
Menghubungkan melalui pin digital 10 hingga 13:
Menghubungkan melalui header ICSP:
Arduino Atmega2560
Menghubungkan melalui pin digital 50 hingga 52:
Anda juga dapat menggunakan header ICSP, mirip dengan Uno di atas.
Arduino Leonardo
Leonardo dan Micro tidak mengekspos pin SPI pada pin digital, tidak seperti Uno dan Mega. Satu-satunya pilihan Anda adalah menggunakan pin header ICSP, seperti yang diilustrasikan di atas untuk Uno.
Banyak budak
Seorang master dapat berkomunikasi dengan banyak budak (namun hanya satu per satu). Itu melakukan ini dengan menyatakan SS untuk satu budak dan membatalkannya untuk yang lain. Budak yang memiliki SS menegaskan (biasanya ini berarti RENDAH) mengkonfigurasi pin MISO sebagai output sehingga budak, dan budak itu sendiri, dapat menanggapi master. Budak lainnya mengabaikan pulsa clock yang masuk jika SS tidak ditegaskan. Dengan demikian Anda memerlukan satu sinyal tambahan untuk setiap budak, seperti ini:
Dalam grafik ini Anda dapat melihat bahwa MISO, MOSI, SCK dibagi di antara kedua budak, namun masing-masing budak memiliki sinyal SS (pemilihan budak) sendiri.
Protokol
SPI spec tidak menentukan protokol seperti itu, jadi terserah pada pasangan master / slave individual untuk menyetujui apa artinya data. Sementara Anda dapat mengirim dan menerima byte secara bersamaan, byte yang diterima tidak bisa menjadi respons langsung ke byte yang dikirim (karena mereka sedang dirangkai secara bersamaan).
Jadi akan lebih logis bagi salah satu ujung untuk mengirim permintaan (mis. 4 mungkin berarti "daftar direktori disk") dan kemudian melakukan transfer (mungkin hanya mengirim nol ke luar) hingga menerima respons lengkap. Respons mungkin berakhir dengan baris baru, atau karakter 0x00.
Baca datasheet untuk perangkat budak Anda untuk melihat urutan protokol apa yang diharapkannya.
Cara membuat budak SPI
Contoh sebelumnya menunjukkan Arduino sebagai master, mengirimkan data ke perangkat slave. Contoh ini menunjukkan bagaimana Arduino bisa menjadi budak.
Pengaturan perangkat keras
Hubungkan dua Arduino Unos bersama-sama dengan pin berikut yang terhubung satu sama lain:
- 10 (SS)
- 11 (MOSI)
- 12 (MISO)
13 (SCK)
+ 5v (jika diperlukan)
- GND (untuk pengembalian sinyal)
Pada Arduino Mega, pinnya adalah 50 (MISO), 51 (MOSI), 52 (SCK), dan 53 (SS).
Bagaimanapun, MOSI di satu ujung terhubung ke MOSI di sisi lain, Anda tidak bertukar sekitar (yaitu Anda tidak memiliki MOSI <-> MISO). Perangkat lunak mengkonfigurasi salah satu ujung MOSI (master end) sebagai output, dan ujung lainnya (slave end) sebagai input.
Contoh utama
#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
Contoh budak
#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
Budak sepenuhnya didorong oleh interupsi, sehingga dapat melakukan hal-hal lain. Data SPI yang masuk dikumpulkan dalam buffer, dan sebuah flag ditetapkan ketika "byte signifikan" (dalam hal ini baris baru) tiba. Ini memberitahu budak untuk memulai dan mulai memproses data.
Contoh menghubungkan master ke slave menggunakan SPI
Cara mendapatkan respons dari seorang budak
Sebagai lanjutan dari kode di atas yang mengirimkan data dari master SPI ke slave, contoh di bawah ini menunjukkan pengiriman data ke slave, setelah itu melakukan sesuatu dengannya, dan mengembalikan respons.
Masternya mirip dengan contoh di atas. Namun poin penting adalah bahwa kita perlu menambahkan sedikit keterlambatan (sekitar 20 mikrodetik). Kalau tidak, budak tidak memiliki kesempatan untuk bereaksi terhadap data yang masuk dan melakukan sesuatu dengannya.
Contoh ini menunjukkan pengiriman "perintah". Dalam hal ini "a" (tambahkan sesuatu) atau "s" (kurangi sesuatu). Ini untuk menunjukkan bahwa budak benar-benar melakukan sesuatu dengan data.
Setelah menyatakan slave-select (SS) untuk memulai transaksi, master mengirim perintah, diikuti oleh sejumlah byte, dan kemudian menaikkan SS untuk mengakhiri transaksi.
Poin yang sangat penting adalah bahwa slave tidak dapat menanggapi byte yang masuk pada saat yang sama. Responsnya harus dalam byte berikutnya. Ini karena bit yang dikirim, dan bit yang diterima, sedang dikirim secara bersamaan. Jadi untuk menambahkan sesuatu ke empat angka kita perlu lima transfer, seperti ini:
transferAndWait ('a'); // add command
transferAndWait (10);
a = transferAndWait (17);
b = transferAndWait (33);
c = transferAndWait (42);
d = transferAndWait (0);
Pertama kami meminta tindakan pada nomor 10. Tapi kami tidak mendapatkan jawaban sampai transfer berikutnya (yang untuk 17). Namun "a" akan ditetapkan ke balasan ke 10. Akhirnya kami akhirnya mengirim "dummy" nomor 0, untuk mendapatkan balasan sebesar 42.
Master (contoh)
#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
Kode untuk slave pada dasarnya melakukan hampir semua hal dalam rutinitas interupsi (dipanggil ketika data SPI yang masuk tiba). Dibutuhkan byte yang masuk, dan menambah atau mengurangi sesuai "byte perintah" yang diingat. Perhatikan bahwa respons akan "dikumpulkan" lain kali melalui loop. Inilah sebabnya mengapa master harus mengirim satu "dummy" transfer terakhir untuk mendapatkan jawaban akhir.
Dalam contoh saya, saya menggunakan loop utama untuk hanya mendeteksi ketika SS tinggi, dan menghapus perintah yang disimpan. Dengan begitu, ketika SS ditarik rendah lagi untuk transaksi berikutnya, byte pertama dianggap sebagai byte perintah.
Lebih dapat dipercaya, ini akan dilakukan dengan interupsi. Artinya, Anda akan secara fisik menghubungkan SS ke salah satu input interupsi (misalnya, pada Uno, hubungkan pin 10 (SS) ke pin 2 (input interupsi), atau gunakan pin-change interrupt pada pin 10.
Kemudian interupsi dapat digunakan untuk melihat ketika SS ditarik rendah atau tinggi.
Budak (contoh)
// 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
Contoh output
Adding results:
25
32
48
57
Subtracting results:
2
9
25
34
Adding results:
25
32
48
57
Subtracting results:
2
9
25
34
Keluaran penganalisis logika
Ini menunjukkan waktu antara mengirim dan menerima dalam kode di atas:
Fungsionalitas baru di IDE 1.6.0 dan seterusnya
Versi 1.6.0 dari IDE telah mengubah cara SPI bekerja, sampai batas tertentu. Anda masih perlu melakukannya SPI.begin()
sebelum menggunakan SPI. Itu mengatur perangkat keras SPI. Namun sekarang, ketika Anda akan memulai berkomunikasi dengan budak Anda juga lakukan SPI.beginTransaction()
untuk mengatur SPI (untuk budak ini) dengan benar:
- Kecepatan jam
- Pesanan sedikit
- Fase jam dan polaritas
Ketika Anda selesai berkomunikasi dengan budak, Anda menelepon SPI.endTransaction()
. Sebagai contoh:
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
Mengapa menggunakan SPI?
Saya akan menambahkan satu pertanyaan awal: kapan / mengapa Anda menggunakan SPI? Kebutuhan untuk konfigurasi multi-master atau jumlah budak yang sangat besar akan memiringkan skala menuju I2C.
Ini adalah pertanyaan yang sangat bagus. Jawaban saya adalah:
- Beberapa perangkat (beberapa) hanya mendukung metode transfer SPI. Misalnya register shift keluaran 74HC595, register shift input 74HC165, driver LED MAX7219, dan beberapa strip LED yang telah saya lihat. Jadi, Anda mungkin menggunakannya karena perangkat target hanya mendukungnya.
- SPI benar-benar metode tercepat yang tersedia pada chip Atmega328 (dan sejenisnya). Tingkat tercepat yang dikutip di atas adalah 888.888 byte per detik. Menggunakan I 2 C Anda hanya bisa mendapatkan sekitar 40.000 byte per detik. Overhead dari I 2 C cukup besar, dan jika Anda mencoba antarmuka sangat cepat, SPI adalah pilihan yang lebih disukai. Cukup banyak keluarga chip (mis. MCP23017 dan MCP23S17) sebenarnya mendukung I 2 C dan SPI sehingga Anda sering dapat memilih antara kecepatan, dan kemampuan untuk memiliki beberapa perangkat pada satu bus.
- Perangkat SPI dan I 2 C keduanya didukung dalam perangkat keras pada Atmega328 sehingga Anda dapat melakukan transfer melalui SPI secara bersamaan dengan I 2 C yang akan memberi Anda dorongan kecepatan.
Kedua metode memiliki tempat masing-masing. I 2 C memungkinkan Anda menghubungkan banyak perangkat ke satu bus (dua kabel, ditambah arde) sehingga akan menjadi pilihan yang lebih disukai jika Anda perlu menginterogasi sejumlah besar perangkat, mungkin cukup jarang. Namun kecepatan SPI bisa lebih relevan untuk situasi di mana Anda perlu membuat output dengan cepat (mis. Strip LED) atau input dengan cepat (mis. Konverter ADC).
Referensi