Данная статья является кратким дискурсом по шине SPI и не должна восприниматься как точная техническая документация. Рассматривается только полнодуплексный вариант применения.
Общие сведения:
SPI - (Serial Peripheral Interface) эспиай, последовательный периферийный интерфейс иногда называемый 4-х проводным интерфейсом, является последовательным синхронным интерфейсом передачи данных. Изобретён компанией Motorola в середине 1980-x. В отличие от I2C и UART, SPI требует больше сигналов для работы, но может работать на более высоких скоростях. Не поддерживает адресацию, вместо этого используется сигнал SS (slave select - выбор ведомого), который также иногда называется CS (chip select), CE (chip enable) или SE (slave enable). Поддерживает только одного ведущего на шине. Ведущий устанавливает скорость обмена данными и другие параметры, такие как полярность и фаза тактирования. Обмен данными происходит в режиме полного дуплекса, что означает устройства на шине могут одновременно передавать и принимать данные. Интерфейс использует следующие сигналы (в номенклатуре AVR, для получения точного названия сигналов обратитесь к технической документации микросхемы, с которой работаете):
- MISO (master in slave out) - вход ведущего, выход ведомого
- MOSI (master out slave in) - выход ведущего, вход ведомого
- SCK (serial clock) - сигнал тактирования
- SS (slave select) - сигнал выбор ведомого.
Несмотря на то, что интерфейс называется 4-х проводным, для подключения нескольких ведомых понадобится по одному проводу SS для каждого ведомого (в полнодуплексной реализации). Сигналы MISO, MOSI и SCK являются общими для всех устройств на шине. Ведущий посылает сигнал SS для того ведомого, обмен данными с которым будет осуществляться. Простыми словами, все ведомые, кроме выбранного ведущим будут игнорировать данные на шине. SS является инверсным (active-low), что означает что ведущему необходимо прижать эту линию для выбора ведомого.
Подключение:

SPI на Arduino:
Arduino UNO/Piranha UNO/Arduino ULTRA
На Arduino UNO/Piranha UNO/Arduino ULTRA выводы аппаратного SPI расположены на 10, 11, 12 и 13 выводах, а так же эти выводы соединены с колодкой ICSP (in circuit serial programmer):

| Сигнал | Вывод |
|---|---|
| SS | 10 |
| MOSI | 11 |
| MISO | 12 |
| SCK | 13 |
Arduino MEGA
На Arduino MEGA выводы аппаратного SPI расположены на 50, 51, 52 и 53 выводах, а так же эти выводы соединены с колодкой ICSP (in circuit serial programmer):

| Сигнал | Вывод |
|---|---|
| SS | 53 |
| MOSI | 51 |
| MISO | 50 |
| SCK | 52 |
Пример для Arduino
В этих примерах мы соединим две Arduino по SPI по следующей схеме:

