SPI (Serial Peripheral Interface) - интерфейс связи между цифровыми устройствами, по нему МК может общаться с датчиками и модулями или с другими МК. Среди основных интерфейсов связи (UART, I2C, SPI) SPI - самый простой, самый быстрый и неприхотливый, но требует больше пинов для подключения.
- Синхронный (есть тактовая линия)
- Последовательный (данные передаются бит за битом по одному проводу)
- Количество пинов: от 2 до 4
- Скорость: зависит от устройства и качества линии, от 0 Гц до 150 МГц
- Топология: одно ведущее устройство
- Количество ведомых на шине: не ограничено
Основы #
Роли #
SPI является шиной, т.е. к одним и тем же выводам может быть подключено несколько устройств. На шине есть только одно ведущее устройство - Master, остальные - ведомые, Slave. Ведущее выбирает, с каким из ведомых взаимодействовать, может отправлять и читать с него данные. Ведущим обычно является основной МК в схеме, а остальные - различные цифровые микросхемы, датчики или вспомогательные МК.
В современной литературе используется другая терминология: Controller и Peripheral. Уход от классических Master (хозяин) и Slave (раб) был связан со слишком близкой аналогией с работой на плантациях
Пины #
SPI использует максимум 4 пина для подключения. Рассмотрим их функции:
MOSI
(Master Out Slave In) - ведущее устройство отправляет данные, ведомое принимает. На "модуле" может быть подписан как SDI, DI, DIN, SIMISO
(Master In Slave Out) - ведомое устройство отправляет данные, ведущее принимает. На "модуле" может быть подписан как SDO, DO, DON, SOSS
(Slave Select) - выбор ведомого устройства, сигналом управляет мастер. На "модуле" может быть подписан как CS (Chip Select)SCK
(Serial Clock) - тактирование (клок, импульсы синхронизации), сигналом управляет мастер. На "модуле" может быть подписан как SCLK, CLK, SPC
"Режимы работы" пинов на устройствах:
Пин | Master | Slave |
---|---|---|
MOSI |
OUTPUT |
INPUT |
MISO |
INPUT |
OUTPUT |
SS |
OUTPUT |
INPUT |
SCK |
OUTPUT |
INPUT |
Подключение #
SPI - очень примитивный интерфейс, поэтому может подключаться на любые цифровые пины МК и работать в "ручном режиме" или при помощи функций типа shiftIn()
и shiftOut()
- опять же это программный SPI. Многие МК имеют и аппаратный SPI, который обычно в разы быстрее программной реализации, а также может отправлять данные асинхронно.
SPI, как шина, позволяет подключать сколько угодно устройств на одни и те же пины: объединять можно все пины, кроме CS
, т.к. при помощи него выбирается активное устройство.
На модуле или микросхеме может быть неполный набор пинов, но их будет как минимум два - дата (MOSI или MISO) и клок (SCK), если микросхема передаёт данные только в одну сторону.
На некоторых микросхемах и китайских модулях отсутствует пин CS - такие модули нельзя подключать на одни пины (MISO-MOSI-SCK) с остальными - они не смогут корректно работать, т.к. "активны" по умолчанию и будут реагировать на передачи для соседей
Расположение аппаратных выводов SPI микроконтроллера или отладочной платы всегда указано на распиновке. Рассмотрим подключение нескольких условных модулей к аппаратному SPI на Arduino Nano:
На распиновке можно увидеть также пин SS
- на тот случай, если Nano будет являться ведомым устройством
Здесь есть все три варианта:
DEVICE1
- только принимает данныеDEVICE2
- только отправляет данныеDEVICE3
- принимает и отправляет данные
Процесс передачи #
Процесс передачи начинается с выбора активного ведомого устройства - для этого его пин SS
обычно подключается к GND
(подаётся низкий сигнал, LOW
). То есть в остальное время на пине должен быть высокий сигнал, начиная с запуска МК.
После выбора начинается передача: на дата-пинах появляется логический уровень, соответствующий передаваемому биту. После этого мастер меняет сигнал на пине SCK
- ведомое устройство видит его и читает данные от мастера на пине MOSI
, а мастер читает данные с пина MISO
. Сигнал на SCK
снова меняется, в это время устройства выводят на свои дата-пины следующий бит.
Процесс повторяется до тех пор, пока мастер не "отпустит" SS
. Сколько бит будет передано или получено - зависит от конкретного устройства и программы на мастере, но в большинстве случаев кратно 8
, т.е. идёт обмен байтами.
Таким образом, передача идёт одновременно в обе стороны. На практике такое встречается редко, обычно устройство или читает, или отвечает, даже если имеет оба дата-пина
Режимы SPI #
На самом деле всё чуть сложнее, чем написано выше - есть 4 варианта, что считать за "изменение сигнала" на пине SCK
: начальный уровень клока (Clock Polarity, CPOL), а также работа по его восходящему или нисходящему фронту (Clock Phase, CPHA). Режимы имеют общепринятые номера от 0 до 3, в Arduino это константы SPI_MODEn
:
Режим | CPOL | CPHA | Вывод данных | Чтение |
---|---|---|---|---|
SPI_MODE0 |
0 | 0 | SCK ↓ или SS | SCK ↑ |
SPI_MODE1 |
0 | 1 | SCK ↑ | SCK ↓ |
SPI_MODE2 |
1 | 0 | SCK ↑ или SS | SCK ↓ |
SPI_MODE3 |
1 | 1 | SCK ↓ | SCK ↑ |
Самым распространённым является первый режим - SPI_MODE0
, а иногда устройства поддерживают сразу SPI_MODE0
и SPI_MODE3
Arduino Shift #
Полное описание смотри в справочнике
В фреймворке Arduino есть две функции, которые позволяют отправить и прочитать байт данных при помощи программного SPI, т.е. с любых цифровых пинов. Это удобно, когда не нужна высокая скорость передачи, неудобно использовать пины аппаратной шины или устройство не имеет пина CS и не может работать на основной шине вместе с остальными:
uint8_t shiftIn(uint8_t dataPin, uint8_t clockPin, uint8_t bitOrder)
- читает и возвращает 1 байт (8 бит) с дата-пинаdataPin
и клок-пинаclockPin
в порядкеbitOrder
void shiftOut(uint8_t dataPin, uint8_t clockPin, uint8_t bitOrder, uint8_t val)
- отправляет 1 байт (8 бит) значениеval
с дата-пинаdataPin
и клок-пинаclockPin
в порядкеbitOrder
Где bitOrder
:
MSBFIRST
- сначала старший битLSBFIRST
- сначала младший бит
В большинстве устройств используется порядок MSBFIRST
, уточнить можно в документации на конкретную микросхему
Обе функции работают в режиме SPI_MODE0
, он не настраивается
Пример отправки #
В качестве примера возьмём сдвиговый регистр 74HC595, он позволит буквально увидеть передаваемые данные при помощи светодиодов.
![]() |
В наборе GyverKIT | START | IOT | EXTRA |
---|---|---|---|---|
Сдвиговый регистр | ✔ |
Подключить всё это можно вот по такой схеме (подробнее - в уроке про 74HC595), также есть аналогичная схема в онлайн-симуляторе, вот ссылка на проект.
Вот программа, которая готовит пины к работе и отправляет биты 0b11001111
в чип, а он в свою очередь выводит их как напряжение на свои пины. Мы подключили к ним светодиоды, так что "увидим" переданные данные:
#define CS_595 10
#define DAT_595 11
#define CLK_595 13
void sendByte(uint8_t data) {
digitalWrite(CS_595, LOW); // выбрать активным
shiftOut(DAT_595, CLK_595, MSBFIRST, data); // отправить
digitalWrite(CS_595, HIGH); // деактивировать
}
void setup() {
// пины как выходы
pinMode(CS_595, OUTPUT);
pinMode(DAT_595, OUTPUT);
pinMode(CLK_595, OUTPUT);
// деактивировать
digitalWrite(CS_595, HIGH);
delay(100);
// отправить
sendByte(0b11001111);
}
void loop() {
}
Результат:
Если сменить режим на LSBFIRST
, то светодиоды загорятся в обратном порядке:
Вот реальный сигнал с пинов, снятый при помощи логического анализатора. Данные передавались в режиме MSBFIRST
:
Скорость #
Частота тактирования (и передачи данных) у этих функций по моим тестам следующая:
Платформа | Частота, MHz |
---|---|
AVR | 0.06 |
ESP8266 | 0.2 |
ESP32 | 0.96 |
ESP32C3 | 0.35 |
Что в десятки раз ниже скорости аппаратного SPI на этих же платформах, например на Arduino Nano можно передавать с частотой до 8 MHz!
В моей библиотеке GyverIO есть ускоренные версии этих функций для всех платформ. Если вам нужен быстрый программный SPI - используйте её. Например, на Arduino Nano частота составляет 1.3 MHz, что в 22 раза быстрее стандартных функций

