Answers:
该串行外围接口总线(SPI)接口用于在短距离上的多个设备之间的通信,并且以高速。
通常,只有一个“主”设备,用于启动通信并提供控制数据传输速率的时钟。可以有一个或多个奴隶。对于多个从设备,每个设备都有其自己的“从设备选择”信号,稍后将进行描述。
在功能完善的SPI系统中,您将有四条信号线:
当多个从机连接到MISO信号时,它们会一直处于该MISO线的三态(保持高阻抗状态),直到被从机选择选中为止。通常,从机选择(SS)会变低以断言它。即,它为低电平有效。一旦选择了特定的从站,它就应该将MISO线配置为输出,以便它可以将数据发送到主站。
此图显示了发送一个字节时交换数据的方式:
请注意,三个信号是主机的输出(MOSI,SCK,SS),一个信号是输入(MISO)。
事件的顺序是:
SS
变低以声明它并激活从机SCK
行切换以指示何时应采样数据行SCK
(使用缺省时钟相位)SCK
(使用默认时钟相位)MISO
MOSI
SS
变为高电平以取消断言注意:
因为数据是在相同的时钟脉冲上发送和接收的,所以从机不可能立即响应主机。SPI协议通常希望主机在一次传输中请求数据,并在随后的传输中获得响应。
使用Arduino上的SPI库,执行单次传输的代码如下所示:
byte outgoing = 0xAB;
byte incoming = SPI.transfer (outgoing);
仅发送示例(忽略任何传入数据):
#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
上面的代码(仅发送)可用于驱动输出串行移位寄存器。这些是仅用于输出的设备,因此我们无需担心任何传入的数据。在这种情况下,SS引脚可能称为“存储”或“锁存”引脚。
例如74HC595串行移位寄存器和各种LED灯条,仅举几例。例如,这种由MAX7219芯片驱动的64像素LED显示器:
在这种情况下,您可以看到电路板制造商使用了略有不同的信号名称:
大多数董事会将遵循类似的模式。有时DIN只是DI(数据输入)。
这是另一个示例,这一次是7段LED显示板(也基于MAX7219芯片):
这使用与另一块板完全相同的信号名称。在这两种情况下,您都可以看到该板只需要连接5条线,其中3条用于SPI,还有电源和地线。
您可以通过四种方式采样SPI时钟。
SPI协议允许改变时钟脉冲的极性。CPOL是时钟极性,CPHA是时钟相位。
这些在此图中说明:
您应参考设备的数据表以正确确定相位和极性。通常会有一个图表显示如何对时钟进行采样。例如,从74HC595芯片的数据表中:
如您所见,时钟通常很低(CPOL = 0),并且在上升沿采样(CPHA = 0),因此这就是SPI模式0。
您可以像这样更改时钟极性和相位(当然,仅选择一个):
SPI.setDataMode (SPI_MODE0);
SPI.setDataMode (SPI_MODE1);
SPI.setDataMode (SPI_MODE2);
SPI.setDataMode (SPI_MODE3);
从Arduino IDE的1.6.0版本开始不推荐使用此方法。对于最新版本,您可以在SPI.beginTransaction
通话中更改时钟模式,如下所示:
SPI.beginTransaction (SPISettings (2000000, MSBFIRST, SPI_MODE0)); // 2 MHz clock, MSB first, mode 0
默认值是最高有效位在前,但是您可以告诉硬件首先处理最低有效位,如下所示:
SPI.setBitOrder (LSBFIRST); // least significant bit first
SPI.setBitOrder (MSBFIRST); // most significant bit first
同样,此版本在Arduino IDE的1.6.0及更高版本中已弃用。对于最新版本,您可以更改SPI.beginTransaction
调用中的位顺序,如下所示:
SPI.beginTransaction (SPISettings (1000000, LSBFIRST, SPI_MODE2)); // 1 MHz clock, LSB first, mode 2
SPI的默认设置是使用系统时钟速度除以四,即每250 ns使用一个SPI时钟脉冲(假设CPU时钟为16 MHz)。您可以使用以下方式更改时钟分频器setClockDivider
:
SPI.setClockDivider (divider);
其中“除法器”是以下之一:
假设16 MHz CPU时钟,最快的速率是“每2 ns除”或每125 ns一个SPI时钟脉冲。因此,发送一个字节需要8 * 125 ns或1 µs。
从Arduino IDE的1.6.0版本开始不推荐使用此方法。对于最新版本,您可以更改SPI.beginTransaction
呼叫的传输速度,如下所示:
SPI.beginTransaction (SPISettings (4000000, MSBFIRST, SPI_MODE0)); // 4 MHz clock, MSB first, mode 0
但是,经验测试表明,在字节之间必须有两个时钟脉冲,因此,每个字节可以移出的最大速率为1.125 µs(时钟分频器为2)。
总而言之,每个字节的最大发送速率为每1.125 µs(带有16 MHz时钟),理论上的最大传输速率为1 / 1.125 µs,即每秒888,888字节(不包括将SS设置为低等开销)上)。
通过数字引脚10至13连接:
通过ICSP接头连接:
通过数字引脚50至52连接:
您也可以使用ICSP标头,类似于上面的Uno。
与Uno和Mega不同,Leonardo和Micro不会在数字引脚上暴露SPI引脚。您唯一的选择是使用ICSP排针,如上图所示。
一台主机可以与多个从机通信(但是一次只能与一个从机通信)。它通过为一个从机声明SS并为所有其他从机取消声明SS来实现此目的。已被SS置为有效的从机(通常意味着LOW)将其MISO引脚配置为输出,以便从机以及单独的从机可以响应主机。如果未声明SS,其他从机将忽略任何传入的时钟脉冲。因此,每个从站都需要一个附加信号,如下所示:
在此图中,您可以看到两个从站之间共享MISO,MOSI,SCK,但是每个从站都有自己的SS(从站选择)信号。
SPI规范没有这样指定协议,因此要由各个主/从配对来决定数据的含义。尽管可以同时发送和接收字节,但是接收到的字节不能直接作为对发送字节的响应(因为它们是同时组装的)。
因此,一端发送一个请求(例如4可能意味着“列出磁盘目录”)然后进行传输(也许只是向外发送零),直到它收到一个完整的响应,这将更加合乎逻辑。响应可能以换行符或0x00字符终止。
阅读您的从设备的数据表,以了解其期望的协议序列。
前面的示例将Arduino作为主设备,将数据发送到从设备。本示例说明了Arduino如何成为奴隶。
将两个Arduino Unos与以下相互连接的引脚连接在一起:
13(SCK)
+ 5v(如果需要)
在Arduino Mega上,引脚为50(MISO),51(MOSI),52(SCK)和53(SS)。
无论如何,MOSI的一端连接到另一端的MOSI,您无需交换它们(即您没有 MOSI <-> MISO)。该软件将MOSI的一端(主端)配置为输出,另一端(从属端)配置为输入。
#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
从站完全由中断驱动,因此它可以做其他事情。传入的SPI数据收集在缓冲区中,并在到达“有效字节”(在这种情况下为换行符)时设置标志。这告诉从站继续并开始处理数据。
从上面的代码(从SPI主设备向从设备发送数据)开始,下面的示例显示了向从设备发送数据,对其执行某些操作并返回响应。
母版类似于上面的示例。但是重要的一点是,我们需要增加一些延迟(大约20微秒)。否则,从站将没有机会对传入的数据做出反应并对其进行处理。
该示例显示了发送“命令”。在这种情况下,“ a”(添加某些内容)或“ s”(减去某些内容)。这表明从站实际上正在处理数据。
声明从属选择(SS)以启动事务后,主机发送命令,后跟任意数量的字节,然后提高SS来终止事务。
非常重要的一点是,从机不能同时响应传入的字节。响应必须在下一个字节中。这是因为正在发送的位和正在接收的位是同时发送的。因此,要将某物添加到四个数字中,我们需要五次转移,如下所示:
transferAndWait ('a'); // add command
transferAndWait (10);
a = transferAndWait (17);
b = transferAndWait (33);
c = transferAndWait (42);
d = transferAndWait (0);
首先,我们要求对数字10进行操作。但是直到下一次传输(即17的传输),我们才得到响应。但是,将“ a”设置为对10的答复。最后,我们最终发送一个“虚拟”数字0,以得到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
从站的代码基本上执行中断例程中的几乎所有操作(当传入的SPI数据到达时调用)。它接收传入的字节,并根据记住的“命令字节”加或减。请注意,下次将通过循环“收集”响应。这就是为什么主机必须发送一次最终的“虚拟”传输以获得最终答复的原因。
在我的示例中,我使用主循环简单地检测SS何时变高,并清除保存的命令。这样,当SS被再次拉低以进行下一个事务时,第一个字节被视为命令字节。
更可靠地,这将通过中断来完成。也就是说,您可以将SS物理连接到中断输入之一(例如,在Uno上,将引脚10(SS)连接到引脚2(中断输入),或者在引脚10上使用引脚转换中断。
然后,该中断可用于通知何时SS被拉低或拉高。
// 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
这显示了以上代码中发送和接收之间的时间:
IDE的1.6.0版在一定程度上改变了SPI的工作方式。使用SPI之前,您仍然需要做 SPI.begin()
。这样就设置了SPI硬件。但是,现在,当您要开始与从设备通信时,您还需要SPI.beginTransaction()
使用正确的设置(为此从设备)设置SPI:
与从站通信完成后,请致电SPI.endTransaction()
。例如:
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
我要补充一个初步的问题:什么时候/为什么要使用SPI?对多主机配置或大量从机的需求将使规模向I2C倾斜。
这是一个很好的问题。我的答案是:
两种方法都有它们的位置。I 2 C使您可以将许多设备连接到一条总线(两根导线,再加上接地),因此,如果您需要询问大量设备(也许很少),这将是首选。但是,SPI的速度可能与需要快速输出(例如,LED条)或快速输入(例如,ADC转换器)的情况相关。
我在SPI上的页面 -还提供了有关位冲击SPI的详细信息,并使用USART在Atmega328芯片上获得了第二个硬件SPI。
Are you going to cover the weirdness that is the Due's SPI?
-我对Due的SPI一无所知(除了假定总体协议相同)。欢迎您添加涵盖该方面的回复。