ArduinoでSPIをどのように使用しますか?


44

Arduino Uno、Mega2560、Leonardo、および同様のボードを参照して:

  • SPIはどのように機能しますか?
  • SPIはどれくらい高速ですか?
  • マスターとスレーブを接続するにはどうすればよいですか?
  • SPIスレーブを作成するにはどうすればよいですか?

注:これは参照用の質問です。


あなたは、この関連の質問に答えることができarduino.stackexchange.com/questions/60703/...
QWR

回答:


80

SPIの概要

シリアル・ペリフェラル・インタフェース(SPI)インターフェースは、短い距離にわたる複数のデバイス間で通信するための、高速で使用されます。

通常、通信を開始し、データ転送速度を制御するクロックを供給する単一の「マスター」デバイスがあります。1つ以上のスレーブが存在する可能性があります。複数のスレーブの場合、各スレーブには独自の「スレーブ選択」信号があります。これについては後述します。


SPI信号

本格的なSPIシステムでは、4つの信号線があります。

  • マスター出力、スレーブ入力(MOSI)-マスターからスレーブに送信されるデータ
  • マスター入力、スレーブ出力(MISO)-スレーブからマスターに送信されるデータ
  • シリアルクロック(SCK)-マスターとスレーブの両方を切り替えると、次のビットがサンプリングされます
  • スレーブ選択(SS)-これは特定のスレーブに「アクティブ」になるように指示します

複数のスレーブがMISO信号に接続されている場合、スレーブ選択がアサートされて選択されるまで、MISOラインをトライステート(高インピーダンスに維持)することが期待されます。通常、スレーブ選択(SS)はLowになってアサートします。つまり、アクティブローです。特定のスレーブが選択されると、MISOラインを出力として設定し、マスターにデータを送信できるようにします。

この画像は、1バイトが送信されるときにデータが交換される方法を示しています。

4つの信号を示すSPIプロトコル

3つの信号がマスターからの出力(MOSI、SCK、SS)であり、1つが入力(MISO)であることに注意してください。


タイミング

イベントのシーケンスは次のとおりです。

  • SS アサートしてスレーブをアクティブにするためにローになります
  • SCKラインは、データラインをサンプリングしなければならないときを示すためにトグル
  • データは、(デフォルトのクロック位相を使用して)のリーディングエッジでマスターとスレーブの両方によってサンプリングされSCKます
  • マスターとスレーブの両方が、必要に応じて/ を変更することにより、(デフォルトのクロック位相を使用して)縁の次のビットの準備SCKをしますMISOMOSI
  • 伝送が終了すると(おそらく複数のバイトが送信された後)、SSアサートを解除するためにHighになります

ご了承ください:

  • 最上位ビットが最初に送信されます(デフォルト)
  • データは同じ瞬間に送受信されます(全二重)

データは同じクロックパルスで送受信されるため、スレーブがすぐにマスターに応答することはできません。SPIプロトコルは通常、マスターが1回の送信でデータを要求し、後続の送信で応答を取得することを想定しています。

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ピンは「ストア」または「ラッチ」ピンと呼ばれることがあります。

3つの信号を示すSPIプロトコル

この例としては、74HC595シリアルシフトレジスタ、およびいくつかのLEDストリップがあります。たとえば、MAX7219チップによって駆動されるこの64ピクセルLEDディスプレイは次のとおりです。

64ピクセルLEDディスプレイ

この場合、ボードメーカーがわずかに異なる信号名を使用していることがわかります。

  • DIN(データ入力)はMOSI(マスター出力、スレーブ入力)です
  • CS(チップセレクト)はSS(スレーブセレクト)
  • CLK(クロック)はSCK(シリアルクロック)

ほとんどのボードは同様のパターンに従います。DINは単なるDI(データ入力)である場合があります。

別の例を次に示します。今回は7セグメントLEDディスプレイボード(MAX7219チップにも基づいています):

7セグメントLEDディスプレイ

これは、他のボードとまったく同じ信号名を使用します。どちらの場合も、ボードに必要な配線は5本、SPI用は3本、さらに電源とグランドのみです。