Библиотека SPI.h #
Полное описание смотри в справочнике
В фреймворке Arduino есть инструмент для работы с аппаратным SPI - библиотека SPI.h, она позволяет отправлять данные с более высокой скоростью, а также с возможностью настроить её и остальные параметры шины. Рассмотрим минимальный пример:
#include <SPI.h>
void setup() {
// запустить шину
SPI.begin();
// активировать CS ведомого
// начать передачу с параметрами 1 МГц, MSBFIRST, SPI_MODE0
SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
// передать байты
SPI.transfer(123);
SPI.transfer(0b10100101);
// массив тоже можно
uint8_t data[] = {0xab, 0xcd};
SPI.transfer(data, 2);
// закончить передачу
SPI.endTransaction();
// деактивировать CS ведомого
// остановить шину (необязательно)
SPI.end();
}
void loop() {}
Методы beginTransaction()
и endTransaction()
позволяют нескольким SPI-устройствам безопасно использовать шину с разными настройками
Примечание (есть в справочнике, но продублирую здесь):
- Вызов
SPI.begin()
переводит пины шины SCK и MOSI в режимOUTPUT
. При запущенной шине пины не подчиняются функцииdigitalWrite()
- Вызов
SPI.end()
не меняет режим пинов шины - Если все три параметра
SPISettings
- константы, то эффективнее вызывать начало передачи сразу с конструктором настроек:SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
, как в примере выше - Если хотя бы один - не константа, то эффективнее создать объект
SPISettings
отдельно и передать его вbeginTransaction()
:
uint32_t freq; // = ...
SPISettings spiset(freq, MSBFIRST, SPI_MODE0);
SPI.beginTransaction(spiset);
Чтение #
Для чтения данных нужно получить их как результат функции transfer(uint8_t)
, т.е. для чтения нужно в любом случае вызвать передачу с любыми данными. Например uint8_t read = SPI.transfer(0);
- мы передаём устройству 0
(оно его игнорирует, если мы знаем что делаем), а в ответ получаем от него байт данных или 0
, если оно ничего не ответило.
Второй вариант - при отправке массива transfer(uint8_t*, size_t)
библиотека записывает в него ответ устройства, т.е. перезаписывает наши данные! Бывают ситуации, когда они нам всё ещё нужны, например буфер кадра дисплея. Даже если устройство ничего не ответит, если провод не подключен - массив заполнится нулями. А если мы ожидаем ответ - то нужными данными: сразу после вызова transfer
они будут доступны для использования.
Пример отправки #
Отправим тот же байт в ту же схему с 74HC595 и светодиодами, на этот раз при помощи аппаратного SPI. Результат на светодиодах будет таким же:
#include <Arduino.h>
#include <SPI.h>
#define CS_595 10 // пин CS, остальные на шине
void sendByte(uint8_t data) {
digitalWrite(CS_595, LOW); // выбрать активным
SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)); // начать
SPI.transfer(data); // отправить
SPI.endTransaction(); // закончить отправку
digitalWrite(CS_595, HIGH); // деактивировать
}
void setup() {
// запустить шину
SPI.begin();
// пин CS как выход
pinMode(CS_595, OUTPUT);
// деактивировать
digitalWrite(CS_595, HIGH);
delay(100);
sendByte(0b11001111);
}
void loop() {
}
Снимок с логического анализатора. Обратите внимание на время передачи, как оно уменьшилось по сравнению с shiftOut()
:
Как можно заметить, скорость очень сильно выросла - теперь её даже ограничивает медленный digitalWrite()
на пине CS. Заменим его на быстрый аналог из GyverIO:
Гораздо лучше, теперь байт передался за 1.6 микросекунды!
Скорость #
Нужно быть аккуратнее с выбором частоты тактирования шины:
- Чем длиннее провода и хуже подключение, тем ниже должна быть скорость. Для подключения на макетной плате лучше не использовать выше 1 МГц
- Нижний порог по скорости обычно отсутствует, поэтому допустимый диапазон скоростей очень большой и можно пользоваться функциями типа
shiftOut()
, которые сделаны буквально "на коленке" - Максимальная частота для устройства обычно указана в документации к нему. Например, тот же 74HC595 может работать на 25 MHz при питании от 4.5V и на 5 MHz от 2V. У драйвера матриц и дисплеев MAX7219 в документации указан минимальный CLK Clock Period 100 ns (наносекунд), т.е. максимальная частота - 10 МГц
Полезные страницы #
- Набор GyverKIT – наш большой стартовый набор Arduino, продаётся в России
- Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress
- Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту ([email protected])
- Поддержать автора за работу над уроками
