您如何在Arduino上使用SPI?


Answers:


80

SPI介绍

串行外围接口总线(SPI)接口用于在短距离上的多个设备之间的通信,并且以高速。

通常,只有一个“主”设备,用于启动通信并提供控制数据传输速率的时钟。可以有一个或多个奴隶。对于多个从设备,每个设备都有其自己的“从设备选择”信号,稍后将进行描述。


SPI信号

在功能完善的SPI系统中,您将有四条信号线:

  • 主机输出,从机输入(MOSI)-这是从主机到从机的数据
  • 主机输入,从机输出(MISO)-从从机到主机的数据
  • 串行时钟(SCK)-当它同时切换主机和从机时采样下一位
  • 从站选择(SS)-告诉特定的从站“活动”

当多个从机连接到MISO信号时,它们会一直处于该MISO线的三态(保持高阻抗状态),直到被从机选择选中为止。通常,从机选择(SS)会变低以断言它。即,它为低电平有效。一旦选择了特定的从站,它就应该将MISO线配置为输出,以便它可以将数据发送到主站。

此图显示了发送一个字节时交换数据的方式:

SPI协议显示4个信号

请注意,三个信号是主机的输出(MOSI,SCK,SS),一个信号是输入(MISO)。


定时

事件的顺序是:

  • SS 变低以声明它并激活从机
  • SCK行切换以指示何时应采样数据行
  • 的数据由主机和从机所采样领先的边缘SCK(使用缺省时钟相位)
  • 主机和从机都通过更改/(如有必要)为沿的下一位做好准备SCK(使用默认时钟相位)MISOMOSI
  • 一旦传输结束(可能在发送了多个字节之后),则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

仅用于输出SPI的接线

上面的代码(仅发送)可用于驱动输出串行移位寄存器。这些是仅用于输出的设备,因此我们无需担心任何传入的数据。在这种情况下,SS引脚可能称为“存储”或“锁存”引脚。

SPI协议显示3个信号

例如74HC595串行移位寄存器和各种LED灯条,仅举几例。例如,这种由MAX7219芯片驱动的64像素LED显示器:

64像素LED显示屏

在这种情况下,您可以看到电路板制造商使用了略有不同的信号名称:

  • DIN(数据输入)为MOSI(主输出,从输入)
  • CS(芯片选择)为SS(从机选择)
  • CLK(时钟)为SCK(串行时钟)

大多数董事会将遵循类似的模式。有时DIN只是DI(数据输入)。

这是另一个示例,这一次是7段LED显示板(也基于MAX7219芯片):

7段LED显示屏

这使用与另一块板完全相同的信号名称。在这两种情况下,您都可以看到该板只需要连接5条线,其中3条用于SPI,还有电源和地线。


时钟相位和极性

您可以通过四种方式采样SPI时钟。

SPI协议允许改变时钟脉冲的极性。CPOL是时钟极性,CPHA是时钟相位。

  • 模式0(默认)-时钟通常为低(CPOL = 0),并且在从低到高的跃迁(前沿)(CPHA = 0)上采样数据
  • 模式1-时钟通常为低电平(CPOL = 0),并且在从高电平到低电平的过渡(后沿)(CPHA = 1)上采样数据
  • 模式2-时钟通常为高电平(CPOL = 1),并且在从高电平到低电平(前沿)(CPHA = 0)的过渡上对数据进行采样
  • 模式3-时钟通常为高电平(CPOL = 1),并且在从低到高的跳变(后沿)(CPHA = 1)上采样数据

这些在此图中说明:

SPI时钟相位和极性

您应参考设备的数据表以正确确定相位和极性。通常会有一个图表显示如何对时钟进行采样。例如,从74HC595芯片的数据表中:

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);

其中“除法器”是以下之一:

  • SPI_CLOCK_DIV2
  • SPI_CLOCK_DIV4
  • SPI_CLOCK_DIV8
  • SPI_CLOCK_DIV16
  • SPI_CLOCK_DIV32
  • SPI_CLOCK_DIV64
  • SPI_CLOCK_DIV128

假设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设置为低等开销)上)。


连接到Arduino

Arduino Uno

通过数字引脚10至13连接:

Arduino Uno SPI引脚

通过ICSP接头连接:

ICSP引脚排列-Uno

