Как вы используете SPI на Arduino?

45

Применительно к платам Arduino Uno, Mega2560, Leonardo и аналогичным:

  • Как работает SPI?
  • Как быстро работает SPI?
  • Как мне установить связь между хозяином и рабом?
  • Как мне сделать SPI рабом?

Пожалуйста, обратите внимание: это задумано как справочный вопрос.

Ник Гаммон
источник
Можете ли вы ответить на этот связанный вопрос arduino.stackexchange.com/questions/60703/…
qwr

Ответы:

81

Введение в SPI

Интерфейс последовательной периферийной интерфейсной шины (SPI) используется для связи между несколькими устройствами на коротких расстояниях и с высокой скоростью.

Как правило, существует одно «главное» устройство, которое инициирует связь и снабжает часы, которые контролируют скорость передачи данных. Может быть один или несколько рабов. Для более чем одного ведомого, каждый имеет свой собственный сигнал выбора ведомого, описанный ниже.


Сигналы SPI

В полноценной системе SPI у вас будет четыре сигнальные линии:

  • Master Out, Slave In ( MOSI ) - данные, передаваемые от мастера к подчиненному
  • Master In, Slave Out ( MISO ) - данные, передаваемые от подчиненного устройства к ведущему
  • Serial Clock ( SCK ) - когда это переключает и главный и подчиненный сэмплы на следующий бит
  • Slave Select ( SS ) - это говорит о том, что конкретное ведомое устройство становится «активным»

Когда к ведомому сигналу MISO подключено несколько подчиненных устройств, ожидается, что они будут в трех состояниях (поддерживать на высоком импедансе) эту линию MISO до тех пор, пока они не будут выбраны с помощью выбора ведомого устройства. Обычно Slave Select (SS) понижается, чтобы утверждать это. То есть это активный низкий уровень. Как только конкретный ведомый выбран, он должен сконфигурировать линию MISO в качестве выхода, чтобы он мог отправлять данные ведущему устройству.

Это изображение показывает способ обмена данными при отправке одного байта:

Протокол SPI, показывающий 4 сигнала

Обратите внимание, что три сигнала являются выходами от мастера (MOSI, SCK, SS), а один является входом (MISO).


тайминг

Последовательность событий:

  • SS идет низко, чтобы утвердить его и активировать раба
  • SCKЛиния переключается , чтобы указать , когда линии данных должны быть выбраны
  • Данные выбираются как ведущим, так и ведомым на переднем фронте SCK(используя фазу синхронизации по умолчанию)
  • И ведущий, и ведомый подготавливают следующий бит на заднем фронте SCK(используя фазу синхронизации по умолчанию), изменяя MISO/ MOSIпри необходимости
  • Как только передача закончена (возможно, после того, как несколько байтов были отправлены), тогда SSидет высокий уровень, чтобы де-утверждать это

Обратите внимание, что:

  • Самый старший бит отправляется первым (по умолчанию)
  • Данные отправляются и принимаются одновременно (полный дуплекс)

Поскольку данные отправляются и принимаются с одним и тем же тактовым импульсом, ведомое устройство не может немедленно ответить ведущему. Протоколы SPI обычно ожидают, что мастер запросит данные за одну передачу и получит ответ о последующей.

Используя библиотеку SPI на Arduino, выполнение одной передачи выглядит так в коде:

 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 и различные светодиодные полосы, просто чтобы упомянуть пару. Например, этот 64-пиксельный светодиодный дисплей, управляемый чипом MAX7219:

64-пиксельный светодиодный дисплей

В этом случае вы можете видеть, что производитель плат использовал несколько разные названия сигналов:

  • DIN (Data In) - MOSI (ведущий, ведомый)
  • CS (Chip Select) - это SS (Slave Select)
  • CLK (часы) - это SCK (последовательные часы)

Большинство досок будут следовать аналогичной схеме. Иногда DIN это просто DI (Data In).

