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

Что касается Arduino Uno, Mega2560, Leonardo и подобных плат:

  • Как работает SPI?
  • Как быстро SPI?
  • Как подключиться между ведущим и ведомым?
  • Как создать ведомый SPI?

Обратите внимание: это задание в качестве справочного вопроса.

30 голосов | спросил Nick Gammon 26 thEurope/Moscowp30Europe/Moscow09bEurope/MoscowSat, 26 Sep 2015 04:06:58 +0300 2015, 04:06:58

1 ответ


60

Введение в SPI

Интерфейс Serial Peripheral Interface Bus (SPI) используется для связи между несколькими устройствами по короткому расстояний и на высокой скорости.

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


Сигналы SPI

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

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

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

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

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

Обратите внимание, что три сигнала - это выходы ведущего устройства (MOSI, SCK, SS), а один - вход (MISO).


Timing

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

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

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

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

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

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

 byte outgoing = 0xAB;
 byte incoming = SPI.transfer (исходящий);

Пример кода

Пример отправки только (игнорирование любых входящих данных):

 #include <SPI.h>

void setup (void)
  {
  digitalWrite (SS, HIGH); //обеспечить, чтобы SS оставался высоким
  SPI.begin ();
  } //конец настройки

void loop (void)
  {
  байт c;

  //включить выбор Slave
  digitalWrite (SS, LOW); //SS - контакт 10

  //отправить тестовую строку
  для (const char * p = "Fab"; c = * p; p ++)
    SPI.transfer (c);

  //отключить выбор Slave
  digitalWrite (SS, HIGH);

  задержка (100);
  } //конец цикла

Подключение только для выхода SPI

Вышеприведенный код (который только отправляет) может использоваться для управления результирующим регистрационным регистром. Это устройства с выходными данными, поэтому нам не нужно беспокоиться о любых входящих данных. В их случае штырь SS можно назвать булавкой «store» или «защелкой».

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

Примерами этого могут служить серийный регистр сдвига 74HC595 и различные светодиодные полосы, чтобы упомянуть пару. Например, этот 64-пиксельный светодиодный дисплей, управляемый микросхемой MAX7219:

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

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

  • DIN (Data In) - MOSI (Master Out, Slave In)
  • CS (Chip Select) - SS (Slave Select)
  • CLK (Clock) - это 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 clock

Как вы можете видеть, часы обычно низки (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 МГц, сначала MSB, режим 0

Порядок данных

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

 SPI.setBitOrder (LSBFIRST); //наименее значимый бит
SPI.setBitOrder (MSBFIRST); //старший бит

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

 SPI.beginTransaction (SPISettings (1000000, LSBFIRST, SPI_MODE2)); //Частота 1 МГц, сначала LSB, режим 2

Speed ​​

По умолчанию для 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 МГц, сначала MSB, режим 0

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

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


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

Arduino Uno

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

 Arduino Uno SPI pins

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

<аhref = "https://i.stack.imgur.com/0c2a8.jpg" rel = "noreferrer"> ICSP распиновки - Uno

 заголовок ICSP

Arduino Atmega2560

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

 Arduino Mega2560 SPI контакты

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

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

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


Несколько подчиненных устройств

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

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

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


Протоколы

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

Таким образом, было бы логичнее, если бы один конец отправил запрос (например, 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); //обеспечить, чтобы SS оставался высоким на данный момент

  //Поместите контакты SCK, MOSI, SS в режим вывода
  //также помещает SCK, MOSI в состояние LOW и SS в состояние HIGH.
  //Затем поставьте оборудование SPI в режим Master и включите SPI
  SPI.begin ();

  //Замещаем мастера немного
  SPI.setClockDivider (SPI_CLOCK_DIV8);

} //конец настройки


void loop (void)
{

  char c;

  //включить выбор Slave
  digitalWrite (SS, LOW); //SS - контакт 10

  //отправить тестовую строку
  for (const char * p = "Hello, world! \ n"; c = * p; p ++)
    SPI.transfer (c);

  //отключить выбор Slave
  digitalWrite (SS, HIGH);

  задержка (1000); //1 секунда задержки
} //конец цикла

