- Назначение и принцип работы,
- Реализация в Ардуино,
- Примеры,
- Вывод,
- FAQ.
Назначение и принцип работы
Название интерфейс SPI является аббревиатурой от “Serial Peripheral Bus”, что можно перевести как “шина для подключения периферийных устройств”. Отсюда вытекает ее главное назначение - связать одно главное устройство - Ведущее (Master) - с одним или несколькими Ведомыми (Slave). Ведущий в этом интерфейсе всегда один, только он руководит всем процессом и только он может формировать тактовые импульсы. Если в нашем случае Ведущим всегда является микроконтроллер (эту роль может исполнять и компьютер, но это другая история), то Ведомыми в подавляющем большинстве случаев являются именно периферические устройства. Датчики, дисплеи, микросхемы ЦАП и АЦП, RFID-ридеры, модули беспроводной связи, включая приемо-передатчики WiFi и Bluetooth, GPRS-адаптеры и так далее. Для связи контроллеров и/или компьютеров обычно не используется, но вообще это возможно и такой пример мы рассмотрим ниже. Особенно этот интерфейс востребован там, где требуется высокая скорость передачи данных и не менее высокая надежность. SPI именно такой, он самый быстрый из всех имеющихся в нашем распоряжении и самый “легкий” с точки зрения потреблемых ресурсов. Расплатой за это является использование большего количеств проводов, чем для других интерфесов. Здесь из требуется аж 3 штуки только непосредственно для передачи данных, не зря же его второе имя 3-ware. А именно:
MOSI - Master Output Slave Input (Ведущий передает, Ведомый принимает),MISO - Master Input Slave Output (Ведущий принимает, Ведомый передает),
SCLK, иначе SCK - Serial Clock (тактовый сигнал).
Как мы видим, в данном протоколе есть признаки обоих известных нам интерфейсов, от UART достались две независимые шины на вход и на выход, от i2c - тактовый сигнал, для надежной синхронизации, так что можно уже примерно вообразить себе общий принцип работы SPI, но в нем есть и несколько специфических решений.
Три провода - это уже рекорд расточительства, но нам этого мало. Для корректной работы соединения потребуется еще несколько. Так как устройств к шине можно подключить одновременно несколько, но при этом они не обладают уникальными идентификаторами, как, например, в i2c, значит нужен какой-то способ отличать одно от другого. Ведущий должен точно знать, кому он отправляет данные и от кого их принимает. Для этого в протокол добавлен провод SS - Slave Select. У каждого Ведомого есть для этого отдельный пин, за состоянием которого он пристально наблюдает, падение в низкий уровень означает, что Ведущий выбрал его любимой женой обращается конкретно к нему и они начинают активную переписку.
Есть три способа подключения Ведущего и Ведомых.
Первый: если Ведомый один, одноименные пины просто соединяются напрямую. Такое даже рисовать не будем.
Второй: классический. Несколько Ведомых соединяются с Ведущим параллельно шинами MISO, MOSI и SCK, а SS ведет к каждому из них индивидуально.
Схема простая и понятная, но требует дополнительно столько пинов, сколько на шине устройств.
Третий: цепочка, он же кольцо. SS один на всех, но данные передаются как бы сквозь устройства с одного на другое. Пока SS в низком уровне, данные растекаются по своим местам, после подъема уровня SS, устройства начинают с принятыми данными работать. Очевидный плюс: от контроллера требуется меньше пинов, но и жирный минус: далеко не все устройства поддерживают сквозную передачу данных.
Что-то это напоминает? Да это же явная отсылка к принципу работы сдвиговых регистров, соединенных каскадом! Скажем больше, выходные регистры 74HC595 запросто работают по этому протоколу, чем можно успешно пользоваться. Ниже мы рассмотрим такой пример.
Протокол передачи данных по интерфейсу SPI очень прост и потому имеет минимум вариантов - так называемых режимов. Они описываются всего двумя параметрами:
CPOL - исходный уровень сигнала синхронизации, низкий = 0 или высокий =1,
CPHA - фаза синхронизации, в какой момент импульса будет выполнена установка и считывание данных. По переднему фронту считывание, по заднему установка = 0, наоборот = 1.
Разумеется, важно, чтобы все устройства в сети работали на одинаковых режимах SPI.
В качестве краткого промежуточного итога отметим преимущества и недостатки интерфейса SPI в сравнении с i2c.
Преимущества: SPI максимально прост и потому максимально быстр. Скорость может достигать десятков мегагерц, что позволяет передавать большие объемы данных в потоковом режиме. Все шины однонаправлены, это упрощает задачу преобразования уровней. Программная реализация тоже максимально проста.
Недостатки: требует большее количество проводов и пинов, которое напрямую зависит от количества устройств, в i2c всего два провода на любое количество абонентов, i2c более стандартизирован, меньше риск столкнуться с разнотипными устройствами, работающими в разных режимах.
Реализация в Ардуино
Обязательно к прочтению:I2c и UART
Реализация в Arduino
Как уже говорилось, интерфейс SPI преимущественно предназначен для связи главного устройства с периферией. Из этого следует две особенности. Первая: подавляющее большинство устройств, предназначенных для работы в среде Ардуино, обзавелись собственными библиотеками, и все общение с ними в программе происходит через готовые функции, а значит программист почти не пользуется SPI напрямую. Вторая: небольшая и простая встроенная библиотека для SPI предназначена, в основном, для главного устройства. Считается, что Ведомым контроллер бывает крайне редко. Для Ведомого проще пользоваться непосредственно портами SPI, что мы и сделаем в примере чуть позже. Сама библиотека включает десяток функций, рассмотрим основные из них.
.begin(); - запускает аппаратную шину SPI на контроллере, устанавливает режимы пинов и их уровни, дополнительно, при помощи pinMode() этого делать не надо,
.end(); - отключает шину SPI, инициализация пинов при этом остается,
.setBitOrder(order); - назначает порядок вывода и ввода битов в байте, где order - LSBFIRST - младший бит первый, MSBFIRST - старший бит первый. Если светодиоды на регистре замигают в другом порядке, стоит лишь воспользоваться этой функцией,
.setClockDivider(); - делитель частоты контроллера для тактовой частоты шины. Чем больше, тем шина медленней. Аргумент SPI_CLOCK_DIVx, где x может принимать значения: 2, 4, 8, 16, 32, 64 и 128. Например, команда SPI.setClockDivider(SPI_CLOCK_DIV4); заставит работать шину с частотой ¼ от частоты контроллера.
.setDataMode(mode); - устанавливает режим работы шины. Аргумент: SPI_MODEx, где x - номер режима от 0 до 3, по числу всех возможных комбинаций:
.transfer(); - собственно сам прием-передача байта данных. Прием и передача осуществляется одновременно.
in = SPI.transfer(out); где in - принятый байт, out - переданный
Пины аппаратного SPI-интерфейса на основных платах Ардуино следующие:
Для нескольких устройств, соединенных параллельно по классической схеме, потребуется еще по одной линии SS каждому, роль ее могут выполнять любые другие пины.
Кроме того, почти на всех платах Ардуино имеется IСSP-разъем из шести пинов и он везде одинаков, независимо от платы. Основное его назначение - возможность прошивки микроконтроллера программатором, например для обновления бутлоадера. Его можно использовать и для подключения устройств, а также для соединения двух плат Ардуино, что удобно, особенно, если они разнотипные.
Примеры
Большая часть устройств имеет собственные библиотеки и собственные примеры в них. Начинающий программист даже не всегда понимает, что его творение работает при помощи SPI протокола. Поэтому мы рассмотрим два случая использования явного “чистого” SPI там, где это еще возможно.
Пример первый. Отправка данных на сдвиговый регистр 74HC595.
Не будем останавливаться на том, что такое сдвиговый регистр и как он работает, об этом есть отдельная статья, кто еще не читал, ознакомьтесь. В ней мы использовали собственную функцию отправки данных: открыли “защелку”, последовательно отправили байт, щелкая “ключом”, закрыли защелку. По сути, мы программным образом имитировали SPI в точности. Роль SS играла “защелка” LATCH, за MOSI выступала DATA, а за SCK, соответственно, CLOCK. Так вот, теперь пришла пора исполнить ту же пьесу настоящими актерами. Собираем схему.
#include <SPI.h> #define ss 8; // SS пин регистра byte b; void setup() { SPI.begin(); // инициализация интерфейса SPI pinMode(SS, OUTPUT); digitalWrite(SS, HIGH); } void loop() { b = 0x00000001; for (int i = 0; i < 8; i++) { digitalWrite(SS, LOW); // выбрали регистр SPI.transfer(b); // передаём байт в регистр digitalWrite(SS, HIGH); // закончили передачу delay(100); // задержка b <<= 1; // сдвинули единичку } }
Наблюдаем правильную работу интерфейса SPI, сдвигового регистра и нашего кода. Согласитесь, так программа выглядит гораздо короче и изящнее, чем в предыдущем варианте. К тому же она хоть не намного, но разгрузила контроллер, переложив задачу отправки данных на аппаратный интерфейс.
Интересное наблюдение, если в Setup добавить настройку SPI.setBitOrder(); и поиграться параметрами, вставляя то LSBFIRST, то MSBFIRST, можно будет видеть, что направление бега огонька будет меняться только от одной этой строки. Как думаете, почему?
Пример второй. Обмен данными между двумя контроллерами.
Повторим еще раз, задача соединять контроллеры - последняя из тех, что ставили перед собой разработчики интерфейса SPI, но иногда потребность в этом может возникнуть, например, если вы захотите сделать собственное периферийное SPI-устройство на базе контроллера, или все другие возможности перекинуться парой байт уже исчерпаны. В любом случае знать как это делается интересно, полезно и может пригодиться.
Со стороны Ведущего все просто, традиционно воспользуемся готовой библиотекой “SPI.h” и будем отправлять Ведомому команды на зажигание светодиода от нажатия кнопки. В ответ будем принимать число этих нажатий, которое будет подсчитываться на Ведомом устройстве.
А вот на Ведомом всю работу по приему и отправке данных сделаем полностью вручную, без библиотеки. На то есть две причины. Во-первых, библиотека плохо поддерживает работу Ведомого, так как заточена под Ведущий. Во-вторых, мы должны знать, как можно обойтись без библиотеки и что это вообще возможно в принципе. На удивление, программа от этого не станет намного больше и страшнее.
Итак, собираем схему. Возьмем для разнообразия две Меги, вспоминаем, где у них находятся пины SPI и соединяем их напрямую. К Ведущей Меге добавляем кнопку, к Ведомой светодиод. Впрочем, на пине 13 и так есть встроенный светодиод, можно наблюдать непосредственно за ним. Заливаем программу на Ведущий. Обращаю внимание, она работает на библиотеке “SPI.h”.
#include "SPI.h" #define BUT 7 // кнопка byte but[2]; byte c; void setup() { pinMode(BUT, INPUT); SPI.begin(); // запускаем SPI pinMode(SS, OUTPUT); digitalWrite(SS, HIGH); Serial.begin(9600); } void loop() { button(); // тут размещаем любую обычную программу } void button() { static unsigned long timer; if (timer + 100 > millis()) return; // опрос кнопок каждые 100 мс but[0] = but[1]; but[1] = digitalRead(BUT); if (but[0] && !but[1]) { // нажали digitalWrite(SS, LOW); c = SPI.transfer(1); // отправляем единичку, получаем байт в ответ digitalWrite(SS, HIGH); Serial.println(c); // печатаем полученный байт (счетчик) в монитор } else if (!but[0] && but[1]) { // отжали digitalWrite(SS, LOW); c = SPI.transfer(0); // отправляем нолик, получаем байт в ответ digitalWrite(SS, HIGH); Serial.println(c); // печатаем полученный байт (счетчик) в монитор } timer = millis(); }
Заливаем программу на Ведомую Мегу. Отмечаем, что библиотеку она не использует, только прямые регистры аппаратного интерфейса.
#define LED 13 // светодиод #define MOSI_PIN 51 // определяем аппаратные пины SPI (чисто для инициализации) #define MISO_PIN 50 #define SCK_PIN 52 #define SS_PIN 53 byte c; // счетчик 0-254 byte rec; // определяем переменную для получаемого байта void setup() { SPCR = B00000000; // обнуляем регистр управления SPI SPCR = (1 << SPE); // разрешаем работу SPI в качестве ведомого (запуск) pinMode(MOSI_PIN, INPUT); // инициализируем пины для работы с SPI pinMode(MISO_PIN, OUTPUT); pinMode(SCK_PIN, INPUT); pinMode(SS_PIN, INPUT); pinMode(LED, OUTPUT); Serial.begin(9600); } void loop() { while (digitalRead(SS_PIN) == LOW) { // пока обращаются к нам, принимаем байт rec = spi_receive(); // принимаем байт Serial.println(rec, HEX); digitalWrite(LED, rec); // отправляем его на светодиод c++; // увеличиваем значение счетчика SPDR = c; // отправляем Ведущему значение счетчика } } byte spi_receive() { // функция возвращает полный принятый байт while (!(SPSR & (1 << SPIF))) {}; // пока не выставлен флаг окончания передачи, принимаем биты return SPDR; // возвращаем содержимое регистра данных SPI }
То, как будет вести себя светодиод и что будет появляться в мониторе Ведущего, увидите, собрав и запустив схему. Главное, что данные передаются и принимаются обоими устройствами исправно.
Да, Ведомое устройство не может ничего сказать, пока к нему не обратится Ведущее, это надо учитывать в разработке и принимать соответствующие меры. Простых вариантов всего два: регулярно опрашивать Ведомое устройство или протянуть еще один проводок от Ведущего к Ведомому, эдакую ниточку для колокольчика “почта, сэр!”, SS наоборот. Решать, как лучше поступить, следует каждый раз индивидуально, в зависимости от множества факторов и от собственных предпочтений.
Вывод
SPI-интерфейс быстр, надежен и прост даже без библиотек. Едва ли найдется хоть один начинающий ардуинщик, который не столкнулся бы с SPI-устройствами в первый же месяц своего постижения мира цифровой электроники. Не важно, виден ли интерфейс сквозь библиотеки периферии, или проще воспользоваться им напрямую, но понимать, как он работает, как лучше подключаться и что вообще при этом происходит, должен каждый уважающий себя DIY-мастер.
FAQ
На каком расстоянии может работать передача данных по SPI-интерфейсу?
Точных цифр никто не назовет, зависит от многих параметров, от толщины и материала проводов, наличия экранирования, скорости тактирования, количества и мощности помех и так далее. Но в любом случае SPI не рассчитан на длинные дистанции, расстояния исчисляются максимум метрами.
Нужно ли притягивать или подтягивать резисторами шины SPI, как это делается в некоторых других интерфейсах?
Явных рекомендаций на эту тему нет, но в некоторых публикациях авторы советуют подтягивать шину SS к питанию.
Видел схему, где на шине MISO нарисован диод от Ведущего к Ведомому, что это и зачем?
Если SPI-устройство правильно спроектировано, собрано и прошито, что бывает чаще всего, необходимости в диоде никакой нет. Но изредка попадаются изделия с браком или недоработками, которые не отключаются от линии MISO при высоком уровне на SS. Если устройств в сети несколько, даже одно из них может и будет создавать коллизии, портить передачу данных по MISO и, вероятно, даже сожжет что-нибудь. Если есть сомнения, лучше перед работой проверить, есть ли на MISO подозрительного устройства связь между питанием или землей при высоком состоянии SS. Диоды защитят другие устройства от выхода из строя, но вред от лишнего сигнала все равно останется.
Можно ли передавать данные других типов, кроме байта?
Конечно, как и через другие интерфейсы - хоть массивы, хоть структуры. Любые данные состоят из байтов, надо лишь их разбить на одной стороне, передать и в том же порядке собрать на другой.