В одну плату необходимо загрузить скетч ведущего, а в другую скетч ведомого. Для проверки работы необходимо открыть проследовательный монитор той платы, в которую загружен скетч ведомого.
Arduino UNO в качестве ведущего:
#include "SPI.h"
// Создаём переменную байтового массива
byte data[] {'H','e','l','l','o'};
// Создаём переменную строки в формате языка Си
char* cstring = " World!";
// создаём переменную с числом 168496141 в шестнадцатеричной системе счисления
long i = 0x0A0B0C0D;
void setup()
{
// Инициируем интерфейс SPI в режиме ведущего
SPI.begin();
// Устанавливаем логическую "1" на выводе Slave Select
pinMode(SS, HIGH);
}
void loop()
{
// Прижимаем вывод Slave Select (устанавливаем логический "0"),
// тем самым активируя передачу данных с подключенным
// к этому выводу ведомым
digitalWrite(SS, LOW);
// Начинаем передачу данных, передавая функции объект настроек шины
// SPISettings( Скорость в Гц, Порядок передачи битов, Режим шины)
SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
// Передаём массив data
for (int i = 0; i < sizeof(data); i++) {
SPI.transfer(char(data[i]));
}
// Передаём один байт
SPI.transfer(',');
// Передаём строку в формате си
for (char* p = cstring; char c = *p; p++) {
SPI.transfer(c);
}
// Передаём один байт - заголовок начала целого числа
// Как пример: байт 0xAD обозначает заголовок целого числа типа long
SPI.transfer(0xAD);
// Побайтово передаём целое число
for (int j = 0; j < sizeof(i); j++) {
byte b = i >> 8 * j;
SPI.transfer(b);
}
// Передаём байт конца пакета
// Как пример: байт 0xAF обозначает конец пакета
SPI.transfer(0xAF);
// Завершаем передачу данных
SPI.endTransaction();
// Завершаем работу с ведомым
digitalWrite(SS, HIGH);
delay(1000);
}
Arduino UNO в качестве ведомого:
#include "SPI.h"
// Определяем размер буфера
#define BUFFER_SIZE 100
// Создаём буфер для данных SPI
char buff[BUFFER_SIZE];
// Создаём переменную индекса буфера
// volatile - указания для препроцессора о том,
// что переменная может измениться при прерывании
// и с ней не стоит производить никаких оптимизаций
// при компилировании
volatile uint8_t index = 0;
// Создаём флаг готовности данных
volatile bool data_ready = false;
// Создаём переменную для полученного целого числа
long i;
// Создаём индекс заголовка целого числа
volatile uint8_t int_index = 0;
void setup (void)
{
// Инициируем работу с последовательным портом
Serial.begin(9600);
// Устанавливаем вывод MISO в режим выхода
pinMode(MISO, OUTPUT);
// Устанавливаем режим ведомого в контрольном регистре SPI (SPI Control Register)
SPCR |= _BV(SPE);
// Подключаем прерывание SPI
SPI.attachInterrupt();
}
// Вызываем функцию обработки прерываний по вектору SPI
// STC - Serial Transfer Comlete
ISR(SPI_STC_vect)
{
// Получаем байт из регистра данных SPI
byte c = SPDR;
// Добавляем байт в буфер
if (index < sizeof(buff)) {
buff[index++] = c;
// Как пример: байт 0xAD обозначает заголовок целого числа типа long
if (c == 0xAD)
// Записываем положение целого числа в байтовом массиве
int_index = index;
// Как пример: байт 0xAF обозначает конец пакета
if (c == 0xAF)
// Устанавливаем флаг готовности данных для обработки
data_ready = true;
}
}
void loop(void)
{
// Если установлен флаг готовых данных
if (data_ready == true) {
// Обнуляем индекс
index = 0;
// Создаём строку и записываем в неё полученный буфер
String message = String(buff);
// Форматируем строку, убирая из неё заголовок
// целого числа и всё после заголовка
message = message.substring(0, int_index - 1);
// Выводим отформатированную строку в последовательный порт
Serial.println(message);
// Обнуляем переменную для хранения полученного целого числа
i = 0;
// Записываем целое число через указатель на элемент массива
/* Пояснение:
* *(выражение) - разыменовывание указателя
* (long *) - приводим последующее выражение к типу указателя на long
* buff+int_index - прибавляем к указателю на первый элемент
* массива buff индекс следующего после заголовка элемента массива,
*/ тем самым получая указатель на наше целое число
i = *((long *)(buff + int_index));
// обнуляем флаг готовности данных
data_ready = false;
Serial.print("long integer over SPI is: ");
// Выводим целое число в последовательный порт
Serial.println(i);
}
}
После соединения двух Arduino по SPI и загрузки соответствующих скетчей, мы будем получать следующее сообщение в мониторе последовательного порта ведомого микроконтроллера раз в секунду:
Hello, World!
long integer over SPI is: 168496141
SPI на Raspberry Pi
На Raspberry Pi выводы аппаратного SPI расположены на выводах GPIO7, GPIO8, GPIO9, GPIO10, GPIO11:

Перед работой с SPI необходимо его включить. Сделать это можно из эмулятора терминала командой sudo raspi-config -> Interfacing options -> Serial -> No -> Yes -> OK -> Finish или из графической среды в главном меню -> Параметры -> Raspberry Pi Configuration -> Interfaces -> SPI
Подробное описание как это сделать можно посмотреть по ссылке Raspberry Pi, включаем I2C, SPI
Пример работы с SPI на Python:
# Импортируем библиотеку для работы с SPI import spidev # Создаём объeкт spi spi = spidev.SpiDev() # Открываем устройство SPI (/dev/spidev0.0) spi.open(0, 0) # Ограничиваем скорость до 1 МГц spi.max_speed_hz = 1000000 # Выводим байтовую строку spi.writebytes(b'Hello, World!') # Выводим заголовок целого числа spi.writebytes([0xAD]) # Создаём целое число в шестнадцатеричной системе счисления i = 0x0A0B0C0D # Конвертируем число в список байтов, # указывая формат little endian b = i.to_bytes(4, byteorder='little') # Передаём число по SPI spi.writebytes(b) # Передаём байт конца пакета spi.writebytes([0xAF])
В отличие от Arduino для Raspberry не существует простых решений для работы в режиме ведомого. Подробней ознакомиться с работой чипа BCM Raspberry можно в технической документации на официальном сайте, стр. 160.
Для проверки работы сценария можно подключить Raspberry по SPI к Arduino со скетчем из примера выше через преобразователь уровней или Trema+Expander Hat:

Подробнее о SPI
Параметры
Существуют четыре режима работы SPI, зависящие от полярности (CPOL) и фазы (CPHA) тактирования:
| Режим | Полярность | Фаза | Фронт тактирования | Фронт установки бита данных |
|---|---|---|---|---|
| SPI_MODE0 | 0 | 0 | Спадающий | Нарастающий |
| SPI_MODE1 | 0 | 1 | Нарастающий | Спадающий |
| SPI_MODE2 | 1 | 0 | Нарастающий | Спадающий |
| SPI_MODE3 | 1 | 1 | Спадающий | Нарастающий |
В Arduino IDE для установки режима необходимо передать функции, возвращающей объект настроек параметр режима работы SPI_MODE, например:
SPISettings(1000000, MSBFIRST, SPI_MODE0)
Для выбора режима работы SPI на Raspberry Pi необходимо вызвать дескриптор объекта SpiDev().mode и присвоить ему битовые значения CPOL и CPHA, например:
# 0b11 соответствует режиму MODE3 spi.mode = 0b11
Скорость передачи данных
Скорость передачи данных устанавливается ведущим и может меняться "на лету". Программист в силах указать лишь максимальную скорость передачи данных.

Обсуждение