Пример подчиненного

 #include <SPI.h>

char buf [100];
volatile byte pos;
volatile bool process_it;

void setup (void)
{
  Serial.begin (115200); //отладка

  //включение SPI в подчиненном режиме
  SPCR | = бит (SPE);

  //нужно отправлять на master in, * slave out *
  pinMode (MISO, OUTPUT);

  //готовиться к прерыванию
  pos = 0; //буфер пуст
  process_it = false;

  //теперь включаем прерывания
  SPI.attachInterrupt ();

} //конец настройки


//Процедура прерывания SPI
ISR (SPI_STC_vect)
{
байт c = SPDR; //захват байта из регистра данных SPI

  //добавляем в буфер, если номер
  if (pos <sizeof buf)
    {
    buf [pos ++] = c;

    //пример: новая строка означает время для обработки буфера
    if (c == '\ n')
      process_it = true;

    } //конец доступной комнаты
} //завершение процедуры прерывания SPI_STC_vect

//main loop - ожидание установки флага в режиме прерывания
void loop (void)
{
  if (process_it)
    {
    buf [pos] = 0;
    Serial.println (buf);
    pos = 0;
    process_it = false;
    } //конец набора флагов

} //конец цикла

slave полностью прерывается, поэтому он может делать другие вещи. Входящие данные SPI собираются в буфере, а флаг устанавливается, когда приходит «значительный байт» (в этом случае новая строка). Это говорит подчиненному устройству, чтобы он начал и начал обрабатывать данные.

Пример подключения ведущего к ведомому с помощью SPI

 Arduino SPI master и slave


Как получить ответ от подчиненного устройства

Следуя приведенному выше коду, который отправляет данные от ведущего устройства SPI к подчиненному устройству, приведенный ниже пример показывает отправку данных в подчиненный, когда он что-то делает с ним и возвращает ответ.

Мастер похож на приведенный выше пример. Однако важно то, что нам нужно добавить небольшую задержку (примерно 20 микросекунд). В противном случае ведомое устройство не будет реагировать на входящие данные и что-то делать с ним.

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

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

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

 transferAndWait ('a'); //добавление команды
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); //обеспечить, чтобы SS оставался высоким на данный момент
    SPI.begin ();

    //Замещаем мастера немного
    SPI.setClockDivider (SPI_CLOCK_DIV8);
    } //конец настройки

  байт передачаAndWait (const byte какой)
    {
    byte a = SPI.transfer (что);
    delayMicroseconds (20);
    return a;
    } //конец передачиAndWait

  void loop (void)
    {

    байт a, b, c, d;

    //включить выбор Slave
    digitalWrite (SS, LOW);

    transferAndWait ('a'); //добавление команды
    transferAndWait (10);
    a = transferAndWait (17);
    b = transferAndWait (33);
    c = transferAndWait (42);
    d = transferAndWait (0);

    //отключить выбор Slave
    digitalWrite (SS, HIGH);

    Serial.println («Добавление результатов:»);
    Serial.println (a, DEC);
    Serial.println (b, DEC);
    Serial.println (c, DEC);
    Serial.println (d, DEC);

    //включить выбор Slave
    digitalWrite (SS, LOW);

    transferAndWait ('s'); //команда вычитания
    transferAndWait (10);
    a = transferAndWait (17);
    b = transferAndWait (33);
    c = transferAndWait (42);
    d = transferAndWait (0);

    //отключить выбор Slave
    digitalWrite (SS, HIGH);

    Serial.println («Вычитание результатов:»);
    Serial.println (a, DEC);
    Serial.println (b, DEC);
    Serial.println (c, DEC);
    Serial.println (d, DEC);

    задержка (1000); //1 секунда задержки
    } //конец цикла