ICSP接头

Arduino的Atmega2560

通过数字引脚50至52连接:

Arduino Mega2560 SPI引脚

您也可以使用ICSP标头,类似于上面的Uno。

Arduino的莱昂纳多

与Uno和Mega不同,Leonardo和Micro不会在数字引脚上暴露SPI引脚。您唯一的选择是使用ICSP排针,如上图所示。


多个奴隶

一台主机可以与多个从机通信(但是一次只能与一个从机通信)。它通过为一个从机声明SS并为所有其他从机取消声明SS来实现此目的。已被SS置为有效的从机(通常意味着LOW)将其MISO引脚配置为输出,以便从机以及单独的从机可以响应主机。如果未声明SS,其他从机将忽略任何传入的时钟脉冲。因此,每个从站都需要一个附加信号,如下所示:

多个SPI从机

在此图中,您可以看到两个从站之间共享MISO,MOSI,SCK,但是每个从站都有自己的SS(从站选择)信号。


通讯协定

SPI规范没有这样指定协议,因此要由各个主/从配对来决定数据的含义。尽管可以同时发送和接收字节,但是接收到的字节不能直接作为对发送字节的响应(因为它们是同时组装的)。

因此,一端发送一个请求(例如4可能意味着“列出磁盘目录”)然后进行传输(也许只是向外发送零),直到它收到一个完整的响应,这将更加合乎逻辑。响应可能以换行符或0x00字符终止。

阅读您的从设备的数据表,以了解其期望的协议序列。


如何制作SPI从设备

前面的示例将Arduino作为主设备,将数据发送到从设备。本示例说明了Arduino如何成为奴隶。

硬件设定

将两个Arduino Unos与以下相互连接的引脚连接在一起:

  • 10(SS)
  • 11(MOSI)
  • 12(MISO)
  • 13(SCK)

  • + 5v(如果需要)

  • GND(用于信号返回)

在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将主机连接到从机的示例

Arduino 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

逻辑分析仪输出

这显示了以上代码中发送和接收之间的时间:

SPI主从时序


从IDE 1.6.0开始的新功能

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?

我要补充一个初步的问题:什么时候/为什么要使用SPI?对多主机配置或大量从机的需求将使规模向I2C倾斜。

这是一个很好的问题。我的答案是:

  • 一些设备(很多)仅支持SPI传输方法。例如,我见过的74HC595输出移位寄存器,74HC165输入移位寄存器,MAX7219 LED驱动器和相当多的LED灯条。因此,您可能会使用它,因为目标设备仅支持它。
  • SPI实际上是Atmega328(和类似产品)芯片上可用的最快方法。上面引用的最快速率是每秒888,888字节。使用I 2 C,您每秒只能得到大约40,000字节。I 2 C 的开销相当大,如果您想真正快速地进行接口连接,则SPI是首选。实际上,有很多芯片系列(例如MCP23017和MCP23S17)同时支持I 2 C和SPI,因此您通常可以在速度和在一条总线上具有多个器件的能力之间进行选择。
  • SPI和I 2 C器件都在Atmega328的硬件中受支持,因此可以想象,您可以同时通过SPI和I 2 C 进行传输,这将大大提高速度。

两种方法都有它们的位置。I 2 C使您可以将许多设备连接到一条总线(两根导线,再加上接地),因此,如果您需要询问大量设备(也许很少),这将是首选。但是,SPI的速度可能与需要快速输出(例如,LED条)或快速输入(例如,ADC转换器)的情况相关。


参考文献


您是否要掩盖Due的SPI的怪异之处?SPI端口的配置与所用的SS引脚绑定在一起,并且有(IIRC)4个硬件SS引脚分配给SPI端口?
马延科

关于选择的其他要点:有时您真的别无选择,因为您想要/需要使用的传感器仅作为I2C提供。
伊戈尔·斯托帕

Are you going to cover the weirdness that is the Due's SPI?-我对Due的SPI一无所知(除了假定总体协议相同)。欢迎您添加涵盖该方面的回复。
尼克·加蒙

这个答案的有声读物什么时候出版,您将自己阅读吗;)
AMADANON Inc.

1
@AMADANONInc。也许是音乐录影带?还是动画?我不确定我的澳大利亚口音是否可以理解。:P
尼克·加蒙
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.