Вот еще один пример, на этот раз 7-сегментная плата светодиодного дисплея (также основанная на чипе MAX7219):

7-сегментный светодиодный дисплей

При этом используются те же имена сигналов, что и на другой плате. В обоих этих случаях вы можете видеть, что плате нужно только 5 проводов к ней, три для 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);

Этот метод не рекомендуется в версиях 1.6.0 и выше в Arduino IDE. Для последних версий вы меняете режим часов в 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

Опять же, это не рекомендуется в версиях 1.6.0 и более поздних версий Arduino IDE. Для последних версий вы меняете порядок битов в SPI.beginTransactionвызове, например так:

SPI.beginTransaction (SPISettings (1000000, LSBFIRST, SPI_MODE2));  // 1 MHz clock, LSB first, mode 2

скорость

По умолчанию для SPI используется системная тактовая частота, деленная на четыре, то есть один тактовый импульс SPI каждые 250 нс при условии тактовой частоты процессора 16 МГц. Вы можете изменить делитель часов, используя 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

Самая быстрая частота - это «деление на 2» или один тактовый импульс SPI каждые 125 нс, при условии тактовой частоты процессора 16 МГц. Поэтому для передачи одного байта потребуется 8 * 125 нс или 1 мкс.

Этот метод не рекомендуется в версиях 1.6.0 и выше в Arduino IDE. Для последних версий вы меняете скорость передачи в SPI.beginTransactionвызове, например так:

SPI.beginTransaction (SPISettings (4000000, MSBFIRST, SPI_MODE0));  // 4 MHz clock, MSB first, mode 0

Однако эмпирическое тестирование показывает, что необходимо иметь два тактовых импульса между байтами, поэтому максимальная частота, с которой байты могут быть тактированы, составляет 1,125 мкс каждый (с делителем тактовой частоты 2).

Подводя итог, можно сказать, что каждый байт может отправляться с максимальной скоростью один на 1,125 мкс (с тактовой частотой 16 МГц), обеспечивая теоретическую максимальную скорость передачи 1 / 1,125 мкс или 888 888 байт в секунду (исключая издержки, такие как установка низкого уровня SS и т. Д. на).


Подключение к Arduino

Arduino Uno

Подключение через цифровые контакты с 10 по 13:

Arduino Uno SPI контакты

Подключение через заголовок ICSP:

Распиновка ICSP - Uno

Заголовок ICSP

Arduino Atmega2560

Подключение через цифровые контакты от 50 до 52:

Arduino Mega2560 SPI контакты

Вы также можете использовать заголовок ICSP, аналогично Uno выше.

Ардуино Леонардо

Леонардо и Микро не выставляют контакты SPI на цифровых выводах, в отличие от Uno и Mega. Единственный вариант - использовать контакты заголовка ICSP, как показано выше для Uno.


Несколько рабов

Мастер может общаться с несколькими ведомыми (но только по одному за раз). Он делает это, утверждая SS для одного раба и отменяя его для всех остальных. Ведомый, у которого установлен SS (обычно это означает НИЗКИЙ), настраивает свой вывод MISO как выходной, чтобы подчиненный и только один ведомый могли ответить ведущему. Другие ведомые устройства игнорируют любые входящие тактовые импульсы, если SS не утвержден. Таким образом, вам нужен один дополнительный сигнал для каждого ведомого, как это:

Несколько ведомых SPI

На этом рисунке вы можете видеть, что MISO, MOSI, SCK совместно используются обоими ведомыми, однако у каждого ведомого есть свой собственный сигнал SS (выбор ведомого).


протоколы

Спецификация SPI не определяет протоколы как таковые, поэтому решение о том, что означают данные, зависит от пары «главный / подчиненный». Хотя вы можете отправлять и получать байты одновременно, полученный байт не может быть прямым ответом на отправленный байт (так как они собираются одновременно).

Таким образом, было бы более логично для одного конца отправить запрос (например, 4 может означать «перечислить каталог диска»), а затем делать передачи (возможно, просто отправляя нули наружу), пока не получит полный ответ. Ответ может заканчиваться символом новой строки или символом 0x00.