Код для ведомого в основном делает почти все в подпрограмме прерывания (называемой при поступлении входящих SPI-данных). Он принимает входящий байт и добавляет или вычитает в соответствии с запоминаемым «командным байтом». Обратите внимание, что ответ будет «собран» в следующий раз через цикл. Вот почему мастер должен отправить окончательный «фиктивный» перевод, чтобы получить окончательный ответ.

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

Более надежно, это было бы сделано с прерыванием. То есть вы физически подключаете SS к одному из входов прерывания (например, на Uno, подключите вывод 10 (SS) к контакту 2 (вход прерывания) или используйте прерывание смены выводов на выводе 10.

Тогда прерывание может использоваться, чтобы заметить, когда SS вытягивается низким или высоким.

Ведомый (пример)

 //что делать с входящими данными
volatile byte command = 0;

void setup (void)
  {

  //нужно отправлять на master in, * slave out *
  pinMode (MISO, OUTPUT);

  //включение SPI в подчиненном режиме
  SPCR | = _BV (SPE);

  //включение прерываний
  SPCR | = _BV (SPIE);

  } //конец настройки


//Процедура прерывания SPI
ISR(SPI_STC_vect)
  {
  байт c = SPDR;

  переключатель (команда)
    {
    //нет команды? то это команда
    случай 0:
      command = c;
      SPDR = 0;
      ломать;

    //добавляем к входящему байту, возвращаем результат
    case 'a':
      SPDR = c + 15; //добавьте 15
      ломать;

    //вычесть из входящего байта, вернуть результат
    case 's':
      SPDR = c - 8; //вычесть 8
      ломать;

    } //конец коммутатора

  } //завершение процедуры обслуживания прерываний (ISR) SPI_STC_vect

void loop (void)
  {

  //если SPI не активен, команда clear current
  if (digitalRead (SS) == HIGH)
    command = 0;
  } //конец цикла

Пример вывода

 Добавление результатов:
25
32
48
57
Вычитание результатов:
2
9
25
34
Добавление результатов:
25
32
48
57
Вычитание результатов:
2
9
25
34

Выход логического анализатора

Показывает время между отправкой и получением в приведенном выше коде:

 SPI master и slave timing


Новые функции в IDE версии 1.6.0 и более

Версия 1.6.0 IDE изменила способ работы SPI. Вам еще нужно еще SPI.begin () использовать SPI. Это устанавливает оборудование SPI. Однако теперь, когда вы собираетесь начать общение с подчиненным, вы также выполните SPI.beginTransaction (), чтобы настроить SPI (для этого подчиненного устройства) с правильной:

  • Частота часов
  • Порядок бит
  • Фаза и полярность часов

Когда вы закончите связь с ведомым, вы вызываете SPI.endTransaction (). Например:

 SPI.beginTransaction (SPISettings (2000000, MSBFIRST, SPI_MODE0));
digitalWrite (SS, LOW); //Утверждение выбора подчиненного устройства
byte foo = SPI.transfer (42); //делаем перевод
digitalWrite (SS, HIGH); //отменять выбор подчиненного
SPI.endTransaction (); //транзакция

Зачем использовать SPI?

  

Я бы добавил один предварительный вопрос: когда /зачем вы используете SPI? Потребность в конфигурации с несколькими ведущими устройствами или очень большое количество подчиненных устройств будет наклонять шкалу к I2C.

Это отличный вопрос. Мои ответы:

  • Некоторые устройства (немало) поддерживают только метод передачи SPI. Например, регистр выходного сдвига 74HC595, регистра сдвига входа 74HC165, драйвер светодиода MAX7219 и довольно много светодиодных полос, которые я видел. Таким образом, вы можете использовать его, потому что целевое устройство поддерживает его только.
  • 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 может быть более актуальной для ситуаций, когда вам нужно быстро выводить (например, светодиодную ленту) или быстро вводить (например, преобразователь АЦП).


Ссылки

ответил Nick Gammon 26 thEurope/Moscowp30Europe/Moscow09bEurope/MoscowSat, 26 Sep 2015 04:06:58 +0300 2015, 04:06:58

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132