クロック位相と極性

SPIクロックをサンプリングする方法は4つあります。

SPIプロトコルでは、クロックパルスの極性を変えることができます。CPOLはクロック極性で、CPHAはクロック位相です。

  • モード0(デフォルト)-クロックは通常低(CPOL = 0)で、データは低から高への遷移でサンプリングされます(リーディングエッジ)(CPHA = 0)
  • モード1-クロックは通常ロー(CPOL = 0)で、データはハイからローへの遷移でサンプリングされます(後端)(CPHA = 1)
  • モード2-クロックは通常High(CPOL = 1)で、データはHighからLowへの遷移でサンプリングされます(リーディングエッジ)(CPHA = 0)
  • モード3-クロックは通常ハイ(CPOL = 1)で、データはローからハイへの遷移でサンプリングされます(後端)(CPHA = 1)

これらはこのグラフィックで説明されます:

SPIクロックの位相と極性

位相と極性を正しくするには、デバイスのデータシートを参照してください。通常、クロックのサンプリング方法を示す図があります。たとえば、74HC595チップのデータシートから:

74HC595クロック

ご覧のとおり、クロックは通常は低く(CPOL = 0)、リーディングエッジでサンプリングされる(CPHA = 0)ため、SPIモード0です。

次のようなコードでクロックの極性と位相を変更できます(もちろん、1つだけ選択してください)。

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のデフォルト設定では、システムクロック速度を4で割った値、つまり16 MHzのCPUクロックを想定して250 nsごとに1つのSPIクロックパルスを使用します。次のsetClockDividerように使用して、クロック分周器を変更できます。

SPI.setClockDivider (divider);

「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で割る」または125 nsごとに1つのSPIクロックパルスです。したがって、1バイトを送信するには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

ただし、経験的なテストでは、バイト間に2つのクロックパルスが必要であることが示されているため、バイトをクロックアウトできる最大レートはそれぞれ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ピン

上記のUnoと同様に、ICSPヘッダーを使用することもできます。

アルドゥイーノ・レオナルド

LeonardoとMicroは、UnoとMegaとは異なり、デジタルピン上のSPIピンを公開しません。唯一のオプションは、上記のUnoで示したように、ICSPヘッダーピンを使用することです。


複数のスレーブ

マスターは複数のスレーブと通信できます(ただし、一度に1つのみ)。これは、1つのスレーブに対してSSをアサートし、他のすべてのスレーブに対してSSをディアサートすることによりこれを行います。SSがアサートされているスレーブ(通常これはLOWを意味します)は、MISOピンを出力として設定し、スレーブとそのスレーブのみがマスターに応答できるようにします。SSがアサートされていない場合、他のスレーブは着信クロックパルスを無視します。したがって、次のように、各スレーブに1つの追加信号が必要です。

複数のSPIスレーブ

この図では、MISO、MOSI、SCKが両方のスレーブ間で共有されていますが、各スレーブには独自のSS(スレーブ選択)信号があります。


プロトコル

SPI仕様はプロトコル自体を指定していないため、データの意味について合意するのは個々のマスター/スレーブの組み合わせです。バイトを同時に送信および受信できますが、受信バイトは送信バイトに対する直接の応答にはなりません(同時に組み立てられるため)。

そのため、一方の側が要求を送信し(たとえば、4は「ディスクディレクトリを一覧表示する」ことを意味する)、完全な応答を受信するまで転送を行う(おそらくゼロを外部に送信する)方が論理的です。応答は、改行または0x00文字で終了する場合があります。

スレーブデバイスのデータシートを読んで、期待されるプロトコルシーケンスを確認してください。


SPIスレーブの作成方法

前の例では、Arduinoをマスターとして使用して、データをスレーブデバイスに送信しています。この例は、Arduinoがスレーブになる方法を示しています。

ハードウェアのセットアップ

2つの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を上げてトランザクションを終了します。

非常に重要な点は、スレーブが同時に着信バイトに応答できないことです。応答は次のバイトにある必要があります。これは、送信中のビットと受信中のビットが同時に送信されているためです。したがって、4つの数値に何かを追加するには、次のように5つの転送が必要です。