Прочитайте таблицу данных для вашего ведомого устройства, чтобы увидеть, какие последовательности протоколов оно ожидает.


Как сделать SPI рабом

В предыдущем примере Arduino показывается как мастер, отправляющий данные на подчиненное устройство. Этот пример показывает, как Arduino может быть рабом.

Настройка оборудования

Соедините два Unix Arduino вместе со следующими контактами, соединенными друг с другом:

  • 10 (СС)
  • 11 (МОСИ)
  • 12 (МИСО)
  • 13 (SCK)

  • + 5В (если требуется)

  • 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 микросекунд). В противном случае у ведомого не будет возможности отреагировать на поступающие данные и что-то с ними сделать.

Пример показывает отправку «команды». В этом случае «а» (добавить что-то) или «s» (вычесть что-то). Это должно показать, что раб фактически делает что-то с данными.

После утверждения выбора ведомого (SS), чтобы инициировать транзакцию, мастер отправляет команду, после которой следует любое количество байтов, а затем вызывает SS, чтобы завершить транзакцию.

Очень важным моментом является то, что ведомый не может ответить на входящий байт в тот же момент. Ответ должен быть в следующем байте. Это связано с тем, что отправляемые биты и принимаемые биты отправляются одновременно. Таким образом, чтобы добавить что-то к четырем числам, нам нужно пять передач, например:

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

Сначала мы запрашиваем действие по номеру 10. Но мы не получаем ответа до следующего перевода (тот, что на 17). Однако «а» будет установлен в ответ на 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.begin() перед использованием SPI. Это настраивает оборудование 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 и несколько светодиодных полос, которые я видел. Таким образом, вы можете использовать его, потому что целевое устройство поддерживает только его.
  • SPI - действительно самый быстрый метод, доступный на чипах Atmega328 (и аналогичных). Самая высокая скорость, указанная выше, составляет 888 888 байт в секунду. Используя I 2 C, вы можете получить только около 40000 байт в секунду. Издержки I 2 C весьма существенны, и если вы пытаетесь очень быстро взаимодействовать, SPI является предпочтительным выбором. Многие семейства микросхем (например, MCP23017 и MCP23S17) фактически поддерживают I 2 C и SPI, поэтому вы часто можете выбирать между скоростью и возможностью иметь несколько устройств на одной шине.
  • Устройства SPI и I 2 C поддерживаются аппаратно на Atmega328, так что вы могли бы выполнять передачу через SPI одновременно с I 2 C, что обеспечило бы вам повышение скорости.

Оба метода имеют свое место. I 2 C позволяет подключать множество устройств к одной шине (два провода плюс заземление), так что это будет предпочтительным выбором, если вам необходимо опросить значительное количество устройств, возможно, довольно редко. Однако скорость SPI может быть более релевантной для ситуаций, когда вам нужно быстро выводить (например, светодиодная лента) или вводить быстро (например, АЦП).


Ссылки

Ник Гаммон
источник
Собираетесь ли вы раскрыть странность, которая является SPI из-за? Где конфигурация порта SPI связана с используемым выводом SS, и есть ли (IIRC) 4 аппаратных вывода SS, назначенных порту SPI?
Majenko
Другой момент, касающийся выбора: иногда у вас действительно нет выбора, потому что датчик, который вы хотите / нужно использовать, доступен только как I2C.
Игорь Стоппа
Are you going to cover the weirdness that is the Due's SPI?- Я ничего не знаю о SPI Due (кроме предположения, что общий протокол тот же). Вы можете добавить ответ, охватывающий этот аспект.
Ник Гэммон
Когда выйдет аудиокнига об этом ответе, и вы будете ее читать сами;)
AMADANON Inc.,
1
@AMADANONInc. Возможно музыкальное видео? Или анимация? Я не уверен, что мой австралийский акцент будет понятен. : P
Ник Гэммон