transferAndWait ('a');  // add command
transferAndWait (10);
a = transferAndWait (17);
b = transferAndWait (33);
c = transferAndWait (42);
d = transferAndWait (0);

最初に10番のアクションを要求します。しかし、次の転送(17の転送)まで応答を受け取りません。ただし、「a」は10への応答に設定されます。最後に、42の応答を取得するために「ダミー」番号0を送信することになります。

マスター(例)

  #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がHighになるタイミングを検出し、保存されたコマンドをクリアします。これにより、SSが次のトランザクションのために再びLowにプルされると、最初のバイトがコマンドバイトと見なされます。

より確実に、これは割り込みで行われます。つまり、SSを割り込み入力の1つに物理的に接続します(たとえば、Unoで、ピン10(SS)をピン2(割り込み入力)に接続するか、ピン10でピン変更割り込みを使用します)。

次に、割り込みを使用して、SSがLowまたはHighにプルされていることを通知できます。

スレーブ(例)

// 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.begin() SPIを使用する前に。これでSPIハードウェアがセットアップされます。ただし、現在、スレーブとの通信を開始しようとしているときに、正しい(このスレーブの)SPIをセットアップすること行いSPI.beginTransaction()ます。

  • クロック速度
  • ビット順
  • クロック位相と極性

スレーブとの通信が完了したら、を呼び出します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を使用する理由

予備的な質問を1つ追加します。いつ、なぜSPIを使用するのですか。マルチマスター構成または非常に多数のスレーブが必要な場合、スケールをI2Cに傾けます。

これは素晴らしい質問です。私の答えは:

  • 一部のデバイス(かなり少数)は、SPI転送方法のみをサポートしています。たとえば、74HC595出力シフトレジスタ、74HC165入力シフトレジスタ、MAX7219 LEDドライバ、および私が見たかなりの数のLEDストリップ。そのため、ターゲットデバイスがサポートしているだけなので、これを使用できます。
  • SPIは本当にAtmega328(および同様の)チップで利用可能な最速の方法です。上記の最速のレートは888,888バイト/秒です。I 2 Cを使用すると、1秒あたり約40,000バイトしか取得できません。I 2 C のオーバーヘッドは非常に大きく、非常に高速にインターフェースをとろうとしている場合は、SPIをお勧めします。かなり少数のチップファミリ(MCP23017やMCP23S17など)がI 2 CとSPIの両方を実際にサポートしているため、多くの場合、速度と、単一バスで複数のデバイスを使用するかを選択できます。
  • SPIおよびI 2 CデバイスはどちらもAtmega328のハードウェアでサポートされているため、SPI経由でI 2 C と同時に転送を行うことで速度を上げることができます。

どちらの方法にもそれぞれの場所があります。I 2 Cでは、多数のデバイスを1つのバス(2本のワイヤとアース)に接続できるため、かなりの数のデバイスを照会する必要がある場合(おそらくほとんど使用しない場合)に選択することをお勧めします。ただし、SPIの速度は、高速で出力する必要がある場合(LEDストリップなど)または高速で入力する必要がある場合(ADCコンバーターなど)に関連する可能性があります。


参照資料


Due's SPIである奇妙さをカバーしますか?SPIポートの構成が使用されるSSピンに関連付けられており、SPIポートに(IIRC)4つのハードウェアSSピンが割り当てられていますか?
マジェンコ

選択に関するその他のポイント:使用したい/必要なセンサーはI2Cとしてのみ使用可能なため、選択の余地がない場合があります。
イゴールストッパ

Are you going to cover the weirdness that is the Due's SPI?-DueのSPIについては何も知りません(プロトコル全体が同じであると仮定する以外は)。その側面をカバーする返信を追加してください。
ニックギャモン

この回答のオーディオブックはいつ発表されますか、そしてあなたは自分でそれを読むでしょうか;)
アマダノン株式会社

1
@AMADANONInc。おそらくミュージックビデオですか?それともアニメーション?私のオーストラリアのアクセントが理解できるかどうかはわかりません。:P
ニックギャモン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.